Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[feature] Use Python's faulthandler module to include a python
stack trace in the QGIS crash handler dialog when a crash occurs
as a result of Python code

Useful for debugging, and in future could be extended so that we
try to automatically detect if the python stack includes a **plugin**,
and then adapt the dialog to firmly direct the user to the offending
plugin.
  • Loading branch information
nyalldawson committed Dec 7, 2021
1 parent 4eb39b5 commit 905251f
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 4 deletions.
3 changes: 2 additions & 1 deletion src/app/qgisapp.cpp
Expand Up @@ -12794,7 +12794,8 @@ void QgisApp::loadPythonSupport()
mPythonUtils = pythonlib_inst();
if ( mPythonUtils )
{
mPythonUtils->initPython( mQgisInterface, true );
QgsCrashHandler::sPythonCrashLogFile = QStandardPaths::standardLocations( QStandardPaths::TempLocation ).at( 0 ) + "/qgis-python-crash-info-" + QString::number( QCoreApplication::applicationPid() );
mPythonUtils->initPython( mQgisInterface, true, QgsCrashHandler::sPythonCrashLogFile );
}

if ( mPythonUtils && mPythonUtils->isEnabled() )
Expand Down
3 changes: 3 additions & 0 deletions src/app/qgscrashhandler.cpp
Expand Up @@ -28,6 +28,8 @@
#include <QStandardPaths>
#include <QUuid>

QString QgsCrashHandler::sPythonCrashLogFile;

#ifdef _MSC_VER
LONG WINAPI QgsCrashHandler::handle( LPEXCEPTION_POINTERS exception )
{
Expand Down Expand Up @@ -104,6 +106,7 @@ void QgsCrashHandler::handleCrash( int processID, int threadID,
stream << QString::number( threadID ) << endl;
stream << ptrStr << endl;
stream << symbolPath << endl;
stream << sPythonCrashLogFile << endl;
stream << arguments.join( ' ' ) << endl;
stream << reportData.join( '\n' ) << endl;
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgscrashhandler.h
Expand Up @@ -45,6 +45,8 @@ class APP_EXPORT QgsCrashHandler
static void handle( int );
#endif

static QString sPythonCrashLogFile;

private:

static void handleCrash( int processId,
Expand Down
3 changes: 3 additions & 0 deletions src/crashhandler/main.cpp
Expand Up @@ -49,6 +49,7 @@ int main( int argc, char *argv[] )
QString threadIdString;
QString exceptionPointersString;
QString symbolPaths;
QString pythonCrashLogFile;
QString reloadArgs;
QStringList versionInfo;

Expand All @@ -58,6 +59,7 @@ int main( int argc, char *argv[] )
threadIdString = file.readLine();
exceptionPointersString = file.readLine();
symbolPaths = file.readLine();
pythonCrashLogFile = file.readLine().trimmed();
reloadArgs = file.readLine();
// The version info is the last stuff to be in the file until the end
// bit gross but :)
Expand Down Expand Up @@ -86,6 +88,7 @@ int main( int argc, char *argv[] )
#ifdef MSVC
report.setStackTrace( stackTrace.get() );
#endif
report.setPythonCrashLogFilePath( pythonCrashLogFile );
report.exportToCrashFolder();

QgsCrashDialog dlg;
Expand Down
52 changes: 52 additions & 0 deletions src/crashhandler/qgscrashreport.cpp
Expand Up @@ -63,6 +63,34 @@ const QString QgsCrashReport::toHtml() const
}
reportData.append( QStringLiteral( "</pre>" ) );
}

QStringList pythonStack;
if ( !mPythonCrashLogFilePath.isEmpty() )
{
QFile pythonLog( mPythonCrashLogFilePath );
if ( pythonLog.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
QTextStream inputStream( &pythonLog );
QString line;
while ( !inputStream.atEnd() )
{
pythonStack.append( inputStream.readLine() );
}
}
pythonLog.close();
}

if ( !pythonStack.isEmpty() )
{
reportData.append( QStringLiteral( "<br>" ) );
QString pythonStackString = QStringLiteral( "<b>Python Stack Trace</b><pre>" );
for ( const QString &line : pythonStack )
{
pythonStackString.append( line + '\n' );
}
pythonStackString.append( QStringLiteral( "</pre>" ) );
reportData.append( pythonStackString );
}
}

