OrcaSlicer latest
OrcaSlicer
Loading...
Searching...
No Matches
Slic3r::PythonInterpreter Class Reference

#include </home/runner/work/OrcaSlicer/OrcaSlicer/src/slic3r/plugin/PythonInterpreter.hpp>

Collaboration diagram for Slic3r::PythonInterpreter:

Public Member Functions

bool initialize ()
bool is_initialized () const
PythonRuntimeLease acquire_runtime_lease ()
const std::string & last_error () const
void shutdown ()
bool add_plugin_sys_path (const std::string &path, std::string &error)
PyObject * load_module_from_file (const std::string &file_path, std::string &error, std::vector< std::string > *plugin_paths=nullptr, std::vector< std::string > *plugin_modules=nullptr)
PyObject * load_module_from_whl (const std::string &whl_path, const std::string &pkg_name, std::string &error, std::vector< std::string > *plugin_paths=nullptr, std::vector< std::string > *plugin_modules=nullptr)
PyObject * load_module_from_directory (const std::string &dir_path, const std::string &pkg_name, std::string &error, std::vector< std::string > *plugin_paths=nullptr, std::vector< std::string > *plugin_modules=nullptr)
void unload_module (PyObject *module, const std::string &module_name, const std::vector< std::string > &plugin_paths, const std::vector< std::string > &plugin_modules)
 ~PythonInterpreter ()

Static Public Member Functions

static PythonInterpreter & instance ()
static std::string shared_packages_dir ()
static std::string bundled_python_executable ()
static std::string bundled_uv_path ()
static std::string python_abi_tag ()
static int join_non_daemon_threads (std::chrono::milliseconds timeout)

Private Member Functions

 PythonInterpreter ()=default
 PythonInterpreter (const PythonInterpreter &)=delete
PythonInterpreter & operator= (const PythonInterpreter &)=delete
bool add_plugin_sys_path_locked (const std::string &path, std::string &error)
void remove_plugin_sys_paths_locked (const std::vector< std::string > &paths)
void record_plugin_modules_locked (const std::string &module_name, const std::vector< std::string > &plugin_paths, std::vector< std::string > *plugin_modules)
void remove_plugin_modules_locked (const std::vector< std::string > &plugin_modules)
void remove_module_tree_locked (const std::string &module_name)
void abandon ()

Private Attributes

std::atomic< bool > m_initialized {false}
std::shared_timed_mutex m_runtime_mutex
PyThreadState * m_main_thread_state = nullptr
std::unique_ptr< pybind11::scoped_interpreter > m_interpreter
std::string m_last_error
std::unordered_map< std::string, std::size_t > m_plugin_path_users
std::unordered_map< std::string, bool > m_plugin_path_owned
std::unordered_map< std::string, std::size_t > m_plugin_module_users
std::unordered_map< std::string, bool > m_plugin_module_owned

Friends

class PythonRuntimeLease

Detailed Description

Constructor & Destructor Documentation

◆ ~PythonInterpreter()

Slic3r::PythonInterpreter::~PythonInterpreter ( )
689{ shutdown(); }
void shutdown()
Definition PythonInterpreter.cpp:691

References shutdown().

Here is the call graph for this function:

◆ PythonInterpreter() [1/2]

Slic3r::PythonInterpreter::PythonInterpreter ( )
privatedefault

Referenced by PythonInterpreter(), instance(), and operator=().

Here is the caller graph for this function:

◆ PythonInterpreter() [2/2]

Slic3r::PythonInterpreter::PythonInterpreter ( const PythonInterpreter & )
privatedelete

References PythonInterpreter().

Here is the call graph for this function:

Member Function Documentation

◆ abandon()

void Slic3r::PythonInterpreter::abandon ( )
private
743{
744 // New leases fail from here on. Releasing the scoped_interpreter without destroying it skips
745 // Py_Finalize, here and in the static destructor; the process exit ends the interpreter.
746 m_initialized.store(false, std::memory_order_release);
747 static_cast<void>(m_interpreter.release());
748}
std::atomic< bool > m_initialized
Definition PythonInterpreter.hpp:125
std::unique_ptr< pybind11::scoped_interpreter > m_interpreter
Definition PythonInterpreter.hpp:128

References m_initialized, and m_interpreter.

Referenced by shutdown().

Here is the caller graph for this function:

◆ acquire_runtime_lease()

PythonRuntimeLease Slic3r::PythonInterpreter::acquire_runtime_lease ( )
inline
163{
164 return PythonRuntimeLease(*this);
165}
friend class PythonRuntimeLease
Definition PythonInterpreter.hpp:110

References PythonRuntimeLease.

Referenced by Slic3r::PythonGILState::PythonGILState(), and Slic3r::ScriptAsyncRuntime::Impl::run_invocation().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ add_plugin_sys_path()

bool Slic3r::PythonInterpreter::add_plugin_sys_path ( const std::string & path,
std::string & error )
806{
807 if (!m_initialized.load(std::memory_order_acquire)) {
808 error = "Python interpreter not initialized";
809 return false;
810 }
811
812 PythonGILState gil;
813 if (!gil) {
814 error = "Python interpreter is shutting down";
815 return false;
816 }
817
818 return add_plugin_sys_path_locked(path, error);
819}
bool add_plugin_sys_path_locked(const std::string &path, std::string &error)
Definition PythonInterpreter.cpp:821

References add_plugin_sys_path_locked(), and m_initialized.

Referenced by Slic3r::plugin_loader::anonymous_namespace{PluginLoader.cpp}::add_wheel_dependencies_to_sys_path().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ add_plugin_sys_path_locked()

