1use cordis::Context;
4use cordis_loader::{Loader, LoaderConfig, PluginRegistry};
5use notify::{Config as NotifyConfig, RecursiveMode, Watcher};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::sync::mpsc;
9use std::time::Duration;
10
11pub const EXIT_RESTART: i32 = 51;
13pub const EXIT_QUIT: i32 = 52;
15
16const PLUGIN_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250);
19
20pub struct WorkerHandle {
23 inner: Arc<WorkerInner>,
24}
25
26impl WorkerHandle {
27 pub fn restart(&self) -> ! {
29 self.inner.teardown();
30 std::process::exit(EXIT_RESTART);
31 }
32
33 pub fn shutdown(&self) -> ! {
35 self.inner.teardown();
36 std::process::exit(EXIT_QUIT);
37 }
38}
39
40struct WorkerInner {
42 root: Context,
43 loader: Option<Loader>,
44}
45
46impl WorkerInner {
47 fn teardown(&self) {
48 if let Some(loader) = &self.loader {
49 let _ = loader.dispose();
50 }
51 let _ = self.root.fiber().and_then(|fiber| fiber.dispose());
52 }
53}
54
55pub fn run(config_path: &Path, plugin_dirs: &[PathBuf]) -> ! {
60 let root = Context::new();
61 let mut registry = PluginRegistry::new();
62 if !plugin_dirs.is_empty() {
63 registry = registry.with_dynamic_dirs(plugin_dirs.iter());
64 }
65 let loader = match Loader::open(
66 &root,
67 LoaderConfig::new(config_path).with_registry(registry),
68 ) {
69 Ok(loader) => loader,
70 Err(error) => {
71 eprintln!(
72 "cordis: failed to start from {}: {error}",
73 config_path.display()
74 );
75 std::process::exit(EXIT_QUIT);
76 }
77 };
78 let inner = Arc::new(WorkerInner {
79 root: root.clone(),
80 loader: Some(loader.clone()),
81 });
82
83 let handle = Arc::new(WorkerHandle {
84 inner: Arc::clone(&inner),
85 });
86 if let Err(error) = root.provide_arc("worker", handle.clone()) {
87 eprintln!("cordis: could not expose the worker service: {error}");
88 }
89
90 let signal_inner = Arc::clone(&inner);
91 if ctrlc::set_handler(move || {
92 eprintln!("cordis: signal received, shutting down");
93 signal_inner.teardown();
94 std::process::exit(EXIT_QUIT);
95 })
96 .is_err()
97 {
98 eprintln!("cordis: could not install signal handlers");
99 }
100
101 match loader.watch() {
102 Ok(_watcher) => {}
103 Err(error) => eprintln!(
104 "cordis: config hot reload disabled ({error}); restart manually to apply changes"
105 ),
106 }
107
108 if !plugin_dirs.is_empty() {
109 match watch_plugin_dirs(plugin_dirs, handle) {
113 Ok(()) => eprintln!(
114 "cordis: dynamic plugins from {} (library changes hot-restart the worker)",
115 plugin_dirs
116 .iter()
117 .map(|dir| dir.display().to_string())
118 .collect::<Vec<_>>()
119 .join(", ")
120 ),
121 Err(error) => eprintln!(
122 "cordis: plugin library watching disabled ({error}); restart manually to apply changes"
123 ),
124 }
125 }
126
127 if let Some(error) = loader.last_error() {
128 eprintln!("cordis: startup issue: {error}");
129 }
130
131 eprintln!(
132 "cordis: worker ready ({} entries, config: {})",
133 loader.tree().entries().len(),
134 config_path.display()
135 );
136
137 loop {
140 std::thread::park();
141 }
142}
143
144fn watch_plugin_dirs(dirs: &[PathBuf], handle: Arc<WorkerHandle>) -> Result<(), String> {
152 let (tx, rx) = mpsc::channel();
153 let mut watcher = notify::RecommendedWatcher::new(tx, NotifyConfig::default())
154 .map_err(|error| format!("cannot create watcher: {error}"))?;
155 for dir in dirs {
156 watcher
157 .watch(dir, RecursiveMode::NonRecursive)
158 .map_err(|error| format!("cannot watch {}: {error}", dir.display()))?;
159 }
160
161 std::thread::Builder::new()
162 .name("cordis-plugin-watch".to_owned())
163 .spawn(move || {
164 let _watcher = watcher; let mut pending = false;
166 loop {
167 match rx.recv_timeout(PLUGIN_CHANGE_DEBOUNCE) {
168 Ok(Ok(event)) => {
169 if event.paths.iter().any(|path| is_plugin_library(path)) {
170 pending = true;
171 }
172 }
173 Ok(Err(_)) => {}
174 Err(mpsc::RecvTimeoutError::Timeout) => {
175 if pending {
176 eprintln!("cordis: plugin library changed, restarting worker");
177 handle.restart();
178 }
179 }
180 Err(mpsc::RecvTimeoutError::Disconnected) => break,
181 }
182 }
183 })
184 .map_err(|error| format!("cannot spawn watcher thread: {error}"))?;
185 Ok(())
186}
187
188fn is_plugin_library(path: &Path) -> bool {
190 path.extension().is_some_and(|extension| {
191 matches!(
192 extension.to_ascii_lowercase().to_str(),
193 Some("so" | "dylib" | "dll")
194 )
195 })
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn dynamic_library_extensions_match() {
204 assert!(is_plugin_library(Path::new("libgreeter.so")));
205 assert!(is_plugin_library(Path::new("libgreeter.dylib")));
206 assert!(is_plugin_library(Path::new("greeter.dll")));
207 assert!(is_plugin_library(Path::new("GREETER.SO")));
208 assert!(!is_plugin_library(Path::new("cordis.yml")));
209 assert!(!is_plugin_library(Path::new("libgreeter.so.tmp")));
210 assert!(!is_plugin_library(Path::new("plugins")));
211 }
212}