isoloom_render/configure/
units.rs1use isoloom_plan::configure::Family;
6use isoloom_spec::shell::quote;
7use isoloom_spec::syntax::{AbsPath, Argv, UnitName, UserName};
8
9#[must_use]
13pub fn linux_unit(family: Family, name: &UnitName, run: &Argv, user: Option<&UserName>, cwd: Option<&AbsPath>) -> String {
14 linux(family, name, run, user, cwd)
15}
16
17#[must_use]
19pub(super) fn linux(family: Family, name: &UnitName, run: &Argv, user: Option<&UserName>, cwd: Option<&AbsPath>) -> String {
20 if family == Family::Alpine {
21 openrc(name, run, user, cwd)
22 } else {
23 systemd(name, run, user, cwd)
24 }
25}
26
27fn systemd(name: &UnitName, run: &Argv, user: Option<&UserName>, cwd: Option<&AbsPath>) -> String {
29 let exec: Vec<String> = run.args().iter().map(|a| systemd_word(a)).collect();
30 let user = user.map(|u| format!("User={u}\n")).unwrap_or_default();
31 let cwd = cwd.map(|d| format!("WorkingDirectory={}\n", d.as_str().replace('%', "%%"))).unwrap_or_default();
33 format!(
34 "[Unit]\nDescription={name}, defined by Isoloom\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nExecStart={}\n{user}{cwd}Restart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n",
35 exec.join(" ")
36 )
37}
38
39#[must_use]
42pub fn systemd_word(s: &str) -> String {
43 let mut out = String::with_capacity(s.len() + 2);
44 out.push('"');
45 for c in s.chars() {
46 match c {
47 '\\' => out.push_str("\\\\"),
48 '"' => out.push_str("\\\""),
49 '\n' => out.push_str("\\n"),
50 '%' => out.push_str("%%"),
51 '$' => out.push_str("$$"),
52 _ => out.push(c),
53 }
54 }
55 out.push('"');
56 out
57}
58
59fn openrc(name: &UnitName, run: &Argv, user: Option<&UserName>, cwd: Option<&AbsPath>) -> String {
62 let args: Vec<String> = run.args().iter().skip(1).map(|a| quote(a)).collect();
63 let user = user.map(|u| format!("command_user={}\n", quote(u.as_str()))).unwrap_or_default();
64 let cwd = cwd.map(|d| format!("directory={}\n", quote(d.as_str()))).unwrap_or_default();
65 format!(
66 "#!/sbin/openrc-run\ndescription={}\ncommand={}\ncommand_args={}\ncommand_background=true\npidfile=\"/run/${{RC_SVCNAME}}.pid\"\n{user}{cwd}\ndepend() {{\n\tneed net\n}}\n",
67 quote(&format!("{name}, defined by Isoloom")),
68 quote(run.program()),
69 quote(&args.join(" "))
70 )
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn argv(args: &[&str]) -> Argv {
78 Argv::try_from(args.iter().map(|a| (*a).to_owned()).collect::<Vec<_>>()).unwrap()
79 }
80
81 #[test]
82 fn a_systemd_unit_keeps_every_word_literal() {
83 let unit = linux(
84 Family::Debian,
85 &"api".parse().unwrap(),
86 &argv(&["/srv/api", "--name", "a \"b\" $HOME 100%"]),
87 "api".parse().ok().as_ref(),
88 "/srv".parse().ok().as_ref(),
89 );
90 assert!(
91 unit.contains("ExecStart=\"/srv/api\" \"--name\" \"a \\\"b\\\" $$HOME 100%%\"\nUser=api\nWorkingDirectory=/srv\nRestart=on-failure\n"),
92 "{unit}"
93 );
94 assert!(unit.ends_with("[Install]\nWantedBy=multi-user.target\n"));
95 }
96
97 #[test]
100 fn a_working_directory_is_a_bare_path_its_percent_signs_doubled() {
101 let unit = linux(
102 Family::Debian,
103 &"api".parse().unwrap(),
104 &argv(&["/srv/api"]),
105 None,
106 "/srv/my app/100%".parse().ok().as_ref(),
107 );
108 assert!(unit.contains("\nWorkingDirectory=/srv/my app/100%%\n"), "{unit}");
109 }
110
111 #[test]
112 fn alpine_gets_an_openrc_script_its_shell_reads_back() {
113 let unit = linux(Family::Alpine, &"api".parse().unwrap(), &argv(&["/srv/api", "it's"]), None, None);
114 assert!(unit.starts_with("#!/sbin/openrc-run\n"), "{unit}");
115 assert!(
116 unit.contains("command='/srv/api'\ncommand_args=''\\''it'\\''\\'\\'''\\''s'\\'''\ncommand_background=true\n"),
117 "{unit}"
118 );
119 }
120}