bool Slic3r::PythonInterpreter::add_plugin_sys_path_locked ( const std::string & path,
std::string & error )
private
822{
823 bool inserted = false;
824 if (!add_sys_path_entry(boost::filesystem::path(path), error, &inserted))
825 return false;
826
827 auto users = m_plugin_path_users.find(path);
828 if (users == m_plugin_path_users.end()) {
829 m_plugin_path_owned[path] = inserted;
830 m_plugin_path_users.emplace(path, 1);
831 } else {
832 ++users->second;
833 }
834
835 return true;
836}
std::unordered_map< std::string, bool > m_plugin_path_owned
Definition PythonInterpreter.hpp:131
std::unordered_map< std::string, std::size_t > m_plugin_path_users
Definition PythonInterpreter.hpp:130
bool add_sys_path_entry(const boost::filesystem::path &path, std::string &error, bool *inserted=nullptr)
Definition PythonInterpreter.cpp:192

References m_plugin_path_owned, and m_plugin_path_users.

Referenced by add_plugin_sys_path(), load_module_from_directory(), and load_module_from_file().

Here is the caller graph for this function:

◆ bundled_python_executable()

std::string Slic3r::PythonInterpreter::bundled_python_executable ( )
static
446{
447 const boost::filesystem::path python_home = find_bundled_python_home();
448 if (python_home.empty())
449 return {};
450
451 const boost::filesystem::path executable = find_python_executable(python_home);
452 return executable.empty() ? std::string{} : executable.string();
453}
boost::filesystem::path find_python_executable(const boost::filesystem::path &python_home)
Definition PythonInterpreter.cpp:372
boost::filesystem::path find_bundled_python_home()
Definition PythonInterpreter.cpp:325

Referenced by Slic3r::plugin_loader::install_packages(), and Slic3r::GUI::TerminalDialog::resolve_and_run().

Here is the caller graph for this function:

◆ bundled_uv_path()

std::string Slic3r::PythonInterpreter::bundled_uv_path ( )
static
456{
457 namespace fs = boost::filesystem;
458
459 const fs::path configured_uv = ORCA_BUNDLED_UV_EXECUTABLE;
460 if (!configured_uv.empty() && fs::exists(configured_uv) && fs::is_regular_file(configured_uv))
461 return configured_uv.string();
462
463 // <binary dir>/tools/uv covers the macOS bundle (Contents/MacOS/tools/uv)
464 // and build-tree runs; <resources>/tools/uv covers the install() and
465 // AppImage layouts.
466 const std::string uv_exe = executable_name("uv");
467 const std::vector<fs::path> candidates = {
468 fs::path(resources_dir()) / "tools" / "uv" / uv_exe,
469 boost::dll::program_location().parent_path() / "tools" / "uv" / uv_exe,
470 };
471
472 for (const fs::path& candidate : candidates) {
473 if (fs::exists(candidate) && fs::is_regular_file(candidate))
474 return candidate.string();
475 }
476
477 return {};
478}
std::string executable_name(const char *base)
Definition PythonInterpreter.cpp:183
const std::string & resources_dir()
Definition utils.cpp:296

References Slic3r::resources_dir().

Referenced by initialize(), Slic3r::plugin_loader::install_packages(), and Slic3r::GUI::TerminalDialog::resolve_and_run().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ initialize()

