1#![deny(unsafe_code)]
21#![warn(missing_docs)]
22
23pub mod dotenv;
24pub mod worker;
25
26use std::path::PathBuf;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicBool, Ordering};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Action {
33 Restart,
35 Stop,
37}
38
39pub fn supervisor_action(exit_code: Option<i32>, shutdown: bool) -> Action {
44 if shutdown || exit_code != Some(worker::EXIT_RESTART) {
45 Action::Stop
46 } else {
47 Action::Restart
48 }
49}
50
51#[derive(Debug, PartialEq, Eq)]
53pub struct Options {
54 pub config: PathBuf,
56 pub plugin_dirs: Vec<PathBuf>,
58 pub worker: bool,
60}
61
62pub fn parse_args(args: &[String]) -> Result<Options, String> {
64 let Some(command) = args.first() else {
65 return Err(usage());
66 };
67 if command != "run" {
68 return Err(format!("unknown command `{command}`\n\n{}", usage()));
69 }
70 let mut config = None;
71 let mut plugin_dirs = Vec::new();
72 let mut worker = false;
73 let mut rest = args[1..].iter();
74 while let Some(arg) = rest.next() {
75 match arg.as_str() {
76 "--worker" | "-w" => worker = true,
77 "--help" | "-h" => return Err(usage()),
78 "--plugin-dir" => {
79 let Some(value) = rest.next() else {
80 return Err(format!("--plugin-dir requires a directory\n\n{}", usage()));
81 };
82 plugin_dirs.push(PathBuf::from(value));
83 }
84 other if other.starts_with("--plugin-dir=") => {
85 let value = other.strip_prefix("--plugin-dir=").unwrap();
86 if value.is_empty() {
87 return Err(format!("--plugin-dir requires a directory\n\n{}", usage()));
88 }
89 plugin_dirs.push(PathBuf::from(value));
90 }
91 other if other.starts_with('-') => {
92 return Err(format!("unknown flag `{other}`\n\n{}", usage()));
93 }
94 other => {
95 if config.replace(PathBuf::from(other)).is_some() {
96 return Err(format!(
97 "unexpected extra argument `{other}`\n\n{}",
98 usage()
99 ));
100 }
101 }
102 }
103 }
104 match config {
105 Some(config) => Ok(Options {
106 config,
107 plugin_dirs,
108 worker,
109 }),
110 None => Err(format!("missing config file\n\n{}", usage())),
111 }
112}
113
114fn usage() -> String {
115 "usage: cordis run <config.yml> [--plugin-dir <dir>]... [--worker]
116
117 run start the loader from an entry config file
118 --plugin-dir also resolve plugins from dynamic libraries in <dir>
119 (repeatable); library changes there hot-restart the worker
120 --worker internal: run as the daemon's worker process
121
122Worker exit codes: 51 = hot restart, 52 = quit."
123 .to_owned()
124}
125
126pub fn run<I, S>(args: I) -> i32
129where
130 I: IntoIterator<Item = S>,
131 S: Into<String>,
132{
133 let args: Vec<String> = args.into_iter().map(Into::into).collect();
134 let options = match parse_args(&args) {
135 Ok(options) => options,
136 Err(message) => {
137 eprintln!("{message}");
138 return 2;
139 }
140 };
141 if let Ok(dir) = std::env::current_dir() {
142 dotenv::load(&dir);
143 }
144 if options.worker {
145 worker::run(&options.config, &options.plugin_dirs);
146 }
147 supervise(&options.config, &options.plugin_dirs)
148}
149
150fn supervise(config: &std::path::Path, plugin_dirs: &[PathBuf]) -> i32 {
152 let shutdown = Arc::new(AtomicBool::new(false));
153 let signal_flag = Arc::clone(&shutdown);
154 if ctrlc::set_handler(move || {
155 eprintln!("cordis: shutdown requested");
156 signal_flag.store(true, Ordering::SeqCst);
157 })
158 .is_err()
159 {
160 eprintln!("cordis: could not install signal handlers");
161 }
162
163 let exe = match std::env::current_exe() {
164 Ok(exe) => exe,
165 Err(error) => {
166 eprintln!("cordis: cannot resolve own executable: {error}");
167 return 1;
168 }
169 };
170 loop {
171 if shutdown.load(Ordering::SeqCst) {
172 break;
173 }
174 let mut command = std::process::Command::new(&exe);
175 command.arg("run").arg(config).arg("--worker");
176 for dir in plugin_dirs {
177 command.arg("--plugin-dir").arg(dir);
178 }
179 let mut child = match command.spawn() {
180 Ok(child) => child,
181 Err(error) => {
182 eprintln!("cordis: cannot spawn worker: {error}");
183 return 1;
184 }
185 };
186 let exit_code = child.wait().ok().and_then(|status| status.code());
187 if supervisor_action(exit_code, shutdown.load(Ordering::SeqCst)) == Action::Stop {
188 break;
189 }
190 eprintln!("cordis: worker requested restart, respawning");
191 }
192 0
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn args(list: &[&str]) -> Vec<String> {
200 list.iter().map(ToString::to_string).collect()
201 }
202
203 #[test]
204 fn parses_run_command_and_flags() {
205 assert_eq!(
206 parse_args(&args(&["run", "cordis.yml"])).unwrap(),
207 Options {
208 config: "cordis.yml".into(),
209 plugin_dirs: Vec::new(),
210 worker: false,
211 }
212 );
213 assert_eq!(
214 parse_args(&args(&["run", "cordis.yml", "--worker"])).unwrap(),
215 Options {
216 config: "cordis.yml".into(),
217 plugin_dirs: Vec::new(),
218 worker: true,
219 }
220 );
221 }
222
223 #[test]
224 fn parses_plugin_dirs_in_both_forms() {
225 let options = parse_args(&args(&[
226 "run",
227 "cordis.yml",
228 "--plugin-dir",
229 "a",
230 "--plugin-dir=b",
231 ]))
232 .unwrap();
233 assert_eq!(
234 options.plugin_dirs,
235 [PathBuf::from("a"), PathBuf::from("b")]
236 );
237 assert!(!options.worker);
238 }
239
240 #[test]
241 fn rejects_malformed_plugin_dirs() {
242 assert!(parse_args(&args(&["run", "cordis.yml", "--plugin-dir"])).is_err());
243 assert!(parse_args(&args(&["run", "cordis.yml", "--plugin-dir="])).is_err());
244 }
245
246 #[test]
247 fn rejects_missing_or_unknown_arguments() {
248 assert!(parse_args(&args(&[])).is_err());
249 assert!(parse_args(&args(&["start", "cordis.yml"])).is_err());
250 assert!(parse_args(&args(&["run"])).is_err());
251 assert!(parse_args(&args(&["run", "a.yml", "b.yml"])).is_err());
252 assert!(parse_args(&args(&["run", "a.yml", "--nope"])).is_err());
253 }
254
255 #[test]
256 fn only_code_51_restarts_and_never_after_shutdown() {
257 assert_eq!(supervisor_action(Some(51), false), Action::Restart);
258 assert_eq!(supervisor_action(Some(51), true), Action::Stop);
259 assert_eq!(supervisor_action(Some(52), false), Action::Stop);
260 assert_eq!(supervisor_action(Some(0), false), Action::Stop);
261 assert_eq!(supervisor_action(None, false), Action::Stop);
262 }
263}