1use cordis::Context;
4use cordis_loader::{Loader, LoaderConfig, PluginRegistry};
5use notify::{Config as NotifyConfig, RecursiveMode, Watcher};
6use std::io::Read;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::sync::mpsc;
10use std::time::Duration;
11
12pub const EXIT_RESTART: i32 = 51;
14pub const EXIT_QUIT: i32 = 52;
16pub const EXIT_BOOT: i32 = 53;
19pub const SUPERVISED_ENV: &str = "CORDIS_SUPERVISED";
24
25const PLUGIN_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250);
28
29pub struct WorkerHandle {
32 inner: Arc<WorkerInner>,
33}
34
35impl WorkerHandle {
36 pub fn restart(&self) -> ! {
38 self.inner.teardown();
39 std::process::exit(EXIT_RESTART);
40 }
41
42 pub fn shutdown(&self) -> ! {
44 self.inner.teardown();
45 std::process::exit(EXIT_QUIT);
46 }
47}
48
49struct WorkerInner {
51 root: Context,
52 loader: Option<Loader>,
53}
54
55impl WorkerInner {
56 fn teardown(&self) {
57 if let Some(loader) = &self.loader {
58 let _ = loader.dispose();
59 }
60 let _ = self.root.fiber().and_then(|fiber| fiber.dispose());
61 }
62}
63
64pub fn run(config_path: &Path, plugin_dirs: &[PathBuf]) -> ! {
69 let root = Context::new();
70 let mut registry = PluginRegistry::new();
71 if !plugin_dirs.is_empty() {
72 registry = registry.with_dynamic_dirs(plugin_dirs.iter());
73 }
74 let loader = match Loader::open(
75 &root,
76 LoaderConfig::new(config_path).with_registry(registry),
77 ) {
78 Ok(loader) => loader,
79 Err(error) => {
80 eprintln!(
81 "cordis: failed to start from {}: {error}",
82 config_path.display()
83 );
84 std::process::exit(EXIT_BOOT);
85 }
86 };
87 let inner = Arc::new(WorkerInner {
88 root: root.clone(),
89 loader: Some(loader.clone()),
90 });
91
92 let handle = Arc::new(WorkerHandle {
93 inner: Arc::clone(&inner),
94 });
95 if let Err(error) = root.provide_arc("worker", handle.clone()) {
96 eprintln!("cordis: could not expose the worker service: {error}");
97 }
98
99 let signal_inner = Arc::clone(&inner);
100 if ctrlc::set_handler(move || {
101 eprintln!("cordis: signal received, shutting down");
102 signal_inner.teardown();
103 std::process::exit(EXIT_QUIT);
104 })
105 .is_err()
106 {
107 eprintln!("cordis: could not install signal handlers");
108 }
109
110 if std::env::var_os(SUPERVISED_ENV).is_some() {
116 let inner = Arc::clone(&inner);
117 let watched = std::thread::Builder::new()
118 .name("cordis-supervisor-watch".to_owned())
119 .spawn(move || {
120 let mut stdin = std::io::stdin();
121 let mut byte = [0_u8];
122 loop {
123 match stdin.read(&mut byte) {
124 Ok(0) | Err(_) => break,
125 Ok(_) => continue,
126 }
127 }
128 eprintln!("cordis: supervisor went away, shutting down");
129 inner.teardown();
130 std::process::exit(EXIT_QUIT);
131 });
132 if watched.is_err() {
133 eprintln!("cordis: could not watch the supervisor pipe");
134 }
135 }
136
137 match loader.watch() {
138 Ok(_watcher) => {}
139 Err(error) => eprintln!(
140 "cordis: config hot reload disabled ({error}); restart manually to apply changes"
141 ),
142 }
143
144 if !plugin_dirs.is_empty() {
145 match watch_plugin_dirs(plugin_dirs, handle) {
149 Ok(()) => eprintln!(
150 "cordis: dynamic plugins from {} (library changes hot-restart the worker)",
151 plugin_dirs
152 .iter()
153 .map(|dir| dir.display().to_string())
154 .collect::<Vec<_>>()
155 .join(", ")
156 ),
157 Err(error) => eprintln!(
158 "cordis: plugin library watching disabled ({error}); restart manually to apply changes"
159 ),
160 }
161 }
162
163 if let Some(error) = loader.last_error() {
164 eprintln!("cordis: startup issue: {error}");
165 }
166
167 eprintln!(
168 "cordis: worker ready ({} entries, config: {})",
169 loader.tree().entries().len(),
170 config_path.display()
171 );
172
173 loop {
176 std::thread::park();
177 }
178}
179
180fn watch_plugin_dirs(dirs: &[PathBuf], handle: Arc<WorkerHandle>) -> Result<(), String> {
188 let (tx, rx) = mpsc::channel();
189 let mut watcher = notify::RecommendedWatcher::new(tx, NotifyConfig::default())
190 .map_err(|error| format!("cannot create watcher: {error}"))?;
191 for dir in dirs {
192 watcher
193 .watch(dir, RecursiveMode::NonRecursive)
194 .map_err(|error| format!("cannot watch {}: {error}", dir.display()))?;
195 }
196
197 std::thread::Builder::new()
198 .name("cordis-plugin-watch".to_owned())
199 .spawn(move || {
200 let _watcher = watcher; let mut pending = false;
202 loop {
203 match rx.recv_timeout(PLUGIN_CHANGE_DEBOUNCE) {
204 Ok(Ok(event)) => {
205 if event.paths.iter().any(|path| is_plugin_library(path)) {
206 pending = true;
207 }
208 }
209 Ok(Err(_)) => {}
210 Err(mpsc::RecvTimeoutError::Timeout) => {
211 if pending {
212 eprintln!("cordis: plugin library changed, restarting worker");
213 handle.restart();
214 }
215 }
216 Err(mpsc::RecvTimeoutError::Disconnected) => break,
217 }
218 }
219 })
220 .map_err(|error| format!("cannot spawn watcher thread: {error}"))?;
221 Ok(())
222}
223
224fn is_plugin_library(path: &Path) -> bool {
226 path.extension().is_some_and(|extension| {
227 matches!(
228 extension.to_ascii_lowercase().to_str(),
229 Some("so" | "dylib" | "dll")
230 )
231 })
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn dynamic_library_extensions_match() {
240 assert!(is_plugin_library(Path::new("libgreeter.so")));
241 assert!(is_plugin_library(Path::new("libgreeter.dylib")));
242 assert!(is_plugin_library(Path::new("greeter.dll")));
243 assert!(is_plugin_library(Path::new("GREETER.SO")));
244 assert!(!is_plugin_library(Path::new("cordis.yml")));
245 assert!(!is_plugin_library(Path::new("libgreeter.so.tmp")));
246 assert!(!is_plugin_library(Path::new("plugins")));
247 }
248}