bool Slic3r::PythonInterpreter::initialize ( )
481{
482 std::unique_lock<std::shared_timed_mutex> runtime_lock(m_runtime_mutex);
483 if (m_initialized.load(std::memory_order_acquire)) {
484 return true;
485 }
486
487 m_last_error.clear();
488 m_plugin_path_users.clear();
489 m_plugin_path_owned.clear();
490 m_plugin_module_users.clear();
491 m_plugin_module_owned.clear();
492
493 try {
494 // Set Python home to the bundled Python installation
495 // This is critical for finding the standard library (encodings module, etc.)
496
497 namespace fs = boost::filesystem;
498 const std::string python_home = find_bundled_python_home().string();
499
500 if (python_home.empty()) {
501 m_last_error = "Could not locate bundled Python installation";
502 BOOST_LOG_TRIVIAL(error) << "Could not locate bundled Python installation";
503 BOOST_LOG_TRIVIAL(error) << "Configured bundled Python root: " << ORCA_BUNDLED_PYTHON_ROOT;
504#ifdef _WIN32
505 BOOST_LOG_TRIVIAL(error) << "Searched next to executable: "
506 << (boost::dll::program_location().parent_path() / "python").string();
507#endif
508 BOOST_LOG_TRIVIAL(error) << "Searched in resources_dir: " << resources_dir();
509#ifndef _WIN32
510 BOOST_LOG_TRIVIAL(error) << "Searched in data_dir: " << data_dir();
511#endif
512 BOOST_LOG_TRIVIAL(error) << "CMAKE_PREFIX_PATH: "
513 << (std::getenv("CMAKE_PREFIX_PATH") ? std::getenv("CMAKE_PREFIX_PATH") : "not set");
514 return false;
515 }
516
517 BOOST_LOG_TRIVIAL(info) << "Found bundled Python home: " << python_home;
518
519// Verify Python standard library directory exists
520#ifdef _WIN32
521 fs::path python_lib = fs::path(python_home) / "Lib";
522#else
523 fs::path python_lib = fs::path(python_home) / "lib" / PYTHON_STDLIB_DIR;
524#endif
525 if (!fs::exists(python_lib)) {
526 m_last_error = "Python standard library directory not found at: " + python_lib.string();
527 BOOST_LOG_TRIVIAL(error) << "Python standard library directory not found at: " << python_lib.string();
528 BOOST_LOG_TRIVIAL(error) << "Please build Python dependencies or check installation";
529 return false;
530 }
531
532 BOOST_LOG_TRIVIAL(info) << "Python standard library found at: " << python_lib.string();
533
534#ifdef _WIN32
535 fs::path python_dll = fs::exists(fs::path(python_home) / PYTHON_DLL) ? fs::path(python_home) / PYTHON_DLL :
536 fs::path(python_home) / PYTHON_DEBUG_DLL;
537 if (!fs::exists(python_dll)) {
538 m_last_error = "Python DLL not found in: " + python_home;
539 BOOST_LOG_TRIVIAL(error) << "Python DLL not found in: " << python_home;
540 return false;
541 }
542 BOOST_LOG_TRIVIAL(info) << "Python DLL found at: " << python_dll.string();
543 if (!fs::exists(fs::path(python_home) / "DLLs")) {
544 BOOST_LOG_TRIVIAL(warning) << "Python DLLs directory not found at: " << (fs::path(python_home) / "DLLs").string();
545 }
546#endif
547
548 // Log the exact paths being used for debugging
549 BOOST_LOG_TRIVIAL(info) << "Setting Python home to: " << python_home;
550 BOOST_LOG_TRIVIAL(info) << "Python 3.12 stdlib path: " << python_lib.string();
551
552 // Verify encodings module exists
553 fs::path encodings_path = python_lib / "encodings";
554 if (fs::exists(encodings_path)) {
555 BOOST_LOG_TRIVIAL(info) << "Encodings module found at: " << encodings_path.string();
556 } else {
557 BOOST_LOG_TRIVIAL(warning) << "Encodings module NOT found at: " << encodings_path.string();
558 }
559
560 BOOST_LOG_TRIVIAL(info) << "Using Python 3.12 PyConfig initialization API";
561
562 // Set Python home - this is the prefix where Python libraries are located
563 PyConfig config;
564 PyConfig_InitPythonConfig(&config);
565 // Do not let the host process's PYTHONPATH or user site-packages override the bundled
566 // runtime used by plugins.
567 config.use_environment = 0;
568 config.user_site_directory = 0;
569
570 BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
571 PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
572 if (PyStatus_Exception(status)) {
573 m_last_error = status.err_msg ? status.err_msg : "Failed to set Python home";
574 BOOST_LOG_TRIVIAL(error) << "Failed to set Python home to: " << python_home << ": " << m_last_error;
575 PyConfig_Clear(&config);
576 return false;
577 }
578 BOOST_LOG_TRIVIAL(debug) << "Python home set successfully";
579
580 // Set program name
581 status = PyConfig_SetBytesString(&config, &config.program_name, "OrcaSlicer");
582 if (PyStatus_Exception(status)) {
583 m_last_error = status.err_msg ? status.err_msg : "Failed to set program name";
584 BOOST_LOG_TRIVIAL(error) << "Failed to set program name: " << m_last_error;
585 PyConfig_Clear(&config);
586 return false;
587 }
588 BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
589
590 // Keep bytecode out of the Python home. The shipped .pyc files are stale against their
591 // sources, so a writable install would rewrite them inside the app (breaking a signed
592 // macOS bundle's seal) and a read-only one would recompile them on every launch.
593 const std::string pycache_prefix = data_dir() + "/python/pycache";
594#ifdef _WIN32
595 status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
596#else
597 status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
598#endif
599 if (PyStatus_Exception(status))
600 BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
601 << (status.err_msg ? status.err_msg : "unknown error");
602
603 // Use pybind11's scoped_interpreter which properly initializes
604 // pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
605 // does not set up thread state tracking that pybind11 requires.
606 BOOST_LOG_TRIVIAL(debug) << "Creating py::scoped_interpreter with PyConfig...";
607 try {
608 m_interpreter = std::make_unique<pybind11::scoped_interpreter>(&config);
609 } catch (const std::exception& ex) {
610 m_last_error = std::string("Python initialization failed: ") + ex.what();
611 BOOST_LOG_TRIVIAL(error) << m_last_error;
612 PyConfig_Clear(&config);
613 return false;
614 }
615 // PyConfig is cleared by pybind11's initialize_interpreter() internally.
616 BOOST_LOG_TRIVIAL(debug) << "py::scoped_interpreter initialized successfully";
617
618 if (!Py_IsInitialized()) {
619 m_last_error = "Python interpreter not initialized";
620 BOOST_LOG_TRIVIAL(error) << "Python interpreter not initialized";
621 return false;
622 }
623
624 // Log Python paths for debugging
625 PyObject* sys = PyImport_ImportModule("sys");
626 if (sys) {
627 PyObject* path = PyObject_GetAttrString(sys, "path");
628 if (path) {
629 PyObject* path_str = PyObject_Str(path);
630 if (path_str) {
631 const char* path_cstr = PyUnicode_AsUTF8(path_str);
632 if (path_cstr) {
633 BOOST_LOG_TRIVIAL(debug) << "Python sys.path: " << path_cstr;
634 }
635 Py_DECREF(path_str);
636 }
637 Py_DECREF(path);
638 }
639 Py_DECREF(sys);
640 }
641
642 BOOST_LOG_TRIVIAL(info) << "Python " << Py_GetVersion() << " initialized successfully";
643
644 const fs::path shared_packages = shared_packages_dir();
645 boost::system::error_code ec;
646 fs::create_directories(shared_packages, ec);
647 if (ec) {
648 BOOST_LOG_TRIVIAL(warning) << "Failed to create Python shared package directory: " << shared_packages.string() << ": "
649 << ec.message();
650 } else {
651 std::string path_error;
652 if (add_sys_path_entry(shared_packages, path_error))
653 BOOST_LOG_TRIVIAL(info) << "Added Python shared package directory to sys.path: " << shared_packages.string();
654 else
655 BOOST_LOG_TRIVIAL(warning) << path_error;
656 }
657
658 const std::string uv_path = bundled_uv_path();
659 if (!uv_path.empty())
660 BOOST_LOG_TRIVIAL(info) << "Bundled uv executable found at: " << uv_path;
661 else
662 BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
663
665
666 // Persist Python stderr (plugin tracebacks, including uncaught
667 // background-thread exceptions) to <data_dir>/log/python_*.log.
669
670 // Attribute plugin-spawned Python threads to the plugin/capability that created them.
671 // Must run while the GIL is still held here, after the audit hook is in place and
672 // before the GIL is released below.
674
675 // Release the GIL so other threads can acquire it via PyGILState_Ensure.
676 // Without this, calls from background threads will block trying to acquire the GIL.
677 m_main_thread_state = PyEval_SaveThread();
678 m_initialized.store(true, std::memory_order_release);
679 BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
680 return true;
681
682 } catch (const std::exception& ex) {
683 m_last_error = std::string("Exception initializing Python: ") + ex.what();
684 BOOST_LOG_TRIVIAL(error) << "Exception initializing Python: " << ex.what();
685 return false;
686 }
687}
void install_hook()
Definition PluginAuditManager.cpp:1800
static PluginAuditManager & instance()
Definition PluginAuditManager.cpp:535
PyThreadState * m_main_thread_state
Definition PythonInterpreter.hpp:127
std::shared_timed_mutex m_runtime_mutex
Definition PythonInterpreter.hpp:126
static std::string bundled_uv_path()
Definition PythonInterpreter.cpp:455
std::unordered_map< std::string, std::size_t > m_plugin_module_users
Definition PythonInterpreter.hpp:132
std::unordered_map< std::string, bool > m_plugin_module_owned
Definition PythonInterpreter.hpp:133
std::string m_last_error
Definition PythonInterpreter.hpp:129
static std::string shared_packages_dir()
Definition PythonInterpreter.cpp:440
void install_python_stderr_redirect()
Definition PythonInterpreter.cpp:276
constexpr const char * PYTHON_STDLIB_DIR
Definition PythonInterpreter.cpp:179
void install_python_thread_audit_shim()
Definition PythonPluginBridge.cpp:660
const std::string & data_dir()
Definition utils.cpp:365

