use cordis::Context;
use cordis_loader::{Loader, LoaderConfig, PluginRegistry};
use notify::{Config as NotifyConfig, RecursiveMode, Watcher};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
pub const EXIT_RESTART: i32 = 51;
pub const EXIT_QUIT: i32 = 52;
pub const EXIT_BOOT: i32 = 53;
pub const SUPERVISED_ENV: &str = "CORDIS_SUPERVISED";
const PLUGIN_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250);
pub struct WorkerHandle {
inner: Arc<WorkerInner>,
}
impl WorkerHandle {
pub fn restart(&self) -> ! {
self.inner.teardown();
std::process::exit(EXIT_RESTART);
}
pub fn shutdown(&self) -> ! {
self.inner.teardown();
std::process::exit(EXIT_QUIT);
}
}
struct WorkerInner {
root: Context,
loader: Option<Loader>,
}
impl WorkerInner {
fn teardown(&self) {
if let Some(loader) = &self.loader {
let _ = loader.dispose();
}
let _ = self.root.fiber().and_then(|fiber| fiber.dispose());
}
}
pub fn run(config_path: &Path, plugin_dirs: &[PathBuf]) -> ! {
let root = Context::new();
let mut registry = PluginRegistry::new();
if !plugin_dirs.is_empty() {
registry = registry.with_dynamic_dirs(plugin_dirs.iter());
}
let loader = match Loader::open(
&root,
LoaderConfig::new(config_path).with_registry(registry),
) {
Ok(loader) => loader,
Err(error) => {
eprintln!(
"cordis: failed to start from {}: {error}",
config_path.display()
);
std::process::exit(EXIT_BOOT);
}
};
let inner = Arc::new(WorkerInner {
root: root.clone(),
loader: Some(loader.clone()),
});
let handle = Arc::new(WorkerHandle {
inner: Arc::clone(&inner),
});
if let Err(error) = root.provide_arc("worker", handle.clone()) {
eprintln!("cordis: could not expose the worker service: {error}");
}
let signal_inner = Arc::clone(&inner);
if ctrlc::set_handler(move || {
eprintln!("cordis: signal received, shutting down");
signal_inner.teardown();
std::process::exit(EXIT_QUIT);
})
.is_err()
{
eprintln!("cordis: could not install signal handlers");
}
if std::env::var_os(SUPERVISED_ENV).is_some() {
let inner = Arc::clone(&inner);
let watched = std::thread::Builder::new()
.name("cordis-supervisor-watch".to_owned())
.spawn(move || {
let mut stdin = std::io::stdin();
let mut byte = [0_u8];
loop {
match stdin.read(&mut byte) {
Ok(0) | Err(_) => break,
Ok(_) => continue,
}
}
eprintln!("cordis: supervisor went away, shutting down");
inner.teardown();
std::process::exit(EXIT_QUIT);
});
if watched.is_err() {
eprintln!("cordis: could not watch the supervisor pipe");
}
}
match loader.watch() {
Ok(_watcher) => {}
Err(error) => eprintln!(
"cordis: config hot reload disabled ({error}); restart manually to apply changes"
),
}
if !plugin_dirs.is_empty() {
match watch_plugin_dirs(plugin_dirs, handle) {
Ok(()) => eprintln!(
"cordis: dynamic plugins from {} (library changes hot-restart the worker)",
plugin_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
Err(error) => eprintln!(
"cordis: plugin library watching disabled ({error}); restart manually to apply changes"
),
}
}
if let Some(error) = loader.last_error() {
eprintln!("cordis: startup issue: {error}");
}
eprintln!(
"cordis: worker ready ({} entries, config: {})",
loader.tree().entries().len(),
config_path.display()
);
loop {
std::thread::park();
}
}
fn watch_plugin_dirs(dirs: &[PathBuf], handle: Arc<WorkerHandle>) -> Result<(), String> {
let (tx, rx) = mpsc::channel();
let mut watcher = notify::RecommendedWatcher::new(tx, NotifyConfig::default())
.map_err(|error| format!("cannot create watcher: {error}"))?;
for dir in dirs {
watcher
.watch(dir, RecursiveMode::NonRecursive)
.map_err(|error| format!("cannot watch {}: {error}", dir.display()))?;
}
std::thread::Builder::new()
.name("cordis-plugin-watch".to_owned())
.spawn(move || {
let _watcher = watcher; let mut pending = false;
loop {
match rx.recv_timeout(PLUGIN_CHANGE_DEBOUNCE) {
Ok(Ok(event)) => {
if event.paths.iter().any(|path| is_plugin_library(path)) {
pending = true;
}
}
Ok(Err(_)) => {}
Err(mpsc::RecvTimeoutError::Timeout) => {
if pending {
eprintln!("cordis: plugin library changed, restarting worker");
handle.restart();
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})
.map_err(|error| format!("cannot spawn watcher thread: {error}"))?;
Ok(())
}
fn is_plugin_library(path: &Path) -> bool {
path.extension().is_some_and(|extension| {
matches!(
extension.to_ascii_lowercase().to_str(),
Some("so" | "dylib" | "dll")
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dynamic_library_extensions_match() {
assert!(is_plugin_library(Path::new("libgreeter.so")));
assert!(is_plugin_library(Path::new("libgreeter.dylib")));
assert!(is_plugin_library(Path::new("greeter.dll")));
assert!(is_plugin_library(Path::new("GREETER.SO")));
assert!(!is_plugin_library(Path::new("cordis.yml")));
assert!(!is_plugin_library(Path::new("libgreeter.so.tmp")));
assert!(!is_plugin_library(Path::new("plugins")));
}
}