/* Copyright (C) 2022 Sebastian Herbord & MO2 Team. All rights reserved. This file is part of python proxy plugin for MO python proxy plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Python proxy plugin is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with python proxy plugin. If not, see . */ #include "proxypython.h" #include #include #include #include #ifdef _WIN32 #include #else #include #endif #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using namespace MOBase; // retrieve the path to the folder containing the proxy DLL fs::path getPluginFolder() { #ifdef _WIN32 wchar_t path[MAX_PATH]; HMODULE hm = NULL; if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)&getPluginFolder, &hm) == 0) { return {}; } if (GetModuleFileName(hm, path, sizeof(path)) == 0) { return {}; } return fs::path(path).parent_path(); #else Dl_info info; if (dladdr((void*)&getPluginFolder, &info) == 0 || info.dli_fname == nullptr) { return {}; } return fs::path(info.dli_fname).parent_path(); #endif } ProxyPython::ProxyPython() : m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr}, m_LoadFailure(FailureType::NONE) { } bool ProxyPython::init(IOrganizer* moInfo) { m_MOInfo = moInfo; if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { return false; } if (QCoreApplication::applicationDirPath().contains(';')) { m_LoadFailure = FailureType::SEMICOLON; return true; } const auto pluginFolder = getPluginFolder(); #ifdef _WIN32 const auto pluginDataRoot = fs::exists(pluginFolder / "plugin_python") ? (pluginFolder / "plugin_python") : pluginFolder; #else // Linux portable installs can contain a legacy Windows "plugin_python" // payload (pythoncore.zip/.pyd) that is incompatible with our bundled // Linux runtime. Always use the plugin root on Linux. const auto pluginDataRoot = pluginFolder; #endif if (pluginFolder.empty()) { #ifdef _WIN32 DWORD error = ::GetLastError(); m_LoadFailure = FailureType::DLL_NOT_FOUND; log::error("failed to resolve Python proxy directory ({}): {}", error, qUtf8Printable(windowsErrorString(::GetLastError()))); #else m_LoadFailure = FailureType::DLL_NOT_FOUND; log::error("failed to resolve Python proxy directory: {}", std::strerror(errno)); #endif return false; } if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { m_LoadFailure = FailureType::INITIALIZATION; if (QMessageBox::question( parentWidget(), tr("Python Initialization failed"), tr("On a previous start the Python Plugin failed to initialize.\n" "Do you want to try initializing python again (at the risk of " "another crash)?\n " "Suggestion: Select \"no\", and click the warning sign for further " "help.Afterwards you have to re-enable the python plugin."), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { // we force enabled here (note: this is a persistent settings since MO2 2.4 // or something), plugin // usually should not handle enabled/disabled themselves but this is a base // plugin so... m_MOInfo->setPersistent(name(), "enabled", false, true); return true; } } if (m_MOInfo) { m_MOInfo->setPersistent(name(), "tryInit", true); } // load the pythonrunner library, this is done in multiple steps: // // 1. On Windows, set the dlls/ subfolder as the DLL directory so Windows // can find the Python, libffi, and runner DLLs. // #ifdef _WIN32 const auto dllPaths = pluginDataRoot / "dlls"; if (SetDllDirectoryW(dllPaths.c_str()) == 0) { DWORD error = ::GetLastError(); m_LoadFailure = FailureType::DLL_NOT_FOUND; log::error("failed to add python DLL directory ({}): {}", error, qUtf8Printable(windowsErrorString(::GetLastError()))); return false; } #endif // 2. we create the Python runner, we do not need to use ::LinkLibrary and // ::GetProcAddress because we use delayed load for the runner DLL (see the // CMakeLists.txt) // m_Runner = mo2::python::createPythonRunner(); if (m_Runner) { const auto libpath = pluginDataRoot / "libs"; #ifdef _WIN32 const std::vector paths{ libpath / "pythoncore.zip", libpath, std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; m_Runner->initialize(paths); #else // On Linux, rely on the unpacked stdlib from PYTHONHOME (AppRun sets // MO2_PYTHON_DIR/PYTHONHOME). Do not prepend pythoncore.zip here: // forcing zipimport can fail when zlib is unavailable in embedded mode. std::vector paths{ libpath, std::filesystem::path{IOrganizer::getPluginDataPath().toStdWString()}}; // pluginDataPath() returns a writable directory (plugin_data/) separate // from the possibly-read-only plugins/data/ that ships bundled assets // like the DDS module. Add the bundled path too so imports still work. { auto bundledData = pluginDataRoot / "data"; if (fs::is_directory(bundledData)) { paths.emplace_back(bundledData); } } // Allow portable/AppImage builds to ship Python packages next to the app. // MO2_PYTHON_DIR (set by AppRun) points to the writable python/ dir // next to the AppImage; fall back to /python. fs::path pythonDir; const char* envPy = std::getenv("MO2_PYTHON_DIR"); if (envPy && envPy[0] != '\0') { pythonDir = fs::path(envPy); } else { pythonDir = fs::path(QCoreApplication::applicationDirPath().toStdWString()) / "python"; } paths.emplace_back(pythonDir / "site-packages"); paths.emplace_back(pythonDir); #ifdef MO2_PYTHON_PURELIB_DIR if (std::strlen(MO2_PYTHON_PURELIB_DIR) > 0) { paths.emplace_back(MO2_PYTHON_PURELIB_DIR); } #endif #ifdef MO2_PYTHON_PLATLIB_DIR if (std::strlen(MO2_PYTHON_PLATLIB_DIR) > 0 && std::strcmp(MO2_PYTHON_PLATLIB_DIR, MO2_PYTHON_PURELIB_DIR) != 0) { paths.emplace_back(MO2_PYTHON_PLATLIB_DIR); } #endif m_Runner->initialize(paths); #endif } if (m_MOInfo) { m_MOInfo->setPersistent(name(), "tryInit", false); } // reset DLL directory #ifdef _WIN32 SetDllDirectoryW(NULL); #endif if (!m_Runner || !m_Runner->isInitialized()) { m_LoadFailure = FailureType::INITIALIZATION; MOBase::log::error("Python proxy: interpreter failed to initialize, " "Python plugins will be unavailable"); return true; // return true so the plugin stays loaded (shows diagnostic) } else { #ifdef _WIN32 m_Runner->addDllSearchPath(pluginDataRoot / "dlls"); #else m_Runner->addDllSearchPath(pluginDataRoot / "libs"); #endif } return true; } QString ProxyPython::name() const { return "Python Proxy"; } QString ProxyPython::localizedName() const { return tr("Python Proxy"); } QString ProxyPython::author() const { return "AnyOldName3, Holt59, Silarn, Tannin"; } QString ProxyPython::description() const { return tr("Proxy Plugin to allow plugins written in python to be loaded"); } VersionInfo ProxyPython::version() const { return VersionInfo(3, 0, 0, VersionInfo::RELEASE_FINAL); } QList ProxyPython::settings() const { return {}; } QStringList ProxyPython::pluginList(const QDir& pluginPath) const { QDir dir(pluginPath); dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); QDirIterator iter(dir); // Note: We put python script (.py) and directory names, not the __init__.py // files in those since it is easier for the runner to import them. QStringList result; while (iter.hasNext()) { QString name = iter.next(); QFileInfo info = iter.fileInfo(); const QString baseName = info.fileName(); if (info.isFile() && name.endsWith(".py")) { // Compatibility shims and disabled plugins staged on Linux; they are not // loaded as MO2 plugins. if (baseName == "winreg.py" || baseName == "lzokay.py" || baseName == "FNISPatches.py" || baseName == "FNISTool.py" || baseName == "FNISToolReset.py" || baseName == "FixGameRegKey.py" || baseName == "rootbuilder.py") { continue; } result.append(name); } else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { // Skip Windows-only directory plugins that use ctypes.windll etc. if (baseName == "crashlogtools" || baseName.compare("rootbuilder", Qt::CaseInsensitive) == 0) { continue; } result.append(name); } } return result; } QList ProxyPython::load(const QString& identifier) { if (!m_Runner || !m_Runner->isInitialized()) { return {}; } return m_Runner->load(identifier); } void ProxyPython::unload(const QString& identifier) { if (m_Runner && m_Runner->isInitialized()) { return m_Runner->unload(identifier); } } std::vector ProxyPython::activeProblems() const { auto failure = m_LoadFailure; // don't know how this could happen but wth if (m_Runner && !m_Runner->isInitialized()) { failure = FailureType::INITIALIZATION; } if (failure != FailureType::NONE) { return {static_cast>(failure)}; } return {}; } QString ProxyPython::shortDescription(unsigned int key) const { switch (static_cast(key)) { case FailureType::SEMICOLON: return tr("ModOrganizer path contains a semicolon"); case FailureType::DLL_NOT_FOUND: return tr("Python DLL not found"); case FailureType::INVALID_DLL: return tr("Invalid Python DLL"); case FailureType::INITIALIZATION: return tr("Initializing Python failed"); default: return tr("invalid problem key %1").arg(key); } } QString ProxyPython::fullDescription(unsigned int key) const { switch (static_cast(key)) { case FailureType::SEMICOLON: return tr("The path to Mod Organizer (%1) contains a semicolon.
" "While this is legal on NTFS drives, many applications do not " "handle it correctly.
" "Unfortunately MO depends on libraries that seem to fall into that " "group.
" "As a result the python plugin cannot be loaded, and the only " "solution we can offer is to remove the semicolon or move MO to a " "path without a semicolon.") .arg(QCoreApplication::applicationDirPath()); case FailureType::DLL_NOT_FOUND: return tr("The Python plugin DLL was not found, maybe your antivirus deleted " "it. Re-installing MO2 might fix the problem."); case FailureType::INVALID_DLL: return tr( "The Python plugin DLL is invalid, maybe your antivirus is blocking it. " "Re-installing MO2 and adding exclusions for it to your AV might fix the " "problem."); case FailureType::INITIALIZATION: return tr("The initialization of the Python plugin DLL failed, unfortunately " "without any details."); default: return tr("invalid problem key %1").arg(key); } } bool ProxyPython::hasGuidedFix(unsigned int) const { return false; } void ProxyPython::startGuidedFix(unsigned int) const {}