References bundled_uv_path(), Slic3r::data_dir(), Slic3r::PluginAuditManager::install_hook(), Slic3r::install_python_thread_audit_shim(), Slic3r::PluginAuditManager::instance(), m_initialized, m_interpreter, m_last_error, m_main_thread_state, m_plugin_module_owned, m_plugin_module_users, m_plugin_path_owned, m_plugin_path_users, m_runtime_mutex, Slic3r::resources_dir(), and shared_packages_dir().

Referenced by Slic3r::PluginManager::initialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ instance()

PythonInterpreter & Slic3r::PythonInterpreter::instance ( )
static

◆ is_initialized()

bool Slic3r::PythonInterpreter::is_initialized ( ) const
inline
48{ return m_initialized.load(std::memory_order_acquire); }

References m_initialized.

Referenced by Slic3r::PluginManager::initialize(), and Slic3r::plugin_loader::load().

Here is the caller graph for this function:

◆ join_non_daemon_threads()

int Slic3r::PythonInterpreter::join_non_daemon_threads ( std::chrono::milliseconds timeout)
static
751{
752 try {
753 const pybind11::module_ threading = pybind11::module_::import("threading");
754 const pybind11::object main_thread = threading.attr("main_thread")();
755 const auto deadline = std::chrono::steady_clock::now() + timeout;
756 const auto seconds_left = [deadline] {
757 return std::chrono::duration<double>(
758 std::max(deadline - std::chrono::steady_clock::now(), std::chrono::steady_clock::duration::zero()))
759 .count();
760 };
761
762 // An idle concurrent.futures worker only ends once the hooks registered with
763 // threading._register_atexit() tell it to, which Py_Finalize does before it joins the
764 // threads. They run first here, on a daemon thread: a hook joins its workers without a
765 // timeout, so one stuck in a task would block the hook, and Py_Finalize, for good.
766 pybind11::object hooks;
767 if (pybind11::hasattr(threading, "_threading_atexits")) {
768 hooks = threading.attr("Thread")(pybind11::arg("target") = pybind11::cpp_function([threading] {
769 const pybind11::list calls = threading.attr("_threading_atexits");
770 for (pybind11::ssize_t i = calls.size(); i-- > 0;)
771 calls[i]();
772 }),
773 pybind11::arg("name") = "orca-threading-atexit", pybind11::arg("daemon") = true);
774 hooks.attr("start")();
775 hooks.attr("join")(seconds_left());
776 }
777
778 // Enumerated again after each pass: a thread being joined may start another one.
779 for (;;) {
780 int seen = 0;
781 int running = 0;
782 for (const pybind11::handle thread : threading.attr("enumerate")()) {
783 // Threads Python did not start (C++ threads that entered Python) are dummy threads,
784 // which are daemon and cannot be joined.
785 if (thread.is(main_thread) || thread.attr("daemon").cast<bool>())
786 continue;
787 ++seen;
788 thread.attr("join")(seconds_left());
789 if (thread.attr("is_alive")().cast<bool>())
790 ++running;
791 }
792 if (seen == 0 || std::chrono::steady_clock::now() >= deadline) {
793 // A hook still running would be run again, and waited for, by Py_Finalize.
794 if (hooks && hooks.attr("is_alive")().cast<bool>())
795 running = std::max(running, 1);
796 return running;
797 }
798 }
799 } catch (const std::exception& ex) {
800 BOOST_LOG_TRIVIAL(warning) << "Could not wait for Python threads: " << ex.what();
801 return 0;
802 }
803}

