Files
renderdoc/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp
T
baldurk 6b02191629 Try to break potential deadlock between UI and replay threads
* During capture shutdown on Python < 3.13 the replay thread will try to remove
  itself as a debuggable thread which requires being able to run python (holding
  the GIL). If the UI thread is the one triggering the capture shutdown and is
  running a call from a python script it may hold the GIL while waiting for the
  replay thread to close, deadlocking.
* To alleviate this, while the UI thread is about to block on the replay thread
  we try to release the python state if we are holding it so that the replay
  thread can run python code if needed.
* We already do this in a manual wrapper around BlockInvoke.
2026-08-31 11:13:35 +01:00

2782 lines
71 KiB
C++

/******************************************************************************
* The MIT License (MIT)
*
* Copyright (c) 2017-2026 Baldur Karlsson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
******************************************************************************/
#ifdef slots
#undef slots
#define slots_was_defined
#endif
// must be included first
#include <Python.h>
#include <pythread.h>
#include "3rdparty/pythoncapi_compat.h"
#ifdef slots_was_defined
#define slots
#endif
#if PYSIDE2_ENABLED
// PySide Qt integration, must be included before Qt headers
// warning C4522: 'Shiboken::AutoDecRef': multiple assignment operators specified
#pragma warning(disable : 4522)
#include <pyside.h>
#include <shiboken.h>
PyTypeObject **SbkPySide2_QtCoreTypes = NULL;
PyTypeObject **SbkPySide2_QtGuiTypes = NULL;
PyTypeObject **SbkPySide2_QtWidgetsTypes = NULL;
#else
// for non-windows, this message is displayed at CMake time.
#ifdef _MSC_VER
#pragma message( \
"Building without PySide2 - Qt will not be accessible in python scripting. See https://github.com/baldurk/renderdoc/wiki/PySide2")
#endif
#endif
#ifdef _MSC_VER
// for the LoadLibrary call on 32-bit windows
#include <windows.h>
#endif
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QSettings>
#include <QStandardPaths>
#include <QThread>
#include <QTimer>
#include "Code/QRDUtils.h"
#include "PythonContext.h"
#include "version.h"
// exported by generated files, used to check interface compliance
bool CheckCoreInterface(rdcstr &log);
bool CheckQtInterface(rdcstr &log);
// defined in SWIG-generated renderdoc_python.cpp
extern "C" PyObject *PyInit_renderdoc(void);
extern "C" PyObject *PassObjectToPython(const char *type, void *obj);
extern "C" PyObject *PassNewObjectToPython(const char *type, void *obj);
// this one is in qrenderdoc_python.cpp
extern "C" PyObject *PyInit_qrenderdoc(void);
extern "C" PyObject *WrapBareQWidget(QWidget *);
extern "C" QWidget *UnwrapBareQWidget(PyObject *);
extern "C" PyObject *GetCurrentGlobalHandle();
QSemaphore debuggerWaitSemaphore;
// little utility function to convert a PyObject * that we know is a string to a QString
static inline QString ToQStr(PyObject *value)
{
if(value)
{
PyObject *repr = PyObject_Str(value);
if(repr == NULL)
return QString();
PyObject *decoded = PyUnicode_AsUTF8String(repr);
if(decoded == NULL)
return QString();
QString ret = QString::fromUtf8(PyBytes_AsString(decoded));
Py_DecRef(decoded);
Py_DecRef(repr);
return ret;
}
return QString();
}
static wchar_t program_name[] = L"qrenderdoc";
static wchar_t python_home[1024] = {0};
struct OutputRedirector
{
PyObject_HEAD;
union
{
// we union with a uint64_t to ensure it's always a ulonglong even on 32-bit
uint64_t dummy;
PythonContext *context;
};
bool selfDeleting;
int isStdError;
bool block;
rdcstr extension;
PyObject *compiled;
PyObject *chain_trace;
};
static PyTypeObject OutputRedirectorType = {PyVarObject_HEAD_INIT(NULL, 0)};
static PyMethodDef OutputRedirector_methods[] = {
{"write", NULL, METH_VARARGS, "Writes to the output window"},
{"flush", NULL, METH_NOARGS, "Does nothing - only provided for compatibility"},
{NULL}};
PyObject *PythonContext::main_dict = NULL;
PyObject *PythonContext::m_DebugPy = NULL;
PyObject *PythonContext::m_CallWrapper = NULL;
PyObject *PythonContext::m_Reflector = NULL;
PyObject *PythonContext::m_StubRD = NULL;
PyObject *PythonContext::m_StubQRD = NULL;
PyObject *PythonContext::m_pyrenderdoc = NULL;
ICaptureContext *PythonContext::m_CtxWrapper = NULL;
QAtomicInt PythonContext::m_DeferredInit = 0;
PyObject *PythonContext::m_CallWrapperGlobals = NULL;
PythonContext *PythonContext::m_ExtensionContext = NULL;
QMap<rdcstr, PyObject *> PythonContext::extensions;
// defined in PythonInvokers.cpp
ICaptureContext *MakeCaptureContextInvoker(ICaptureContext &ctx);
void FreeCaptureContextInvoker(ICaptureContext *ctx);
static PyObject *current_global_handle = NULL;
static QMutex decrefQueueMutex;
static QList<PyObject *> decrefQueue;
extern "C" void ProcessDecRefQueue();
extern "C" void HandleException(PyObject *global_handle);
// helper overload to give us the semantics we want - return NULL without an exception if the attr doesn't exist
PyObject *PyObject_SafeGetAttrString(PyObject *obj, const char *string)
{
if(PyObject_HasAttrString(obj, string) == 0)
return NULL;
return PyObject_GetAttrString(obj, string);
}
void FetchException(QString &typeStr, QString &valueStr, int &finalLine, QList<QString> &frames)
{
PyObject *exObj = NULL, *valueObj = NULL, *tracebackObj = NULL;
PyErr_Fetch(&exObj, &valueObj, &tracebackObj);
PyErr_NormalizeException(&exObj, &valueObj, &tracebackObj);
if(exObj && PyType_Check(exObj))
{
PyTypeObject *type = (PyTypeObject *)exObj;
typeStr = QString::fromUtf8(type->tp_name);
}
else
{
typeStr = QString();
}
if(valueObj)
valueStr = ToQStr(valueObj);
if(tracebackObj)
{
PyObject *tracebackModule = PyImport_ImportModule("traceback");
if(tracebackModule)
{
PyObject *func = PyObject_SafeGetAttrString(tracebackModule, "format_tb");
if(func && PyCallable_Check(func))
{
PyObject *args = Py_BuildValue("(O)", tracebackObj);
PyObject *formattedTB = PyObject_CallObject(func, args);
PyTracebackObject *tb = (PyTracebackObject *)tracebackObj;
while(tb->tb_next)
tb = tb->tb_next;
finalLine = tb->tb_lineno;
if(formattedTB)
{
Py_ssize_t size = PyList_Size(formattedTB);
for(Py_ssize_t i = 0; i < size; i++)
{
PyObject *el = PyList_GetItem(formattedTB, i);
frames << ToQStr(el).trimmed();
}
Py_DecRef(formattedTB);
}
Py_DecRef(args);
Py_DecRef(func);
}
else
{
qCritical() << "Couldn't get format_tb from traceback module";
}
Py_DecRef(tracebackModule);
}
}
Py_DecRef(exObj);
Py_DecRef(valueObj);
Py_DecRef(tracebackObj);
}
QStringList GetPystubsLocations(QString basePath)
{
QString versionSubdir =
QFormatStr("v%1_%2").arg(RENDERDOC_VERSION_MAJOR).arg(RENDERDOC_VERSION_MINOR);
QString latestSubdir = lit("latest");
QDir dir(basePath);
if(dir.mkpath(versionSubdir))
{
QDir latestDir = dir;
latestDir.mkpath(latestSubdir);
latestDir.cd(latestSubdir);
QString latestPath = latestDir.absolutePath();
dir.cd(versionSubdir);
QString versionedPath = dir.absolutePath();
return {versionedPath, latestPath};
}
return {};
}
struct StubsVersion
{
int major = 0, minor = 0;
QString commit;
static StubsVersion Current()
{
StubsVersion ret;
ret.major = RENDERDOC_VERSION_MAJOR;
ret.minor = RENDERDOC_VERSION_MINOR;
if(RENDERDOC_STABLE_BUILD)
ret.commit = lit("stable");
else
ret.commit = QString::fromUtf8(RENDERDOC_GetCommitHash());
return ret;
}
bool ShouldReplace(const StubsVersion &o)
{
if(major != o.major)
return major > o.major;
if(minor != o.minor)
return minor > o.minor;
// if we're identical major/minor, don't regenerate if it's the same commit
if(commit == o.commit)
return false;
// never replace a stable version with non-stable
if(o.commit == lit("stable"))
return false;
// otherwise we don't know when this was m ade, regenerate
return true;
}
};
QDebug operator<<(QDebug debug, const StubsVersion &ver)
{
debug << QFormatStr("%1.%2 (%3)").arg(ver.major).arg(ver.minor).arg(ver.commit.mid(0, 6));
return debug;
}
StubsVersion GetStubsVersion(QString basePath)
{
StubsVersion ret;
QFile file(QDir(basePath).absoluteFilePath(lit("version.txt")));
if(file.open(QFile::ReadOnly))
{
QByteArray version = file.readAll();
QTextStream ts(version);
ts >> ret.major >> ret.minor >> ret.commit;
file.close();
}
return ret;
}
void SetStubsVersion(QString basePath, const StubsVersion &ver)
{
QFile file(QDir(basePath).absoluteFilePath(lit("version.txt")));
if(file.open(QFile::WriteOnly | QFile::Truncate))
{
QString version;
QTextStream ts(&version);
ts << ver.major << "\n" << ver.minor << "\n" << ver.commit;
file.write(version.toUtf8());
file.close();
}
}
void PythonContext::GenerateStubs(const rdcarray<rdcstr> &extraPaths)
{
QElapsedTimer timer;
timer.start();
StubsVersion cur = StubsVersion::Current();
QStringList paths;
// take the first standard location that works, these should be in priority order and typically
// the first one is writeable as normal.
for(QString path : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
{
paths << GetPystubsLocations(path + lit("/pystubs"));
if(!paths.empty())
break;
}
for(rdcstr path : extraPaths)
paths << GetPystubsLocations(path);
PyObject *stubgen_module = NULL;
PyObject *gen = NULL;
for(QString target : paths)
{
StubsVersion ver = GetStubsVersion(target);
if(!cur.ShouldReplace(ver))
{
qInfo() << "Not modifying stubs of version " << ver << "in" << target;
continue;
}
SetStubsVersion(target, cur);
if(gen == NULL)
{
QByteArray stubgen;
{
QFile file(lit(":/py/stubgen.py"));
file.open(QFile::ReadOnly);
stubgen = file.readAll();
file.close();
}
PyObject *stubgen_compiled = Py_CompileString(stubgen.data(), "stubgen.py", Py_file_input);
stubgen_module = PyImport_ExecCodeModule("__rd_stubgen", stubgen_compiled);
Py_XDECREF(stubgen_compiled);
gen = PyObject_GetAttrString(stubgen_module, "gen");
}
PyObject *args =
Py_BuildValue("(Os)", PyDict_GetItemString(main_dict, "renderdoc"), target.toUtf8().data());
PyObject *retval = PyObject_CallObject(gen, args);
if(!retval)
qCritical() << "Didn't generate renderdoc stubs";
Py_XDECREF(retval);
Py_XDECREF(args);
args =
Py_BuildValue("(Os)", PyDict_GetItemString(main_dict, "qrenderdoc"), target.toUtf8().data());
retval = PyObject_CallObject(gen, args);
if(!retval)
qCritical() << "Didn't generate qrenderdoc stubs";
Py_XDECREF(retval);
Py_XDECREF(args);
qInfo() << "Generated stubs for " << cur << "into" << target;
}
Py_XDECREF(gen);
Py_XDECREF(stubgen_module);
qInfo() << "Stubs generation processed in" << timer.elapsed() << "ms";
}
void PythonContext::PrepareDebugTracing()
{
// prep the frame for debugpy if we have it enabled, as if we're calling straight from C++ debugpy
// won't be enabled and breakpoints won't get hit
if(m_DebugPy && m_CallWrapper && m_CallWrapperGlobals)
{
Py_XDECREF(PyEval_EvalCode(m_CallWrapper, m_CallWrapperGlobals, NULL));
}
}
void PythonContext::GlobalInit(PersistentConfig &config)
{
// must happen on the UI thread
if(qApp->thread() != QThread::currentThread())
{
qFatal("PythonContext::GlobalInit MUST be called from the UI thread");
return;
}
// for the exception signal
qRegisterMetaType<QList<QString>>("QList<QString>");
PyImport_AppendInittab("renderdoc", &PyInit_renderdoc);
PyImport_AppendInittab("qrenderdoc", &PyInit_qrenderdoc);
#if PY_VERSION_HEX > 0x030B0000
PyConfig pyconfig;
PyConfig_InitPythonConfig(&pyconfig);
pyconfig.configure_c_stdio = 0;
pyconfig.parse_argv = 0;
#endif
#if defined(STATIC_QRENDERDOC)
// add the location where our libs will be for statically-linked python installs
{
QDir bin = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir();
QString pylibs = QDir::cleanPath(bin.absoluteFilePath(lit("../share/renderdoc/pylibs")));
pylibs.toWCharArray(python_home);
#if PY_VERSION_HEX > 0x030B0000
pyconfig.home = python_home;
#else
Py_SetPythonHome(python_home);
#endif
}
#endif
// pydev complains about the zip of standard library files, but that's fine
// and the recommendation is to ignore this warning: https://github.com/microsoft/debugpy/issues/890
//
// we need to set this early so it's snapshotted by python for os.getenv/os.environ
qputenv("PYDEVD_DISABLE_FILE_VALIDATION", lit("1").toUtf8());
#if PY_VERSION_HEX > 0x030B0000
pyconfig.program_name = program_name;
pyconfig.use_environment = 0;
Py_InitializeFromConfig(&pyconfig);
#else
Py_SetProgramName(program_name);
Py_IgnoreEnvironmentFlag = 1;
Py_Initialize();
#endif
#if PY_VERSION_HEX < 0x03090000
PyEval_InitThreads();
#endif
OutputRedirectorType.tp_name = "renderdoc_output_redirector";
OutputRedirectorType.tp_basicsize = sizeof(OutputRedirector);
OutputRedirectorType.tp_flags = Py_TPFLAGS_DEFAULT;
OutputRedirectorType.tp_doc =
"Output redirector, to be able to catch output to stdout and stderr";
OutputRedirectorType.tp_new = PyType_GenericNew;
OutputRedirectorType.tp_dealloc = &PythonContext::outstream_del;
OutputRedirectorType.tp_methods = OutputRedirector_methods;
OutputRedirectorType.tp_call = &PythonContext::outstream_trace;
OutputRedirector_methods[0].ml_meth = &PythonContext::outstream_write;
OutputRedirector_methods[1].ml_meth = &PythonContext::outstream_flush;
PyObject *main_module = PyImport_AddModule("__main__");
PyModule_AddObject(main_module, "renderdoc", PyImport_ImportModule("renderdoc"));
PyModule_AddObject(main_module, "qrenderdoc", PyImport_ImportModule("qrenderdoc"));
main_dict = PyModule_GetDict(main_module);
GenerateStubs(config.Python_StubDirs);
// replace sys.stdout and sys.stderr with our own objects. These have a 'this' pointer of NULL,
// which then indicates they need to forward to a global object
// import sys
PyObject *sysobj = PyImport_ImportModule("sys");
PyDict_SetItemString(main_dict, "sys", sysobj);
// try to import threading library to make debuggers happier, leak it deliberately
if(!PyImport_ImportModule("threading"))
{
// ignore a failed import
PyErr_Clear();
}
// sysobj.stdout = renderdoc_output_redirector()
// sysobj.stderr = renderdoc_output_redirector()
if(PyType_Ready(&OutputRedirectorType) >= 0)
{
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
PyObject *redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
PyObject_SetAttrString(sysobj, "stdout", redirector);
PyObject_SetAttrString(sysobj, "_renderdoc_internal", redirector);
OutputRedirector *output = (OutputRedirector *)redirector;
output->isStdError = 0;
output->selfDeleting = false;
output->context = NULL;
output->block = false;
redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
PyObject_SetAttrString(sysobj, "stderr", redirector);
output = (OutputRedirector *)redirector;
output->isStdError = 1;
output->selfDeleting = false;
output->context = NULL;
output->block = false;
}
// if we need to append to sys.path to locate PySide2, do that now
#if defined(PYSIDE2_SYS_PATH)
{
PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path");
if(!syspath)
qCritical() << "couldn't get sys.path";
#ifndef STRINGIZE
#define STRINGIZE2(a) #a
#define STRINGIZE(a) STRINGIZE2(a)
#endif
PyObject *str = PyUnicode_FromString(STRINGIZE(PYSIDE2_SYS_PATH));
PyList_Append(syspath, str);
Py_DecRef(str);
Py_DecRef(syspath);
}
#endif
#if RENDERDOC_STABLE_BUILD == 0
// if we're running in the git checkout and we can find the test scripts, add that location to the
// path
{
QDir bin = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir();
QString testpath = QDir::cleanPath(bin.absoluteFilePath(lit("../../util/test")));
if(QDir(testpath).exists(lit("run_tests.py")))
{
PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path");
if(!syspath)
qCritical() << "couldn't get sys.path";
PyObject *str = PyUnicode_FromString(testpath.toUtf8().data());
PyList_Append(syspath, str);
Py_DecRef(str);
Py_DecRef(syspath);
}
}
#endif
// set up PySide
#if PYSIDE2_ENABLED
{
// hack for win32 builds, where our pyside2 accidentally depends on Qt5Qml.dll for no good
// reason and we ship a stub to allow the dll to load instead of rebuilding the whole of pyside2
// :S
#if defined(_MSC_VER) && !defined(_M_X64)
QString Qt5QmlStub = QApplication::applicationDirPath() + lit("/PySide2/Qt5Qml.dll");
LoadLibraryA(Qt5QmlStub.toUtf8().data());
#endif
Shiboken::AutoDecRef core(Shiboken::Module::import("PySide2.QtCore"));
if(!core.isNull())
SbkPySide2_QtCoreTypes = Shiboken::Module::getTypes(core);
else
qCritical() << "Failed to load PySide2.QtCore";
Shiboken::AutoDecRef gui(Shiboken::Module::import("PySide2.QtGui"));
if(!gui.isNull())
SbkPySide2_QtGuiTypes = Shiboken::Module::getTypes(gui);
else
qCritical() << "Failed to load PySide2.QtGui";
Shiboken::AutoDecRef widgets(Shiboken::Module::import("PySide2.QtWidgets"));
if(!widgets.isNull())
SbkPySide2_QtWidgetsTypes = Shiboken::Module::getTypes(widgets);
else
qCritical() << "Failed to load PySide2.QtWidgets";
}
#endif
// release GIL so that python work can now happen on any thread
PyEval_SaveThread();
// load debugpy from installed vscode if we find it. This requires a minimum of python 3.8
std::function<void()> initDebugPy;
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 8
if(config.Python_DebugEnabled)
{
QString debugpy_path;
// use the user's specified debugpy path if it exists
if(QDir(config.Python_DebugPyDir).exists() &&
QDir(config.Python_DebugPyDir).exists(lit("__init__.py")))
{
debugpy_path = config.Python_DebugPyDir;
}
// next load debugpy from VS Code, if it exists
if(debugpy_path.isEmpty())
{
QString homePath = QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0];
QDir homeDir(homePath);
homeDir.cd(lit(".vscode/extensions"));
if(homeDir.exists())
{
QStringList debugpy_exts = homeDir.entryList(QStringList() << lit("ms-python.debugpy*"));
if(debugpy_exts.size() >= 1)
{
// assume that sorting works by version, so if there are multiple versions of the
// extension we will pick the latest
debugpy_exts.sort();
QDir debugpy_libs = homeDir;
debugpy_libs.cd(debugpy_exts.last());
debugpy_libs.cd(lit("bundled/libs"));
if(debugpy_libs.exists())
{
debugpy_path = debugpy_libs.absolutePath();
}
}
}
}
// next try pycharm, if we can find it
if(debugpy_path.isEmpty())
{
#ifdef Q_OS_WIN32
QDir programBase(lit("C:/Program Files/JetBrains"));
#else
QDir programBase(lit("/opt"));
#endif
QString pycharm;
QStringList pycharms =
programBase.entryList(QStringList() << lit("PyCharm*") << lit("pycharm-*"));
if(!pycharms.empty())
{
// assume sorting will pick the latest
pycharms.sort();
pycharm = programBase.absoluteFilePath(pycharms.last());
}
#ifdef Q_OS_WIN32
if(pycharm.isEmpty())
{
// if we didn't find it in the default location, check in the registry on windows
QSettings pycharm_reg(lit("HKEY_LOCAL_MACHINE\\SOFTWARE\\JetBrains\\PyCharm"),
QSettings::NativeFormat);
QStringList groups = pycharm_reg.childGroups();
if(!groups.empty())
{
// assume sorting will pick the latest
groups.sort();
pycharm = pycharm_reg.value(groups.last() + lit("/Default")).toString();
}
}
#endif
if(!pycharm.isEmpty())
{
QDir helpers(pycharm);
helpers.cd(lit("plugins/python-ce/helpers"));
if(helpers.exists() && helpers.exists(lit("debugpy")))
{
debugpy_path = helpers.absolutePath();
}
}
}
// if we got a path to debugpy, try to load it
if(!debugpy_path.isEmpty())
{
QObject *invokeObj = new QObject();
initDebugPy = [sysobj, debugpy_path, invokeObj]() {
qInfo() << "Importing debugpy from" << debugpy_path;
PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path");
{
PyObject *str = PyUnicode_FromString(debugpy_path.toUtf8().data());
PyList_Append(syspath, str);
Py_DecRef(str);
}
m_DebugPy = PyImport_ImportModule("debugpy");
if(!m_DebugPy)
{
qCritical() << "Failed to import debugpy";
HandleException(NULL);
}
else
{
PyObject *configure = PyObject_SafeGetAttrString(m_DebugPy, "configure");
// don't let debugpy create a subprocess, for obvious reasons
if(configure)
{
PyObject *props = PyDict_New();
PyDict_SetItemString(props, "subProcess", Py_False);
PyObject *ret = PyObject_CallFunction(configure, "O", props);
if(!ret)
{
qCritical() << "Failed calling debugpy.configure";
HandleException(NULL);
Py_XDECREF(m_DebugPy);
m_DebugPy = NULL;
}
Py_XDECREF(ret);
Py_XDECREF(props);
}
else
{
qCritical() << "Couldn't find debugpy.configure";
}
Py_XDECREF(configure);
if(m_DebugPy)
{
PyObject *listen = PyObject_SafeGetAttrString(m_DebugPy, "listen");
if(listen)
{
// listen on default port
PyObject *args = PyTuple_Pack(1, PyLong_FromLong(5678));
PyObject *kwargs = PyDict_New();
PyDict_SetItemString(kwargs, "in_process_debug_adapter", Py_True);
PyObject *ret = PyObject_Call(listen, args, kwargs);
if(!ret)
{
qCritical() << "Failed calling debugpy.listen";
HandleException(NULL);
Py_XDECREF(m_DebugPy);
m_DebugPy = NULL;
}
Py_XDECREF(ret);
Py_XDECREF(args);
Py_XDECREF(kwargs);
}
else
{
qCritical() << "Couldn't find debugpy.listen";
}
Py_XDECREF(listen);
}
}
// remove the search path from sys.path now
Py_XDECREF(PyObject_CallMethod(syspath, "pop", NULL));
Py_DecRef(syspath);
RemoveDebuggableThread();
if(m_DebugPy)
{
GUIInvoke::defer(invokeObj, [invokeObj]() {
PyGILState_STATE gil = PyGILState_Ensure();
AddDebuggableThread();
m_CallWrapper = Py_CompileString("debugpy.trace_this_thread(True)", "__callwrapper.py",
Py_eval_input);
m_CallWrapperGlobals = PyDict_Copy(main_dict);
PyDict_SetItemString(m_CallWrapperGlobals, "debugpy", m_DebugPy);
// don't pollute the globals
PyObject *tmpGlobals = PyDict_Copy(main_dict);
// monkey patch to get around a bug in debugpy/pydevd: https://github.com/microsoft/debugpy/issues/2011
PyObject *ret = PyRun_String(R"(
try:
monkey_class = sys.modules['_pydevd_bundle'].pydevd_process_net_command_json.PyDevJsonCommandProcessor
orig = monkey_class.on_continue_request
def on_continue_request_hack(self, py_db, request):
request.arguments.threadId = '*'
orig(self, py_db, request)
monkey_class.on_continue_request = on_continue_request_hack
# while we're here, disable termination. No clean way to do this
# otherwise
def _request_terminate_process_hack(self, py_db):
self.api.request_resume_thread('*')
monkey_class._request_terminate_process = _request_terminate_process_hack
print("Monkey-patched debugpy successfully")
except:
print("Failed to monkey-patch debugpy")
pass
)",
Py_file_input, tmpGlobals, NULL);
Py_XDECREF(ret);
Py_XDECREF(tmpGlobals);
PyGILState_Release(gil);
invokeObj->deleteLater();
});
}
};
}
}
#endif
LambdaThread *thread = new LambdaThread([initDebugPy, sysobj]() {
PyGILState_STATE gil = PyGILState_Ensure();
if(initDebugPy)
initDebugPy();
{
QByteArray parse_reflection;
{
QFile file(lit(":/py/parse_reflection.py"));
file.open(QFile::ReadOnly);
parse_reflection = file.readAll();
file.close();
}
QString filename = lit("parse_reflection.py");
// in debug, to allow attaching with local builds, set the expected/typical real path to the py file
#if !defined(RELEASE)
{
QDir dir(QApplication::applicationDirPath());
dir.cdUp();
dir.cdUp();
dir.cd(lit("qrenderdoc"));
dir.cd(lit("Code"));
dir.cd(lit("pyrenderdoc"));
filename = dir.absoluteFilePath(lit("parse_reflection.py"));
}
#endif
PyObject *parse_compiled =
Py_CompileString(parse_reflection.data(), filename.toUtf8().data(), Py_file_input);
PyObject *parse_module = PyImport_ExecCodeModule("__rd_refl", parse_compiled);
Py_XDECREF(parse_compiled);
m_Reflector = PyObject_GetAttrString(parse_module, "PyReflector");
Py_XDECREF(parse_module);
}
if(m_Reflector)
{
bool found = false;
// take the first standard location that works, these should be in priority order
// and typically the first one is writeable as normal.
for(QString path : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
{
QStringList paths = GetPystubsLocations(path + lit("/pystubs"));
if(paths.empty())
continue;
found = true;
QDir versioned_stubpath(paths[0]);
// move up to the parent path to ensure we can import without conflicting with the real module
versioned_stubpath.cdUp();
PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path");
PyObject *str = PyUnicode_FromString(versioned_stubpath.absolutePath().toUtf8().data());
PyList_Append(syspath, str);
Py_DecRef(str);
Py_XDECREF(syspath);
m_StubRD = PyImport_ImportModule(QFormatStr("v%1_%2.renderdoc")
.arg(RENDERDOC_VERSION_MAJOR)
.arg(RENDERDOC_VERSION_MINOR)
.toUtf8()
.data());
if(!m_StubRD)
{
qCritical() << "Failed importing stubs for renderdoc";
HandleException(NULL);
}
m_StubQRD = PyImport_ImportModule(QFormatStr("v%1_%2.qrenderdoc")
.arg(RENDERDOC_VERSION_MAJOR)
.arg(RENDERDOC_VERSION_MINOR)
.toUtf8()
.data());
if(!m_StubQRD)
{
qCritical() << "Failed importing stubs for qrenderdoc";
HandleException(NULL);
}
PyObject *alias_modules = PyObject_SafeGetAttrString(m_Reflector, "alias_modules");
if(m_StubRD)
PyDict_SetItemString(alias_modules, "renderdoc", m_StubRD);
if(m_StubQRD)
PyDict_SetItemString(alias_modules, "qrenderdoc", m_StubQRD);
Py_XDECREF(alias_modules);
break;
}
if(!found)
qCritical() << "Couldn't find valid stubs path";
}
Py_XDECREF(sysobj);
m_DeferredInit = 1;
PyGILState_Release(gil);
});
thread->setName(lit("Python deferred initialisation"));
thread->selfDelete(true);
thread->start();
// this will leak effectively
m_ExtensionContext = new PythonContext(true, NULL);
}
void PythonContext::setCtxGlobal(ICaptureContext &ctx)
{
m_CtxWrapper = MakeCaptureContextInvoker(ctx);
PyGILState_STATE gil = PyGILState_Ensure();
m_pyrenderdoc =
PassObjectToPython((rdcstr(TypeName<ICaptureContext>()) + " *").c_str(), m_CtxWrapper);
PyGILState_Release(gil);
}
bool PythonContext::initialised()
{
return main_dict != NULL;
}
PythonContext::PythonContext(bool extensionContext, QObject *parent) : QObject(parent)
{
if(!initialised())
return;
// acquire the GIL and make sure this thread is init'd
PyGILState_STATE gil = PyGILState_Ensure();
// clone our own local context
context_namespace = PyDict_Copy(main_dict);
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
// set global output that point to this context. It is responsible for deleting the context when
// it goes out of scope
PyObject *redirector = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
if(redirector)
{
PyDict_SetItemString(context_namespace, "_renderdoc_internal", redirector);
OutputRedirector *output = (OutputRedirector *)redirector;
output->context = this;
output->selfDeleting = !extensionContext;
output->block = false;
Py_XDECREF(redirector);
}
if(m_pyrenderdoc)
{
PyDict_SetItemString(context_namespace, "pyrenderdoc", m_pyrenderdoc);
}
// release the GIL again
PyGILState_Release(gil);
// every 100ms while running, check for new output
outputTicker = new QTimer(this);
outputTicker->setInterval(100);
QObject::connect(outputTicker, &QTimer::timeout, this, &PythonContext::outputTick);
// we have to start it here, because we can't start on another thread.
outputTicker->start();
}
PythonContext::~PythonContext()
{
PyGILState_STATE gil = PyGILState_Ensure();
PyGILState_Release(gil);
// do a final tick to gather any remaining output
outputTick();
}
bool PythonContext::CheckInterfaces(rdcstr &log)
{
bool errors = false;
PyGILState_STATE gil = PyGILState_Ensure();
errors |= CheckCoreInterface(log);
errors |= CheckQtInterface(log);
for(rdcstr module_name : {"renderdoc", "qrenderdoc"})
{
PyObject *mod = PyImport_ImportModule(module_name.c_str());
PyObject *dict = PyModule_GetDict(mod);
PyObject *key, *value;
Py_ssize_t pos = 0;
while(PyDict_Next(dict, &pos, &key, &value))
{
rdcstr name = ToQStr(key);
if(name.beginsWith("__"))
continue;
if(!PyCallable_Check(value))
{
log += "Non-callable object found: " + module_name + "." + name +
". Expected only classes and functions.\n";
errors = true;
}
}
Py_XDECREF(mod);
}
PyGILState_Release(gil);
log.trim();
return errors;
}
void PythonContext::Finish()
{
PyGILState_STATE gil = PyGILState_Ensure();
if(m_Completer)
{
Py_DecRef(m_Completer);
m_Completer = NULL;
}
// release our external handle to globals. It'll now only be ref'd from inside
Py_XDECREF(context_namespace);
// force a GC to detect the cycle left
PyObject *gc = PyImport_ImportModule("gc");
if(gc)
{
Py_XDECREF(PyObject_CallMethod(gc, "collect", NULL));
Py_XDECREF(gc);
}
PyGILState_Release(gil);
}
void *PythonContext::PausePythonThreading()
{
return PyGILState_Check() == 0 ? NULL : PyEval_SaveThread();
}
void PythonContext::ResumePythonThreading(void *ctx)
{
if(ctx)
PyEval_RestoreThread((PyThreadState *)ctx);
}
void PythonContext::GlobalShutdown()
{
if(!initialised())
return;
// must happen on the UI thread
if(qApp->thread() != QThread::currentThread())
{
qFatal("PythonContext::GlobalShutdown MUST be called from the UI thread");
return;
}
// acquire the GIL, so we can shut down
PyGILState_Ensure();
Py_Finalize();
FreeCaptureContextInvoker(m_CtxWrapper);
}
QStringList PythonContext::GetApplicationExtensionsPaths()
{
QStringList ret;
for(QString d : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
{
QDir dir(d);
dir.cd(lit("extensions"));
if(dir.exists())
ret.append(dir.absolutePath());
}
return ret;
}
void PythonContext::ProcessExtensionWork(std::function<void()> callback)
{
PyGILState_STATE gil = PyGILState_Ensure();
PrepareDebugTracing();
callback();
PyGILState_Release(gil);
}
QString PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extension)
{
QString ret;
PyObject *sysobj = PyDict_GetItemString(main_dict, "sys");
PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path");
if(!syspath)
qCritical() << "couldn't get sys.path";
// add extensions directories in
for(QString p : PythonContext::GetApplicationExtensionsPaths())
{
QDir dir(p);
rdcstr path = dir.absolutePath();
if(dir.exists())
{
PyObject *str = PyUnicode_FromString(path.c_str());
PyList_Append(syspath, str);
Py_DecRef(str);
}
}
PyObject *ext = NULL;
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
bool reload = false;
if(extensions[extension] == NULL)
{
qInfo() << "First load of " << QString(extension);
ext = PyImport_ImportModule(extension.c_str());
}
else
{
qInfo() << "Reloading " << QString(extension);
reload = true;
// call unregister() if it exists
PyObject *unregister_func = PyObject_SafeGetAttrString(extensions[extension], "unregister");
bool reloadSuccess = true;
if(unregister_func)
{
PyObject *retval = PyObject_CallFunction(unregister_func, "");
if(retval == NULL)
{
FetchException(typeStr, valueStr, finalLine, frames);
reloadSuccess = false;
}
// discard the return value, regardless of error we don't abort the reload
Py_XDECREF(retval);
Py_XDECREF(unregister_func);
}
if(reloadSuccess)
{
// if the extension is a package, we need to manually reload any loaded submodules
PyObject *sysmodules = PyObject_SafeGetAttrString(sysobj, "modules");
if(!syspath)
qCritical() << "couldn't get sys.modules";
PyObject *keys = PyDict_Keys(sysmodules);
QString search = extension + lit(".");
if(keys)
{
Py_ssize_t len = PyList_Size(keys);
for(Py_ssize_t i = 0; i < len; i++)
{
PyObject *key = PyList_GetItem(keys, i);
PyObject *value = PyDict_GetItem(sysmodules, key);
QString keystr = ToQStr(key);
if(keystr.contains(search))
{
qInfo() << "Reloading submodule " << keystr;
PyObject *mod = PyImport_ReloadModule(value);
if(mod == NULL)
{
qCritical() << "Failed to reload " << keystr;
ret += tr("Failed to reload submodule '%1'\n").arg(keystr);
reloadSuccess = false;
break;
}
// we don't need the reference, we just wanted to reload it
Py_XDECREF(mod);
value = PyDict_GetItem(sysmodules, key);
if(value != mod)
qCritical() << "sys.modules[" << keystr << "]"
<< " after reload doesn't match reloaded object";
}
}
Py_XDECREF(keys);
}
Py_XDECREF(sysmodules);
}
if(reloadSuccess)
ext = PyImport_ReloadModule(extensions[extension]);
}
if(!m_pyrenderdoc)
qCritical() << "pyrenderdoc variable is NULL";
// if import succeeded, store this extension module in our map. If import failed, we might have
// failed a reimport in which case the original module is still there and valid, so don't
// overwrite the value.
if(ext)
{
extensions[extension] = ext;
// for compatibility with earlier versions of python that took a char * instead of const char *
char noparams[1] = "";
PyObject *ext_context = PyObject_CallFunction((PyObject *)&OutputRedirectorType, noparams);
OutputRedirector *redir = (OutputRedirector *)ext_context;
redir->isStdError = 0;
redir->selfDeleting = false;
redir->context = NULL;
redir->block = false;
redir->extension = extension;
PyModule_AddObject(ext, "_renderdoc_internal", ext_context);
Py_XINCREF(m_pyrenderdoc);
int pyret = PyModule_AddObject(ext, "pyrenderdoc", m_pyrenderdoc);
if(pyret != 0)
{
Py_XDECREF(m_pyrenderdoc);
qCritical() << "Couldn't set pyrenderdoc global in loaded module";
ret += tr("Couldn't set pyrenderdoc global in loaded module\n");
ext = NULL;
}
}
if(ext)
{
PrepareDebugTracing();
// if import succeeded, call register()
PyObject *register_func = PyObject_SafeGetAttrString(ext, "register");
if(register_func)
{
PyObject *retval = NULL;
if(m_pyrenderdoc)
{
retval =
PyObject_CallFunction(register_func, "sO", MAJOR_MINOR_VERSION_STRING, m_pyrenderdoc);
}
else
{
qCritical() << "Internal error passing pyrenderdoc to extension register()";
ret += tr("Internal error passing pyrenderdoc to extension register()\n");
}
Py_XDECREF(register_func);
if(retval == NULL)
{
qCritical() << "register() function failed";
ret += tr("register() function failed\n");
ext = NULL;
}
Py_XDECREF(retval);
}
else
{
qCritical() << "register() function not found in extension";
ret += tr("register() function not found in extension\n");
ext = NULL;
}
}
else
{
ext = NULL;
}
Py_XDECREF(m_pyrenderdoc);
if(ext)
{
emit m_ExtensionContext->extensionLoaded(extension);
}
else
{
if(typeStr.isEmpty())
FetchException(typeStr, valueStr, finalLine, frames);
ret = ret.trimmed();
ret += lit("\n");
if(!valueStr.isEmpty())
{
qCritical("Error importing extension module '%s'. %s: %s", extension.c_str(),
typeStr.toUtf8().data(), valueStr.toUtf8().data());
ret +=
tr("Error importing extension module '%1'. %2: %3\n\n").arg(extension).arg(typeStr).arg(valueStr);
if(!frames.isEmpty())
{
qCritical() << "Traceback (most recent call last):";
ret += tr("Traceback (most recent call last):\n");
for(const QString &f : frames)
{
QStringList lines = f.split(QLatin1Char('\n'));
for(const QString &line : lines)
{
qCritical(" %s", line.toUtf8().data());
ret += line + lit("\n");
}
}
}
}
if(!ret.isEmpty())
m_ExtensionContext->addText(extension, true, ret);
}
Py_ssize_t len = PyList_Size(syspath);
PyList_SetSlice(syspath, len - 1, len, NULL);
Py_DecRef(syspath);
return ret;
}
void PythonContext::ConvertPyArgs(const ExtensionCallbackData &data,
rdcarray<rdcpair<rdcstr, PyObject *>> &args)
{
PyGILState_STATE gil = PyGILState_Ensure();
args.resize(data.size());
for(size_t i = 0; i < data.size(); i++)
{
rdcpair<rdcstr, PyObject *> &a = args[i];
a.first = data[i].first;
// convert QVariant to python object
const QVariant &in = data[i].second;
PyObject *&out = a.second;
// coverity[mixed_enums]
QMetaType::Type type = (QMetaType::Type)in.type();
switch(type)
{
case QMetaType::Bool: out = PyBool_FromLong(in.toBool()); break;
case QMetaType::Short:
case QMetaType::Long:
case QMetaType::Int: out = PyLong_FromLong(in.toInt()); break;
case QMetaType::UShort:
case QMetaType::ULong:
case QMetaType::UInt: out = PyLong_FromUnsignedLong(in.toUInt()); break;
case QMetaType::LongLong: out = PyLong_FromLongLong(in.toLongLong()); break;
case QMetaType::ULongLong: out = PyLong_FromUnsignedLongLong(in.toULongLong()); break;
case QMetaType::Float: out = PyFloat_FromDouble(in.toFloat()); break;
case QMetaType::Double: out = PyFloat_FromDouble(in.toDouble()); break;
case QMetaType::QString: out = PyUnicode_FromString(in.toString().toUtf8().data()); break;
default: break;
}
if(!out)
{
// try various other types
if(in.userType() == qMetaTypeId<ResourceId>())
out = PassNewObjectToPython("ResourceId *", new ResourceId(in.value<ResourceId>()));
}
if(!out)
{
qCritical() << "Couldn't convert" << in << "to python object";
out = Py_XNewRef(Py_None);
}
}
PyGILState_Release(gil);
}
void PythonContext::FreePyArgs(rdcarray<rdcpair<rdcstr, PyObject *>> &args)
{
PyGILState_STATE gil = PyGILState_Ensure();
for(rdcpair<rdcstr, PyObject *> &a : args)
{
Py_XDECREF(a.second);
}
PyGILState_Release(gil);
}
QString PythonContext::versionString()
{
return QFormatStr("%1.%2.%3").arg(PY_MAJOR_VERSION).arg(PY_MINOR_VERSION).arg(PY_MICRO_VERSION);
}
QString PythonContext::GetTempFilename(QString filename)
{
for(QString path : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation))
{
QDir tmpDir(path);
tmpDir.mkpath(lit("pytmp"));
tmpDir.cd(lit("pytmp"));
if(tmpDir.exists())
return tmpDir.absoluteFilePath(filename);
}
return QString();
}
void PythonContext::executeString(const QString &filename, const QString &source)
{
if(!initialised())
{
FlushOutput();
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
QString tempFilename = filename;
if(filename.isEmpty())
{
tempFilename = lit("<interactive.py>");
}
location.file = tempFilename;
location.line = 1;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *compiled =
Py_CompileString(source.toUtf8().data(), tempFilename.toUtf8().data(),
source.count(QLatin1Char('\n')) == 0 ? Py_single_input : Py_file_input);
bool debugAttached = false;
bool caughtException = false;
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
if(compiled)
{
PrepareDebugTracing();
PyObject *sys = PyImport_ImportModule("sys");
PyObject *settrace = NULL;
PyObject *gettrace = NULL;
PyObject *tracer = NULL;
if(sys)
{
settrace = PyObject_SafeGetAttrString(sys, "settrace");
gettrace = PyObject_SafeGetAttrString(sys, "gettrace");
tracer = PyDict_GetItemString(context_namespace, "_renderdoc_internal");
}
if(settrace && gettrace && tracer)
{
OutputRedirector *redir = (OutputRedirector *)tracer;
redir->compiled = compiled;
redir->chain_trace = PyObject_CallNoArgs(gettrace);
Py_XDECREF(PyObject_CallFunction(settrace, "O", tracer));
}
m_Abort = false;
m_State = PyGILState_GetThisThreadState();
PyObject *ret = PyEval_EvalCode(compiled, context_namespace, context_namespace);
caughtException = (ret == NULL);
if(caughtException)
FetchException(typeStr, valueStr, finalLine, frames);
Py_XDECREF(ret);
m_State = NULL;
// catch any output
outputTick();
if(settrace && gettrace && tracer)
{
OutputRedirector *redir = (OutputRedirector *)tracer;
redir->compiled = NULL;
Py_XDECREF(PyObject_CallFunction(settrace, "O", redir->chain_trace));
redir->chain_trace = NULL;
}
ProcessDecRefQueue();
Py_XDECREF(sys);
Py_XDECREF(settrace);
Py_XDECREF(gettrace);
}
else
{
caughtException = true;
FetchException(typeStr, valueStr, finalLine, frames);
}
Py_XDECREF(compiled);
PyGILState_Release(gil);
if(caughtException)
{
FlushOutput();
emit exception(QString(), typeStr, valueStr, finalLine, frames);
}
}
void PythonContext::executeString(const QString &source)
{
executeString(QString(), source);
}
void PythonContext::executeFile(const QString &filename)
{
QFile f(filename);
if(!f.exists())
{
FlushOutput();
emit exception(QString(), lit("FileNotFoundError"),
tr("No such file or directory: %1").arg(filename), -1, {});
return;
}
if(f.open(QIODevice::ReadOnly | QIODevice::Text))
{
QByteArray py = f.readAll();
executeString(filename, QString::fromUtf8(py));
}
else
{
FlushOutput();
emit exception(QString(), lit("IOError"),
QFormatStr("%1: %2").arg(f.errorString()).arg(filename), -1, {});
}
}
void PythonContext::setGlobal(const char *varName, const char *typeName, void *object)
{
if(!initialised())
{
FlushOutput();
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
PyGILState_STATE gil = PyGILState_Ensure();
// we don't need separate functions for each module, as they share type info
PyObject *obj = PassObjectToPython(typeName, object);
int ret = -1;
if(obj)
ret = PyDict_SetItemString(context_namespace, varName, obj);
PyGILState_Release(gil);
if(ret != 0)
{
FlushOutput();
emit exception(QString(), lit("RuntimeError"),
tr("Failed to set variable '%1' of type '%2'")
.arg(QString::fromUtf8(varName))
.arg(QString::fromUtf8(typeName)),
-1, {});
return;
}
setPyGlobal(varName, obj);
}
template <>
void PythonContext::setGlobal(const char *varName, PyObject *object)
{
setPyGlobal(varName, object);
}
template <>
void PythonContext::setGlobal(const char *varName, QObject *object)
{
setQtGlobal(varName, object);
}
template <>
void PythonContext::setGlobal(const char *varName, QWidget *object)
{
setQtGlobal(varName, object);
}
QWidget *PythonContext::QWidgetFromPy(PyObject *widget)
{
#if PYSIDE2_ENABLED
if(!initialised())
return NULL;
if(Py_IsNone(widget) || widget == NULL)
return NULL;
if(!SbkPySide2_QtCoreTypes || !SbkPySide2_QtGuiTypes || !SbkPySide2_QtWidgetsTypes)
return UnwrapBareQWidget(widget);
if(!Shiboken::Object::checkType(widget))
return UnwrapBareQWidget(widget);
return (QWidget *)Shiboken::Object::cppPointer((SbkObject *)widget, Shiboken::SbkType<QWidget>());
#else
return UnwrapBareQWidget(widget);
#endif
}
void PythonContext::reflectSource(QString src)
{
if(!m_Reflector)
{
for(int i = 0; i < 50 && m_DeferredInit == 0; i++)
QThread::msleep(20);
m_DeferredInit = 1;
if(!m_Reflector)
return;
}
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *refl = PyObject_CallFunction(m_Reflector, "sOO", (const char *)src.toUtf8().data(),
context_namespace, Py_False);
if(refl)
{
PyDict_SetItemString(context_namespace, "_renderdoc_refl", refl);
Py_XDECREF(refl);
}
else
{
HandleException(NULL);
}
PyGILState_Release(gil);
}
void PythonContext::makeHelpContext()
{
// expand out members of renderdoc and qrenderdoc using the stubs
if(!m_StubRD || !m_StubQRD)
{
for(int i = 0; i < 50 && m_DeferredInit == 0; i++)
QThread::msleep(20);
m_DeferredInit = 1;
if(!m_StubRD || !m_StubQRD)
return;
}
PyGILState_STATE gil = PyGILState_Ensure();
PyDict_SetItemString(context_namespace, "stub_rd", m_StubRD);
PyDict_SetItemString(context_namespace, "stub_qrd", m_StubQRD);
PyGILState_Release(gil);
executeString(lit(R"(
def publicise_module(mod):
for name in dir(mod):
if name[0] == '_' or '_circular' in name:
continue
obj = getattr(mod, name)
if hasattr(obj, '__module__') and mod.__name__ not in getattr(obj, '__module__'):
continue
globals()[name] = obj
publicise_module(stub_rd)
publicise_module(stub_qrd)
del publicise_module
)"));
gil = PyGILState_Ensure();
PyDict_DelItemString(context_namespace, "stub_rd");
PyDict_DelItemString(context_namespace, "stub_qrd");
if(PyDict_ContainsString(context_namespace, "pyrenderdoc"))
PyDict_DelItemString(context_namespace, "pyrenderdoc");
if(PyDict_ContainsString(context_namespace, "sys"))
PyDict_DelItemString(context_namespace, "sys");
PyGILState_Release(gil);
reflectSource(QString());
}
QString PythonContext::tooltipForLoc(int line, int col)
{
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl");
if(!refl)
{
PyGILState_Release(gil);
return QString();
}
PyObject *tooltip = PyObject_CallMethod(refl, "get_location_tooltip", "ii", line, col);
QString ret;
if(tooltip)
{
ret = ToQStr(tooltip);
Py_XDECREF(tooltip);
}
else
{
HandleException(NULL);
}
PyGILState_Release(gil);
return ret;
}
QString PythonContext::typenameForLoc(int line, int col)
{
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl");
if(!refl)
{
PyGILState_Release(gil);
return QString();
}
PyObject *typeObj = PyObject_CallMethod(refl, "get_location_type", "ii", line, col);
if(typeObj)
{
PyObject *typing = PyImport_ImportModule("typing");
PyObject *Any = PyObject_SafeGetAttrString(typing, "Any");
if(typeObj == Any)
{
Py_XDECREF(typeObj);
typeObj = NULL;
}
Py_XDECREF(Any);
Py_XDECREF(typing);
}
else
{
HandleException(NULL);
}
QString ret;
if(typeObj)
{
PyObject *name = PyObject_CallMethod(refl, "get_name", "O", typeObj);
if(name)
{
ret = ToQStr(name);
Py_XDECREF(name);
}
else
{
HandleException(NULL);
}
Py_XDECREF(typeObj);
}
PyGILState_Release(gil);
return ret;
}
QList<QPair<QString, QString>> PythonContext::completionOptions(int line, QString expr,
int &prefix_len)
{
QList<QPair<QString, QString>> ret;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl");
if(!refl)
{
PyGILState_Release(gil);
return ret;
}
PyObject *completions = PyObject_CallMethod(refl, "get_autocompletion", "is", line,
(const char *)expr.toUtf8().data());
prefix_len = 0;
if(completions)
{
PyObject *comp_list = PyTuple_GetItem(completions, 0);
prefix_len = PyLong_AsLong(PyTuple_GetItem(completions, 1));
PyObject *tip_list = PyTuple_GetItem(completions, 2);
if(comp_list)
{
for(Py_ssize_t i = 0, len = PyList_Size(comp_list); i < len; i++)
{
QPair<QString, QString> item;
item.first = ToQStr(PyList_GetItem(comp_list, i));
if(tip_list)
item.second = ToQStr(PyList_GetItem(tip_list, i));
ret << item;
}
}
}
else
{
HandleException(NULL);
}
Py_XDECREF(completions);
PyGILState_Release(gil);
return ret;
}
QString PythonContext::tryFunctionCompletion(int line, QString expr)
{
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl");
if(!refl)
{
PyGILState_Release(gil);
return QString();
}
PyObject *funcComp = PyObject_CallMethod(refl, "get_funccompletion", "is", line,
(const char *)expr.toUtf8().data());
QString ret;
if(funcComp)
{
ret = ToQStr(PyTuple_GetItem(funcComp, 2));
Py_XDECREF(funcComp);
}
else
{
HandleException(NULL);
}
PyGILState_Release(gil);
return ret;
}
void PythonContext::AddDebuggableThread()
{
if(!m_DebugPy)
return;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *debug_this_thread = PyObject_SafeGetAttrString(m_DebugPy, "debug_this_thread");
if(debug_this_thread)
{
PyObject *ret = PyObject_CallNoArgs(debug_this_thread);
if(!ret)
qCritical() << "Failed calling debugpy.debug_this_thread";
Py_XDECREF(ret);
}
else
{
qCritical() << "Couldn't find debugpy.debug_this_thread";
}
Py_XDECREF(debug_this_thread);
QString threadName = QThread::currentThread()->objectName();
if(!threadName.isEmpty())
{
PyObject *threading = PyImport_ImportModule("threading");
// expect to load threading
if(threading)
{
PyObject *current_thread = PyObject_SafeGetAttrString(threading, "current_thread");
Q_ASSERT(current_thread);
PyObject *cur = PyObject_CallNoArgs(current_thread);
if(cur)
{
if(PyObject_HasAttrString(cur, "name"))
PyObject_SetAttrString(cur, "name", PyUnicode_FromString(threadName.toUtf8().data()));
Py_XDECREF(cur);
}
Py_XDECREF(current_thread);
}
else
{
qCritical() << "Couldn't get threading module";
}
Py_XDECREF(threading);
}
PyGILState_Release(gil);
}
void PythonContext::RemoveDebuggableThread()
{
if(!m_DebugPy)
return;
// 3.13 fixes this to track the lifetime of dummy/external threads, before that we do it manually
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 13
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *threading = PyImport_ImportModule("threading");
// expect to load threading
if(threading)
{
PyObject *_active = PyObject_SafeGetAttrString(threading, "_active");
if(_active)
{
PyObject *key = PyLong_FromInt32(PyThread_get_thread_ident());
if(PyDict_Contains(_active, key) == 1)
PyDict_DelItem(_active, key);
Py_XDECREF(key);
}
Py_XDECREF(_active);
}
else
{
qCritical() << "Couldn't get threading module";
}
Py_XDECREF(threading);
PyGILState_Release(gil);
#endif
}
PyObject *PythonContext::QtObjectToPython(const char *typeName, QObject *object)
{
#if PYSIDE2_ENABLED
if(!initialised())
Py_RETURN_NONE;
if(!SbkPySide2_QtCoreTypes || !SbkPySide2_QtGuiTypes || !SbkPySide2_QtWidgetsTypes)
{
QWidget *w = qobject_cast<QWidget *>(object);
if(w)
return WrapBareQWidget(w);
Py_RETURN_NONE;
}
if(object == NULL)
{
Py_RETURN_NONE;
}
PyObject *obj =
Shiboken::Object::newObject(reinterpret_cast<SbkObjectType *>(Shiboken::SbkType<QObject>()),
object, false, false, typeName);
return obj;
#else
QWidget *w = qobject_cast<QWidget *>(object);
if(w)
return WrapBareQWidget(w);
Py_RETURN_NONE;
#endif
}
// callback to flush output every so often (not constantly, to avoid spamming signals)
void PythonContext::outputTick()
{
QMutexLocker lock(&outputMutex);
for(QString &extension : outputCaches.keys())
{
OutputPair &o = outputCaches[extension];
if(!o.outstr.isEmpty())
{
emit textOutput(extension, false, o.outstr);
}
if(!o.errstr.isEmpty())
{
emit textOutput(extension, true, o.errstr);
}
o.outstr.clear();
o.errstr.clear();
}
}
void PythonContext::addText(QString context, bool isStdError, const QString &output)
{
QMutexLocker lock(&outputMutex);
if(isStdError)
outputCaches[context].errstr += output;
else
outputCaches[context].outstr += output;
}
void PythonContext::setPyGlobal(const char *varName, PyObject *obj)
{
if(!initialised())
{
FlushOutput();
emit exception(QString(), lit("SystemError"), tr("Python integration failed to initialise."),
-1, {});
return;
}
int ret = -1;
PyGILState_STATE gil = PyGILState_Ensure();
if(obj)
ret = PyDict_SetItemString(context_namespace, varName, obj);
PyGILState_Release(gil);
if(ret == 0)
return;
FlushOutput();
emit exception(QString(), lit("RuntimeError"),
tr("Failed to set variable '%1'").arg(QString::fromUtf8(varName)), -1, {});
}
void PythonContext::outstream_del(PyObject *self)
{
OutputRedirector *redirector = (OutputRedirector *)self;
if(redirector && redirector->selfDeleting)
{
PythonContext *context = redirector->context;
// delete the context on the UI thread.
if(context)
GUIInvoke::call(context, [context]() { delete context; });
}
}
PyObject *PythonContext::outstream_write(PyObject *self, PyObject *args)
{
const char *text = NULL;
if(!PyArg_ParseTuple(args, "z:write", &text))
return NULL;
if(PyErr_Occurred())
return NULL;
OutputRedirector *redirector = (OutputRedirector *)self;
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)
{
PyFrameObject *frame = PyEval_GetFrame();
// inc reference count here so all frames can be decref'd whether they come from here or
// PyFrame_GetBack
Py_XINCREF(frame);
while(frame)
{
PyObject *globals = PyFrame_GetGlobals(frame);
if(globals)
{
OutputRedirector *global =
(OutputRedirector *)PyDict_GetItemString(globals, "_renderdoc_internal");
if(global)
{
context = global->context;
extension = global->extension;
}
}
Py_XDECREF(globals);
// first get the next frame without decrefing the current
PyFrameObject *back = PyFrame_GetBack(frame);
// now decref the old frame
Py_XDECREF(frame);
// and iterate on the next one, if we got one
frame = back;
if(context)
{
// release the last trailing ref, which we know is either NULL or a strong reference from
// PyFrame_GetBack
Py_XDECREF(frame);
break;
}
}
}
if(context)
{
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;
PyFrameObject *frame = PyEval_GetFrame();
while(!message.empty() && (message.back() == '\n' || message.back() == '\r'))
message.pop_back();
QString filename = lit("unknown");
int line = 0;
if(frame)
{
PyCodeObject *code = PyFrame_GetCode(frame);
filename = ToQStr(code->co_filename);
Py_XDECREF(code);
line = PyFrame_GetLineNumber(frame);
}
if(!message.empty())
RENDERDOC_LogMessage(redirector->isStdError ? LogType::Warning : LogType::Comment, "EXTN",
filename, line, message);
}
}
Py_RETURN_NONE;
}
PyObject *PythonContext::outstream_flush(PyObject *self, PyObject *args)
{
if(PyErr_Occurred())
return NULL;
Py_RETURN_NONE;
}
PyObject *PythonContext::outstream_trace(PyObject *self, PyObject *args, PyObject *kwargs)
{
if(PyErr_Occurred())
return NULL;
const char *what = NULL;
PyObject *frameObj = NULL;
PyObject *arg = NULL;
if(!PyArg_ParseTuple(args, "OzO:trace", &frameObj, &what, &arg))
return NULL;
OutputRedirector *redirector = (OutputRedirector *)self;
if(PyFrame_Check(frameObj) && what && strcmp(what, "line") == 0)
{
PyFrameObject *frame = (PyFrameObject *)frameObj;
PythonContext *context = redirector->context;
// increment ref here so we can loop with a held ref either from the base frame or from PyFrame_GetBack()
Py_XINCREF(frame);
// step up the frame looking for one in our code we're watching
while(frame)
{
PyCodeObject *code = PyFrame_GetCode(frame);
if(redirector->compiled == (PyObject *)code && context)
{
context->location.line = PyFrame_GetLineNumber(frame);
emit context->traceLine(context->location.file, context->location.line);
Py_XDECREF(frame);
Py_XDECREF(code);
break;
}
Py_XDECREF(code);
// first get the next frame without decrefing the current
PyFrameObject *back = PyFrame_GetBack(frame);
// now decref the old frame
Py_XDECREF(frame);
// and iterate on the next one, if we got one
frame = back;
}
if(context && context->shouldAbort())
{
PyErr_SetString(PyExc_SystemExit, "Execution aborted.");
return NULL;
}
}
if(redirector->chain_trace && !Py_IsNone(redirector->chain_trace))
{
PyObject *prev_trace = redirector->chain_trace;
PyObject *new_trace = PyObject_Call(redirector->chain_trace, args, kwargs);
Py_XDECREF(prev_trace);
redirector->chain_trace = new_trace;
}
Py_XINCREF(self);
return self;
}
bool PythonContext::IsDebuggerConnected()
{
if(!m_DebugPy)
return false;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *is_connected = PyObject_CallMethod(m_DebugPy, "is_client_connected", NULL);
bool ret = (PyBool_Check(is_connected) && is_connected == Py_True);
Py_XDECREF(is_connected);
PyGILState_Release(gil);
return ret;
}
void PythonContext::PrepareDebuggerWait()
{
if(!m_DebugPy)
return;
// reset the semaphore
while(debuggerWaitSemaphore.available())
debuggerWaitSemaphore.tryAcquire();
// set up one count in the semaphore
debuggerWaitSemaphore.release();
}
bool PythonContext::WaitForDebugger()
{
if(!m_DebugPy)
return false;
PyGILState_STATE gil = PyGILState_Ensure();
// don't care about the return value
Py_XDECREF(PyObject_CallMethod(m_DebugPy, "wait_for_client", NULL));
// return whether a client connected
PyObject *is_connected = PyObject_CallMethod(m_DebugPy, "is_client_connected", NULL);
bool ret = (PyBool_Check(is_connected) && is_connected == Py_True);
Py_XDECREF(is_connected);
// indicate that the work has finished
debuggerWaitSemaphore.tryAcquire(1);
PyGILState_Release(gil);
return ret;
}
void PythonContext::LaunchDebugger(QWidget *window, PersistentConfig &config, QString context_location)
{
if(!m_DebugPy)
return;
if(context_location.isEmpty())
return;
// don't overwrite an existing file, to allow user customisation
QDir context_dir(context_location);
if(!context_dir.exists(lit(".vscode/launch.json")))
{
context_dir.mkpath(lit(".vscode"));
QFile launch(context_dir.absoluteFilePath(lit(".vscode/launch.json")));
launch.open(QFile::Truncate | QFile::WriteOnly);
launch.write(R"(
{
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Remote Attach",
"type": "debugpy",
"request": "attach",
"connect": { "host": "localhost", "port": 5678 }
}
]
}
)");
launch.close();
// write tasks to auto-attach. This will require user approval
QFile tasks(context_dir.absoluteFilePath(lit(".vscode/tasks.json")));
tasks.open(QFile::Truncate | QFile::WriteOnly);
tasks.write(R"(
{
"version": "2.0.0",
"tasks": [
{
"label": "attach on startup",
"command": "${command:workbench.action.debug.start}",
"runOptions": {
"runOn": "folderOpen"
}
}
]
}
)");
tasks.close();
}
GUIInvoke::defer(window, [window, &config, context_location]() {
// wait a short while before displaying the progress dialog in case a debugger is already connected
for(int i = 0; debuggerWaitSemaphore.available() == 1 && i < 40; i++)
QThread::msleep(5);
// if we should launch vs code and there's nothing connected, do that now
if(config.Python_LaunchVSCode)
{
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *is_connected_ret = PyObject_CallMethod(m_DebugPy, "is_client_connected", NULL);
bool debugger_connected = (PyBool_Check(is_connected_ret) && is_connected_ret == Py_True);
Py_XDECREF(is_connected_ret);
PyGILState_Release(gil);
QString code_path = config.Python_VSCodePath;
if(!QFileInfo(code_path).isExecutable())
code_path = QStandardPaths::findExecutable(lit("code"));
if(!debugger_connected && !code_path.isEmpty())
{
QStringList args;
args << lit("-n");
if(!context_location.isEmpty())
args << context_location;
QProcess::startDetached(code_path, args);
}
}
ShowProgressDialog(
window, tr("Waiting for debugger to connect.\n\nListening on localhost:5678"),
[]() { return debuggerWaitSemaphore.available() == 0; }, NULL,
[]() {
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *wait_for_client = PyObject_SafeGetAttrString(m_DebugPy, "wait_for_client");
if(wait_for_client)
{
Py_XDECREF(PyObject_CallMethod(wait_for_client, "cancel", NULL));
Py_XDECREF(wait_for_client);
}
PyGILState_Release(gil);
});
});
}
PyParseError PythonContext::CheckPyParse(const QByteArray &script, const rdcstr &scriptNameForErrors)
{
PyParseError parseError;
PyGILState_STATE gil = PyGILState_Ensure();
PyObject *ast = PyImport_ImportModule("ast");
PyObject *scriptText = PyUnicode_FromStringAndSize(script.data(), script.size());
PyObject *scriptName = PyUnicode_FromString(scriptNameForErrors.c_str());
PyObject *parseResult = PyObject_CallMethod(ast, "parse", "OO", scriptText, scriptName);
if(!parseResult)
{
PyObject *exObj = NULL, *valueObj = NULL, *tracebackObj = NULL;
PyErr_Fetch(&exObj, &valueObj, &tracebackObj);
PyErr_NormalizeException(&exObj, &valueObj, &tracebackObj);
{
PyObject *a;
PyObject_GetOptionalAttrString(valueObj, "lineno", &a);
parseError.lineno = PyLong_AsInt(a);
Py_XDECREF(a);
PyObject_GetOptionalAttrString(valueObj, "offset", &a);
parseError.offset = PyLong_AsInt(a);
Py_XDECREF(a);
PyObject *repr = PyObject_Str(valueObj);
PyObject *utf8 = PyUnicode_AsUTF8String(repr);
parseError.errStr = PyBytes_AsString(utf8);
for(int i = 1; i < parseError.offset - 1; i++)
parseError.errStr.insert(0, ' ');
parseError.errStr.insert(parseError.offset - 2, "^\n");
Py_XDECREF(repr);
Py_XDECREF(utf8);
}
Py_XDECREF(exObj);
Py_XDECREF(valueObj);
Py_XDECREF(tracebackObj);
}
Py_XDECREF(scriptText);
Py_XDECREF(scriptName);
Py_XDECREF(parseResult);
Py_XDECREF(ast);
PyGILState_Release(gil);
return parseError;
}
extern "C" PyThreadState *GetExecutingThreadState(PyObject *global_handle)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector->context)
return redirector->context->GetExecutingThreadState();
return NULL;
}
extern "C" PyObject *GetCurrentGlobalHandle()
{
PyObject *frame_global_handle = NULL;
// walk the frames until we find one with _renderdoc_internal. If we call a function in another
// module the globals may not have the entry, but the root level is expected to.
{
PyFrameObject *frame = PyEval_GetFrame();
// inc reference count here so all frames can be decref'd whether they come from here or
// PyFrame_GetBack
Py_XINCREF(frame);
while(frame)
{
PyObject *globals = PyFrame_GetGlobals(frame);
frame_global_handle = PyDict_GetItemString(globals, "_renderdoc_internal");
Py_XDECREF(globals);
// first get the next frame without decrefing the current
PyFrameObject *back = PyFrame_GetBack(frame);
// now decref the old frame
Py_XDECREF(frame);
// and iterate on the next one, if we got one
frame = back;
if(frame_global_handle)
{
// release the last trailing ref, which we know is either NULL or a strong reference from
// PyFrame_GetBack
Py_XDECREF(frame);
break;
}
}
}
if(frame_global_handle)
return frame_global_handle;
if(current_global_handle)
return current_global_handle;
PyObject *sys = PyImport_ImportModule("sys");
if(sys)
{
PyObject *ret = PyObject_SafeGetAttrString(sys, "_renderdoc_internal");
Py_XDECREF(sys);
Py_XDECREF(ret);
return ret;
}
return NULL;
}
extern "C" void HandleException(PyObject *global_handle)
{
QString typeStr;
QString valueStr;
int finalLine = -1;
QList<QString> frames;
FetchException(typeStr, valueStr, finalLine, frames);
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector && redirector->context)
{
redirector->context->FlushOutput();
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;
if(!frames.isEmpty())
{
exString += "Traceback (most recent call last):\n";
for(const QString &f : frames)
{
exString += " ";
exString += f;
exString += "\n";
}
}
exString += typeStr;
exString += ": ";
exString += valueStr;
exString += "\n";
PyFrameObject *frame = PyEval_GetFrame();
QString filename = lit("unknown");
int linenum = 0;
if(frame)
{
PyCodeObject *code = PyFrame_GetCode(frame);
filename = ToQStr(code->co_filename);
Py_XDECREF(code);
linenum = PyFrame_GetLineNumber(frame);
}
else if(redirector)
{
filename = redirector->extension;
}
RENDERDOC_LogMessage(LogType::Error, "EXTN", filename, linenum, exString);
}
}
extern "C" bool IsThreadBlocking(PyObject *global_handle)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector)
return redirector->block;
return false;
}
extern "C" void SetThreadBlocking(PyObject *global_handle, bool block)
{
OutputRedirector *redirector = (OutputRedirector *)global_handle;
if(redirector)
redirector->block = block;
}
extern "C" void QueueDecRef(PyObject *obj)
{
QMutexLocker lock(&decrefQueueMutex);
decrefQueue.push_back(obj);
}
extern "C" PyObject *DoFunctionCall(PyObject *object, PyObject *args)
{
if(!PyCFunction_Check(object))
PythonContext::PrepareDebugTracing();
return PyObject_Call(object, args, NULL);
}
extern "C" void ProcessDecRefQueue()
{
QMutexLocker lock(&decrefQueueMutex);
if(decrefQueue.isEmpty())
return;
for(PyObject *obj : decrefQueue)
Py_XDECREF(obj);
decrefQueue.clear();
}
extern "C" QWidget *QWidgetFromPy(PyObject *widget)
{
return PythonContext::QWidgetFromPy(widget);
}
extern "C" PyObject *QWidgetToPy(QWidget *widget)
{
return PythonContext::QWidgetToPy(widget);
}