diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/plugin_python/src/runner | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/plugin_python/src/runner')
| -rw-r--r-- | libs/plugin_python/src/runner/CMakeLists.txt | 34 | ||||
| -rw-r--r-- | libs/plugin_python/src/runner/error.h | 82 | ||||
| -rw-r--r-- | libs/plugin_python/src/runner/pythonrunner.cpp | 415 | ||||
| -rw-r--r-- | libs/plugin_python/src/runner/pythonrunner.h | 53 | ||||
| -rw-r--r-- | libs/plugin_python/src/runner/pythonutils.cpp | 155 | ||||
| -rw-r--r-- | libs/plugin_python/src/runner/pythonutils.h | 25 |
6 files changed, 764 insertions, 0 deletions
diff --git a/libs/plugin_python/src/runner/CMakeLists.txt b/libs/plugin_python/src/runner/CMakeLists.txt new file mode 100644 index 0000000..d164460 --- /dev/null +++ b/libs/plugin_python/src/runner/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +if(NOT TARGET mo2::uibase) + find_package(mo2-uibase CONFIG REQUIRED) +endif() + +add_library(runner SHARED + error.h + pythonrunner.cpp + pythonrunner.h + pythonutils.h + pythonutils.cpp +) +mo2_configure_target(runner + NO_SOURCES + WARNINGS 4 + EXTERNAL_WARNINGS 4 + AUTOMOC ON + TRANSLATIONS OFF +) +mo2_default_source_group() +target_link_libraries(runner PUBLIC mo2::uibase PRIVATE pybind11::embed pybind11::qt) +target_include_directories(runner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(runner PRIVATE RUNNER_BUILD) +if(NOT WIN32 AND Python_SHARED_LIBRARY) + get_filename_component(_pylib_name "${Python_SHARED_LIBRARY}" NAME) + target_compile_definitions(runner PRIVATE + MO2_PYTHON_SHARED_LIBRARY="${_pylib_name}") +endif() + +# proxy will install runner + +# force runner to build mobase +add_dependencies(runner mobase) diff --git a/libs/plugin_python/src/runner/error.h b/libs/plugin_python/src/runner/error.h new file mode 100644 index 0000000..341c7ea --- /dev/null +++ b/libs/plugin_python/src/runner/error.h @@ -0,0 +1,82 @@ +#ifndef ERROR_H +#define ERROR_H + +#include <format> + +#include <QString> + +#include <pybind11/pybind11.h> + +#include <uibase/utility.h> + +namespace pyexcept { + + /** + * @brief Exception to throw when a python implementation does not implement + * a pure virtual function. + */ + class MissingImplementation : public MOBase::Exception { + public: + MissingImplementation(std::string const& className, + std::string const& methodName) + : Exception(QString::fromStdString( + std::format("Python class implementing \"{}\" has no " + "implementation of method \"{}\".", + className, methodName))) + { + } + }; + + /** + * @brief Exception to throw when a python error occurs. + */ + class PythonError : public MOBase::Exception { + public: + /** + * @brief Create a new PythonError, fetching the error message from + * python. If the message cannot be retrieved, `defaultErrorMessage()` + * is used instead. + */ + PythonError(pybind11::error_already_set const& ex) : Exception(ex.what()) {} + + /** + * @brief Create a new PythonError with the given message. + * + * @param message Message for the exception. + */ + PythonError(QString message) : Exception(message) {} + }; + + /** + * @brief Exception to throw when an unknown error occured. This is + * typically thrown from a catch(...) block. + */ + class UnknownException : public MOBase::Exception { + public: + /** + * @brief Create a new UnknownException with the default message. + * + * @see defaultErrorMessage + */ + UnknownException() : Exception(defaultErrorMessage()) {} + + /** + * @brief Create a new UnknownException with the given message. + * + * @param message Message for the exception. + */ + UnknownException(QString message) : Exception(message) {} + + protected: + /** + * + */ + static QString defaultErrorMessage() + { + return QObject::tr("An unknown exception was thrown in python code."); + } + }; + +} // namespace pyexcept + +#endif // ERROR_H diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp new file mode 100644 index 0000000..05a0f30 --- /dev/null +++ b/libs/plugin_python/src/runner/pythonrunner.cpp @@ -0,0 +1,415 @@ +#include "pythonrunner.h" + +#ifdef _WIN32 +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) + +#include <Windows.h> +#else +#include <dlfcn.h> +#endif + +#include <algorithm> +#include <cstdlib> +#include <optional> + +#include <QCoreApplication> +#include <QDir> +#include <QFile> + +#include "pybind11_qt/pybind11_qt.h" +#include <pybind11/embed.h> +#include <pybind11/functional.h> +#include <pybind11/stl/filesystem.h> + +#include <uibase/log.h> +#include <uibase/utility.h> + +#include "error.h" +#include "pythonutils.h" + +using namespace MOBase; +namespace py = pybind11; + +namespace mo2::python { + + /** + * + */ + class PythonRunner : public IPythonRunner { + + public: + PythonRunner() = default; + ~PythonRunner() = default; + + QList<QObject*> load(const QString& identifier) override; + void unload(const QString& identifier) override; + + bool initialize(std::vector<std::filesystem::path> const& pythonPaths) override; + void addDllSearchPath(std::filesystem::path const& dllPath) override; + bool isInitialized() const override; + + private: + /** + * @brief Ensure that the given folder is in sys.path. + */ + void ensureFolderInPath(QString folder); + + private: + // for each "identifier" (python file or python module folder), contains the + // list of python objects - this does not keep the objects alive, it simply used + // to unload plugins + std::unordered_map<QString, std::vector<py::handle>> m_PythonObjects; + }; + + std::unique_ptr<IPythonRunner> createPythonRunner() + { + return std::make_unique<PythonRunner>(); + } + + bool PythonRunner::initialize(std::vector<std::filesystem::path> const& pythonPaths) + { + // we only initialize Python once for the whole lifetime of the program, even if + // MO2 is restarted and the proxy or PythonRunner objects are deleted and + // recreated, Python is not re-initialized + // + // in an ideal world, we would initialize Python here (or in the constructor) + // and then finalize it in the destructor + // + // unfortunately, many library, including PyQt6, do not handle properly + // re-initializing the Python interpreter, so we cannot do that and we keep the + // interpreter alive + // + + if (Py_IsInitialized()) { + return true; + } + + std::optional<QByteArray> oldPythonHome; + std::optional<QByteArray> oldPythonPath; + auto restorePythonEnv = [&]() { + if (oldPythonHome.has_value()) { + setenv("PYTHONHOME", oldPythonHome->constData(), 1); + } else { + unsetenv("PYTHONHOME"); + } + if (oldPythonPath.has_value()) { + setenv("PYTHONPATH", oldPythonPath->constData(), 1); + } else { + unsetenv("PYTHONPATH"); + } + }; + + try { + static const char* argv0 = "ModOrganizer.exe"; + +#ifndef _WIN32 +#ifdef MO2_PYTHON_SHARED_LIBRARY + // Ensure libpython symbols are globally visible for extension modules + // loaded later (_struct, PyQt6, etc.). + void* pyHandle = + dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD); + if (pyHandle == nullptr) { + pyHandle = dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL); + } + if (pyHandle == nullptr) { + MOBase::log::warn("failed to dlopen python shared library '{}': {}", + MO2_PYTHON_SHARED_LIBRARY, dlerror()); + } +#endif +#endif + + // For portable/AppImage builds, set PYTHONHOME so the interpreter + // finds the bundled stdlib instead of looking at system paths. + // MO2_PYTHON_DIR (set by AppRun) points to the writable python/ + // dir next to the AppImage; fall back to <exe_dir>/python. + QString pythonHome; + const char* envPy = std::getenv("MO2_PYTHON_DIR"); + if (envPy && envPy[0] != '\0') { + pythonHome = QString::fromUtf8(envPy); + } else { + pythonHome = QCoreApplication::applicationDirPath() + "/python"; + } + if (const char* v = std::getenv("PYTHONHOME"); v != nullptr) { + oldPythonHome = QByteArray(v); + } + if (const char* v = std::getenv("PYTHONPATH"); v != nullptr) { + oldPythonPath = QByteArray(v); + } + + if (QDir(pythonHome).exists()) { + setenv("PYTHONHOME", pythonHome.toUtf8().constData(), 1); + + const QDir libDir(pythonHome + "/lib"); + const auto pyDirs = + libDir.entryList({"python3.*"}, QDir::Dirs | QDir::NoDotAndDotDot); + if (!pyDirs.isEmpty()) { + const QString pyver = pyDirs.first(); + const QString pyPath = QString("%1/lib/%2:%1/lib/%2/site-packages:%1") + .arg(pythonHome, pyver); + setenv("PYTHONPATH", pyPath.toUtf8().constData(), 1); + } + } + + // Paths we want to prepend/append for MO2 plugin loading. + auto paths = pythonPaths; + + PyConfig config; + PyConfig_InitPythonConfig(&config); + + // from PyBind11 + config.parse_argv = 0; + config.install_signal_handlers = 0; + + // from MO2 + config.site_import = 1; + config.optimization_level = 2; + + py::initialize_interpreter(&config, 1, &argv0, true); + + // Restore process environment after interpreter startup so + // subprocesses (umu/NaK/tools) are not forced onto MO2's Python. + restorePythonEnv(); + + if (!Py_IsInitialized()) { + MOBase::log::error( + "failed to init python: failed to initialize interpreter."); + + if (PyGILState_Check()) { + PyEval_SaveThread(); + } + + return false; + } + + { + for (auto const& path : paths) { + ensureFolderInPath(QString::fromStdString(absolute(path).string())); + } + + py::module_ mainModule = py::module_::import("__main__"); + py::object mainNamespace = mainModule.attr("__dict__"); + mainNamespace["sys"] = py::module_::import("sys"); + mainNamespace["mobase"] = py::module_::import("mobase"); + + mo2::python::configure_python_stream(); + mo2::python::configure_python_logging(mainNamespace["mobase"]); + } + + // we need to release the GIL here - which is what this does + // + // when Python is initialized, the GIl is acquired, and if it is not + // release, trying to acquire it on a different thread will deadlock + PyEval_SaveThread(); + + return true; + } + catch (const py::error_already_set& ex) { + restorePythonEnv(); + MOBase::log::error("failed to init python: {}", ex.what()); + return false; + } + } + + void PythonRunner::addDllSearchPath(std::filesystem::path const& dllPath) + { + py::gil_scoped_acquire lock; +#ifdef _WIN32 + py::module_::import("os").attr("add_dll_directory")(absolute(dllPath)); +#else + // On Linux, there is no add_dll_directory equivalent; prepend the folder to + // sys.path so Python extension modules can be found. + ensureFolderInPath(QString::fromStdString(absolute(dllPath).string())); +#endif + } + + void PythonRunner::ensureFolderInPath(QString folder) + { + py::module_ sys = py::module_::import("sys"); + py::list sysPath = sys.attr("path"); + + // Converting to QStringList for Qt::CaseInsensitive and because .index() + // raise an exception: + const QStringList currentPath = sysPath.cast<QStringList>(); + if (!currentPath.contains(folder, Qt::CaseInsensitive)) { + sysPath.insert(0, folder); + } + } + + QList<QObject*> PythonRunner::load(const QString& identifier) + { + py::gil_scoped_acquire lock; + + const QFileInfo idInfo(identifier); + const QString baseName = idInfo.fileName(); + if (baseName == "winreg.py" || baseName == "lzokay.py") { + log::debug("Skipping Python compatibility shim '{}'.", identifier); + return {}; + } + + // `pluginName` can either be a python file (single-file plugin or a folder + // (whole module). + // + // For whole module, we simply add the parent folder to path, then we load + // the module with a simple py::import, and we retrieve the associated + // __dict__ from which we extract either createPlugin or createPlugins. + // + // For single file, we need to use py::eval_file, and we will use the + // context (global variables) from __main__ (already contains mobase, and + // other required module). Since the context is shared between called of + // `instantiate`, we need to make sure to remove createPlugin(s) from + // previous call. + try { + + // dictionary that will contain createPlugin() or createPlugins(). + py::dict moduleDict; + + if (identifier.endsWith(".py")) { + py::object mainModule = py::module_::import("__main__"); + + // make a copy, otherwise we might end up calling the createPlugin() or + // createPlugins() function multiple time + py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")(); + + std::string temp = ToString(identifier); + py::eval_file(temp, moduleNamespace).is_none(); + moduleDict = moduleNamespace; + } + else { + // Retrieve the module name: + QStringList parts = identifier.split("/"); + std::string moduleName = ToString(parts.takeLast()); + ensureFolderInPath(parts.join("/")); + + // check if the module is already loaded + py::dict modules = py::module_::import("sys").attr("modules"); + if (modules.contains(moduleName)) { + py::module_ prev = modules[py::str(moduleName)]; + py::module_(prev).reload(); + moduleDict = prev.attr("__dict__"); + } + else { + moduleDict = + py::module_::import(moduleName.c_str()).attr("__dict__"); + } + } + + if (py::len(moduleDict) == 0) { + MOBase::log::error("No plugins found in {}.", identifier); + return {}; + } + + // Create the plugins: + std::vector<py::object> plugins; + + if (moduleDict.contains("createPlugin")) { + plugins.push_back(moduleDict["createPlugin"]()); + } + else if (moduleDict.contains("createPlugins")) { + py::object pyPlugins = moduleDict["createPlugins"](); + if (!py::isinstance<py::sequence>(pyPlugins)) { + MOBase::log::error( + "Plugin {}: createPlugins must return a sequence.", identifier); + } + else { + py::sequence pyList(pyPlugins); + size_t nPlugins = pyList.size(); + for (size_t i = 0; i < nPlugins; ++i) { + plugins.push_back(pyList[i]); + } + } + } + else { + MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", + identifier); + } + + // If we have no plugins, there was an issue, and we already logged the + // problem: + if (plugins.empty()) { + return QList<QObject*>(); + } + + QList<QObject*> allInterfaceList; + + for (py::object pluginObj : plugins) { + + // save to be able to unload it + m_PythonObjects[identifier].push_back(pluginObj); + + QList<QObject*> interfaceList = py::module_::import("mobase.private") + .attr("extract_plugins")(pluginObj) + .cast<QList<QObject*>>(); + + if (interfaceList.isEmpty()) { + MOBase::log::error("Plugin {}: no plugin interface implemented.", + identifier); + } + + // Append the plugins to the main list: + allInterfaceList.append(interfaceList); + } + + return allInterfaceList; + } + catch (const py::error_already_set& ex) { + MOBase::log::error("Failed to import plugin from {}.", identifier); + throw pyexcept::PythonError(ex); + } + } + + void PythonRunner::unload(const QString& identifier) + { + auto it = m_PythonObjects.find(identifier); + if (it != m_PythonObjects.end()) { + + py::gil_scoped_acquire lock; + + if (!identifier.endsWith(".py")) { + + // At this point, the identifier is the full path to the module. + QDir folder(identifier); + + // We want to "unload" (remove from sys.modules) modules that come + // from this plugin (whose __path__ points under this module, + // including the module of the plugin itself). + py::object sys = py::module_::import("sys"); + py::dict modules = sys.attr("modules"); + py::list keys = modules.attr("keys")(); + for (std::size_t i = 0; i < py::len(keys); ++i) { + py::object mod = modules[keys[i]]; + if (PyObject_HasAttrString(mod.ptr(), "__path__")) { + QString mpath = + mod.attr("__path__")[py::int_(0)].cast<QString>(); + + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // If the path is under identifier, we need to unload + // it. + log::debug("Unloading module {} from {} for {}.", + keys[i].cast<std::string>(), mpath, identifier); + + PyDict_DelItem(modules.ptr(), keys[i].ptr()); + } + } + } + } + + // Boost.Python does not handle cyclic garbace collection, so we need to + // release everything hold by the objects before deleting the objects + // themselves (done when erasing from m_PythonObjects). + for (auto& obj : it->second) { + obj.attr("__dict__").attr("clear")(); + } + + log::debug("Deleting {} python objects for {}.", it->second.size(), + identifier); + m_PythonObjects.erase(it); + } + } + + bool PythonRunner::isInitialized() const + { + return Py_IsInitialized() != 0; + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/runner/pythonrunner.h b/libs/plugin_python/src/runner/pythonrunner.h new file mode 100644 index 0000000..5f9751b --- /dev/null +++ b/libs/plugin_python/src/runner/pythonrunner.h @@ -0,0 +1,53 @@ +#ifndef PYTHONRUNNER_H +#define PYTHONRUNNER_H + +#include <filesystem> +#include <memory> + +#include <QList> +#include <QObject> +#include <QString> +#include <QStringList> + +#ifdef RUNNER_BUILD +#define RUNNER_DLL_EXPORT Q_DECL_EXPORT +#else +#define RUNNER_DLL_EXPORT Q_DECL_IMPORT +#endif + +namespace mo2::python { + + // python runner interface + // + class IPythonRunner { + public: + virtual QList<QObject*> load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + + // initialize Python + // + // pythonPaths contains the list of built-in paths for the Python library + // (pythonxxx.zip, etc.), an empty list uses the default Python paths (e.g., the + // PYTHONPATH environment variable) + // + virtual bool + initialize(std::vector<std::filesystem::path> const& pythonPaths = {}) = 0; + + // add a DLL search path + // + virtual void addDllSearchPath(std::filesystem::path const& dllPath) = 0; + + // check if the runner has been initialized, i.e., initialize() has been + // called and succeeded + virtual bool isInitialized() const = 0; + + virtual ~IPythonRunner() {} + }; + + // create the Python runner + // + RUNNER_DLL_EXPORT std::unique_ptr<IPythonRunner> createPythonRunner(); + +} // namespace mo2::python + +#endif // PYTHONRUNNER_H diff --git a/libs/plugin_python/src/runner/pythonutils.cpp b/libs/plugin_python/src/runner/pythonutils.cpp new file mode 100644 index 0000000..c94a50b --- /dev/null +++ b/libs/plugin_python/src/runner/pythonutils.cpp @@ -0,0 +1,155 @@ +#include "pythonutils.h" + +#include <filesystem> +#include <set> +#include <sstream> + +#include <pybind11/eval.h> +#include <pybind11/pybind11.h> + +#include <uibase/log.h> + +namespace py = pybind11; + +namespace mo2::python { + + class PrintWrapper { + MOBase::log::Levels level_; + std::stringstream buffer_; + + public: + PrintWrapper(MOBase::log::Levels level) : level_{level} {} + + void write(std::string_view message) + { + buffer_ << message; + if (buffer_.tellp() != 0 && buffer_.str().back() == '\n') { + const auto full_message = buffer_.str(); + MOBase::log::log(level_, "{}", + full_message.substr(0, full_message.length() - 1)); + buffer_ = std::stringstream{}; + } + } + }; + + /** + * @brief Construct a dynamic Python type. + * + */ + template <class... Args> + pybind11::object make_python_type(std::string_view name, + pybind11::tuple base_classes, Args&&... args) + { + // this is ugly but that's how it's done in C Python + auto type = py::reinterpret_borrow<py::object>((PyObject*)&PyType_Type); + + // create the python class + return type(name, base_classes, py::dict(std::forward<Args>(args)...)); + } + + void configure_python_stream() + { + // create the "MO2Handler" python class + auto printWrapper = make_python_type( + "MO2PrintWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Debug); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + auto errorWrapper = make_python_type( + "MO2ErrorWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Error); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + py::module_ sys = py::module_::import("sys"); + sys.attr("stdout") = printWrapper(); + sys.attr("stderr") = errorWrapper(); + + // this is required to handle exception in Python code OUTSIDE of pybind11 call, + // typically on Qt classes with methods overridden on the Python side + // + // without this, the application will crash instead of properly handling the + // exception as it would do with a py::error_already_set{} + // + // IMPORTANT: sys.attr("excepthook") = sys.attr("__excepthook__") DOES NOT WORK, + // and I have no clue why since the attribute does not seem to get updated (at + // least a print does not show it) + // + sys.attr("excepthook") = + py::eval("lambda x, y, z: sys.__excepthook__(x, y, z)"); + } + + // Small structure to hold the levels - There are copy paste from + // my Python version and I assume these will not change soon: + struct PyLogLevel { + static constexpr int CRITICAL = 50; + static constexpr int ERROR = 40; + static constexpr int WARNING = 30; + static constexpr int INFO = 20; + static constexpr int DEBUG = 10; + }; + + // This is the function we are going to use as our Handler .emit + // method. + void emit_function(py::object record) + { + + // There are other parameters that could be used, but this is minimal + // for now (filename, line number, etc.). + const int level = record.attr("levelno").cast<int>(); + const std::wstring msg = py::str(record.attr("msg")).cast<std::wstring>(); + + switch (level) { + case PyLogLevel::CRITICAL: + case PyLogLevel::ERROR: + MOBase::log::error("{}", msg); + break; + case PyLogLevel::WARNING: + MOBase::log::warn("{}", msg); + break; + case PyLogLevel::INFO: + MOBase::log::info("{}", msg); + break; + case PyLogLevel::DEBUG: + default: // There is a "NOTSET" level in theory: + MOBase::log::debug("{}", msg); + break; + } + }; + + void configure_python_logging(py::module_ mobase) + { + // most of this is dealing with actual Python objects since it is not + // possible to derive from logging.Handler in C++ using pybind11, + // and since a lot of this would require extra register only for this. + + // see also + // https://github.com/pybind/pybind11/issues/1193#issuecomment-429451094 + + // retrieve the logging module and the Handler class. + auto logging = py::module_::import("logging"); + auto Handler = logging.attr("Handler"); + + // create the "MO2Handler" python class + auto MO2Handler = + make_python_type("LogHandler", py::make_tuple(Handler), + py::arg("emit") = py::cpp_function(emit_function)); + + // create the default logger + auto handler = MO2Handler(); + handler.attr("setLevel")(PyLogLevel::DEBUG); + auto logger = logging.attr("getLogger")(py::object(mobase.attr("__name__"))); + logger.attr("setLevel")(PyLogLevel::DEBUG); + + // set mobase attributes + mobase.attr("LogHandler") = MO2Handler; + mobase.attr("logger") = logger; + + logging.attr("root").attr("setLevel")(PyLogLevel::DEBUG); + logging.attr("root").attr("addHandler")(handler); + } + +} // namespace mo2::python diff --git a/libs/plugin_python/src/runner/pythonutils.h b/libs/plugin_python/src/runner/pythonutils.h new file mode 100644 index 0000000..019e782 --- /dev/null +++ b/libs/plugin_python/src/runner/pythonutils.h @@ -0,0 +1,25 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include <string_view> + +#include <pybind11/pybind11.h> + +namespace mo2::python { + + /** + * @brief Configure Python stdout and stderr to log to MO2. + * + */ + void configure_python_stream(); + + /** + * @brief Configure logging for MO2 python plugin. + * + * @param mobase The mobase module. + */ + void configure_python_logging(pybind11::module_ mobase); + +} // namespace mo2::python + +#endif |
