use isoloom_plan::configure::{ContainerProcess, Daemon, Resource, RunPlan, ServiceSource, Stream};
use isoloom_spec::shell::quote;
use isoloom_spec::syntax::{AbsPath, UserName};
use super::{Configured, DIR, shell};
use crate::{GeneratedFile, header};
#[must_use]
pub fn path(machine: &str) -> String {
format!("{DIR}/{machine}/run")
}
#[must_use]
pub fn file(c: &Configured<'_>, process: &ContainerProcess) -> Option<GeneratedFile> {
Some(GeneratedFile {
path: path(c.machine()),
contents: script(c, process)?,
})
}
const AS: &str = r#"isoloom_as() {
_user=$1 _cwd=$2
shift 2
if [ -n "$_cwd" ]; then cd "$_cwd"; fi
if [ -z "$_user" ] || [ "$_user" = root ]; then exec "$@"; fi
if command -v setpriv >/dev/null 2>&1; then
exec setpriv --reuid="$_user" --regid="$(id -g "$_user")" --init-groups -- "$@"
fi
_line='exec'
for _arg do
_line="$_line '$(printf '%s' "$_arg" | sed "s/'/'\\\\''/g")'"
done
exec su -s /bin/sh -c "$_line" "$_user"
}
"#;
const SUPERVISE: &str = r#"isoloom_dir=$(mktemp -d)
isoloom_pids=
# Prints each line of its input as `NAME | line`.
isoloom_prefix() {
while IFS= read -r _line || [ -n "$_line" ]; do printf '%s | %s\n' "$1" "$_line"; done
}
# Starts NAME as USER in CWD: ARGS..., its output through a FIFO, so its own PID is kept.
isoloom_start() {
mkfifo "$isoloom_dir/$1"
isoloom_prefix "$1" <"$isoloom_dir/$1" &
_name=$1
shift
isoloom_as "$@" >"$isoloom_dir/$_name" 2>&1 &
isoloom_pids="$isoloom_pids $!"
}
# Whether process PID runs (not ended, nor a zombie waiting for this runner).
isoloom_alive() {
[ -r "/proc/$1/stat" ] && [ "$(sed 's/.*) //' "/proc/$1/stat" | cut -c 1)" != Z ]
}
# Stops every process and waits for them.
isoloom_stop() {
for _pid in $isoloom_pids; do kill -TERM "$_pid" 2>/dev/null || true; done
for _pid in $isoloom_pids; do wait "$_pid" 2>/dev/null || true; done
}
trap 'isoloom_stop; exit 143' TERM INT
# Waits until one ends, then stops the others and exits with its status.
isoloom_watch() {
while :; do
for _pid in $isoloom_pids; do
isoloom_alive "$_pid" && continue
_status=0
wait "$_pid" || _status=$?
echo "isoloom: process $_pid ended (status $_status): stopping the others" >&2
isoloom_stop
exit "$_status"
done
sleep 1
done
}
"#;
#[must_use]
pub fn script(c: &Configured<'_>, process: &ContainerProcess) -> Option<String> {
let (daemons, command) = match process {
ContainerProcess::One(d) => (std::slice::from_ref(d), false),
ContainerProcess::Supervised { daemons, command } => (daemons.as_slice(), command.is_some()),
ContainerProcess::Started { entrypoint, .. } => return Some(started(c, entrypoint)),
_ => return None,
};
let start = start(c);
let logs: String = daemons.iter().map(logs).collect();
let run = if let ([one], false) = (daemons, command) {
format!("{}\n", invocation(c, "isoloom_as", one))
} else {
let mut each = String::new();
for d in daemons {
each.push_str(&invocation(c, &format!("isoloom_start {}", quote(d.unit.as_str())), d));
each.push('\n');
}
let command = if command { "isoloom_start command '' '' \"$@\"\n" } else { "" };
format!("{SUPERVISE}{each}{command}isoloom_watch\n")
};
Some(format!(
"#!/bin/sh\n{}# The process of machine `{}`'s container: its services, run by Isoloom's runner.\nset -eu\n{start}{AS}{logs}{run}",
header("#"),
c.machine()
))
}
fn started(c: &Configured<'_>, entrypoint: &[String]) -> String {
let exec: Vec<String> = entrypoint.iter().map(|a| quote(a)).collect();
let exec = if exec.is_empty() { String::new() } else { format!("{} ", exec.join(" ")) };
format!(
"#!/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",
header("#"),
c.machine(),
start(c)
)
}
fn start(c: &Configured<'_>) -> String {
let steps = &c.recipe().start;
if steps.is_empty() {
return String::new();
}
let calls: String = steps
.iter()
.filter_map(|r| match r {
Resource::Run(run) => Some(start_script(run)),
other => shell::call(c, other),
})
.map(|line| {
if shell::literal_dollar(&line) {
format!("# shellcheck disable=SC2016\n{line}")
} else {
line
}
})
.collect();
let library = if steps.iter().any(|r| !matches!(r, Resource::Run(_))) {
". /usr/local/lib/isoloom/configure.sh\n"
} else {
""
};
format!("{library}cd /opt/isoloom\n{calls}")
}
fn start_script(run: &RunPlan) -> String {
let line = format!("sh {}", quote(&format!("/opt/isoloom/{}", run.script)));
match &run.creates {
Some(done) => format!("[ -e {} ] || {line}\n", quote(done.as_str())),
None => format!("{line}\n"),
}
}
fn logs(d: &Daemon) -> String {
d.logs
.iter()
.map(|l| {
let path = quote(l.path.as_str());
let stream = if l.stream == Stream::Stderr { "/dev/stderr" } else { "/dev/stdout" };
format!("mkdir -p \"$(dirname {path})\" && rm -f {path} && ln -s {stream} {path}\n")
})
.collect::<Vec<_>>()
.concat()
}
fn invocation(c: &Configured<'_>, function: &str, d: &Daemon) -> String {
let user = quote(d.user.as_ref().map(UserName::as_str).unwrap_or_default());
let cwd = c.recipe().resources().find_map(|r| match r {
Resource::Service(s) if s.unit == d.unit => match &s.source {
ServiceSource::Defined { cwd, .. } => cwd.as_ref(),
_ => None,
},
_ => None,
});
let args: Vec<String> = d.foreground.args().iter().map(|a| quote(a)).collect();
format!("{function} {user} {} {}", quote(cwd.map(AbsPath::as_str).unwrap_or_default()), args.join(" "))
}
#[cfg(test)]
mod tests;