Referenced by shutdown().

Here is the caller graph for this function:

◆ last_error()

const std::string & Slic3r::PythonInterpreter::last_error ( ) const
inline
54{ return m_last_error; }

References m_last_error.

Referenced by Slic3r::PluginManager::initialize(), and Slic3r::plugin_loader::load().

Here is the caller graph for this function:

◆ load_module_from_directory()

PyObject * Slic3r::PythonInterpreter::load_module_from_directory ( const std::string & dir_path,
const std::string & pkg_name,
std::string & error,
std::vector< std::string > * plugin_paths = nullptr,
std::vector< std::string > * plugin_modules = nullptr )
1149{
1150 if (!m_initialized.load(std::memory_order_acquire)) {
1151 error = "Python interpreter not initialized";
1152 return nullptr;
1153 }
1154
1155 namespace fs = boost::filesystem;
1156 fs::path dir(dir_path);
1157
1158 if (!fs::exists(dir) || !fs::is_directory(dir)) {
1159 error = "Directory does not exist: " + dir_path;
1160 return nullptr;
1161 }
1162
1163 PythonGILState gil;
1164 if (!gil) {
1165 error = "Python interpreter is shutting down";
1166 return nullptr;
1167 }
1168
1169 const std::string directory = dir.string();
1170 if (!add_plugin_sys_path_locked(directory, error))
1171 return nullptr;
1172 if (plugin_paths)
1173 plugin_paths->push_back(directory);
1174
1175 remove_module_tree_locked(pkg_name);
1176
1177 PyObject* module = PyImport_ImportModule(pkg_name.c_str());
1178 record_plugin_modules_locked(pkg_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
1179 if (!module) {
1180 PyObject *ptype, *pvalue, *ptraceback;
1181 PyErr_Fetch(&ptype, &pvalue, &ptraceback);
1182 error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
1183 Py_XDECREF(ptype);
1184 Py_XDECREF(pvalue);
1185 Py_XDECREF(ptraceback);
1186 return nullptr;
1187 }
1188
1189 return module;
1190}
void remove_module_tree_locked(const std::string &module_name)
Definition PythonInterpreter.cpp:1032
void record_plugin_modules_locked(const std::string &module_name, const std::vector< std::string > &plugin_paths, std::vector< std::string > *plugin_modules)
Definition PythonInterpreter.cpp:877
std::string format_python_error(PyObject *ptype, PyObject *pvalue, PyObject *ptraceback)
Definition PythonInterpreter.cpp:132
FileTransferModule & module()
Definition FileTransferUtils.hpp:248

References add_plugin_sys_path_locked(), m_initialized, Slic3r::module(), record_plugin_modules_locked(), and remove_module_tree_locked().

Referenced by Slic3r::plugin_loader::anonymous_namespace{PluginLoader.cpp}::import_plugin_module(), and load_module_from_whl().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ load_module_from_file()

PyObject * Slic3r::PythonInterpreter::load_module_from_file ( const std::string & file_path,
std::string & error,
std::vector< std::string > * plugin_paths = nullptr,
std::vector< std::string > * plugin_modules = nullptr )
1095{
1096 if (!m_initialized.load(std::memory_order_acquire)) {
1097 error = "Python interpreter not initialized";
1098 return nullptr;
1099 }
1100
1101 namespace fs = boost::filesystem;
1102 fs::path path(file_path);
1103
1104 if (!fs::exists(path)) {
1105 error = "File does not exist: " + file_path;
1106 return nullptr;
1107 }
1108
1109 PythonGILState gil;
1110 if (!gil) {
1111 error = "Python interpreter is shutting down";
1112 return nullptr;
1113 }
1114
1115 // Add the directory to sys.path
1116 fs::path dir_path = path.parent_path();
1117 std::string module_name = path.stem().string();
1118
1119 const std::string dir = dir_path.string();
1120 if (!add_plugin_sys_path_locked(dir, error))
1121 return nullptr;
1122 if (plugin_paths)
1123 plugin_paths->push_back(dir);
1124
1125 // Ensure module is re-imported fresh by removing any cached instance.
1126 remove_module_tree_locked(module_name);
1127
1128 // Import the module
1129 PyObject* module = PyImport_ImportModule(module_name.c_str());
1130 record_plugin_modules_locked(module_name, plugin_paths ? *plugin_paths : std::vector<std::string>{}, plugin_modules);
1131 if (!module) {
1132 PyObject *ptype, *pvalue, *ptraceback;
1133 PyErr_Fetch(&ptype, &pvalue, &ptraceback);
1134 error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
1135 Py_XDECREF(ptype);
1136 Py_XDECREF(pvalue);
1137 Py_XDECREF(ptraceback);
1138 return nullptr;
1139 }
1140
1141 return module;
1142}

References add_plugin_sys_path_locked(), m_initialized, Slic3r::module(), record_plugin_modules_locked(), and remove_module_tree_locked().

Referenced by Slic3r::plugin_loader::anonymous_namespace{PluginLoader.cpp}::import_plugin_module().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ load_module_from_whl()

PyObject * Slic3r::PythonInterpreter::load_module_from_whl ( const std::string & whl_path,
const std::string & pkg_name,
std::string & error,
std::vector< std::string > * plugin_paths = nullptr,
std::vector< std::string > * plugin_modules = nullptr )
1197{
1198 if (!m_initialized.load(std::memory_order_acquire)) {
1199 error = "Python interpreter not initialized";
1200 return nullptr;
1201 }
1202
1203 namespace fs = boost::filesystem;
1204
1205 fs::path whl_path(file_path);
1206
1207 if (!fs::exists(whl_path)) {
1208 error = "File does not exist: " + file_path;
1209 return nullptr;
1210 }
1211
1212 fs::path extract_dir = whl_path.parent_path() / "__whl_extracted__" / pkg_name;
1213
1214 if (!fs::exists(extract_dir)) {
1215 if (!extract_zip_to_directory(whl_path, extract_dir, error))
1216 return nullptr;
1217 }
1218
1219 return load_module_from_directory(extract_dir.string(), pkg_name, error, plugin_paths, plugin_modules);
1220}
PyObject * load_module_from_directory(const std::string &dir_path, const std::string &pkg_name, std::string &error, std::vector< std::string > *plugin_paths=nullptr, std::vector< std::string > *plugin_modules=nullptr)
Definition PythonInterpreter.cpp:1144
bool extract_zip_to_directory(const boost::filesystem::path &zip_path, const boost::filesystem::path &destination, std::string &error)
Definition PluginFsUtils.cpp:659

References Slic3r::extract_zip_to_directory(), load_module_from_directory(), and m_initialized.

Referenced by Slic3r::plugin_loader::anonymous_namespace{PluginLoader.cpp}::import_plugin_module().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ operator=()

PythonInterpreter & Slic3r::PythonInterpreter::operator= ( const PythonInterpreter & )
privatedelete

References PythonInterpreter().

Here is the call graph for this function:

◆ python_abi_tag()

std::string Slic3r::PythonInterpreter::python_abi_tag ( )
static
430{
431 return std::string("cp") + std::to_string(PY_MAJOR_VERSION) + std::to_string(PY_MINOR_VERSION);
432}

Referenced by Slic3r::read_wheel_plugin_metadata(), and shared_packages_dir().

Here is the caller graph for this function:

◆ record_plugin_modules_locked()

void Slic3r::PythonInterpreter::record_plugin_modules_locked ( const std::string & module_name,
const std::vector< std::string > & plugin_paths,
std::vector< std::string > * plugin_modules )
private
880{
881 if (!plugin_modules)
882 return;
883
884 PyObject* modules = PyImport_GetModuleDict();
885 if (!modules)
886 return;
887
888 PyObjectPtr names(PyDict_Keys(modules));
889 if (!names) {
890 PyErr_Clear();
891 return;
892 }
893
894 const std::string prefix = module_name + ".";
895 const auto path_matches = [](const std::string& file, const std::string& root) {
896 namespace fs = boost::filesystem;
897
898 boost::system::error_code ec;
899 fs::path file_path = fs::weakly_canonical(fs::path(file), ec);
900 if (ec) {
901 ec.clear();
902 file_path = fs::absolute(fs::path(file), ec);
903 }
904 if (ec)
905 return false;
906
907 ec.clear();
908 fs::path root_path = fs::weakly_canonical(fs::path(root), ec);
909 if (ec) {
910 ec.clear();
911 root_path = fs::absolute(fs::path(root), ec);
912 }
913 if (ec)
914 return false;
915
916 const std::string file_string = file_path.generic_string();
917 std::string root_string = root_path.generic_string();
918 if (root_string.empty())
919 return false;
920 if (root_string.back() != '/')
921 root_string.push_back('/');
922 return file_string.compare(0, root_string.size(), root_string) == 0;
923 };
924
925 const Py_ssize_t count = PyList_Size(names.get());
926 for (Py_ssize_t index = 0; index < count; ++index) {
927 PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
928 if (!name_obj || !PyUnicode_Check(name_obj))
929 continue;
930
931 const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
932 if (!name_utf8) {
933 PyErr_Clear();
934 continue;
935 }
936
937 const std::string name(name_utf8);
938 const bool is_plugin_namespace = name == module_name || name.compare(0, prefix.size(), prefix) == 0;
939 bool is_plugin_path_module = false;
940 if (!is_plugin_namespace) {
941 PyObject* module = PyDict_GetItem(modules, name_obj); // borrowed reference
942 if (!module)
943 continue;
944
945 PyObjectPtr file(PyObject_GetAttrString(module, "__file__"));
946 if (file && PyUnicode_Check(file.get())) {
947 const char* file_utf8 = PyUnicode_AsUTF8(file.get());
948 if (file_utf8) {
949 for (const std::string& path : plugin_paths) {
950 if (path_matches(file_utf8, path)) {
951 is_plugin_path_module = true;
952 break;
953 }
954 }
955 } else {
956 PyErr_Clear();
957 }
958 } else {
959 PyErr_Clear();
960 }
961
962 if (!is_plugin_path_module) {
963 PyObjectPtr package_path(PyObject_GetAttrString(module, "__path__"));
964 if (package_path) {
965 const Py_ssize_t path_count = PySequence_Size(package_path.get());
966 if (path_count < 0) {
967 PyErr_Clear();
968 } else {
969 for (Py_ssize_t path_index = 0; path_index < path_count && !is_plugin_path_module; ++path_index) {
970 PyObjectPtr path_entry(PySequence_GetItem(package_path.get(), path_index));
971 if (!path_entry) {
972 PyErr_Clear();
973 continue;
974 }
975 if (!PyUnicode_Check(path_entry.get()))
976 continue;
977 const char* path_utf8 = PyUnicode_AsUTF8(path_entry.get());
978 if (!path_utf8) {
979 PyErr_Clear();
980 continue;
981 }
982 for (const std::string& path : plugin_paths) {
983 if (path_matches(path_utf8, path)) {
984 is_plugin_path_module = true;
985 break;
986 }
987 }
988 }
989 }
990 } else {
991 PyErr_Clear();
992 }
993 }
994 }
995
996 if (!is_plugin_namespace && !is_plugin_path_module)
997 continue;
998
999 auto users = m_plugin_module_users.find(name);
1000 if (users == m_plugin_module_users.end()) {
1001 m_plugin_module_owned[name] = true;
1002 m_plugin_module_users.emplace(name, 1);
1003 } else {
1004 ++users->second;
1005 }
1006 plugin_modules->push_back(name);
1007 }
1008}
static unsigned int count
Definition AuxiliaryDataViewModel.cpp:246

References count, Slic3r::PyObjectPtr::get(), m_plugin_module_owned, m_plugin_module_users, and Slic3r::module().

Referenced by load_module_from_directory(), and load_module_from_file().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ remove_module_tree_locked()

void Slic3r::PythonInterpreter::remove_module_tree_locked ( const std::string & module_name)
private
1033{
1034 if (module_name.empty())
1035 return;
1036
1037 PyObject* modules = PyImport_GetModuleDict();
1038 if (!modules)
1039 return;
1040
1041 PyObjectPtr names(PyDict_Keys(modules));
1042 if (!names) {
1043 PyErr_Clear();
1044 return;
1045 }
1046
1047 const std::string prefix = module_name + ".";
1048 const Py_ssize_t count = PyList_Size(names.get());
1049 for (Py_ssize_t index = 0; index < count; ++index) {
1050 PyObject* name_obj = PyList_GetItem(names.get(), index); // borrowed reference
1051 if (!name_obj || !PyUnicode_Check(name_obj))
1052 continue;
1053
1054 const char* name_utf8 = PyUnicode_AsUTF8(name_obj);
1055 if (!name_utf8) {
1056 PyErr_Clear();
1057 continue;
1058 }
1059
1060 const std::string name(name_utf8);
1061 if (name != module_name && name.compare(0, prefix.size(), prefix) != 0)
1062 continue;
1063
1064 if (PyDict_DelItem(modules, name_obj) != 0)
1065 PyErr_Clear();
1066 }
1067}

References count, and Slic3r::PyObjectPtr::get().

Referenced by load_module_from_directory(), load_module_from_file(), and unload_module().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ remove_plugin_modules_locked()

void Slic3r::PythonInterpreter::remove_plugin_modules_locked ( const std::vector< std::string > & plugin_modules)
private
1011{
1012 PyObject* modules = PyImport_GetModuleDict();
1013 if (!modules)
1014 return;
1015
1016 for (const std::string& name : plugin_modules) {
1017 auto users = m_plugin_module_users.find(name);
1018 if (users == m_plugin_module_users.end())
1019 continue;
1020
1021 if (--users->second != 0)
1022 continue;
1023
1024 const bool owned = m_plugin_module_owned[name];
1025 m_plugin_module_users.erase(users);
1026 m_plugin_module_owned.erase(name);
1027 if (owned && PyDict_DelItemString(modules, name.c_str()) != 0)
1028 PyErr_Clear();
1029 }
1030}

References m_plugin_module_owned, and m_plugin_module_users.

Referenced by unload_module().

Here is the caller graph for this function:

◆ remove_plugin_sys_paths_locked()

void Slic3r::PythonInterpreter::remove_plugin_sys_paths_locked ( const std::vector< std::string > & paths)
private
839{
840 PyObject* sys_path = PySys_GetObject("path");
841 if (!sys_path || !PyList_Check(sys_path)) {
842 PyErr_Clear();
843 return;
844 }
845
846 for (const std::string& path : paths) {
847 auto users = m_plugin_path_users.find(path);
848 if (users == m_plugin_path_users.end())
849 continue;
850
851 if (--users->second != 0)
852 continue;
853
854 const bool owned = m_plugin_path_owned[path];
855 m_plugin_path_users.erase(users);
856 m_plugin_path_owned.erase(path);
857 if (!owned)
858 continue;
859
860 PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
861 if (!py_path) {
862 PyErr_Clear();
863 continue;
864 }
865
866 for (Py_ssize_t index = PyList_Size(sys_path) - 1; index >= 0; --index) {
867 PyObject* entry = PyList_GetItem(sys_path, index); // borrowed reference
868 int equal = entry ? PyObject_RichCompareBool(entry, py_path.get(), Py_EQ) : 0;
869 if (equal == 1 && PySequence_DelItem(sys_path, index) != 0)
870 PyErr_Clear();
871 else if (equal < 0)
872 PyErr_Clear();
873 }
874 }
875}

References Slic3r::PyObjectPtr::get(), m_plugin_path_owned, and m_plugin_path_users.

Referenced by unload_module().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ shared_packages_dir()

std::string Slic3r::PythonInterpreter::shared_packages_dir ( )
static
441{
442 return (boost::filesystem::path(data_dir()) / "python" / "packages" / PythonInterpreter::python_abi_tag()).string();
443}
static std::string python_abi_tag()
Definition PythonInterpreter.cpp:429

References Slic3r::data_dir(), and python_abi_tag().

Referenced by initialize(), and Slic3r::plugin_loader::install_packages().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ shutdown()

void Slic3r::PythonInterpreter::shutdown ( )
692{
693 // Checked before waiting for the lock too, so a second call after abandon() returns at once.
694 if (!m_initialized.load(std::memory_order_acquire))
695 return;
696
697 // A plugin call that never returns keeps its runtime lease, and Py_Finalize joins every
698 // non-daemon thread, so either would keep a closing app alive. Both are waited for briefly.
699 constexpr auto wait = std::chrono::seconds(3);
700
701 std::unique_lock<std::shared_timed_mutex> runtime_lock(m_runtime_mutex, std::defer_lock);
702 if (!runtime_lock.try_lock_for(wait)) {
703 // The GIL is not taken back here: the stuck call may be holding it, and nothing on this
704 // thread runs Python after this point.
705 BOOST_LOG_TRIVIAL(warning) << "Python interpreter left unfinalized: a plugin call is still running";
706 abandon();
707 return;
708 }
709 if (!m_initialized.load(std::memory_order_acquire))
710 return;
711
712 BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
713
714 // Reacquire the GIL using the saved thread state before finalizing.
716 BOOST_LOG_TRIVIAL(debug) << "Restoring Python main thread state before shutdown";
717 PyEval_RestoreThread(m_main_thread_state);
718 m_main_thread_state = nullptr;
719 }
720
721 // Before the stdio tee is torn down, so tracebacks from threads still ending reach the log.
722 if (const int running = join_non_daemon_threads(wait); running > 0) {
723 // The GIL stays with this thread, so anything released during process exit still holds it.
724 BOOST_LOG_TRIVIAL(warning) << "Python interpreter left unfinalized: " << running << " non-daemon thread(s) still running";
725 abandon();
726 return;
727 }
728
730
731 BOOST_LOG_TRIVIAL(info) << "Finalizing Python interpreter";
732 m_interpreter.reset();
733 BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
734
735 m_plugin_path_users.clear();
736 m_plugin_path_owned.clear();
737 m_plugin_module_users.clear();
738 m_plugin_module_owned.clear();
739 m_initialized.store(false, std::memory_order_release);
740}
static int join_non_daemon_threads(std::chrono::milliseconds timeout)
Definition PythonInterpreter.cpp:750
void abandon()
Definition PythonInterpreter.cpp:742
void log_and_restore_python_stdio()
Definition PythonInterpreter.cpp:298

