diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index 0633524..b5656a3 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -26,172 +26,86 @@ along with python proxy plugin. If not, see . #include #include #include -#include "resource.h" +#include "log.h" using namespace MOBase; - -const char *ProxyPython::s_DownloadPythonURL = "http://www.python.org/download/releases/"; - - -HMODULE GetOwnModuleHandle() -{ - HMODULE hMod = nullptr; - GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(&GetOwnModuleHandle), &hMod); - - return hMod; -} - - -QString ExtractResource(WORD resourceID, const QString &szFilename) -{ - HMODULE mod = GetOwnModuleHandle(); - - HRSRC hResource = FindResourceW(mod, MAKEINTRESOURCE(resourceID), L"BINARY"); - if (hResource == nullptr) { - throw Exception("embedded dll not available: " + windowsErrorString(::GetLastError())); - } - - HGLOBAL hFileResource = LoadResource(mod, hResource); - if (hFileResource == nullptr) { - throw Exception("failed to load embedded dll resource: " + windowsErrorString(::GetLastError())); - } - - LPVOID lpFile = LockResource(hFileResource); - if (lpFile == nullptr) { - throw Exception(QString("failed to lock resource: %1").arg(windowsErrorString(::GetLastError()))); - } - - DWORD dwSize = SizeofResource(mod, hResource); - - QString outFile = QDir::tempPath() + "/" + szFilename; - - HANDLE hFile = CreateFileW(outFile.toStdWString().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile == INVALID_HANDLE_VALUE) { - if (::GetLastError() == ERROR_SHARING_VIOLATION) { - // dll exists and is opened by another instance of MO, shouldn't be outdated then... - return outFile; - } else { - throw Exception(QString("failed to open python runner: %1").arg(windowsErrorString(::GetLastError()))); - } - } - HANDLE hFileMap = CreateFileMapping(hFile, nullptr, PAGE_READWRITE, 0, dwSize, nullptr); - if (hFileMap == NULL) { - throw Exception(QString("failed to map python runner: %1").arg(windowsErrorString(::GetLastError()))); - } - LPVOID lpAddress = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); - if (lpAddress == nullptr) { - throw Exception(QString("failed to map view of file: %1").arg(windowsErrorString(::GetLastError()))); - } - - CopyMemory(lpAddress, lpFile, dwSize); - - UnmapViewOfFile(lpAddress); - - CloseHandle(hFileMap); - ::FlushFileBuffers(hFile); - CloseHandle(hFile); - - return outFile; -} - - ProxyPython::ProxyPython() - : m_MOInfo(nullptr), m_Runner(nullptr), m_LoadFailure(FAIL_NOTINIT) + : m_MOInfo{ nullptr }, + m_RunnerLib{ nullptr }, + m_Runner{ nullptr }, + m_LoadFailure(FailureType::NONE) { } -ProxyPython::~ProxyPython() -{ - delete m_Runner; - - if (!m_TempRunnerFile.isEmpty()) { - ::FreeLibrary(m_RunnerLib); - QFile(m_TempRunnerFile).remove(); - } -} - -typedef IPythonRunner* (*CreatePythonRunner_func)(const QString &pythonPath); - bool ProxyPython::init(IOrganizer *moInfo) { + using CreatePythonRunner_func = IPythonRunner * (*)(); + m_MOInfo = moInfo; if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { - m_LoadFailure = FAIL_NONE; return false; } - m_LoadFailure = FAIL_OTHER; if (QCoreApplication::applicationDirPath().contains(';')) { - m_LoadFailure = FAIL_SEMICOLON; + m_LoadFailure = FailureType::SEMICOLON; return true; } - QString pythonPath; + // load the pythonrunner library + m_RunnerLib = ::LoadLibraryW(QDir::toNativeSeparators( + IOrganizer::getPluginDataPath() + "/pythonRunner.dll").toStdWString().c_str()); - if (m_MOInfo) { - pythonPath = m_MOInfo->pluginSetting(name(), "python_dir").toString(); + if (!m_RunnerLib) { + DWORD error = ::GetLastError(); + log::error("failed to load python runner ({}): {}", qUtf8Printable(windowsErrorString(error))); + if (error == ERROR_MOD_NOT_FOUND) { + m_LoadFailure = FailureType::DLL_NOT_FOUND; + } + else { + m_LoadFailure = FailureType::INVALID_DLL; + } + return true; + } - if (!pythonPath.isEmpty() && !QFile::exists(pythonPath + "/python.exe")) { - m_LoadFailure = FAIL_WRONGPYTHONPATH; + const CreatePythonRunner_func createPythonRunner = (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); + if (!createPythonRunner) { + m_LoadFailure = FailureType::INVALID_DLL; + return true; + } + + 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; } } - m_RunnerLib = ::LoadLibraryW(QDir::toNativeSeparators( - IOrganizer::getPluginDataPath() + "/pythonRunner.dll").toStdWString().c_str()); - - if (m_RunnerLib != nullptr) { - CreatePythonRunner_func CreatePythonRunner = (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); - if (CreatePythonRunner == nullptr) { - throw Exception("embedded dll is invalid: " + windowsErrorString(::GetLastError())); - } - - if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { - if (pythonPath.isEmpty()) { - m_LoadFailure = FAIL_PYTHONDETECTION; - } else { - m_LoadFailure = FAIL_WRONGPYTHONPATH; - } - if (QMessageBox::question(parentWidget(), tr("Python Initialization failed"), - tr("On a previous start the Python Plugin failed to initialize.\n" - "Either the value in Settings->Plugins->ProxyPython->plugin_dir is set incorrectly or it is empty and auto-detection doesn't work " - "for whatever reason.\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) { - m_MOInfo->setPluginSetting(name(), "enabled", false); - return true; - } - } - - if (m_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", true); - } - - m_Runner = CreatePythonRunner(pythonPath); - - if (m_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", false); - } - - if (m_Runner != nullptr) { - m_LoadFailure = FAIL_NONE; - } else { - m_LoadFailure = FAIL_INITFAIL; - } - return true; - } else { - DWORD error = ::GetLastError(); - qCritical("Failed to load python runner (%s): %s", qUtf8Printable(m_TempRunnerFile), qUtf8Printable(windowsErrorString(error))); - if (error == ERROR_MOD_NOT_FOUND) { - m_LoadFailure = FAIL_MISSINGDEPENDENCIES; - } - return true; + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", true); } + + m_Runner = std::unique_ptr{ createPythonRunner() }; + + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", false); + } + + if (!m_Runner) { + m_LoadFailure = FailureType::INITIALIZATION; + } + + return true; } QString ProxyPython::name() const @@ -216,15 +130,12 @@ QString ProxyPython::description() const VersionInfo ProxyPython::version() const { - return VersionInfo(2, 2, 0, VersionInfo::RELEASE_FINAL); + return VersionInfo(2, 3, 0, VersionInfo::RELEASE_FINAL); } QList ProxyPython::settings() const { - QList result; - result.push_back(PluginSetting("python_dir", "Path to your python installation. Leave empty for auto-detection", "")); - result.push_back(PluginSetting("enabled", "Set to true to enable support for python plugins", true)); - return result; + return {}; } QStringList ProxyPython::pluginList(const QDir& pluginPath) const @@ -269,102 +180,63 @@ void ProxyPython::unload(const QString& identifier) std::vector ProxyPython::activeProblems() const { - std::vector result; - if (m_LoadFailure == FAIL_MISSINGDEPENDENCIES) { - result.push_back(PROBLEM_PYTHONMISSING); - } else if (m_LoadFailure == FAIL_WRONGPYTHONPATH) { - result.push_back(PROBLEM_WRONGPYTHONPATH); - } else if (m_LoadFailure == FAIL_PYTHONDETECTION) { - result.push_back(PROBLEM_PYTHONDETECTION); - } else if (m_LoadFailure == FAIL_INITFAIL) { - result.push_back(PROBLEM_INITFAIL); - } else if (m_LoadFailure == FAIL_SEMICOLON) { - result.push_back(PROBLEM_SEMICOLON); - } else if (m_Runner != nullptr) { - if (!m_Runner->isPythonInstalled()) { - // don't know how this could happen but wth - result.push_back(PROBLEM_PYTHONMISSING); - } - if (!m_Runner->isPythonVersionSupported()) { - result.push_back(PROBLEM_PYTHONWRONGVERSION); - } + auto failure = m_LoadFailure; + + // don't know how this could happen but wth + if (m_Runner && !m_Runner->isPythonInitialized()) { + failure = FailureType::INITIALIZATION; } - return result; + if (failure != FailureType::NONE) { + return { static_cast>(failure) }; + } + + return {}; } QString ProxyPython::shortDescription(unsigned int key) const { - switch (key) { - case PROBLEM_PYTHONMISSING: { - return tr("Python not installed or not found"); - } break; - case PROBLEM_PYTHONWRONGVERSION: { - return tr("Python version is incompatible"); - } break; - case PROBLEM_WRONGPYTHONPATH: { - return tr("Invalid python path"); - } break; - case PROBLEM_INITFAIL: { - return tr("Initializing Python failed"); - } break; - case PROBLEM_PYTHONDETECTION: { - return tr("Python auto-detection failed"); - } break; - case PROBLEM_SEMICOLON: { + switch (static_cast(key)) { + case FailureType::SEMICOLON: return tr("ModOrganizer path contains a semicolon"); - } break; + 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: - throw Exception(tr("invalid problem key %1").arg(key)); + return tr("invalid problem key %1").arg(key); } } QString ProxyPython::fullDescription(unsigned int key) const { - switch (key) { - case PROBLEM_PYTHONMISSING: { - return tr("Some MO plugins require the python interpreter to be installed. " - "These plugins will not even show up in settings->plugins.
" - "If you want to use those plugins, please install the 32-bit version of Python 2.7.x from %1.
" - "This is only required to use some extended functionality in MO, you do not need Python to play the game.").arg(s_DownloadPythonURL); - } break; - case PROBLEM_PYTHONWRONGVERSION: { - return tr("The loaded version of python does not match the expected 3.10.
" - "This may cause some or all python plugins to stop working.
" - "The expected python libraries should come bundled with MO2,
" - "you may want to perform a clean install of the application."); - } break; - case PROBLEM_WRONGPYTHONPATH: { - return tr("Please set python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation."); - } break; - case PROBLEM_PYTHONDETECTION: { - return tr("The auto-detection of the python path failed. I don't know why this would happen but you can try to fix it " - "by setting python_dir in Settings->Plugins->ProxyPython to the path of your python 2.7 (32 bit) installation."); - } break; - case PROBLEM_INITFAIL: { - return tr("Sorry, I don't know any details. Most likely your python installation is not supported."); - } break; - case PROBLEM_SEMICOLON: { - return tr("The path to Mod Organizer (%1) contains a semicolon.
" - "While this is legal on NTFS drives there is a lot of software that doesn't handle it correctly.
" - "Unfortunately MO depends on libraries that seem to fall into that group.
" - "As a result the python plugin can't be loaded.
" - "The only solution I can offer is to remove the semicolon / move MO to a path without a semicolon.").arg(QCoreApplication::applicationDirPath()); - } break; - default: - throw Exception(QString("invalid problem key %1").arg(key)); + 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 softwares 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 key) const { - return (key == PROBLEM_PYTHONMISSING) || (key == PROBLEM_PYTHONWRONGVERSION); + return false; } void ProxyPython::startGuidedFix(unsigned int key) const { - if ((key == PROBLEM_PYTHONMISSING) || (key == PROBLEM_PYTHONWRONGVERSION)) { - ::ShellExecuteA(nullptr, "open", s_DownloadPythonURL, nullptr, nullptr, SW_SHOWNORMAL); - } } diff --git a/src/proxy/proxypython.h b/src/proxy/proxypython.h index 4e17a3e..249d198 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -20,11 +20,14 @@ along with python proxy plugin. If not, see . #ifndef PROXYPYTHON_H #define PROXYPYTHON_H +#include +#include + +#include #include #include -#include -#include + #include @@ -38,59 +41,42 @@ class ProxyPython : public QObject, public MOBase::IPluginProxy, public MOBase:: public: ProxyPython(); - ~ProxyPython(); virtual bool init(MOBase::IOrganizer *moInfo); - virtual QString name() const; - virtual QString localizedName() const; - virtual QString author() const; - virtual QString description() const; - virtual MOBase::VersionInfo version() const; - virtual QList settings() const; + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; - QStringList pluginList(const QDir& pluginPath) const; - QList load(const QString& identifier); - void unload(const QString& identifier); - - /** - * @return the parent widget for newly created dialogs - * @note needs to be public so it can be exposed to plugins - */ - virtual QWidget *getParentWidget() { return parentWidget(); } + QStringList pluginList(const QDir& pluginPath) const override; + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; public: // IPluginDiagnose - virtual std::vector activeProblems() const; - virtual QString shortDescription(unsigned int key) const; - virtual QString fullDescription(unsigned int key) const; - virtual bool hasGuidedFix(unsigned int key) const; - virtual void startGuidedFix(unsigned int key) const; + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; private: - static const unsigned int PROBLEM_PYTHONMISSING = 1; - static const unsigned int PROBLEM_PYTHONWRONGVERSION = 2; - static const unsigned int PROBLEM_WRONGPYTHONPATH = 3; - static const unsigned int PROBLEM_INITFAIL = 4; - static const unsigned int PROBLEM_PYTHONDETECTION = 5; - static const unsigned int PROBLEM_SEMICOLON = 6; - static const char *s_DownloadPythonURL; - MOBase::IOrganizer *m_MOInfo; - QString m_TempRunnerFile; HMODULE m_RunnerLib; - IPythonRunner *m_Runner; + std::unique_ptr m_Runner; - enum { - FAIL_NONE, - FAIL_SEMICOLON, - FAIL_NOTINIT, - FAIL_MISSINGDEPENDENCIES, - FAIL_INITFAIL, - FAIL_WRONGPYTHONPATH, - FAIL_PYTHONDETECTION, - FAIL_OTHER - } m_LoadFailure; + enum class FailureType : unsigned int { + NONE = 0, + SEMICOLON = 1, + DLL_NOT_FOUND = 2, + INVALID_DLL = 3, + INITIALIZATION = 4 + }; + + FailureType m_LoadFailure; }; diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 20a7edb..4d1e35e 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -663,7 +663,9 @@ BOOST_PYTHON_MODULE(mobase) .def("getSupportedExtensions", &IInstallationManager::getSupportedExtensions) .def("extractFile", &IInstallationManager::extractFile, (bpy::arg("entry"), bpy::arg("silent") = false)) .def("extractFiles", &IInstallationManager::extractFiles, (bpy::arg("entries"), bpy::arg("silent") = false)) - .def("createFile", &IInstallationManager::createFile, bpy::arg("entry")) + .def("createFile", +[](IInstallationManager* m, std::shared_ptr entry) { + return m->createFile(utils::clean_shared_ptr(entry)); + }, bpy::arg("entry")) // accept both QString and GuessedValue since the conversion is not automatic in Python, and // return a tuple to get back the mod name and the mod ID @@ -799,13 +801,21 @@ BOOST_PYTHON_MODULE(mobase) .def("priority", &MOBase::IPluginList::priority, bpy::arg("name")) .def("setPriority", &MOBase::IPluginList::setPriority, (bpy::arg("name"), "priority")) .def("loadOrder", &MOBase::IPluginList::loadOrder, bpy::arg("name")) - .def("isMaster", &MOBase::IPluginList::isMaster, bpy::arg("name")) + .def("hasMasterExtension", &MOBase::IPluginList::hasMasterExtension, bpy::arg("name")) + .def("hasLightExtension", &MOBase::IPluginList::hasLightExtension, bpy::arg("name")) + .def("isMasterFlagged", &MOBase::IPluginList::isMasterFlagged, bpy::arg("name")) + .def("isLightFlagged", &MOBase::IPluginList::isLightFlagged, bpy::arg("name")) .def("masters", &MOBase::IPluginList::masters, bpy::arg("name")) .def("origin", &MOBase::IPluginList::origin, bpy::arg("name")) .def("onRefreshed", &MOBase::IPluginList::onRefreshed, bpy::arg("callback")) .def("onPluginMoved", &MOBase::IPluginList::onPluginMoved, bpy::arg("callback")) + .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, bpy::arg("callback")) + .def("pluginNames", &MOBase::IPluginList::pluginNames) + .def("setState", &MOBase::IPluginList::setState, (bpy::arg("name"), "state")) + .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, bpy::arg("loadorder")) - // Kept but deprecated for backward compatibility: + // DEPRECATED + .def("isMaster", &MOBase::IPluginList::isMaster, bpy::arg("name")) .def("onPluginStateChanged", +[](IPluginList* modList, const std::function& fn) { utils::show_deprecation_warning("onPluginStateChanged", "onPluginStateChanged(Callable[[str, IPluginList.PluginStates], None]) is deprecated, " @@ -814,12 +824,8 @@ BOOST_PYTHON_MODULE(mobase) for (const auto& entry : map) { fn(entry.first, entry.second); } - }); - }, bpy::arg("callback")) - .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, bpy::arg("callback")) - .def("pluginNames", &MOBase::IPluginList::pluginNames) - .def("setState", &MOBase::IPluginList::setState, (bpy::arg("name"), "state")) - .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, bpy::arg("loadorder")) + }); + }, bpy::arg("callback")) ; bpy::enum_("ModState") @@ -1157,12 +1163,12 @@ public: PythonRunner(); ~PythonRunner(); - bool initPython(const QString& pythonDir); + bool initPython(); QList load(const QString& identifier); void unload(const QString& identifier); - bool isPythonInstalled() const; + bool isPythonInitialized() const; bool isPythonVersionSupported() const; private: @@ -1194,14 +1200,13 @@ private: wchar_t* m_PythonHome; }; -IPythonRunner* CreatePythonRunner(const QString& pythonDir) +IPythonRunner* CreatePythonRunner() { - PythonRunner* result = new PythonRunner; - if (result->initPython(pythonDir)) { - return result; + std::unique_ptr result = std::make_unique(); + if (result->initPython()) { + return result.release(); } else { - delete result; return nullptr; } } @@ -1299,19 +1304,11 @@ BOOST_PYTHON_MODULE(moprivate) }, bpy::arg("callback") = bpy::object{}); } -bool PythonRunner::initPython(const QString &pythonPath) +bool PythonRunner::initPython() { if (Py_IsInitialized()) return true; try { - if (!pythonPath.isEmpty() && !QFile::exists(pythonPath + "/python.exe")) { - return false; - } - pythonPath.toWCharArray(m_PythonHome); - if (!pythonPath.isEmpty()) { - Py_SetPythonHome(m_PythonHome); - } - wchar_t argBuffer[MAX_PATH]; const size_t cSize = strlen(argv0) + 1; mbstowcs(argBuffer, argv0, MAX_PATH); @@ -1561,15 +1558,7 @@ void PythonRunner::unload(const QString& identifier) } } -bool PythonRunner::isPythonInstalled() const +bool PythonRunner::isPythonInitialized() const { return Py_IsInitialized() != 0; } - - -bool PythonRunner::isPythonVersionSupported() const -{ - const char *version = Py_GetVersion(); - return strstr(version, "3.10") == version; -} - diff --git a/src/runner/pythonrunner.h b/src/runner/pythonrunner.h index be47860..ce8cad6 100644 --- a/src/runner/pythonrunner.h +++ b/src/runner/pythonrunner.h @@ -14,8 +14,7 @@ public: virtual QList load(const QString& identifier) = 0; virtual void unload(const QString& identifier) = 0; - virtual bool isPythonInstalled() const = 0; - virtual bool isPythonVersionSupported() const = 0; + virtual bool isPythonInitialized() const = 0; virtual ~IPythonRunner() { } }; @@ -27,7 +26,7 @@ public: #define PYDLLEXPORT Q_DECL_IMPORT #endif // PYTHONRUNNER_LIBRARY -extern "C" PYDLLEXPORT IPythonRunner *CreatePythonRunner(const QString &pythonDir); +extern "C" PYDLLEXPORT IPythonRunner *CreatePythonRunner();