isoloom_render/configure/
runner.rs1use isoloom_plan::configure::{ContainerProcess, Daemon, Resource, RunPlan, ServiceSource, Stream};
16use isoloom_spec::shell::quote;
17use isoloom_spec::syntax::{AbsPath, UserName};
18
19use super::{Configured, DIR, shell};
20use crate::{GeneratedFile, header};
21
22#[must_use]
24pub fn path(machine: &str) -> String {
25 format!("{DIR}/{machine}/run")
26}
27
28#[must_use]
30pub fn file(c: &Configured<'_>, process: &ContainerProcess) -> Option<GeneratedFile> {
31 Some(GeneratedFile {
32 path: path(c.machine()),
33 contents: script(c, process)?,
34 })
35}
36
37const AS: &str = r#"isoloom_as() {
40 _user=$1 _cwd=$2
41 shift 2
42 if [ -n "$_cwd" ]; then cd "$_cwd"; fi
43 if [ -z "$_user" ] || [ "$_user" = root ]; then exec "$@"; fi
44 if command -v setpriv >/dev/null 2>&1; then
45 exec setpriv --reuid="$_user" --regid="$(id -g "$_user")" --init-groups -- "$@"
46 fi
47 _line='exec'
48 for _arg do
49 _line="$_line '$(printf '%s' "$_arg" | sed "s/'/'\\\\''/g")'"
50 done
51 exec su -s /bin/sh -c "$_line" "$_user"
52}
53"#;
54
55const SUPERVISE: &str = r#"isoloom_dir=$(mktemp -d)
57isoloom_pids=
58# Prints each line of its input as `NAME | line`.
59isoloom_prefix() {
60 while IFS= read -r _line || [ -n "$_line" ]; do printf '%s | %s\n' "$1" "$_line"; done
61}
62# Starts NAME as USER in CWD: ARGS..., its output through a FIFO, so its own PID is kept.
63isoloom_start() {
64 mkfifo "$isoloom_dir/$1"
65 isoloom_prefix "$1" <"$isoloom_dir/$1" &
66 _name=$1
67 shift
68 isoloom_as "$@" >"$isoloom_dir/$_name" 2>&1 &
69 isoloom_pids="$isoloom_pids $!"
70}
71# Whether process PID runs (not ended, nor a zombie waiting for this runner).
72isoloom_alive() {
73 [ -r "/proc/$1/stat" ] && [ "$(sed 's/.*) //' "/proc/$1/stat" | cut -c 1)" != Z ]
74}
75# Stops every process and waits for them.
76isoloom_stop() {
77 for _pid in $isoloom_pids; do kill -TERM "$_pid" 2>/dev/null || true; done
78 for _pid in $isoloom_pids; do wait "$_pid" 2>/dev/null || true; done
79}
80trap 'isoloom_stop; exit 143' TERM INT
81# Waits until one ends, then stops the others and exits with its status.
82isoloom_watch() {
83 while :; do
84 for _pid in $isoloom_pids; do
85 isoloom_alive "$_pid" && continue
86 _status=0
87 wait "$_pid" || _status=$?
88 echo "isoloom: process $_pid ended (status $_status): stopping the others" >&2
89 isoloom_stop
90 exit "$_status"
91 done
92 sleep 1
93 done
94}
95"#;
96
97#[must_use]
99pub fn script(c: &Configured<'_>, process: &ContainerProcess) -> Option<String> {
100 let (daemons, command) = match process {
101 ContainerProcess::One(d) => (std::slice::from_ref(d), false),
102 ContainerProcess::Supervised { daemons, command } => (daemons.as_slice(), command.is_some()),
103 ContainerProcess::Started { entrypoint, .. } => return Some(started(c, entrypoint)),
104 _ => return None,
105 };
106 let start = start(c);
107 let logs: String = daemons.iter().map(logs).collect();
108 let run = if let ([one], false) = (daemons, command) {
109 format!("{}\n", invocation(c, "isoloom_as", one))
110 } else {
111 let mut each = String::new();
112 for d in daemons {
113 each.push_str(&invocation(c, &format!("isoloom_start {}", quote(d.unit.as_str())), d));
114 each.push('\n');
115 }
116 let command = if command { "isoloom_start command '' '' \"$@\"\n" } else { "" };
117 format!("{SUPERVISE}{each}{command}isoloom_watch\n")
118 };
119 Some(format!(
120 "#!/bin/sh\n{}# The process of machine `{}`'s container: its services, run by Isoloom's runner.\nset -eu\n{start}{AS}{logs}{run}",
121 header("#"),
122 c.machine()
123 ))
124}
125
126fn started(c: &Configured<'_>, entrypoint: &[String]) -> String {
130 let exec: Vec<String> = entrypoint.iter().map(|a| quote(a)).collect();
131 let exec = if exec.is_empty() { String::new() } else { format!("{} ", exec.join(" ")) };
132 format!(
133 "#!/bin/sh\n{}# The process of machine `{}`'s container: its entry's start steps, run by Isoloom's runner, then the image's own.\nset -eu\n{}exec {exec}\"$@\"\n",
134 header("#"),
135 c.machine(),
136 start(c)
137 )
138}
139
140fn start(c: &Configured<'_>) -> String {
144 let steps = &c.recipe().start;
145 if steps.is_empty() {
146 return String::new();
147 }
148 let calls: String = steps
149 .iter()
150 .filter_map(|r| match r {
151 Resource::Run(run) => Some(start_script(run)),
152 other => shell::call(c, other),
153 })
154 .map(|line| {
155 if shell::literal_dollar(&line) {
156 format!("# shellcheck disable=SC2016\n{line}")
157 } else {
158 line
159 }
160 })
161 .collect();
162 let library = if steps.iter().any(|r| !matches!(r, Resource::Run(_))) {
163 ". /usr/local/lib/isoloom/configure.sh\n"
164 } else {
165 ""
166 };
167 format!("{library}cd /opt/isoloom\n{calls}")
168}
169
170fn start_script(run: &RunPlan) -> String {
172 let line = format!("sh {}", quote(&format!("/opt/isoloom/{}", run.script)));
173 match &run.creates {
174 Some(done) => format!("[ -e {} ] || {line}\n", quote(done.as_str())),
175 None => format!("{line}\n"),
176 }
177}
178
179fn logs(d: &Daemon) -> String {
181 d.logs
182 .iter()
183 .map(|l| {
184 let path = quote(l.path.as_str());
185 let stream = if l.stream == Stream::Stderr { "/dev/stderr" } else { "/dev/stdout" };
186 format!("mkdir -p \"$(dirname {path})\" && rm -f {path} && ln -s {stream} {path}\n")
187 })
188 .collect::<Vec<_>>()
189 .concat()
190}
191
192fn invocation(c: &Configured<'_>, function: &str, d: &Daemon) -> String {
195 let user = quote(d.user.as_ref().map(UserName::as_str).unwrap_or_default());
196 let cwd = c.recipe().resources().find_map(|r| match r {
197 Resource::Service(s) if s.unit == d.unit => match &s.source {
198 ServiceSource::Defined { cwd, .. } => cwd.as_ref(),
199 _ => None,
200 },
201 _ => None,
202 });
203 let args: Vec<String> = d.foreground.args().iter().map(|a| quote(a)).collect();
204 format!("{function} {user} {} {}", quote(cwd.map(AbsPath::as_str).unwrap_or_default()), args.join(" "))
205}
206
207#[cfg(test)]
208mod tests;