diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index 0445405c5..ed970e60b 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -129,6 +129,7 @@ struct OutputRedirector uint64_t dummy; PythonContext *context; }; + bool selfDeleting; int isStdError; bool block; rdcstr extension; @@ -147,6 +148,7 @@ PyObject *PythonContext::main_dict = NULL; PyObject *PythonContext::m_DebugPy = NULL; PyObject *PythonContext::m_CallWrapper = NULL; PyObject *PythonContext::m_CallWrapperGlobals = NULL; +PythonContext *PythonContext::m_ExtensionContext = NULL; QMap PythonContext::extensions; static PyObject *current_global_handle = NULL; @@ -550,6 +552,7 @@ void PythonContext::GlobalInit(PersistantConfig &config) OutputRedirector *output = (OutputRedirector *)redirector; output->isStdError = 0; + output->selfDeleting = false; output->context = NULL; output->block = false; @@ -558,6 +561,7 @@ void PythonContext::GlobalInit(PersistantConfig &config) output = (OutputRedirector *)redirector; output->isStdError = 1; + output->selfDeleting = false; output->context = NULL; output->block = false; } @@ -897,6 +901,9 @@ except: // release GIL so that python work can now happen on any thread PyEval_SaveThread(); + + // this will leak effectively + m_ExtensionContext = new PythonContext(true, NULL); } bool PythonContext::initialised() @@ -904,7 +911,7 @@ bool PythonContext::initialised() return main_dict != NULL; } -PythonContext::PythonContext(QObject *parent) : QObject(parent) +PythonContext::PythonContext(bool extensionContext, QObject *parent) : QObject(parent) { if(!initialised()) return; @@ -915,8 +922,6 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent) // clone our own local context context_namespace = PyDict_Copy(main_dict); - PyObject *rlcompleter = PyImport_ImportModule("rlcompleter"); - // for compatibility with earlier versions of python that took a char * instead of const char * char noparams[1] = ""; @@ -929,10 +934,13 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent) OutputRedirector *output = (OutputRedirector *)redirector; output->context = this; + output->selfDeleting = !extensionContext; output->block = false; Py_DECREF(redirector); } + PyObject *rlcompleter = PyImport_ImportModule("rlcompleter"); + if(rlcompleter) { PyObject *Completer = PyObject_SafeGetAttrString(rlcompleter, "Completer"); @@ -1237,6 +1245,7 @@ QString PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extensi OutputRedirector *redir = (OutputRedirector *)ext_context; redir->isStdError = 0; + redir->selfDeleting = false; redir->context = NULL; redir->block = false; redir->extension = extension; @@ -1415,7 +1424,8 @@ void PythonContext::executeString(const QString &filename, const QString &source if(!initialised()) { FlushOutput(); - emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {}); + emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."), + -1, {}); return; } @@ -1534,7 +1544,7 @@ void PythonContext::executeString(const QString &filename, const QString &source if(caughtException) { FlushOutput(); - emit exception(typeStr, valueStr, finalLine, frames); + emit exception(QString(), typeStr, valueStr, finalLine, frames); } } @@ -1550,8 +1560,8 @@ void PythonContext::executeFile(const QString &filename) if(!f.exists()) { FlushOutput(); - emit exception(lit("FileNotFoundError"), tr("No such file or directory: %1").arg(filename), -1, - {}); + emit exception(QString(), lit("FileNotFoundError"), + tr("No such file or directory: %1").arg(filename), -1, {}); return; } @@ -1564,7 +1574,8 @@ void PythonContext::executeFile(const QString &filename) else { FlushOutput(); - emit exception(lit("IOError"), QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {}); + emit exception(QString(), lit("IOError"), + QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {}); } } @@ -1573,7 +1584,8 @@ void PythonContext::setGlobal(const char *varName, const char *typeName, void *o if(!initialised()) { FlushOutput(); - emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {}); + emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."), + -1, {}); return; } @@ -1592,7 +1604,7 @@ void PythonContext::setGlobal(const char *varName, const char *typeName, void *o if(ret != 0) { FlushOutput(); - emit exception(lit("RuntimeError"), + emit exception(QString(), lit("RuntimeError"), tr("Failed to set variable '%1' of type '%2'") .arg(QString::fromUtf8(varName)) .arg(QString::fromUtf8(typeName)), @@ -1854,28 +1866,33 @@ void PythonContext::outputTick() { QMutexLocker lock(&outputMutex); - if(!outstr.isEmpty()) + for(QString &extension : outputCaches.keys()) { - emit textOutput(false, outstr); - } + OutputPair &o = outputCaches[extension]; - if(!errstr.isEmpty()) - { - emit textOutput(true, errstr); - } + if(!o.outstr.isEmpty()) + { + emit textOutput(extension, false, o.outstr); + } - outstr.clear(); - errstr.clear(); + if(!o.errstr.isEmpty()) + { + emit textOutput(extension, true, o.errstr); + } + + o.outstr.clear(); + o.errstr.clear(); + } } -void PythonContext::addText(bool isStdError, const QString &output) +void PythonContext::addText(QString context, bool isStdError, const QString &output) { QMutexLocker lock(&outputMutex); if(isStdError) - errstr += output; + outputCaches[context].errstr += output; else - outstr += output; + outputCaches[context].outstr += output; } void PythonContext::setPyGlobal(const char *varName, PyObject *obj) @@ -1883,7 +1900,8 @@ void PythonContext::setPyGlobal(const char *varName, PyObject *obj) if(!initialised()) { FlushOutput(); - emit exception(lit("SystemError"), tr("Python integration failed to initialise."), -1, {}); + emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."), + -1, {}); return; } @@ -1900,7 +1918,7 @@ void PythonContext::setPyGlobal(const char *varName, PyObject *obj) return; FlushOutput(); - emit exception(lit("RuntimeError"), + emit exception(QString(), lit("RuntimeError"), tr("Failed to set variable '%1'").arg(QString::fromUtf8(varName)), -1, {}); } @@ -1908,7 +1926,7 @@ void PythonContext::outstream_del(PyObject *self) { OutputRedirector *redirector = (OutputRedirector *)self; - if(redirector) + if(redirector && redirector->selfDeleting) { PythonContext *context = redirector->context; @@ -1933,6 +1951,7 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args) if(redirector) { PythonContext *context = redirector->context; + QString extension = redirector->extension; // most likely this is NULL because the sys.stdout override is static and shared amongst // contexts. So look up the global variable that stores the context if(context == NULL) @@ -1950,7 +1969,10 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args) OutputRedirector *global = (OutputRedirector *)PyDict_GetItemString(globals, "_renderdoc_internal"); if(global) + { context = global->context; + extension = global->extension; + } } Py_XDECREF(globals); @@ -1974,10 +1996,16 @@ PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args) if(context) { - context->addText(redirector->isStdError ? true : false, QString::fromUtf8(text)); + context->addText(extension, redirector->isStdError ? true : false, QString::fromUtf8(text)); } else { + if(!extension.isEmpty()) + { + m_ExtensionContext->addText(extension, redirector->isStdError ? true : false, + QString::fromUtf8(text)); + } + // if context is still NULL we're running in the extension context rdcstr message = text; @@ -2303,10 +2331,17 @@ extern "C" void HandleException(PyObject *global_handle) if(redirector && redirector->context) { redirector->context->FlushOutput(); - emit redirector->context->exception(typeStr, valueStr, finalLine, frames); + emit redirector->context->exception(redirector->extension, typeStr, valueStr, finalLine, frames); } else { + if(redirector && !redirector->extension.empty()) + { + PythonContext::GetExtensionContext()->FlushOutput(); + emit PythonContext::GetExtensionContext()->exception(redirector->extension, typeStr, valueStr, + finalLine, frames); + } + // if still NULL we're running in the extension context rdcstr exString; diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.h b/qrenderdoc/Code/pyrenderdoc/PythonContext.h index 80bd111bd..3f7f4876c 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.h +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.h @@ -47,8 +47,9 @@ private: // on and needs to finish executing after the external code is done with the context ~PythonContext(); + explicit PythonContext(bool extensionContext, QObject *parent); public: - explicit PythonContext(QObject *parent = NULL); + explicit PythonContext(QObject *parent = NULL) : PythonContext(false, parent) {} void Finish(); PyThreadState *GetExecutingThreadState() { return m_State; } @@ -94,7 +95,7 @@ public: if(obj) setPyGlobal(varName, obj); else - emit exception(lit("RuntimeError"), + emit exception(QString(), lit("RuntimeError"), tr("Failed to set variable '%1' of type '%2'") .arg(QString::fromUtf8(varName)) .arg(QString::fromUtf8(typeName)), @@ -114,10 +115,14 @@ public: int currentLine() { return location.line; } static void AddDebuggableThread(); static void RemoveDebuggableThread(); + + static PythonContext *GetExtensionContext() { return m_ExtensionContext; } + signals: void traceLine(const QString &file, int line); - void exception(const QString &type, const QString &value, int finalLine, QList frames); - void textOutput(bool isStdError, const QString &output); + void exception(const QString &extension, const QString &type, const QString &value, int finalLine, + QList frames); + void textOutput(const QString &extension, bool isStdError, const QString &output); public slots: void executeString(const QString &source); @@ -139,6 +144,10 @@ private: static PyObject *m_CallWrapper; static PyObject *m_CallWrapperGlobals; + // a statically created PythonContext for extension events/output. + // each extension has its own dictionary but this is used so that users can connect to it and receieve events + static PythonContext *m_ExtensionContext; + // the list of extension objects, to be able to reload them static QMap extensions; @@ -170,10 +179,15 @@ private: QTimer *outputTicker = NULL; QMutex outputMutex; - QString outstr, errstr; + + struct OutputPair + { + QString outstr, errstr; + }; + QMap outputCaches; void outputTick(); - void addText(bool isStdError, const QString &output); + void addText(QString extension, bool isStdError, const QString &output); // Python callbacks static void outstream_del(PyObject *self); diff --git a/qrenderdoc/Code/qrenderdoc.cpp b/qrenderdoc/Code/qrenderdoc.cpp index fd71499ae..98a703876 100644 --- a/qrenderdoc/Code/qrenderdoc.cpp +++ b/qrenderdoc/Code/qrenderdoc.cpp @@ -673,31 +673,31 @@ int main(int argc, char *argv[]) py.ctx().setGlobal("pyrenderdoc", (ICaptureContext *)&ctx); - QObject::connect( - &py.ctx(), &PythonContext::exception, - [&pythonExited](const QString &type, const QString &value, int, QList frames) { - if(type == lit("SystemExit")) - { - pythonExited = true; - return; - } + QObject::connect(&py.ctx(), &PythonContext::exception, + [&pythonExited](const QString &, const QString &type, const QString &value, + int, QList frames) { + if(type == lit("SystemExit")) + { + pythonExited = true; + return; + } - QString exString; + QString exString; - if(!frames.isEmpty()) - { - exString += tr("Traceback (most recent call last):\n"); - for(const QString &f : frames) - exString += QFormatStr(" %1\n").arg(f); - } + if(!frames.isEmpty()) + { + exString += tr("Traceback (most recent call last):\n"); + for(const QString &f : frames) + exString += QFormatStr(" %1\n").arg(f); + } - exString += QFormatStr("%1: %2\n").arg(type).arg(value); + exString += QFormatStr("%1: %2\n").arg(type).arg(value); - qCritical("%s", exString.toUtf8().data()); - }); + qCritical("%s", exString.toUtf8().data()); + }); QObject::connect(&py.ctx(), &PythonContext::textOutput, - [](bool isStdError, const QString &output) { + [](const QString &, bool isStdError, const QString &output) { if(isStdError) qCritical("%s", output.toUtf8().data()); else diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index 57d8b1e01..540921337 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -1052,6 +1052,11 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) enableButtons(true); + QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::textOutput, this, + &PythonShell::textOutput); + QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::exception, this, + &PythonShell::exception); + // reset output to default on_clear_clicked(); on_newScript_clicked(); @@ -1282,8 +1287,8 @@ void PythonShell::traceLine(const QString &file, int line) scriptEditor->markerAdd(line > 0 ? line - 1 : 0, CURRENT_MARKER + 1); } -void PythonShell::exception(const QString &type, const QString &value, int finalLine, - QList frames) +void PythonShell::exception(const QString &extension, const QString &type, const QString &value, + int finalLine, QList frames) { QTextEdit *out = ui->scriptOutput; if(QObject::sender() == (QObject *)interactiveContext) @@ -1307,7 +1312,7 @@ void PythonShell::exception(const QString &type, const QString &value, int final appendText(out, exString); } -void PythonShell::textOutput(bool isStdError, const QString &output) +void PythonShell::textOutput(const QString &extension, bool isStdError, const QString &output) { QTextEdit *out = ui->scriptOutput; if(QObject::sender() == (QObject *)interactiveContext) @@ -1431,9 +1436,10 @@ void PythonShell::refreshCurrentHelp() ui->helpText->clear(); - QObject::connect( - context, &PythonContext::textOutput, - [this](bool isStdError, const QString &output) { appendText(ui->helpText, output); }); + QObject::connect(context, &PythonContext::textOutput, + [this](const QString &, bool isStdError, const QString &output) { + appendText(ui->helpText, output); + }); context->executeString(lit(R"( try: diff --git a/qrenderdoc/Windows/PythonShell.h b/qrenderdoc/Windows/PythonShell.h index 15b159be6..51ccc5fe2 100644 --- a/qrenderdoc/Windows/PythonShell.h +++ b/qrenderdoc/Windows/PythonShell.h @@ -78,8 +78,9 @@ private slots: void interactive_keypress(QKeyEvent *e); void helpSearch_keypress(QKeyEvent *e); void traceLine(const QString &file, int line); - void exception(const QString &type, const QString &value, int finalLine, QList frames); - void textOutput(bool isStdError, const QString &output); + void exception(const QString &extension, const QString &type, const QString &value, int finalLine, + QList frames); + void textOutput(const QString &extension, bool isStdError, const QString &output); void editor_contextMenu(const QPoint &pos); private: