mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-18 05:00:55 +00:00
feat: preset APIs
This commit is contained in:
15 files changed
+2762
-277
No files matched your search
@@ -12,8 +12,11 @@
|
||||
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginHooks.hpp"
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "host/PluginHostUi.hpp"
|
||||
|
||||
#include "OrcaCloudServiceAgent.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
@@ -41,6 +44,7 @@
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/thread.h>
|
||||
#include <wx/utils.h>
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
|
||||
@@ -68,8 +72,10 @@ void Plugin::release_module()
|
||||
PluginManager& PluginManager::instance()
|
||||
{
|
||||
// Touch the interpreter first so its static outlives this one: ~Plugin needs it alive to
|
||||
// release module, sys.modules, and sys.path ownership safely.
|
||||
// release module, sys.modules, and sys.path ownership safely. Touch the async Script runtime
|
||||
// next, so ~PluginManager can still drain it during its own shutdown().
|
||||
PythonInterpreter::instance();
|
||||
ScriptAsyncRuntime::instance();
|
||||
static PluginManager inst;
|
||||
return inst;
|
||||
}
|
||||
@@ -101,6 +107,9 @@ bool PluginManager::initialize()
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
// Re-arm the async Script runtime in case shutdown() ran earlier in this process.
|
||||
ScriptAsyncRuntime::instance().reset();
|
||||
|
||||
// Bring every capability's stored config into memory. Deliberately unconditional and
|
||||
// independent of which plugins are installed: an entry outlives uninstall/unsubscribe, so a
|
||||
// plugin that comes back later finds its settings intact. A missing or malformed file just
|
||||
@@ -167,6 +176,10 @@ void PluginManager::shutdown()
|
||||
unload_all_plugins();
|
||||
PythonPluginBridge::instance().clear_pending_captures();
|
||||
|
||||
// Every async Script run has been drained by unload_all_plugins above; wait for every
|
||||
// invocation worker before the interpreter is allowed to finalize.
|
||||
ScriptAsyncRuntime::instance().shutdown();
|
||||
|
||||
// Every config write already goes to disk as it happens (store_capability_config), so this
|
||||
// only catches an in-memory-only mutation. Note we flush rather than clear: unloading the
|
||||
// plugins above must never discard their stored config.
|
||||
@@ -754,6 +767,10 @@ void PluginManager::release_load_slot(const std::string& plugin_key)
|
||||
void PluginManager::cancel_and_wait_for_capabilities(
|
||||
const std::vector<std::shared_ptr<PluginCapabilityInterface>>& capabilities)
|
||||
{
|
||||
// Ask the plugin to cancel first, then schedule the async cancellation: on_cancelled() can
|
||||
// observe/record state while the run is still intact, and only afterwards is its asyncio task
|
||||
// cancelled. Draining of capability references and posted host UI callbacks still follows, so
|
||||
// the module is not unloaded until both have settled.
|
||||
for (const auto& capability : capabilities) {
|
||||
if (!capability || !capability->is_enabled())
|
||||
continue;
|
||||
@@ -769,15 +786,55 @@ void PluginManager::cancel_and_wait_for_capabilities(
|
||||
}
|
||||
}
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
for (const auto& capability : capabilities) {
|
||||
if (capability)
|
||||
ScriptAsyncRuntime::instance().cancel_plugin(capability->audit_plugin_key());
|
||||
}
|
||||
|
||||
// Distinct plugins whose posted host UI work must settle before module unload. A Tab::select_preset
|
||||
// dialog accepted while the plugin is being unloaded would otherwise mutate presets after unload,
|
||||
// so the drain below waits for every posted UI callback to return, not just for the run's ref.
|
||||
std::vector<std::string> plugin_keys;
|
||||
for (const auto& capability : capabilities) {
|
||||
if (!capability)
|
||||
continue;
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
if (!key.empty() && std::find(plugin_keys.begin(), plugin_keys.end(), key) == plugin_keys.end())
|
||||
plugin_keys.push_back(key);
|
||||
}
|
||||
|
||||
const auto host_pending = [&plugin_keys]() {
|
||||
return std::any_of(plugin_keys.begin(), plugin_keys.end(),
|
||||
[](const std::string& key) { return PluginHostUi::pending_async_count(key) > 0; });
|
||||
};
|
||||
|
||||
// Script rows are bounded only by their own worker: a cancelled or stalled invocation of another
|
||||
// plugin can no longer couple to this one (each invocation owns a thread). A Script worker or a
|
||||
// posted host UI callback is drained without a deadline — forcing module unload while either is
|
||||
// outstanding would leave Python executing against a freed module, or apply a preset selection
|
||||
// after the plugin is gone. Non-Script synchronous calls keep the historical bounded grace. On
|
||||
// the GUI thread the event loop is pumped so the UI callback (possibly a modal) can settle
|
||||
// instead of being starved by a blocking wait.
|
||||
const auto grace_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
for (;;) {
|
||||
const bool all_released = std::all_of(capabilities.begin(), capabilities.end(), [](const auto& capability) {
|
||||
return !capability || capability->ref_count() == 0;
|
||||
});
|
||||
if (all_released)
|
||||
// A cancellation that arrived before the invocation ran skips the ref counter, so an active
|
||||
// worker must also be gone: never unload the module while any Script thread is still live.
|
||||
const bool invocation_active = std::any_of(capabilities.begin(), capabilities.end(), [](const auto& capability) {
|
||||
return capability != nullptr && ScriptAsyncRuntime::instance().has_active_run(capability.get());
|
||||
});
|
||||
const bool ui_pending = host_pending();
|
||||
if (all_released && !invocation_active && !ui_pending)
|
||||
return;
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (!invocation_active && !ui_pending && std::chrono::steady_clock::now() >= grace_deadline)
|
||||
break;
|
||||
|
||||
if (wxTheApp != nullptr && wxIsMainThread())
|
||||
wxYield();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
|
||||
for (const auto& capability : capabilities) {
|
||||
@@ -1006,6 +1063,12 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
|
||||
|
||||
bool PluginManager::unload_plugin(const std::string& plugin_key)
|
||||
{
|
||||
// Stop delivery from any posted host UI awaitable for this plugin before anything is torn down,
|
||||
// so the earliest possible UI callback cannot resume the plugin's coroutine into a run that is
|
||||
// already being cancelled. Registrations stay live so cancel_and_wait_for_capabilities() drains
|
||||
// them; a Tab::select_preset dialog accepted after unload must not mutate presets post-teardown.
|
||||
PluginHostUi::abandon_pending_async(plugin_key);
|
||||
|
||||
auto notify_unload_complete = [this, &plugin_key]() {
|
||||
if (!m_shutting_down.load(std::memory_order_acquire))
|
||||
run_on_unload_callbacks(plugin_key);
|
||||
@@ -1080,6 +1143,11 @@ void PluginManager::unload_all_plugins()
|
||||
|
||||
// Bulk shutdown intentionally skips the normal unload notification path, but resources owned
|
||||
// by GUI subscribers still need to be torn down before the interpreter is finalized.
|
||||
// Abandon posted host awaitables first, so a late UI callback cannot resolve into a plugin
|
||||
// whose module is about to go; they stay tracked for cancel_and_wait_for_capabilities() below.
|
||||
for (const std::string& key : removed_keys)
|
||||
PluginHostUi::abandon_pending_async(key);
|
||||
|
||||
for (const std::string& key : removed_keys)
|
||||
run_on_unload_callbacks(key);
|
||||
|
||||
@@ -2070,41 +2138,385 @@ bool PluginManager::delete_and_unsubscribe_cloud_plugin(const std::string& plugi
|
||||
return finalize_cloud_plugin_removal(descriptor, false, error);
|
||||
}
|
||||
|
||||
// ── Async Script runtime ────────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
// The asyncio loop of the Script invocation running on the calling thread. Owned +1 while set;
|
||||
// always cleared under the GIL before the worker thread exits.
|
||||
thread_local PyObject* tl_script_loop = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ScriptAsyncRuntime::Impl
|
||||
{
|
||||
struct Run
|
||||
{
|
||||
std::string plugin_key;
|
||||
std::string capability_name;
|
||||
std::shared_ptr<PluginCapabilityInterface> capability;
|
||||
std::function<void(const ScriptCompletion&)> on_done;
|
||||
std::atomic<bool> cancel_requested{false};
|
||||
// True while an asyncio loop/task is live for this run. Read without the GIL by
|
||||
// cancel_plugin() to avoid taking the GIL for a purely synchronous invocation.
|
||||
std::atomic<bool> has_loop{false};
|
||||
|
||||
// Live asyncio loop/task for cancellation. Guarded by handle_mutex and only touched
|
||||
// with the GIL held.
|
||||
std::mutex handle_mutex;
|
||||
pybind11::object loop;
|
||||
pybind11::object task;
|
||||
};
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::shared_ptr<Run>> active;
|
||||
bool stopping = false;
|
||||
|
||||
// Runs one invocation to completion on the calling (worker) thread.
|
||||
void run_invocation(const std::shared_ptr<Run>& run);
|
||||
};
|
||||
|
||||
void ScriptAsyncRuntime::Impl::run_invocation(const std::shared_ptr<Run>& run)
|
||||
{
|
||||
ScriptCompletion completion;
|
||||
|
||||
{
|
||||
PythonRuntimeLease lease = PythonInterpreter::instance().acquire_runtime_lease();
|
||||
if (!lease) {
|
||||
completion.error = _u8L("Python interpreter is shutting down");
|
||||
} else {
|
||||
pybind11::gil_scoped_acquire gil;
|
||||
|
||||
if (run->cancel_requested.load(std::memory_order_acquire)) {
|
||||
// Cancelled before it ran: expected termination, never an error.
|
||||
completion.cancelled = true;
|
||||
} else {
|
||||
// Hold the capability reference, the audit scope and the GIL for the whole
|
||||
// invocation, so unload/shutdown cannot tear the module out from under it.
|
||||
PluginCapabilityInterface::RefCounter ref_counter(*run->capability);
|
||||
|
||||
std::optional<ScopedPluginAuditContext> audit_scope;
|
||||
if (!run->capability->audit_plugin_key().empty())
|
||||
audit_scope.emplace(run->capability->audit_plugin_key(), run->capability->name());
|
||||
|
||||
// Detection and execution both happen here, on the worker: a synchronous
|
||||
// `def execute` runs to completion off the UI thread, exactly like an `async def`.
|
||||
ScriptExecuteInvocation invocation = invoke_script_execute(run->capability);
|
||||
if (!invocation.error.empty()) {
|
||||
completion.error = invocation.error;
|
||||
} else if (!invocation.is_async) {
|
||||
completion.result = invocation.result;
|
||||
} else if (run->cancel_requested.load(std::memory_order_acquire)) {
|
||||
completion.cancelled = true;
|
||||
} else {
|
||||
try {
|
||||
pybind11::module_ asyncio = pybind11::module_::import("asyncio");
|
||||
pybind11::object loop = asyncio.attr("new_event_loop")();
|
||||
asyncio.attr("set_event_loop")(loop);
|
||||
|
||||
tl_script_loop = loop.ptr();
|
||||
Py_INCREF(tl_script_loop);
|
||||
|
||||
pybind11::object task = loop.attr("create_task")(std::move(invocation.awaitable));
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(run->handle_mutex);
|
||||
run->loop = loop;
|
||||
run->task = task;
|
||||
}
|
||||
run->has_loop.store(true, std::memory_order_release);
|
||||
// A cancellation that arrived before the loop came up must still land.
|
||||
if (run->cancel_requested.load(std::memory_order_acquire))
|
||||
loop.attr("call_soon_threadsafe")(task.attr("cancel"));
|
||||
|
||||
try {
|
||||
pybind11::object returned = loop.attr("run_until_complete")(task);
|
||||
completion.result = returned.cast<ExecutionResult>();
|
||||
} catch (pybind11::error_already_set& err) {
|
||||
// Only our own cancel_plugin() cancellation is expected termination; a
|
||||
// CancelledError the plugin raised on its own stays a real error.
|
||||
if (err.matches(asyncio.attr("CancelledError")) &&
|
||||
run->cancel_requested.load(std::memory_order_acquire)) {
|
||||
completion.cancelled = true;
|
||||
} else {
|
||||
log_python_exception_keep(err);
|
||||
completion.error = err.what();
|
||||
}
|
||||
}
|
||||
|
||||
run->has_loop.store(false, std::memory_order_release);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(run->handle_mutex);
|
||||
run->task = pybind11::object();
|
||||
run->loop = pybind11::object();
|
||||
}
|
||||
Py_CLEAR(tl_script_loop);
|
||||
loop.attr("close")();
|
||||
asyncio.attr("set_event_loop")(pybind11::none());
|
||||
} catch (pybind11::error_already_set& err) {
|
||||
log_python_exception_keep(err);
|
||||
completion.error = err.what();
|
||||
} catch (const std::exception& ex) {
|
||||
completion.error = ex.what();
|
||||
}
|
||||
|
||||
// Covers an exception thrown after the loop came up but before the clear above.
|
||||
if (run->has_loop.load(std::memory_order_acquire)) {
|
||||
run->has_loop.store(false, std::memory_order_release);
|
||||
std::lock_guard<std::mutex> lock(run->handle_mutex);
|
||||
run->task = pybind11::object();
|
||||
run->loop = pybind11::object();
|
||||
}
|
||||
|
||||
if (tl_script_loop != nullptr)
|
||||
Py_CLEAR(tl_script_loop);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the capability reference while the GIL is held: releasing a pybind holder from
|
||||
// a non-Python thread would DECREF the capability without the GIL. Runs on every
|
||||
// lease-valid path, including an invocation cancelled before it ran.
|
||||
run->capability.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// A cancellation requested by cancel_plugin() (unload/shutdown) is expected termination and is
|
||||
// authoritative: whatever the body did — finished, swallowed CancelledError, or failed — nothing
|
||||
// is surfaced as an error or as a normal success.
|
||||
if (run->cancel_requested.load(std::memory_order_acquire)) {
|
||||
completion.cancelled = true;
|
||||
completion.error.clear();
|
||||
}
|
||||
|
||||
auto on_done = std::move(run->on_done);
|
||||
auto deliver = [on_done, completion]() {
|
||||
if (on_done)
|
||||
on_done(completion);
|
||||
};
|
||||
|
||||
if (wxTheApp == nullptr || wxIsMainThread())
|
||||
deliver();
|
||||
else
|
||||
GUI::wxGetApp().CallAfter(deliver);
|
||||
|
||||
// Leave `active` last, so a drain cannot observe the run as finished before its completion was
|
||||
// queued/delivered and its capability reference released.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
active.erase(std::remove(active.begin(), active.end(), run), active.end());
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
ScriptAsyncRuntime& ScriptAsyncRuntime::instance()
|
||||
{
|
||||
static ScriptAsyncRuntime inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
ScriptAsyncRuntime::ScriptAsyncRuntime() : m_impl(std::make_unique<Impl>()) {}
|
||||
|
||||
ScriptAsyncRuntime::~ScriptAsyncRuntime() { shutdown(); }
|
||||
|
||||
bool ScriptAsyncRuntime::start(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
std::shared_ptr<PluginCapabilityInterface> capability,
|
||||
std::function<void(const ScriptCompletion&)> on_done)
|
||||
{
|
||||
if (!PythonInterpreter::instance().is_initialized())
|
||||
return false;
|
||||
|
||||
auto run = std::make_shared<Impl::Run>();
|
||||
run->plugin_key = plugin_key;
|
||||
run->capability_name = capability_name;
|
||||
run->capability = std::move(capability);
|
||||
run->on_done = std::move(on_done);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
if (m_impl->stopping)
|
||||
return false;
|
||||
m_impl->active.push_back(run);
|
||||
}
|
||||
|
||||
// One worker thread per invocation. A stalled invocation of one plugin can never delay another
|
||||
// plugin's unload or another invocation, and no invocation body ever runs on the caller (UI)
|
||||
// thread. The thread holds `run` (and so the capability) until it finishes; the runtime tracks
|
||||
// it in `active` and waits for it on shutdown.
|
||||
Impl* impl = m_impl.get();
|
||||
try {
|
||||
std::thread([impl, run] { impl->run_invocation(run); }).detach();
|
||||
} catch (...) {
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
m_impl->active.erase(std::remove(m_impl->active.begin(), m_impl->active.end(), run), m_impl->active.end());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScriptAsyncRuntime::cancel_plugin(const std::string& plugin_key)
|
||||
{
|
||||
std::vector<std::shared_ptr<Impl::Run>> runs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
for (const auto& run : m_impl->active)
|
||||
if (run->plugin_key == plugin_key)
|
||||
runs.push_back(run);
|
||||
}
|
||||
|
||||
for (const auto& run : runs)
|
||||
run->cancel_requested.store(true, std::memory_order_release);
|
||||
|
||||
for (const auto& run : runs) {
|
||||
// A synchronous invocation has no loop to cancel; skip it so the caller never blocks on
|
||||
// the GIL while that invocation is (uninterruptibly) running.
|
||||
if (!run->has_loop.load(std::memory_order_acquire))
|
||||
continue;
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
continue;
|
||||
|
||||
pybind11::object loop;
|
||||
pybind11::object task;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(run->handle_mutex);
|
||||
loop = run->loop;
|
||||
task = run->task;
|
||||
}
|
||||
if (!loop || !task)
|
||||
continue;
|
||||
|
||||
try {
|
||||
loop.attr("call_soon_threadsafe")(task.attr("cancel"));
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Script async cancel failed for plugin '" << plugin_key << "': " << ex.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptAsyncRuntime::has_active_run(const PluginCapabilityInterface* capability) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
for (const auto& run : m_impl->active)
|
||||
if (run->capability.get() == capability)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScriptAsyncRuntime::shutdown()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
m_impl->stopping = true;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(m_impl->mutex);
|
||||
m_impl->cv.wait(lock, [this] { return m_impl->active.empty(); });
|
||||
}
|
||||
|
||||
void ScriptAsyncRuntime::reset()
|
||||
{
|
||||
shutdown();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_impl->mutex);
|
||||
m_impl->stopping = false;
|
||||
}
|
||||
|
||||
pybind11::object ScriptAsyncRuntime::current_loop()
|
||||
{
|
||||
if (tl_script_loop == nullptr)
|
||||
return pybind11::object();
|
||||
return pybind11::reinterpret_borrow<pybind11::object>(pybind11::handle(tl_script_loop));
|
||||
}
|
||||
|
||||
ExecutionResult PluginManager::run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error)
|
||||
{
|
||||
if (plugin_key.empty() || capability_name.empty()) {
|
||||
return {};
|
||||
ExecutionResult result;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
bool done = false;
|
||||
|
||||
ScriptLaunchResult launch = launch_script_capability(
|
||||
plugin_key, capability_name, [&](const ScriptCompletion& completion) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
result = completion.result;
|
||||
// A cancel_plugin() cancellation is expected termination, not an error to report.
|
||||
error = completion.cancelled ? std::string() : completion.error;
|
||||
done = true;
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
if (launch.launched_async) {
|
||||
// Async run: the completion is delivered on the GUI thread. When this call is itself on
|
||||
// the GUI thread it must pump the event loop, or the completion can never be delivered.
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
if (wxTheApp != nullptr && wxIsMainThread()) {
|
||||
while (!done) {
|
||||
cv.wait_for(lock, std::chrono::milliseconds(10));
|
||||
lock.unlock();
|
||||
wxYield();
|
||||
lock.lock();
|
||||
}
|
||||
} else {
|
||||
cv.wait(lock, [&] { return done; });
|
||||
}
|
||||
} else {
|
||||
result = launch.outcome.result;
|
||||
error = launch.outcome.error;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
PluginManager::ScriptLaunchResult PluginManager::launch_script_capability(const std::string& plugin_key,
|
||||
const std::string& capability_name,
|
||||
ScriptCompletionFn on_done)
|
||||
{
|
||||
ScriptLaunchResult launch;
|
||||
|
||||
// Mirror the historical synchronous behavior for a dangling reference: nothing to run and
|
||||
// no error (the caller's guarded UI normally prevents this).
|
||||
if (plugin_key.empty() || capability_name.empty())
|
||||
return launch;
|
||||
|
||||
if (m_shutting_down.load(std::memory_order_acquire)) {
|
||||
launch.outcome.error = _u8L("Plugin system is shutting down.");
|
||||
return launch;
|
||||
}
|
||||
|
||||
auto cap = get_plugin_capability({PluginCapabilityType::Script, capability_name, plugin_key});
|
||||
if (!cap)
|
||||
return {};
|
||||
return launch;
|
||||
|
||||
auto cap_interface = std::dynamic_pointer_cast<ScriptPluginCapability>(cap);
|
||||
if (!cap_interface)
|
||||
return {};
|
||||
if (!std::dynamic_pointer_cast<ScriptPluginCapability>(cap))
|
||||
return launch;
|
||||
|
||||
ExecutionResult result;
|
||||
try {
|
||||
PythonGILState gil;
|
||||
if (!gil) {
|
||||
error = "Python interpreter is shutting down";
|
||||
return {};
|
||||
}
|
||||
result = cap_interface->execute();
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw exception. plugin_key=" << plugin_key << " error=" << error;
|
||||
} catch (...) {
|
||||
error = _u8L("Unknown error");
|
||||
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
|
||||
// Every Script body runs off the caller's thread now: the invocation — including detection and
|
||||
// a synchronous `def execute` — is handed to the runtime, which runs it on its own worker and
|
||||
// reports the outcome through the completion callback. Nothing executes before returning here,
|
||||
// so the UI thread is never blocked by a Script invocation.
|
||||
const bool started = ScriptAsyncRuntime::instance().start(
|
||||
plugin_key, capability_name, cap,
|
||||
[this, plugin_key, on_done](const ScriptCompletion& completion) {
|
||||
// A cancellation requested by cancel_plugin() (unload/shutdown) is expected termination:
|
||||
// record no plugin error and surface nothing. Genuine user/plugin errors still record.
|
||||
if (!completion.cancelled && !completion.error.empty())
|
||||
set_plugin_error(plugin_key, completion.error);
|
||||
if (on_done)
|
||||
on_done(completion);
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
launch.outcome.error = _u8L("Plugin system is shutting down.");
|
||||
set_plugin_error(plugin_key, launch.outcome.error);
|
||||
BOOST_LOG_TRIVIAL(error) << "Script plugin execution failed. plugin_key=" << plugin_key
|
||||
<< " error=" << launch.outcome.error;
|
||||
return launch;
|
||||
}
|
||||
|
||||
if (!error.empty()) {
|
||||
set_plugin_error(plugin_key, error);
|
||||
}
|
||||
|
||||
return result;
|
||||
launch.launched_async = true;
|
||||
return launch;
|
||||
}
|
||||
|
||||
void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) {
|
||||
|
||||
Reference in new issue
Block a user