#if 0
Expand Down Expand Up @@ -168,6 +196,30 @@ void QgsCrashReport::exportToCrashFolder()
stream << htmlToMarkdown( toHtml() ) << endl;
}
file.close();

if ( !mPythonCrashLogFilePath.isEmpty() )
{
fileName = folder + "/python.txt";
QFile pythonLog( mPythonCrashLogFilePath );
if ( pythonLog.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
QTextStream inputStream( &pythonLog );
file.setFileName( fileName );
if ( file.open( QIODevice::WriteOnly | QIODevice::Text ) )
{
QTextStream outputStream( &file );

QString line;
while ( !inputStream.atEnd() )
{
line = inputStream.readLine();
outputStream << line;
}
}
file.close();
pythonLog.close();
}
}
}

QString QgsCrashReport::crashReportFolder()
Expand Down
6 changes: 6 additions & 0 deletions src/crashhandler/qgscrashreport.h
Expand Up @@ -88,6 +88,11 @@ class QgsCrashReport

void setVersionInfo( const QStringList &versionInfo ) { mVersionInfo = versionInfo; }

/**
* Sets the \a path to the associated Python crash log.
*/
void setPythonCrashLogFilePath( const QString &path ) { mPythonCrashLogFilePath = path; }

/**
* convert htmlToMarkdown (copied from QgsStringUtils::htmlToMarkdown)
* \param html text in html
Expand All @@ -99,6 +104,7 @@ class QgsCrashReport
Flags mFlags;
QgsStackTrace *mStackTrace = nullptr;
QStringList mVersionInfo;
QString mPythonCrashLogFilePath;

};

Expand Down
5 changes: 4 additions & 1 deletion src/python/qgspythonutils.h
Expand Up @@ -61,8 +61,11 @@ class PYTHON_EXPORT QgsPythonUtils
* NULLPTR if no interface is available.
*
* If \a installErrorHook is true then the custom QGIS GUI error hook will be used.
*
* Since QGIS 3.24, the \a faultHandlerLogPath argument can be used to specify a file path
* for Python's faulthandler to dump tracebacks in if Python code causes QGIS to crash.
*/
virtual void initPython( QgisInterface *iface, bool installErrorHook ) = 0;
virtual void initPython( QgisInterface *iface, bool installErrorHook, const QString &faultHandlerLogPath = QString() ) = 0;

#ifdef HAVE_SERVER_PYTHON_PLUGINS

Expand Down
12 changes: 11 additions & 1 deletion src/python/qgspythonutilsimpl.cpp
Expand Up @@ -209,7 +209,7 @@ void QgsPythonUtilsImpl::doCustomImports()
}
}

void QgsPythonUtilsImpl::initPython( QgisInterface *interface, const bool installErrorHook )
void QgsPythonUtilsImpl::initPython( QgisInterface *interface, const bool installErrorHook, const QString &faultHandlerLogPath )
{
init();
if ( !checkSystemImports() )
Expand All @@ -218,6 +218,16 @@ void QgsPythonUtilsImpl::initPython( QgisInterface *interface, const bool instal
return;
}

if ( !faultHandlerLogPath.isEmpty() )
{
runString( QStringLiteral( "import faulthandler" ) );
QString escapedPath = faultHandlerLogPath;
escapedPath.replace( '\\', QLatin1String( "\\\\" ) );
escapedPath.replace( '\'', QLatin1String( "\\'" ) );
runString( QStringLiteral( "fault_handler_file=open('%1', 'wt')" ).arg( escapedPath ) );
runString( QStringLiteral( "faulthandler.enable(file=fault_handler_file)" ) );
}

if ( interface )
{
// initialize 'iface' object
Expand Down
2 changes: 1 addition & 1 deletion src/python/qgspythonutilsimpl.h
Expand Up @@ -36,7 +36,7 @@ class QgsPythonUtilsImpl : public QgsPythonUtils

/* general purpose functions */

void initPython( QgisInterface *interface, bool installErrorHook ) override;
void initPython( QgisInterface *interface, bool installErrorHook, const QString &faultHandlerLogPath = QString() ) override;
#ifdef HAVE_SERVER_PYTHON_PLUGINS
void initServerPython( QgsServerInterface *interface ) override;
bool startServerPlugin( QString packageName ) override;
Expand Down

0 comments on commit 905251f

Please sign in to comment.