Fix 3: optimize shutdown times

This commit is contained in:
Sebastian 2026-06-24 12:10:20 +02:00 committed by Hakan Seven
commit a93d425002
4 changed files with 128 additions and 21 deletions

View file

@ -1,7 +1,7 @@
//! Process management for out-of-process plugins.
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::sync::Mutex;
@ -82,6 +82,9 @@ impl PluginProcess {
.arg("--ocs-plugin-runner")
.arg(&socket_name)
.arg(cdylib_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
// Accept the runner connection with a timeout so a hung/crashed runner
@ -237,17 +240,25 @@ impl PluginProcess {
}
/// Tear down the plugin process without blocking the caller. The stream is
/// closed and the child is killed and reaped in a detached background thread.
/// closed and the child is killed synchronously; the blocking `wait()` is
/// done in a detached background thread so the host never waits on a plugin.
pub fn shutdown(&self) {
let (stream, child) = self.take_resources();
drop(stream);
if let Some(mut child) = child {
let _ = child.kill();
std::thread::spawn(move || {
let _ = child.wait();
});
}
}
/// Take the stream and child handles out of the process. After this the
/// process is considered shut down and any further IPC will fail.
fn take_resources(&self) -> (Option<Stream>, Option<Child>) {
let stream = self.stream.lock().unwrap_or_else(|e| e.into_inner()).take();
let child = self.child.lock().unwrap_or_else(|e| e.into_inner()).take();
std::thread::spawn(move || {
drop(stream);
if let Some(mut child) = child {
let _ = child.kill();
let _ = child.wait();
}
});
(stream, child)
}
}
@ -328,21 +339,61 @@ fn call(
/// For testing or unusual deployment layouts, set `OCS_PLUGIN_RUNNER_EXE` to
/// the host executable path.
fn runner_executable() -> Result<PathBuf, PluginError> {
if let Ok(path) = std::env::var("OCS_PLUGIN_RUNNER_EXE") {
static RUNNER: Mutex<Option<PathBuf>> = Mutex::new(None);
let mut cached = RUNNER.lock().unwrap_or_else(|e| e.into_inner());
if let Some(ref path) = *cached {
return Ok(path.clone());
}
let path = if let Ok(path) = std::env::var("OCS_PLUGIN_RUNNER_EXE") {
let path = PathBuf::from(path);
if path.exists() {
return Ok(path);
path
} else {
return Err(PluginError::Runner(format!(
"OCS_PLUGIN_RUNNER_EXE does not exist: {}",
path.display()
)));
}
}
let path = std::env::current_exe()?;
if path.exists() {
Ok(path)
} else {
Err(PluginError::Runner(format!(
"cannot find current executable at {}",
path.display()
)))
let host = std::env::current_exe()?;
if !host.exists() {
return Err(PluginError::Runner(format!(
"cannot find current executable at {}",
host.display()
)));
}
// Create a hard link with a distinct name next to the host binary. This
// makes runner processes visible as separate sub-processes in task
// managers / ps, while keeping the runner the exact same binary as the
// host so they can never drift out of sync.
let runner = distinct_runner_path(&host);
let _ = std::fs::remove_file(&runner);
match std::fs::hard_link(&host, &runner) {
Ok(()) => runner,
Err(_) => host,
}
};
*cached = Some(path.clone());
Ok(path)
}
/// Build a runner path like `<host>-plugin-runner<ext>` in the same directory.
/// Using a distinct image name lets task managers show plugin processes as
/// children/sub-processes of the host instead of collapsing them into one row.
fn distinct_runner_path(host: &Path) -> PathBuf {
let mut runner = host.as_os_str().to_owned();
if let Some(ext) = host.extension().and_then(|s| s.to_str()) {
let base = host.file_stem().unwrap_or_default();
runner = std::ffi::OsString::from(format!("{}-plugin-runner.{}", base.to_string_lossy(), ext));
} else {
runner.push("-plugin-runner");
}
let mut path = host.parent().unwrap_or_else(|| Path::new(".")).to_path_buf();
path.push(runner);
path
}
/// Generate a unique local socket name.
@ -351,3 +402,22 @@ fn generate_socket_name() -> String {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("ocs_plugin_{}_{}", std::process::id(), n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distinct_runner_path_appends_suffix() {
let host = PathBuf::from("/app/OpenCADStudio.exe");
let runner = distinct_runner_path(&host);
assert_eq!(runner, PathBuf::from("/app/OpenCADStudio-plugin-runner.exe"));
}
#[test]
fn distinct_runner_path_handles_no_extension() {
let host = PathBuf::from("/app/OpenCADStudio");
let runner = distinct_runner_path(&host);
assert_eq!(runner, PathBuf::from("/app/OpenCADStudio-plugin-runner"));
}
}

View file

@ -1,6 +1,7 @@
//! Process manager for out-of-process plugins.
use std::path::Path;
use std::process::Child;
use std::sync::Arc;
use crate::host::HostApi;
@ -120,10 +121,27 @@ impl PluginManager {
}
/// Begin asynchronous shutdown of every plugin process.
///
/// Kills every child synchronously on the calling thread and moves the
/// blocking `wait()` calls into a single detached reaper thread, so host
/// shutdown is fast regardless of how many plugins are loaded.
pub fn shutdown_all(&mut self) {
let plugins = std::mem::take(&mut self.plugins);
let mut children: Vec<Child> = Vec::with_capacity(plugins.len());
for p in plugins {
p.process.shutdown();
let (stream, child) = p.process.take_resources();
drop(stream);
if let Some(mut child) = child {
let _ = child.kill();
children.push(child);
}
}
if !children.is_empty() {
std::thread::spawn(move || {
for mut child in children {
let _ = child.wait();
}
});
}
}
}