1use std::{
2 ffi::OsString,
3 fs::{File, OpenOptions},
4 path::{Path, PathBuf},
5 process::{Child, Command, ExitStatus, Stdio},
6 time::Duration,
7};
8
9use synd_api::session::DaemonSessionConfig;
10use synd_support::{dirs::SyndicationdDirs, time::humantime::HumanDuration};
11use tracing::{debug, warn};
12
13use crate::{
14 Result,
15 placement::{PlacementSpec, RUNTIME_ROOT_ENV},
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DaemonLaunchConfig {
21 executable: DaemonExecutable,
22 log: DaemonLaunchLog,
23 session: DaemonSessionConfig,
24}
25
26impl DaemonLaunchConfig {
27 pub fn new(executable: DaemonExecutable, log: DaemonLaunchLog) -> Self {
28 Self {
29 executable,
30 log,
31 session: DaemonSessionConfig::default(),
32 }
33 }
34
35 pub fn executable(&self) -> &DaemonExecutable {
36 &self.executable
37 }
38
39 pub fn log(&self) -> &DaemonLaunchLog {
40 &self.log
41 }
42
43 pub fn session(&self) -> DaemonSessionConfig {
44 self.session
45 }
46
47 #[must_use]
48 pub fn with_log(mut self, log: DaemonLaunchLog) -> Self {
49 self.log = log;
50 self
51 }
52
53 #[must_use]
54 pub fn with_session(mut self, session: DaemonSessionConfig) -> Self {
55 self.session = session;
56 self
57 }
58
59 #[must_use]
60 pub fn with_session_lease_duration(mut self, lease_duration: Duration) -> Self {
61 self.session = self.session.with_lease_duration(lease_duration);
62 self
63 }
64
65 #[must_use]
66 pub fn with_session_idle_shutdown_grace(mut self, idle_shutdown_grace: Duration) -> Self {
67 self.session = self.session.with_idle_shutdown_grace(idle_shutdown_grace);
68 self
69 }
70}
71
72impl Default for DaemonLaunchConfig {
73 fn default() -> Self {
74 Self::new(
75 DaemonExecutable::current(),
76 DaemonLaunchLog::file(SyndicationdDirs::current().log_file()),
77 )
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct DaemonExecutable {
84 kind: DaemonExecutableKind,
85}
86
87impl DaemonExecutable {
88 pub fn current() -> Self {
89 Self {
90 kind: DaemonExecutableKind::Current,
91 }
92 }
93
94 pub fn path(path: impl Into<PathBuf>) -> Self {
95 Self {
96 kind: DaemonExecutableKind::Path(path.into()),
97 }
98 }
99
100 fn resolve(&self) -> Result<PathBuf> {
101 Ok(match &self.kind {
102 DaemonExecutableKind::Current => std::env::current_exe()?,
103 DaemonExecutableKind::Path(path) => path.clone(),
104 })
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109enum DaemonExecutableKind {
110 Current,
111 Path(PathBuf),
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct DaemonLaunchLog {
117 path: PathBuf,
118}
119
120impl DaemonLaunchLog {
121 pub fn file(path: impl Into<PathBuf>) -> Self {
122 Self { path: path.into() }
123 }
124
125 pub fn path(&self) -> &Path {
126 &self.path
127 }
128
129 fn open(&self) -> Result<OpenedDaemonLaunchLog> {
130 if let Some(parent) = self.path.parent() {
131 std::fs::create_dir_all(parent)?;
132 }
133
134 let stdout = OpenOptions::new()
135 .append(true)
136 .create(true)
137 .open(&self.path)?;
138 let stderr = stdout.try_clone()?;
139
140 Ok(OpenedDaemonLaunchLog { stdout, stderr })
141 }
142}
143
144struct OpenedDaemonLaunchLog {
145 stdout: File,
146 stderr: File,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct DaemonLaunchInfo {
152 executable: PathBuf,
153 arguments: Vec<OsString>,
154 environment: Vec<(OsString, OsString)>,
155 log: PathBuf,
156}
157
158impl DaemonLaunchInfo {
159 fn new(
160 executable: PathBuf,
161 arguments: Vec<OsString>,
162 environment: Vec<(OsString, OsString)>,
163 log: PathBuf,
164 ) -> Self {
165 Self {
166 executable,
167 arguments,
168 environment,
169 log,
170 }
171 }
172
173 pub fn executable(&self) -> &Path {
174 &self.executable
175 }
176
177 pub fn arguments(&self) -> &[OsString] {
178 &self.arguments
179 }
180
181 pub fn environment(&self) -> &[(OsString, OsString)] {
182 &self.environment
183 }
184
185 pub fn log(&self) -> &Path {
186 &self.log
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191struct ResolvedDaemonLaunchCommand {
192 executable: PathBuf,
193 arguments: DaemonServeArguments,
194}
195
196impl ResolvedDaemonLaunchCommand {
197 fn daemon_serve(
198 executable: PathBuf,
199 database_path: &Path,
200 session: DaemonSessionConfig,
201 ) -> Self {
202 Self {
203 executable,
204 arguments: DaemonServeArguments::new(database_path, session),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211struct DaemonLaunchEnvironment {
212 values: Vec<(OsString, OsString)>,
213}
214
215impl DaemonLaunchEnvironment {
216 fn from_placement(placement: &PlacementSpec) -> Self {
217 Self::runtime_root(placement.root().path())
218 }
219
220 fn runtime_root(root: &Path) -> Self {
221 Self {
222 values: vec![(
223 OsString::from(RUNTIME_ROOT_ENV),
224 root.as_os_str().to_os_string(),
225 )],
226 }
227 }
228
229 fn apply_to(&self, command: &mut Command) {
230 for (key, value) in &self.values {
231 command.env(key, value);
232 }
233 }
234
235 fn as_slice(&self) -> &[(OsString, OsString)] {
236 &self.values
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242struct DaemonServeArguments {
243 values: Vec<OsString>,
244}
245
246impl DaemonServeArguments {
247 fn new(database_path: &Path, session: DaemonSessionConfig) -> Self {
248 let mut values = vec![
249 OsString::from("daemon"),
250 OsString::from("serve"),
251 OsString::from("--sqlite-db"),
252 database_path.as_os_str().to_os_string(),
253 ];
254 values.extend(DaemonSessionServeArguments::from(session));
255
256 Self { values }
257 }
258
259 fn as_slice(&self) -> &[OsString] {
260 &self.values
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266struct DaemonSessionServeArguments {
267 lease_duration: Duration,
268 idle_shutdown_grace: Duration,
269}
270
271impl From<DaemonSessionConfig> for DaemonSessionServeArguments {
272 fn from(config: DaemonSessionConfig) -> Self {
273 Self {
274 lease_duration: config.lease_policy().lease_duration(),
275 idle_shutdown_grace: config.idle_shutdown_grace(),
276 }
277 }
278}
279
280impl IntoIterator for DaemonSessionServeArguments {
281 type Item = OsString;
282 type IntoIter = std::vec::IntoIter<Self::Item>;
283
284 fn into_iter(self) -> Self::IntoIter {
285 vec![
286 OsString::from("--session-lease-duration"),
287 OsString::from(String::from(HumanDuration::from(self.lease_duration))),
288 OsString::from("--session-idle-shutdown-grace"),
289 OsString::from(String::from(HumanDuration::from(self.idle_shutdown_grace))),
290 ]
291 .into_iter()
292 }
293}
294
295pub(crate) struct DaemonLauncher<'a> {
297 config: &'a DaemonLaunchConfig,
298 placement: PlacementSpec,
299}
300
301impl<'a> DaemonLauncher<'a> {
302 pub(crate) fn new(config: &'a DaemonLaunchConfig, placement: PlacementSpec) -> Self {
303 Self { config, placement }
304 }
305
306 pub(crate) fn launch(self) -> Result<DaemonHandle> {
307 let command = ResolvedDaemonLaunchCommand::daemon_serve(
308 self.config.executable().resolve()?,
309 self.placement.instance().canonical_database_path(),
310 self.config.session(),
311 );
312 let environment = DaemonLaunchEnvironment::from_placement(&self.placement);
313 let log = self.config.log().open()?;
314 let launch = DaemonLaunchInfo::new(
315 command.executable.clone(),
316 command.arguments.as_slice().to_vec(),
317 environment.as_slice().to_vec(),
318 self.config.log().path().to_path_buf(),
319 );
320 debug!(
321 daemon_launch = ?launch,
322 "Launching daemon"
323 );
324
325 let mut child_command = Command::new(&command.executable);
326 child_command.args(command.arguments.as_slice());
327 environment.apply_to(&mut child_command);
328 let child = child_command
329 .stdin(Stdio::null())
330 .stdout(Stdio::from(log.stdout))
331 .stderr(Stdio::from(log.stderr))
332 .spawn()?;
333
334 Ok(DaemonHandle { child, launch })
335 }
336}
337
338pub(crate) struct DaemonHandle {
340 child: Child,
341 launch: DaemonLaunchInfo,
342}
343
344impl DaemonHandle {
345 pub(crate) fn launch(&self) -> &DaemonLaunchInfo {
346 &self.launch
347 }
348
349 pub(crate) fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
350 Ok(self.child.try_wait()?)
351 }
352
353 pub(crate) fn reap_in_background(mut self) {
354 std::thread::spawn(move || match self.child.wait() {
355 Ok(status) => {
356 debug!(%status, "Daemon process exited");
357 }
358 Err(error) => {
359 warn!(%error, "Failed to wait for daemon process");
360 }
361 });
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use std::{ffi::OsString, path::Path, time::Duration};
368
369 use synd_api::session::{DaemonSessionConfig, DaemonSessionLeasePolicy};
370
371 use super::{
372 DaemonExecutable, DaemonLaunchEnvironment, DaemonLaunchLog, ResolvedDaemonLaunchCommand,
373 };
374
375 mod daemon_serve_command {
376 use super::*;
377
378 #[test]
379 fn uses_database_path() {
380 let command = ResolvedDaemonLaunchCommand::daemon_serve(
381 DaemonExecutable::path("/usr/bin/synd").resolve().unwrap(),
382 Path::new("/tmp/synd.db"),
383 DaemonSessionConfig::default(),
384 );
385
386 assert_eq!(command.executable, Path::new("/usr/bin/synd"));
387 assert_eq!(
388 command.arguments.as_slice(),
389 [
390 OsString::from("daemon"),
391 OsString::from("serve"),
392 OsString::from("--sqlite-db"),
393 OsString::from("/tmp/synd.db"),
394 OsString::from("--session-lease-duration"),
395 OsString::from("30s"),
396 OsString::from("--session-idle-shutdown-grace"),
397 OsString::from("30s"),
398 ]
399 );
400 }
401
402 #[test]
403 fn includes_session_config() {
404 let command = ResolvedDaemonLaunchCommand::daemon_serve(
405 DaemonExecutable::path("/usr/bin/synd").resolve().unwrap(),
406 Path::new("/tmp/synd.db"),
407 DaemonSessionConfig::new(
408 DaemonSessionLeasePolicy::new(Duration::from_mins(1), Duration::from_secs(5)),
409 Duration::from_mins(2),
410 ),
411 );
412
413 assert_eq!(
414 command.arguments.as_slice(),
415 [
416 OsString::from("daemon"),
417 OsString::from("serve"),
418 OsString::from("--sqlite-db"),
419 OsString::from("/tmp/synd.db"),
420 OsString::from("--session-lease-duration"),
421 OsString::from("1m"),
422 OsString::from("--session-idle-shutdown-grace"),
423 OsString::from("2m"),
424 ]
425 );
426 }
427 }
428
429 mod launch_log {
430 use super::*;
431
432 #[test]
433 fn creates_parent_dir() {
434 let tmp = tempfile::tempdir().unwrap();
435 let log = DaemonLaunchLog::file(tmp.path().join("nested").join("daemon.log"));
436
437 let _opened = log.open().unwrap();
438
439 assert!(log.path().exists());
440 }
441 }
442
443 mod launch_environment {
444 use super::*;
445
446 #[test]
447 fn includes_runtime_root() {
448 let environment = DaemonLaunchEnvironment::runtime_root(Path::new("/tmp/synd-runtime"));
449
450 assert_eq!(
451 environment.as_slice(),
452 [(
453 OsString::from(crate::placement::RUNTIME_ROOT_ENV),
454 OsString::from("/tmp/synd-runtime"),
455 )]
456 );
457 }
458 }
459}