1#![deny(unsafe_code)]
16#![warn(missing_docs)]
17
18pub mod dotenv;
19pub mod worker;
20
21use std::path::PathBuf;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicBool, Ordering};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Action {
28 Restart,
30 Stop,
32}
33
34pub fn supervisor_action(exit_code: Option<i32>, shutdown: bool) -> Action {
39 if shutdown || exit_code != Some(worker::EXIT_RESTART) {
40 Action::Stop
41 } else {
42 Action::Restart
43 }
44}
45
46#[derive(Debug, PartialEq, Eq)]
48pub struct Options {
49 pub config: PathBuf,
51 pub worker: bool,
53}
54
55pub fn parse_args(args: &[String]) -> Result<Options, String> {
57 let Some(command) = args.first() else {
58 return Err(usage());
59 };
60 if command != "run" {
61 return Err(format!("unknown command `{command}`\n\n{}", usage()));
62 }
63 let mut config = None;
64 let mut worker = false;
65 for arg in &args[1..] {
66 match arg.as_str() {
67 "--worker" | "-w" => worker = true,
68 "--help" | "-h" => return Err(usage()),
69 other if other.starts_with('-') => {
70 return Err(format!("unknown flag `{other}`\n\n{}", usage()));
71 }
72 other => {
73 if config.replace(PathBuf::from(other)).is_some() {
74 return Err(format!(
75 "unexpected extra argument `{other}`\n\n{}",
76 usage()
77 ));
78 }
79 }
80 }
81 }
82 match config {
83 Some(config) => Ok(Options { config, worker }),
84 None => Err(format!("missing config file\n\n{}", usage())),
85 }
86}
87
88fn usage() -> String {
89 "usage: cordis run <config.yml> [--worker]
90
91 run start the loader from an entry config file
92 --worker internal: run as the daemon's worker process
93
94Worker exit codes: 51 = hot restart, 52 = quit."
95 .to_owned()
96}
97
98pub fn run<I, S>(args: I) -> i32
101where
102 I: IntoIterator<Item = S>,
103 S: Into<String>,
104{
105 let args: Vec<String> = args.into_iter().map(Into::into).collect();
106 let options = match parse_args(&args) {
107 Ok(options) => options,
108 Err(message) => {
109 eprintln!("{message}");
110 return 2;
111 }
112 };
113 if let Ok(dir) = std::env::current_dir() {
114 dotenv::load(&dir);
115 }
116 if options.worker {
117 worker::run(&options.config);
118 }
119 supervise(&options.config)
120}
121
122fn supervise(config: &std::path::Path) -> i32 {
124 let shutdown = Arc::new(AtomicBool::new(false));
125 let signal_flag = Arc::clone(&shutdown);
126 if ctrlc::set_handler(move || {
127 eprintln!("cordis: shutdown requested");
128 signal_flag.store(true, Ordering::SeqCst);
129 })
130 .is_err()
131 {
132 eprintln!("cordis: could not install signal handlers");
133 }
134
135 let exe = match std::env::current_exe() {
136 Ok(exe) => exe,
137 Err(error) => {
138 eprintln!("cordis: cannot resolve own executable: {error}");
139 return 1;
140 }
141 };
142 loop {
143 if shutdown.load(Ordering::SeqCst) {
144 break;
145 }
146 let child = std::process::Command::new(&exe)
147 .arg("run")
148 .arg(config)
149 .arg("--worker")
150 .spawn();
151 let mut child = match child {
152 Ok(child) => child,
153 Err(error) => {
154 eprintln!("cordis: cannot spawn worker: {error}");
155 return 1;
156 }
157 };
158 let exit_code = child.wait().ok().and_then(|status| status.code());
159 if supervisor_action(exit_code, shutdown.load(Ordering::SeqCst)) == Action::Stop {
160 break;
161 }
162 eprintln!("cordis: worker requested restart, respawning");
163 }
164 0
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 fn args(list: &[&str]) -> Vec<String> {
172 list.iter().map(ToString::to_string).collect()
173 }
174
175 #[test]
176 fn parses_run_command_and_flags() {
177 assert_eq!(
178 parse_args(&args(&["run", "cordis.yml"])).unwrap(),
179 Options {
180 config: "cordis.yml".into(),
181 worker: false
182 }
183 );
184 assert_eq!(
185 parse_args(&args(&["run", "cordis.yml", "--worker"])).unwrap(),
186 Options {
187 config: "cordis.yml".into(),
188 worker: true
189 }
190 );
191 }
192
193 #[test]
194 fn rejects_missing_or_unknown_arguments() {
195 assert!(parse_args(&args(&[])).is_err());
196 assert!(parse_args(&args(&["start", "cordis.yml"])).is_err());
197 assert!(parse_args(&args(&["run"])).is_err());
198 assert!(parse_args(&args(&["run", "a.yml", "b.yml"])).is_err());
199 assert!(parse_args(&args(&["run", "a.yml", "--nope"])).is_err());
200 }
201
202 #[test]
203 fn only_code_51_restarts_and_never_after_shutdown() {
204 assert_eq!(supervisor_action(Some(51), false), Action::Restart);
205 assert_eq!(supervisor_action(Some(51), true), Action::Stop);
206 assert_eq!(supervisor_action(Some(52), false), Action::Stop);
207 assert_eq!(supervisor_action(Some(0), false), Action::Stop);
208 assert_eq!(supervisor_action(None, false), Action::Stop);
209 }
210}