mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 04:11:13 +00:00
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes. clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer. * Remove Unused Project Includes From Files With Platform-Specific Code A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block. * Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass The platform-file pass treated pchheader.hpp as an ordinary header and emptied it, and left GUI_Preview.hpp and 14 other files relying on headers they no longer reached directly. * Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call * Include Headers That Files Reached Through Ones the Cleanup Removed * Drop Includes Duplicated by the Cleanup or by Main's Own Additions * Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
1116 lines
38 KiB
C++
1116 lines
38 KiB
C++
#include "PythonInterpreter.hpp"
|
|
#include "GeneratedConfig.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "PluginAuditManager.hpp"
|
|
#include <atomic>
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <exception>
|
|
#include <pytypedefs.h>
|
|
#include "PluginFsUtils.hpp"
|
|
#include <pybind11/pybind11.h>
|
|
|
|
#include <pybind11/embed.h>
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/dll/runtime_symbol_info.hpp>
|
|
#include <boost/nowide/convert.hpp>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
#include <sstream>
|
|
#include <utility>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <pybind11/cast.h>
|
|
#include <pybind11/pytypes.h>
|
|
#include <Python.h>
|
|
|
|
namespace Slic3r {
|
|
|
|
thread_local PythonInterpreter* PythonRuntimeLease::s_owner = nullptr;
|
|
thread_local unsigned int PythonRuntimeLease::s_depth = 0;
|
|
|
|
PythonRuntimeLease::PythonRuntimeLease(PythonInterpreter& interpreter)
|
|
{
|
|
if (s_owner == &interpreter) {
|
|
m_interpreter = &interpreter;
|
|
++s_depth;
|
|
return;
|
|
}
|
|
|
|
m_lock = std::shared_lock<std::shared_mutex>(interpreter.m_runtime_mutex);
|
|
if (!interpreter.m_initialized.load(std::memory_order_acquire)) {
|
|
m_lock.unlock();
|
|
return;
|
|
}
|
|
|
|
m_interpreter = &interpreter;
|
|
s_owner = &interpreter;
|
|
s_depth = 1;
|
|
}
|
|
|
|
PythonRuntimeLease::PythonRuntimeLease(PythonRuntimeLease&& other) noexcept
|
|
: m_interpreter(other.m_interpreter), m_lock(std::move(other.m_lock))
|
|
{
|
|
other.m_interpreter = nullptr;
|
|
}
|
|
|
|
PythonRuntimeLease& PythonRuntimeLease::operator=(PythonRuntimeLease&& other) noexcept
|
|
{
|
|
if (this != &other) {
|
|
release();
|
|
m_lock = std::move(other.m_lock);
|
|
m_interpreter = other.m_interpreter;
|
|
other.m_interpreter = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void PythonRuntimeLease::release()
|
|
{
|
|
if (!m_interpreter)
|
|
return;
|
|
|
|
if (s_owner == m_interpreter && s_depth > 0) {
|
|
--s_depth;
|
|
if (s_depth == 0)
|
|
s_owner = nullptr;
|
|
}
|
|
m_interpreter = nullptr;
|
|
}
|
|
|
|
PythonRuntimeLease::~PythonRuntimeLease()
|
|
{
|
|
release();
|
|
}
|
|
|
|
void log_python_exception_keep(pybind11::error_already_set& err)
|
|
{
|
|
// The GIL may already be released here: the macro's gil_scoped_acquire is a
|
|
// local destroyed by stack unwinding before this catch runs. Touching Python
|
|
// state below needs the GIL. PyGILState_Ensure is reentrant — harmless if held.
|
|
PythonGILState gil;
|
|
if (!gil)
|
|
return;
|
|
|
|
// Traceback output is host-owned work. In particular, the stderr tee opens the Python log
|
|
// file on every write. Keep that open outside the plugin audit context, otherwise an ordinary
|
|
// exception raised by a plugin can recursively trigger the filesystem permission dialog while
|
|
// its original exception is being reported.
|
|
ScopedPluginAuditContext audit_suppression("");
|
|
|
|
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
|
|
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
|
|
// catchers can still read err.what() for the user-facing dialog. We must NOT use
|
|
// restore()+PyErr_Print() here as those empty err.
|
|
try {
|
|
namespace py = pybind11;
|
|
py::module_ tb = py::module_::import("traceback");
|
|
py::module_ sys = py::module_::import("sys");
|
|
tb.attr("print_exception")(err.type(), err.value(), err.trace(), py::none(), sys.attr("stderr"));
|
|
} catch (...) {
|
|
// Fallback: at least get the formatted message out. err.what() includes the
|
|
// traceback in recent pybind11 and does not consume err.
|
|
try {
|
|
pybind11::module_::import("sys").attr("stderr").attr("write")(std::string(err.what()) + "\n");
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::string format_python_error(PyObject* ptype, PyObject* pvalue, PyObject* ptraceback)
|
|
{
|
|
std::string result;
|
|
|
|
if (ptype) {
|
|
PyObject* type_name = PyObject_GetAttrString(ptype, "__name__");
|
|
if (type_name) {
|
|
const char* s = PyUnicode_AsUTF8(type_name);
|
|
if (s) result += s;
|
|
Py_DECREF(type_name);
|
|
}
|
|
}
|
|
|
|
if (pvalue) {
|
|
if (!result.empty()) result += ": ";
|
|
PyObject* s = PyObject_Str(pvalue);
|
|
if (s) {
|
|
const char* cstr = PyUnicode_AsUTF8(s);
|
|
if (cstr) result += cstr;
|
|
Py_DECREF(s);
|
|
}
|
|
}
|
|
|
|
if (ptraceback) {
|
|
result += "\nTraceback (most recent call last):";
|
|
PyTracebackObject* tb = reinterpret_cast<PyTracebackObject*>(ptraceback);
|
|
while (tb) {
|
|
PyFrameObject* frame = tb->tb_frame;
|
|
int line = PyFrame_GetLineNumber(frame);
|
|
PyCodeObject* code = PyFrame_GetCode(frame); // returns a NEW strong reference; must be released
|
|
const char* filename = code ? PyUnicode_AsUTF8(code->co_filename) : nullptr;
|
|
const char* funcname = code ? PyUnicode_AsUTF8(code->co_name) : nullptr;
|
|
result += "\n File \"" + std::string(filename ? filename : "?") +
|
|
"\", line " + std::to_string(line) +
|
|
", in " + std::string(funcname ? funcname : "?");
|
|
Py_XDECREF(code);
|
|
tb = tb->tb_next;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
constexpr const char* PYTHON_DLL = "python312.dll";
|
|
constexpr const char* PYTHON_DEBUG_DLL = "python312_d.dll";
|
|
#else
|
|
constexpr const char* PYTHON_STDLIB_DIR = "python3.12";
|
|
constexpr const char* PYTHON_EXECUTABLE = "python3.12";
|
|
#endif
|
|
|
|
std::string executable_name(const char* base)
|
|
{
|
|
#ifdef _WIN32
|
|
return std::string(base) + ".exe";
|
|
#else
|
|
return base;
|
|
#endif
|
|
}
|
|
|
|
bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error, bool* inserted = nullptr)
|
|
{
|
|
if (inserted)
|
|
*inserted = false;
|
|
|
|
PyObject* sys_path = PySys_GetObject("path");
|
|
if (!sys_path || !PyList_Check(sys_path)) {
|
|
error = "Python sys.path is not available";
|
|
return false;
|
|
}
|
|
|
|
const std::string path_str = path.string();
|
|
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path_str.c_str()));
|
|
if (!py_path) {
|
|
error = "Failed to decode path for Python sys.path: " + path_str;
|
|
PyErr_Clear();
|
|
return false;
|
|
}
|
|
|
|
const int contains = PySequence_Contains(sys_path, py_path.get());
|
|
if (contains == 1)
|
|
return true;
|
|
if (contains < 0)
|
|
PyErr_Clear();
|
|
|
|
if (PyList_Insert(sys_path, 0, py_path.get()) != 0) {
|
|
error = "Failed to add path to Python sys.path: " + path_str;
|
|
PyErr_Clear();
|
|
return false;
|
|
}
|
|
|
|
if (inserted)
|
|
*inserted = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void log_python_stream(PyObject* sys, const char* name)
|
|
{
|
|
PyObject* stream = PyObject_GetAttrString(sys, name);
|
|
if (!stream) {
|
|
PyErr_Clear();
|
|
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " is not set";
|
|
return;
|
|
}
|
|
|
|
PyObject* original = nullptr;
|
|
std::string original_name = std::string("__") + name + "__";
|
|
original = PyObject_GetAttrString(sys, original_name.c_str());
|
|
if (!original)
|
|
PyErr_Clear();
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " type=" << Py_TYPE(stream)->tp_name << " ptr=" << stream
|
|
<< " original_ptr=" << original << " is_original=" << (stream == original);
|
|
|
|
Py_XDECREF(original);
|
|
Py_DECREF(stream);
|
|
}
|
|
|
|
void restore_python_stream(PyObject* sys, const char* name)
|
|
{
|
|
std::string original_name = std::string("__") + name + "__";
|
|
PyObject* original = PyObject_GetAttrString(sys, original_name.c_str());
|
|
if (!original) {
|
|
PyErr_Clear();
|
|
original = Py_NewRef(Py_None);
|
|
}
|
|
|
|
if (PyObject_SetAttrString(sys, name, original) < 0) {
|
|
PyErr_Clear();
|
|
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to restore sys." << name;
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(info) << "Python shutdown: restored sys." << name << " to type=" << Py_TYPE(original)->tp_name
|
|
<< " ptr=" << original;
|
|
}
|
|
|
|
Py_DECREF(original);
|
|
}
|
|
|
|
// Tee Python sys.stderr to <data_dir>/log/python_*.log so plugin errors are
|
|
// persisted. Uncaught exceptions in plugin-spawned threads never cross the
|
|
// pybind11 boundary back into C++ — CPython's default threading.excepthook
|
|
// prints them to sys.stderr and lets the thread die — so capturing stderr is
|
|
// the one place all of them can be observed.
|
|
void install_python_stderr_redirect()
|
|
{
|
|
// Mirror the session filename convention from GUI_App: python_<weekday>_<mon>_<day>_<HH>_<MM>_<SS>_<pid>.log
|
|
std::time_t t = std::time(nullptr);
|
|
std::tm* now = std::localtime(&t);
|
|
std::ostringstream name;
|
|
name << std::put_time(now, "python_%a_%b_%d_%H_%M_%S_") << get_current_pid() << ".log";
|
|
const std::string log_path = (boost::filesystem::path(data_dir()) / "log" / name.str()).generic_string();
|
|
|
|
const std::string redirect_script =
|
|
"import io, os, sys, threading\n"
|
|
"_ORCA_PYTHON_LOG = \"" + log_path + "\"\n"
|
|
+ std::string(R"REDIRECT_SCRIPT(
|
|
class _OrcaTeeStderr(io.TextIOBase):
|
|
def __init__(self, original, path):
|
|
self._original = original
|
|
self._path = path
|
|
self._lock = threading.Lock()
|
|
def writable(self):
|
|
return True
|
|
def write(self, s):
|
|
try:
|
|
if self._original is not None:
|
|
self._original.write(s)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
with self._lock, open(self._path, "a", encoding="utf-8", errors="replace") as f:
|
|
f.write(s)
|
|
except Exception:
|
|
pass
|
|
return len(s)
|
|
def flush(self):
|
|
try:
|
|
if self._original is not None:
|
|
self._original.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
os.makedirs(os.path.dirname(_ORCA_PYTHON_LOG), exist_ok=True)
|
|
sys.stderr = _OrcaTeeStderr(sys.__stderr__, _ORCA_PYTHON_LOG)
|
|
del _OrcaTeeStderr
|
|
)REDIRECT_SCRIPT");
|
|
|
|
if (PyRun_SimpleString(redirect_script.c_str()) != 0) {
|
|
PyErr_Clear();
|
|
BOOST_LOG_TRIVIAL(warning) << "Failed to install Python stderr redirect to " << log_path;
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(info) << "Python stderr redirected to " << log_path;
|
|
}
|
|
}
|
|
|
|
void log_and_restore_python_stdio()
|
|
{
|
|
PyObject* sys = PyImport_ImportModule("sys");
|
|
if (!sys) {
|
|
PyErr_Clear();
|
|
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to import sys for stdio diagnostics";
|
|
return;
|
|
}
|
|
|
|
log_python_stream(sys, "stdout");
|
|
log_python_stream(sys, "stderr");
|
|
restore_python_stream(sys, "stdout");
|
|
restore_python_stream(sys, "stderr");
|
|
|
|
Py_DECREF(sys);
|
|
}
|
|
|
|
bool valid_python_home(const boost::filesystem::path& candidate)
|
|
{
|
|
#ifdef _WIN32
|
|
return boost::filesystem::exists(candidate / "Lib" / "encodings") &&
|
|
(boost::filesystem::exists(candidate / PYTHON_DLL) || boost::filesystem::exists(candidate / PYTHON_DEBUG_DLL));
|
|
#else
|
|
return boost::filesystem::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
|
|
#endif
|
|
}
|
|
|
|
boost::filesystem::path find_bundled_python_home()
|
|
{
|
|
namespace fs = boost::filesystem;
|
|
|
|
#ifdef __APPLE__
|
|
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
|
if (valid_python_home(bundle_python))
|
|
return bundle_python;
|
|
#elif !defined(_WIN32)
|
|
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
|
if (valid_python_home(linux_python))
|
|
return linux_python;
|
|
#endif
|
|
|
|
// Next to the executable: the Windows install layout, and the runtime copied
|
|
// beside every platform's unit-test binary (tests/slic3rutils/CMakeLists.txt).
|
|
// The CI test runner only receives the build/tests tree, so the candidates
|
|
// below -- all of which point into the deps or install trees -- never resolve
|
|
// there.
|
|
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
|
if (valid_python_home(exe_python))
|
|
return exe_python;
|
|
|
|
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
|
if (!configured_python.empty() && valid_python_home(configured_python))
|
|
return configured_python;
|
|
|
|
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
|
|
if (prefix_path && std::strlen(prefix_path) > 0) {
|
|
fs::path libpython = fs::path(prefix_path) / "libpython";
|
|
if (valid_python_home(libpython))
|
|
return libpython;
|
|
}
|
|
|
|
fs::path res_python = fs::path(resources_dir()) / "python";
|
|
if (valid_python_home(res_python))
|
|
return res_python;
|
|
|
|
#ifndef _WIN32
|
|
fs::path data_python = fs::path(data_dir()) / "python";
|
|
if (valid_python_home(data_python))
|
|
return data_python;
|
|
#endif
|
|
|
|
return {};
|
|
}
|
|
|
|
boost::filesystem::path find_python_executable(const boost::filesystem::path& python_home)
|
|
{
|
|
namespace fs = boost::filesystem;
|
|
|
|
#ifdef _WIN32
|
|
const std::vector<fs::path> candidates = {
|
|
python_home / "python.exe",
|
|
python_home / "python_d.exe",
|
|
};
|
|
#else
|
|
const std::vector<fs::path> candidates = {
|
|
python_home / "bin" / PYTHON_EXECUTABLE,
|
|
python_home / "bin" / "python3",
|
|
python_home / "bin" / "python",
|
|
};
|
|
#endif
|
|
|
|
for (const fs::path& candidate : candidates) {
|
|
if (fs::exists(candidate) && fs::is_regular_file(candidate))
|
|
return candidate;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string PythonInterpreter::python_abi_tag()
|
|
{
|
|
return std::string("cp") + std::to_string(PY_MAJOR_VERSION) + std::to_string(PY_MINOR_VERSION);
|
|
}
|
|
|
|
PythonInterpreter& PythonInterpreter::instance()
|
|
{
|
|
static PythonInterpreter inst;
|
|
return inst;
|
|
}
|
|
|
|
std::string PythonInterpreter::shared_packages_dir()
|
|
{
|
|
return (boost::filesystem::path(data_dir()) / "python" / "packages" / PythonInterpreter::python_abi_tag()).string();
|
|
}
|
|
|
|
std::string PythonInterpreter::bundled_python_executable()
|
|
{
|
|
const boost::filesystem::path python_home = find_bundled_python_home();
|
|
if (python_home.empty())
|
|
return {};
|
|
|
|
const boost::filesystem::path executable = find_python_executable(python_home);
|
|
return executable.empty() ? std::string{} : executable.string();
|
|
}
|
|
|
|
std::string PythonInterpreter::bundled_uv_path()
|
|
{
|
|
namespace fs = boost::filesystem;
|
|
|
|
const fs::path configured_uv = ORCA_BUNDLED_UV_EXECUTABLE;
|
|
if (!configured_uv.empty() && fs::exists(configured_uv) && fs::is_regular_file(configured_uv))
|
|
return configured_uv.string();
|
|
|
|
// <binary dir>/tools/uv covers the macOS bundle (Contents/MacOS/tools/uv)
|
|
// and build-tree runs; <resources>/tools/uv covers the install() and
|
|
// AppImage layouts.
|
|
const std::string uv_exe = executable_name("uv");
|
|
const std::vector<fs::path> candidates = {
|
|
fs::path(resources_dir()) / "tools" / "uv" / uv_exe,
|
|
boost::dll::program_location().parent_path() / "tools" / "uv" / uv_exe,
|
|
};
|
|
|
|
for (const fs::path& candidate : candidates) {
|
|
if (fs::exists(candidate) && fs::is_regular_file(candidate))
|
|
return candidate.string();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool PythonInterpreter::initialize()
|
|
{
|
|
std::unique_lock<std::shared_mutex> runtime_lock(m_runtime_mutex);
|
|
if (m_initialized.load(std::memory_order_acquire)) {
|
|
return true;
|
|
}
|
|
|
|
m_last_error.clear();
|
|
m_plugin_path_users.clear();
|
|
m_plugin_path_owned.clear();
|
|
m_plugin_module_users.clear();
|
|
m_plugin_module_owned.clear();
|
|
|
|
try {
|
|
// Set Python home to the bundled Python installation
|
|
// This is critical for finding the standard library (encodings module, etc.)
|
|
|
|
namespace fs = boost::filesystem;
|
|
const std::string python_home = find_bundled_python_home().string();
|
|
|
|
if (python_home.empty()) {
|
|
m_last_error = "Could not locate bundled Python installation";
|
|
BOOST_LOG_TRIVIAL(error) << "Could not locate bundled Python installation";
|
|
BOOST_LOG_TRIVIAL(error) << "Configured bundled Python root: " << ORCA_BUNDLED_PYTHON_ROOT;
|
|
#ifdef _WIN32
|
|
BOOST_LOG_TRIVIAL(error) << "Searched next to executable: "
|
|
<< (boost::dll::program_location().parent_path() / "python").string();
|
|
#endif
|
|
BOOST_LOG_TRIVIAL(error) << "Searched in resources_dir: " << resources_dir();
|
|
#ifndef _WIN32
|
|
BOOST_LOG_TRIVIAL(error) << "Searched in data_dir: " << data_dir();
|
|
#endif
|
|
BOOST_LOG_TRIVIAL(error) << "CMAKE_PREFIX_PATH: "
|
|
<< (std::getenv("CMAKE_PREFIX_PATH") ? std::getenv("CMAKE_PREFIX_PATH") : "not set");
|
|
return false;
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Found bundled Python home: " << python_home;
|
|
|
|
// Verify Python standard library directory exists
|
|
#ifdef _WIN32
|
|
fs::path python_lib = fs::path(python_home) / "Lib";
|
|
#else
|
|
fs::path python_lib = fs::path(python_home) / "lib" / PYTHON_STDLIB_DIR;
|
|
#endif
|
|
if (!fs::exists(python_lib)) {
|
|
m_last_error = "Python standard library directory not found at: " + python_lib.string();
|
|
BOOST_LOG_TRIVIAL(error) << "Python standard library directory not found at: " << python_lib.string();
|
|
BOOST_LOG_TRIVIAL(error) << "Please build Python dependencies or check installation";
|
|
return false;
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Python standard library found at: " << python_lib.string();
|
|
|
|
#ifdef _WIN32
|
|
fs::path python_dll = fs::exists(fs::path(python_home) / PYTHON_DLL) ? fs::path(python_home) / PYTHON_DLL :
|
|
fs::path(python_home) / PYTHON_DEBUG_DLL;
|
|
if (!fs::exists(python_dll)) {
|
|
m_last_error = "Python DLL not found in: " + python_home;
|
|
BOOST_LOG_TRIVIAL(error) << "Python DLL not found in: " << python_home;
|
|
return false;
|
|
}
|
|
BOOST_LOG_TRIVIAL(info) << "Python DLL found at: " << python_dll.string();
|
|
if (!fs::exists(fs::path(python_home) / "DLLs")) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Python DLLs directory not found at: " << (fs::path(python_home) / "DLLs").string();
|
|
}
|
|
#endif
|
|
|
|
// Log the exact paths being used for debugging
|
|
BOOST_LOG_TRIVIAL(info) << "Setting Python home to: " << python_home;
|
|
BOOST_LOG_TRIVIAL(info) << "Python 3.12 stdlib path: " << python_lib.string();
|
|
|
|
// Verify encodings module exists
|
|
fs::path encodings_path = python_lib / "encodings";
|
|
if (fs::exists(encodings_path)) {
|
|
BOOST_LOG_TRIVIAL(info) << "Encodings module found at: " << encodings_path.string();
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(warning) << "Encodings module NOT found at: " << encodings_path.string();
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Using Python 3.12 PyConfig initialization API";
|
|
|
|
// Set Python home - this is the prefix where Python libraries are located
|
|
PyConfig config;
|
|
PyConfig_InitPythonConfig(&config);
|
|
// Do not let the host process's PYTHONPATH or user site-packages override the bundled
|
|
// runtime used by plugins.
|
|
config.use_environment = 0;
|
|
config.user_site_directory = 0;
|
|
|
|
BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
|
|
PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
|
|
if (PyStatus_Exception(status)) {
|
|
m_last_error = status.err_msg ? status.err_msg : "Failed to set Python home";
|
|
BOOST_LOG_TRIVIAL(error) << "Failed to set Python home to: " << python_home << ": " << m_last_error;
|
|
PyConfig_Clear(&config);
|
|
return false;
|
|
}
|
|
BOOST_LOG_TRIVIAL(debug) << "Python home set successfully";
|
|
|
|
// Set program name
|
|
status = PyConfig_SetBytesString(&config, &config.program_name, "OrcaSlicer");
|
|
if (PyStatus_Exception(status)) {
|
|
m_last_error = status.err_msg ? status.err_msg : "Failed to set program name";
|
|
BOOST_LOG_TRIVIAL(error) << "Failed to set program name: " << m_last_error;
|
|
PyConfig_Clear(&config);
|
|
return false;
|
|
}
|
|
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
|
|
|
|
// Use pybind11's scoped_interpreter which properly initializes
|
|
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
|
|
// does not set up thread state tracking that pybind11 requires.
|
|
BOOST_LOG_TRIVIAL(debug) << "Creating py::scoped_interpreter with PyConfig...";
|
|
try {
|
|
m_interpreter = std::make_unique<pybind11::scoped_interpreter>(&config);
|
|
} catch (const std::exception& ex) {
|
|
m_last_error = std::string("Python initialization failed: ") + ex.what();
|
|
BOOST_LOG_TRIVIAL(error) << m_last_error;
|
|
PyConfig_Clear(&config);
|
|
return false;
|
|
}
|
|
// PyConfig is cleared by pybind11's initialize_interpreter() internally.
|
|
BOOST_LOG_TRIVIAL(debug) << "py::scoped_interpreter initialized successfully";
|
|
|
|
if (!Py_IsInitialized()) {
|
|
m_last_error = "Python interpreter not initialized";
|
|
BOOST_LOG_TRIVIAL(error) << "Python interpreter not initialized";
|
|
return false;
|
|
}
|
|
|
|
// Log Python paths for debugging
|
|
PyObject* sys = PyImport_ImportModule("sys");
|
|
if (sys) {
|
|
PyObject* path = PyObject_GetAttrString(sys, "path");
|
|
if (path) {
|
|
PyObject* path_str = PyObject_Str(path);
|
|
if (path_str) {
|
|
const char* path_cstr = PyUnicode_AsUTF8(path_str);
|
|
if (path_cstr) {
|
|
BOOST_LOG_TRIVIAL(debug) << "Python sys.path: " << path_cstr;
|
|
}
|
|
Py_DECREF(path_str);
|
|
}
|
|
Py_DECREF(path);
|
|
}
|
|
Py_DECREF(sys);
|
|
}
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Python " << Py_GetVersion() << " initialized successfully";
|
|
|
|
const fs::path shared_packages = shared_packages_dir();
|
|
boost::system::error_code ec;
|
|
fs::create_directories(shared_packages, ec);
|
|
if (ec) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Failed to create Python shared package directory: " << shared_packages.string() << ": "
|
|
<< ec.message();
|
|
} else {
|
|
std::string path_error;
|
|
if (add_sys_path_entry(shared_packages, path_error))
|
|
BOOST_LOG_TRIVIAL(info) << "Added Python shared package directory to sys.path: " << shared_packages.string();
|
|
else
|
|
BOOST_LOG_TRIVIAL(warning) << path_error;
|
|
}
|
|
|
|
const std::string uv_path = bundled_uv_path();
|
|
if (!uv_path.empty())
|
|
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable found at: " << uv_path;
|
|
else
|
|
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
|
|
|
|
PluginAuditManager::instance().install_hook();
|
|
|
|
// Persist Python stderr (plugin tracebacks, including uncaught
|
|
// background-thread exceptions) to <data_dir>/log/python_*.log.
|
|
install_python_stderr_redirect();
|
|
|
|
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
|
|
// Without this, calls from background threads will block trying to acquire the GIL.
|
|
m_main_thread_state = PyEval_SaveThread();
|
|
m_initialized.store(true, std::memory_order_release);
|
|
BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
|
|
return true;
|
|
|
|
} catch (const std::exception& ex) {
|
|
m_last_error = std::string("Exception initializing Python: ") + ex.what();
|
|
BOOST_LOG_TRIVIAL(error) << "Exception initializing Python: " << ex.what();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PythonInterpreter::~PythonInterpreter() { shutdown(); }
|
|
|
|
void PythonInterpreter::shutdown()
|
|
{
|
|
std::unique_lock<std::shared_mutex> runtime_lock(m_runtime_mutex);
|
|
if (!m_initialized.load(std::memory_order_acquire))
|
|
return;
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
|
|
|
|
// Reacquire the GIL using the saved thread state before finalizing.
|
|
if (m_main_thread_state) {
|
|
BOOST_LOG_TRIVIAL(debug) << "Restoring Python main thread state before shutdown";
|
|
PyEval_RestoreThread(m_main_thread_state);
|
|
m_main_thread_state = nullptr;
|
|
}
|
|
|
|
log_and_restore_python_stdio();
|
|
|
|
BOOST_LOG_TRIVIAL(info) << "Finalizing Python interpreter";
|
|
m_interpreter.reset();
|
|
BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
|
|
|
|
m_plugin_path_users.clear();
|
|
m_plugin_path_owned.clear();
|
|
m_plugin_module_users.clear();
|
|
m_plugin_module_owned.clear();
|
|
m_initialized.store(false, std::memory_order_release);
|
|
}
|
|
|
|
bool PythonInterpreter::add_plugin_sys_path(const std::string& path, std::string& error)
|
|
{
|
|
if (!m_initialized.load(std::memory_order_acquire)) {
|
|
error = "Python interpreter not initialized";
|
|
return false;
|
|
}
|
|
|
|
PythonGILState gil;
|
|
if (!gil) {
|
|
error = "Python interpreter is shutting down";
|
|
return false;
|
|
}
|
|
|
|
return add_plugin_sys_path_locked(path, error);
|
|
}
|
|
|
|
bool PythonInterpreter::add_plugin_sys_path_locked(const std::string& path, std::string& error)
|
|
{
|
|
bool inserted = false;
|
|
if (!add_sys_path_entry(boost::filesystem::path(path), error, &inserted))
|
|
return false;
|
|
|
|
auto users = m_plugin_path_users.find(path);
|
|
if (users == m_plugin_path_users.end()) {
|
|
m_plugin_path_owned[path] = inserted;
|
|
m_plugin_path_users.emplace(path, 1);
|
|
} else {
|
|
++users->second;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PythonInterpreter::remove_plugin_sys_paths_locked(const std::vector<std::string>& paths)
|
|
{
|
|
PyObject* sys_path = PySys_GetObject("path");
|
|
if (!sys_path || !PyList_Check(sys_path)) {
|
|
PyErr_Clear();
|
|
return;
|
|
}
|
|
|
|
for (const std::string& path : paths) {
|
|
auto users = m_plugin_path_users.find(path);
|
|
if (users == m_plugin_path_users.end())
|
|
continue;
|
|
|
|
if (--users->second != 0)
|
|
continue;
|
|
|
|
const bool owned = m_plugin_path_owned[path];
|
|
m_plugin_path_users.erase(users);
|
|
m_plugin_path_owned.erase(path);
|
|
if (!owned)
|
|
continue;
|
|
|
|
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
|
|
if (!py_path) {
|
|
PyErr_Clear();
|
|
continue;
|
|
}
|
|
|
|
for (Py_ssize_t index = PyList_Size(sys_path) - 1; index >= 0; --index) {
|
|
PyObject* entry = PyList_GetItem(sys_path, index); // borrowed reference
|
|
int equal = entry ? PyObject_RichCompareBool(entry, py_path.get(), Py_EQ) : 0;
|
|
if (equal == 1 && PySequence_DelItem(sys_path, index) != 0)
|
|
PyErr_Clear();
|
|
else if (equal < 0)
|
|
PyErr_Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PythonInterpreter::record_plugin_modules_locked(const std::string& module_name,
|
|
const std::vector<std::string>& plugin_paths,
|
|
std::vector<std::string>* plugin_modules)
|
|
{
|
|
if (!plugin_modules)
|
|
return;
|
|
|
|
PyObject* modules = PyImport_GetModuleDict();
|
|
if (!modules)
|
|
return;
|
|
|
|
PyObjectPtr names(PyDict_Keys(modules));
|
|
if (!names) {
|
|
PyErr_Clear();
|
|
return;
|
|
}
|
|
|
|
const std::string prefix = module_name + ".";
|
|
const auto path_matches = [](const std::string& file, const std::string& root) {
|
|
namespace fs = boost::filesystem;
|
|
|
|
boost::system::error_code ec;
|
|
fs::path file_path = fs::weakly_canonical(fs::path(file), ec);
|
|
if (ec) {
|
|
ec.clear();
|
|
file_path = fs::absolute(fs::path(file), ec);
|
|
}
|
|
if (ec)
|
|
return false;
|
|
|
|
ec.clear();
|
|
fs::path root_path = fs::weakly_canonical(fs::path(root), ec);
|
|
if (ec) {
|
|
ec.clear();
|
|
root_path = fs::absolute(fs::path(root), ec);
|
|
}
|
|
if (ec)
|
|
return false;
|
|
|
|
const std::string file_string = file_path.generic_string();
|
|
std::string root_string = root_path.generic_string();
|
|
if (root_string.empty())
|
|
return false;
|
|
if (root_string.back() != '/')
|
|
root_string.push_back('/');
|
|
return file_string.compare(0, root_string.size(), root_string) == 0;
|
|
};
|
|
|
|
const Py_ssize_t count = PyList_Size(names.get());
|
|
for (Py_ssize_t index = 0; index < count; ++index) {
|
|
PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
|
|
if (!name_obj || !PyUnicode_Check(name_obj))
|
|
continue;
|
|
|
|
const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
|
|
if (!name_utf8) {
|
|
PyErr_Clear();
|
|
continue;
|
|
}
|
|
|
|
const std::string name(name_utf8);
|
|
const bool is_plugin_namespace = name == module_name || name.compare(0, prefix.size(), prefix) == 0;
|
|
bool is_plugin_path_module = false;
|
|
if (!is_plugin_namespace) {
|
|
PyObject* module = PyDict_GetItem(modules, name_obj); // borrowed reference
|
|
if (!module)
|
|
continue;
|
|
|
|
PyObjectPtr file(PyObject_GetAttrString(module, "__file__"));
|
|
if (file && PyUnicode_Check(file.get())) {
|
|
const char* file_utf8 = PyUnicode_AsUTF8(file.get());
|
|
if (file_utf8) {
|
|
for (const std::string& path : plugin_paths) {
|
|
if (path_matches(file_utf8, path)) {
|
|
is_plugin_path_module = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
PyErr_Clear();
|
|
}
|
|
} else {
|
|
PyErr_Clear();
|
|
}
|
|
|
|
if (!is_plugin_path_module) {
|
|
PyObjectPtr package_path(PyObject_GetAttrString(module, "__path__"));
|
|
if (package_path) {
|
|
const Py_ssize_t path_count = PySequence_Size(package_path.get());
|
|
if (path_count < 0) {
|
|
PyErr_Clear();
|
|
} else {
|
|
for (Py_ssize_t path_index = 0; path_index < path_count && !is_plugin_path_module; ++path_index) {
|
|
PyObjectPtr path_entry(PySequence_GetItem(package_path.get(), path_index));
|
|
if (!path_entry) {
|
|
PyErr_Clear();
|
|
continue;
|
|
}
|
|
if (!PyUnicode_Check(path_entry.get()))
|
|
continue;
|
|
const char* path_utf8 = PyUnicode_AsUTF8(path_entry.get());
|
|
if (!path_utf8) {
|
|
PyErr_Clear();
|
|
continue;
|
|
}
|
|
for (const std::string& path : plugin_paths) {
|
|
if (path_matches(path_utf8, path)) {
|
|
is_plugin_path_module = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
PyErr_Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!is_plugin_namespace && !is_plugin_path_module)
|
|
continue;
|
|
|
|
auto users = m_plugin_module_users.find(name);
|
|
if (users == m_plugin_module_users.end()) {
|
|
m_plugin_module_owned[name] = true;
|
|
m_plugin_module_users.emplace(name, 1);
|
|
} else {
|
|
++users->second;
|
|
}
|
|
plugin_modules->push_back(name);
|
|
}
|
|
}
|
|
|
|
void PythonInterpreter::remove_plugin_modules_locked(const std::vector<std::string>& plugin_modules)
|
|
{
|
|
PyObject* modules = PyImport_GetModuleDict();
|
|
if (!modules)
|
|
return;
|
|
|
|
for (const std::string& name : plugin_modules) {
|
|
auto users = m_plugin_module_users.find(name);
|
|
if (users == m_plugin_module_users.end())
|
|
continue;
|
|
|
|
if (--users->second != 0)
|
|
continue;
|
|
|
|
const bool owned = m_plugin_module_owned[name];
|
|
m_plugin_module_users.erase(users);
|
|
m_plugin_module_owned.erase(name);
|
|
if (owned && PyDict_DelItemString(modules, name.c_str()) != 0)
|
|
PyErr_Clear();
|
|
}
|
|
}
|
|
|
|
void PythonInterpreter::remove_module_tree_locked(const std::string& module_name)
|
|
{
|
|
if (module_name.empty())
|
|
return;
|
|
|
|
PyObject* modules = PyImport_GetModuleDict();
|
|
if (!modules)
|
|
return;
|
|
|
|
PyObjectPtr names(PyDict_Keys(modules));
|
|
if (!names) {
|
|
PyErr_Clear();
|
|
return;
|
|
}
|
|
|
|
const std::string prefix = module_name + ".";
|
|
const Py_ssize_t count = PyList_Size(names.get());
|
|
for (Py_ssize_t index = 0; index < count; ++index) {
|
|
PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
|
|
if (!name_obj || !PyUnicode_Check(name_obj))
|
|
continue;
|
|
|
|
const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
|
|
if (!name_utf8) {
|
|
PyErr_Clear();
|
|
continue;
|
|
}
|
|
|
|
const std::string name(name_utf8);
|
|
if (name != module_name && name.compare(0, prefix.size(), prefix) != 0)
|
|
continue;
|
|
|
|
if (PyDict_DelItem(modules, name_obj) != 0)
|
|
PyErr_Clear();
|
|
}
|
|
}
|
|
|
|
void PythonInterpreter::unload_module(PyObject* module,
|
|
const std::string& module_name,
|
|
const std::vector<std::string>& plugin_paths,
|
|
const std::vector<std::string>& plugin_modules)
|
|
{
|
|
if (!module && plugin_paths.empty() && plugin_modules.empty())
|
|
return;
|
|
|
|
if (!m_initialized.load(std::memory_order_acquire))
|
|
return;
|
|
|
|
PythonGILState gil;
|
|
if (!gil)
|
|
return;
|
|
|
|
remove_plugin_modules_locked(plugin_modules);
|
|
if (plugin_modules.empty())
|
|
remove_module_tree_locked(module_name);
|
|
remove_plugin_sys_paths_locked(plugin_paths);
|
|
Py_XDECREF(module);
|
|
}
|
|
|
|
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path,
|
|
std::string& error,
|
|
std::vector<std::string>* plugin_paths,
|
|
std::vector<std::string>* plugin_modules)
|
|
{
|
|
if (!m_initialized.load(std::memory_order_acquire)) {
|
|
error = "Python interpreter not initialized";
|
|
return nullptr;
|
|
}
|
|
|
|
namespace fs = boost::filesystem;
|
|
fs::path path(file_path);
|
|
|
|
if (!fs::exists(path)) {
|
|
error = "File does not exist: " + file_path;
|
|
return nullptr;
|
|
}
|
|
|
|
PythonGILState gil;
|
|
if (!gil) {
|
|
error = "Python interpreter is shutting down";
|
|
return nullptr;
|
|
}
|
|
|
|
// Add the directory to sys.path
|
|
fs::path dir_path = path.parent_path();
|
|
std::string module_name = path.stem().string();
|
|
|
|
const std::string dir = dir_path.string();
|
|
if (!add_plugin_sys_path_locked(dir, error))
|
|
return nullptr;
|
|
if (plugin_paths)
|
|
plugin_paths->push_back(dir);
|
|
|
|
// Ensure module is re-imported fresh by removing any cached instance.
|
|
remove_module_tree_locked(module_name);
|
|
|
|
// Import the module
|
|
PyObject* module = PyImport_ImportModule(module_name.c_str());
|
|
record_plugin_modules_locked(module_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
|
|
if (!module) {
|
|
PyObject *ptype, *pvalue, *ptraceback;
|
|
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
|
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
|
|
Py_XDECREF(ptype);
|
|
Py_XDECREF(pvalue);
|
|
Py_XDECREF(ptraceback);
|
|
return nullptr;
|
|
}
|
|
|
|
return module;
|
|
}
|
|
|
|
PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_path,
|
|
const std::string& pkg_name,
|
|
std::string& error,
|
|
std::vector<std::string>* plugin_paths,
|
|
std::vector<std::string>* plugin_modules)
|
|
{
|
|
if (!m_initialized.load(std::memory_order_acquire)) {
|
|
error = "Python interpreter not initialized";
|
|
return nullptr;
|
|
}
|
|
|
|
namespace fs = boost::filesystem;
|
|
fs::path dir(dir_path);
|
|
|
|
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
|
error = "Directory does not exist: " + dir_path;
|
|
return nullptr;
|
|
}
|
|
|
|
PythonGILState gil;
|
|
if (!gil) {
|
|
error = "Python interpreter is shutting down";
|
|
return nullptr;
|
|
}
|
|
|
|
const std::string directory = dir.string();
|
|
if (!add_plugin_sys_path_locked(directory, error))
|
|
return nullptr;
|
|
if (plugin_paths)
|
|
plugin_paths->push_back(directory);
|
|
|
|
remove_module_tree_locked(pkg_name);
|
|
|
|
PyObject* module = PyImport_ImportModule(pkg_name.c_str());
|
|
record_plugin_modules_locked(pkg_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
|
|
if (!module) {
|
|
PyObject *ptype, *pvalue, *ptraceback;
|
|
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
|
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
|
|
Py_XDECREF(ptype);
|
|
Py_XDECREF(pvalue);
|
|
Py_XDECREF(ptraceback);
|
|
return nullptr;
|
|
}
|
|
|
|
return module;
|
|
}
|
|
|
|
PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path,
|
|
const std::string& pkg_name,
|
|
std::string& error,
|
|
std::vector<std::string>* plugin_paths,
|
|
std::vector<std::string>* plugin_modules)
|
|
{
|
|
if (!m_initialized.load(std::memory_order_acquire)) {
|
|
error = "Python interpreter not initialized";
|
|
return nullptr;
|
|
}
|
|
|
|
namespace fs = boost::filesystem;
|
|
|
|
fs::path whl_path(file_path);
|
|
|
|
if (!fs::exists(whl_path)) {
|
|
error = "File does not exist: " + file_path;
|
|
return nullptr;
|
|
}
|
|
|
|
fs::path extract_dir = whl_path.parent_path() / "__whl_extracted__" / pkg_name;
|
|
|
|
if (!fs::exists(extract_dir)) {
|
|
if (!extract_zip_to_directory(whl_path, extract_dir, error))
|
|
return nullptr;
|
|
}
|
|
|
|
return load_module_from_directory(extract_dir.string(), pkg_name, error, plugin_paths, plugin_modules);
|
|
}
|
|
|
|
} // namespace Slic3r
|