diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index 47691ed..46c2264 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -227,7 +227,7 @@ QList ProxyPython::settings() const return result; } -QStringList ProxyPython::pluginList(const QString &pluginPath) const +QStringList ProxyPython::pluginList(const QDir& pluginPath) const { QDir dir(pluginPath); dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); @@ -240,29 +240,32 @@ QStringList ProxyPython::pluginList(const QString &pluginPath) const QString name = iter.next(); QFileInfo info = iter.fileInfo(); + if (info.isFile() && name.endsWith(".py")) { result.append(name); } else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { result.append(name); - // result.append(info.baseName()); } } return result; } - -QList ProxyPython::instantiate(const QString &pluginName) +QList ProxyPython::load(const QString& identifier) { - if (m_Runner != nullptr) { - QList result = m_Runner->instantiate(pluginName); - return result; - } else { - return QList(); + if (!m_Runner) { + return {}; } + return m_Runner->load(identifier); } +void ProxyPython::unload(const QString& identifier) +{ + if (m_Runner) { + return m_Runner->unload(identifier); + } +} std::vector ProxyPython::activeProblems() const { diff --git a/src/proxy/proxypython.h b/src/proxy/proxypython.h index 708ce4f..4e17a3e 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -48,8 +48,9 @@ public: virtual MOBase::VersionInfo version() const; virtual QList settings() const; - QStringList pluginList(const QString &pluginPath) const; - QList instantiate(const QString &pluginName); + QStringList pluginList(const QDir& pluginPath) const; + QList load(const QString& identifier); + void unload(const QString& identifier); /** * @return the parent widget for newly created dialogs diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index ce81c23..622c1a2 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -1106,7 +1106,10 @@ public: ~PythonRunner(); bool initPython(const QString& pythonDir); - QList instantiate(const QString& pluginName); + + QList load(const QString& identifier); + void unload(const QString& identifier); + bool isPythonInstalled() const; bool isPythonVersionSupported() const; @@ -1132,9 +1135,10 @@ private: private: - // List of python objects representing plugins to keep all the bpy::object "alive" - // during the execution. - std::vector m_PythonObjects; + // For each "identifier" (python file or python module folder), contains the list list + // of python objects representing to keep "alive" during the execution. + std::unordered_map> m_PythonObjects; + wchar_t* m_PythonHome; }; @@ -1158,6 +1162,15 @@ PythonRunner::PythonRunner() PythonRunner::~PythonRunner() { // We need the GIL lock when destroying Python objects. GILock lock; + + // Boost.Python does not handle cyclic garbace collection, so we need to release + // everything hold by the objects before deleting the objects themselves: + for (auto& [name, objects] : m_PythonObjects) { + for (auto& obj : objects) { + obj.attr("__dict__").attr("clear")(); + } + } + m_PythonObjects.clear(); } @@ -1336,7 +1349,7 @@ void PythonRunner::appendIfInstance(bpy::object const& obj, QList &int } } -QList PythonRunner::instantiate(const QString &pluginName) +QList PythonRunner::load(const QString& identifier) { GILock lock; @@ -1354,25 +1367,25 @@ QList PythonRunner::instantiate(const QString &pluginName) // Dictionary that will contain createPlugin() or createPlugins(). bpy::dict moduleDict; - if (pluginName.endsWith(".py")) { + if (identifier.endsWith(".py")) { bpy::object mainModule = bpy::import("__main__"); bpy::dict moduleNamespace = bpy::extract(mainModule.attr("__dict__"))(); - std::string temp = ToString(pluginName); + std::string temp = ToString(identifier); if (!handled_exec_file(temp.c_str(), moduleNamespace)) { moduleDict = moduleNamespace; } } else { // Retrieve the module name: - QStringList parts = pluginName.split("/"); + QStringList parts = identifier.split("/"); std::string moduleName = ToString(parts.takeLast()); ensureFolderInPath(parts.join("/")); moduleDict = bpy::dict(bpy::import(moduleName.c_str()).attr("__dict__")); } if (bpy::len(moduleDict) == 0) { - MOBase::log::error("Failed to import plugin from {}.", pluginName); + MOBase::log::error("Failed to import plugin from {}.", identifier); throw pyexcept::PythonError(); } @@ -1388,7 +1401,7 @@ QList PythonRunner::instantiate(const QString &pluginName) else if (moduleDict.has_key("createPlugins")) { bpy::object pyPlugins = moduleDict["createPlugins"](); if (!PySequence_Check(pyPlugins.ptr())) { - MOBase::log::error("Plugin {}: createPlugins must return a list.", pluginName); + MOBase::log::error("Plugin {}: createPlugins must return a list.", identifier); } else { bpy::list pyList(pyPlugins); @@ -1402,7 +1415,7 @@ QList PythonRunner::instantiate(const QString &pluginName) bpy::delitem(moduleDict, bpy::str("createPlugins")); } else { - MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", pluginName); + 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: @@ -1415,7 +1428,7 @@ QList PythonRunner::instantiate(const QString &pluginName) for (bpy::object pluginObj : plugins) { // Add the plugin to keep it alive: - m_PythonObjects.push_back(pluginObj); + m_PythonObjects[identifier].push_back(pluginObj); QList interfaceList; @@ -1435,7 +1448,7 @@ QList PythonRunner::instantiate(const QString &pluginName) } if (interfaceList.isEmpty()) { - MOBase::log::error("Plugin {}: no plugin interface implemented.", pluginName); + MOBase::log::error("Plugin {}: no plugin interface implemented.", identifier); } // Append the plugins to the main list: @@ -1445,11 +1458,53 @@ QList PythonRunner::instantiate(const QString &pluginName) return allInterfaceList; } catch (const bpy::error_already_set&) { - MOBase::log::error("Failed to import plugin from {}.", pluginName); + MOBase::log::error("Failed to import plugin from {}.", identifier); throw pyexcept::PythonError(); } } +void PythonRunner::unload(const QString& identifier) +{ + auto it = m_PythonObjects.find(identifier); + if (it != m_PythonObjects.end()) { + + GILock lock; + + // We need to remove modules from the plugin from sys.module. At this point, + // the identifier is the full path to the module. + if (!identifier.endsWith(".py")) { + QDir folder(identifier); + + bpy::object sys = bpy::import("sys"); + bpy::dict modules = bpy::extract(sys.attr("modules")); + bpy::list keys = modules.keys(); + for (std::size_t i = 0; i < bpy::len(keys); ++i) { + bpy::object mod = modules[keys[i]]; + if (PyObject_HasAttrString(mod.ptr(), "__path__")) { + QString mpath = bpy::extract(mod.attr("__path__")[0]); + + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // If the path is under identifier, we need to unload it. + log::debug("Unloading module {} from {} for {}.", bpy::extract(keys[i])(), mpath, identifier); + bpy::delitem(modules, keys[i]); + } + } + } + } + + // 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::isPythonInstalled() const { return Py_IsInitialized() != 0; diff --git a/src/runner/pythonrunner.h b/src/runner/pythonrunner.h index c494ae1..be47860 100644 --- a/src/runner/pythonrunner.h +++ b/src/runner/pythonrunner.h @@ -10,7 +10,10 @@ class IPythonRunner { public: - virtual QList instantiate(const QString &pluginName) = 0; + + virtual QList load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + virtual bool isPythonInstalled() const = 0; virtual bool isPythonVersionSupported() const = 0;