References abandon(), join_non_daemon_threads(), m_initialized, m_interpreter, m_main_thread_state, m_plugin_module_owned, m_plugin_module_users, m_plugin_path_owned, m_plugin_path_users, and m_runtime_mutex.

Referenced by Slic3r::GUI::GUI_App::~GUI_App(), ~PythonInterpreter(), and Slic3r::GUI::GUI_App::on_init_inner().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ unload_module()

void Slic3r::PythonInterpreter::unload_module ( PyObject * module,
const std::string & module_name,
const std::vector< std::string > & plugin_paths,
const std::vector< std::string > & plugin_modules )
1073{
1074 if (!module && plugin_paths.empty() && plugin_modules.empty())
1075 return;
1076
1077 if (!m_initialized.load(std::memory_order_acquire))
1078 return;
1079
1080 PythonGILState gil;
1081 if (!gil)
1082 return;
1083
1084 remove_plugin_modules_locked(plugin_modules);
1085 if (plugin_modules.empty())
1086 remove_module_tree_locked(module_name);
1087 remove_plugin_sys_paths_locked(plugin_paths);
1088 Py_XDECREF(module);
1089}
void remove_plugin_sys_paths_locked(const std::vector< std::string > &paths)
Definition PythonInterpreter.cpp:838
void remove_plugin_modules_locked(const std::vector< std::string > &plugin_modules)
Definition PythonInterpreter.cpp:1010

References m_initialized, Slic3r::module(), remove_module_tree_locked(), remove_plugin_modules_locked(), and remove_plugin_sys_paths_locked().

Referenced by Slic3r::Plugin::release_module().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ PythonRuntimeLease

friend class PythonRuntimeLease
friend

Member Data Documentation

◆ m_initialized

◆ m_interpreter

std::unique_ptr<pybind11::scoped_interpreter> Slic3r::PythonInterpreter::m_interpreter
private

Referenced by abandon(), initialize(), and shutdown().

◆ m_last_error

std::string Slic3r::PythonInterpreter::m_last_error
private

Referenced by initialize(), and last_error().

◆ m_main_thread_state

PyThreadState* Slic3r::PythonInterpreter::m_main_thread_state = nullptr
private

Referenced by initialize(), and shutdown().

◆ m_plugin_module_owned

std::unordered_map<std::string, bool> Slic3r::PythonInterpreter::m_plugin_module_owned
private

◆ m_plugin_module_users

std::unordered_map<std::string, std::size_t> Slic3r::PythonInterpreter::m_plugin_module_users
private

◆ m_plugin_path_owned

std::unordered_map<std::string, bool> Slic3r::PythonInterpreter::m_plugin_path_owned
private

◆ m_plugin_path_users

std::unordered_map<std::string, std::size_t> Slic3r::PythonInterpreter::m_plugin_path_users
private

◆ m_runtime_mutex

std::shared_timed_mutex Slic3r::PythonInterpreter::m_runtime_mutex
mutableprivate

The documentation for this class was generated from the following files: