use std::fs;
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use arcbox_constants::cmdline::AGENT_DHCP_ROUTE_PROTO;
pub const SENTINEL: &str = "/run/arcbox-boot-done";
pub const HOOK_SCRIPT: &str = "/etc/arcbox/boot-done.sh";
const BOOT_ID: &str = "/proc/sys/kernel/random/boot_id";
const SYSTEMD_UNIT: &str = "/etc/systemd/system/arcbox-boot-done.service";
const SYSTEMD_TARGET_DROP_IN: &str =
"/etc/systemd/system/multi-user.target.d/arcbox-boot-done.conf";
const OPENRC_SERVICE: &str = "/etc/init.d/arcbox-boot-done";
const OPENRC_RUNLEVEL: &str = "/etc/runlevels/default/arcbox-boot-done";
const SYSVINIT_INITTAB: &str = "/etc/inittab";
const SYSVINIT_RC: &str = "/etc/init.d/rc";
const SYSVINIT_ENTRY_ID: &str = "abd";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Hook {
pub primary_interface: Option<String>,
}
fn hook_script_body(hook: &Hook) -> String {
let route_cleanup = hook.primary_interface.as_deref().map_or_else(String::new, |iface| {
format!(
"if [ \"$($bb ip -4 route show default dev {iface} | $bb wc -l)\" -gt 1 ]; then\n $bb ip route del default dev {iface} proto {AGENT_DHCP_ROUTE_PROTO}\nfi\n"
)
});
format!(
"#!/bin/sh\n# ArcBox boot-completion hook, rewritten by `arcbox-agent machine-init` on\n# every boot. Runs once the distro's own init has finished booting.\nbb=/bin/busybox\n{route_cleanup}$bb cat {BOOT_ID} > {SENTINEL}\n"
)
}
fn systemd_unit_body() -> String {
format!(
"[Unit]
Description=ArcBox boot-completion sentinel
After=multi-user.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart={HOOK_SCRIPT}
"
)
}
const SYSTEMD_TARGET_DROP_IN_BODY: &str = "[Unit]\nWants=arcbox-boot-done.service\n";
fn openrc_service_body() -> String {
format!(
"#!/sbin/openrc-run
description=\"ArcBox boot-completion sentinel\"
depend() {{
after *
}}
start() {{
{HOOK_SCRIPT}
}}
stop() {{
return 0
}}
"
)
}
fn sysvinit_entry() -> String {
format!("{SYSVINIT_ENTRY_ID}:2345:wait:{HOOK_SCRIPT}")
}
fn is_sysvinit_entry(line: &str) -> bool {
line.split(':').next() == Some(SYSVINIT_ENTRY_ID)
}
fn has_sysvinit_entry(inittab: &str) -> bool {
inittab.lines().any(is_sysvinit_entry)
}
fn with_sysvinit_entry(inittab: &str) -> String {
let entry = sysvinit_entry();
let mut lines: Vec<&str> = inittab.lines().collect();
match lines.iter().position(|line| is_sysvinit_entry(line)) {
Some(i) => lines[i] = &entry,
None => lines.push(&entry),
}
let mut out = lines.join("\n");
out.push('\n');
out
}
struct Layout {
root: PathBuf,
}
impl Layout {
#[cfg(target_os = "linux")]
fn guest() -> Self {
Self {
root: PathBuf::from("/"),
}
}
fn path(&self, absolute: &str) -> PathBuf {
self.root.join(absolute.trim_start_matches('/'))
}
fn hook_installed(&self) -> bool {
self.path(SYSTEMD_UNIT).exists()
|| self.path(OPENRC_SERVICE).exists()
|| fs::read_to_string(self.path(SYSVINIT_INITTAB))
.is_ok_and(|inittab| has_sysvinit_entry(&inittab))
}
fn boot_complete(&self) -> bool {
let (Ok(sentinel), Ok(boot_id)) = (
fs::read_to_string(self.path(SENTINEL)),
fs::read_to_string(self.path(BOOT_ID)),
) else {
return false;
};
sentinel_matches(&sentinel, &boot_id)
}
}
#[cfg(target_os = "linux")]
#[must_use]
pub fn hook_installed() -> bool {
Layout::guest().hook_installed()
}
#[cfg(target_os = "linux")]
#[must_use]
pub fn boot_complete() -> bool {
Layout::guest().boot_complete()
}
fn sentinel_matches(sentinel: &str, boot_id: &str) -> bool {
let boot_id = boot_id.trim();
!boot_id.is_empty() && sentinel.trim() == boot_id
}
#[cfg(target_os = "linux")]
pub fn install(hook: &Hook) -> bool {
Layout::guest().install(hook)
}
impl Layout {
fn install(&self, hook: &Hook) -> bool {
if let Err(e) = write_file(&self.path(HOOK_SCRIPT), &hook_script_body(hook), 0o755) {
tracing::warn!(error = %e, "failed to write the boot-done hook script");
return false;
}
if self.path("/usr/lib/systemd/systemd").exists()
|| self.path("/lib/systemd/systemd").exists()
{
return self.install_systemd();
}
if self.path("/sbin/openrc").exists() || self.path("/usr/libexec/rc").is_dir() {
return self.install_openrc();
}
if self.path(SYSVINIT_INITTAB).is_file() && self.path(SYSVINIT_RC).is_file() {
return self.install_sysvinit();
}
tracing::info!(
"no recognized distro init; machine readiness will not wait for boot to settle"
);
false
}
fn install_systemd(&self) -> bool {
let unit = self.path(SYSTEMD_UNIT);
if let Err(e) = write_file(&unit, &systemd_unit_body(), 0o644) {
tracing::warn!(error = %e, "failed to write the systemd boot-done unit");
return false;
}
let drop_in = self.path(SYSTEMD_TARGET_DROP_IN);
if let Err(e) = write_file(&drop_in, SYSTEMD_TARGET_DROP_IN_BODY, 0o644) {
tracing::warn!(error = %e, "failed to wire the systemd boot-done unit into the boot");
let _ = fs::remove_file(&unit);
return false;
}
tracing::info!("installed the systemd boot-completion hook");
true
}
fn install_openrc(&self) -> bool {
let service = self.path(OPENRC_SERVICE);
if let Err(e) = write_file(&service, &openrc_service_body(), 0o755) {
tracing::warn!(error = %e, "failed to write the openrc boot-done service");
return false;
}
let runlevel = self.path(OPENRC_RUNLEVEL);
if let Some(parent) = runlevel.parent()
&& let Err(e) = fs::create_dir_all(parent)
{
tracing::warn!(error = %e, "failed to create the openrc default runlevel dir");
let _ = fs::remove_file(&service);
return false;
}
let _ = fs::remove_file(&runlevel);
if let Err(e) = std::os::unix::fs::symlink(OPENRC_SERVICE, &runlevel) {
tracing::warn!(error = %e, "failed to add the openrc boot-done service to the runlevel");
let _ = fs::remove_file(&service);
return false;
}
tracing::info!("installed the openrc boot-completion hook");
true
}
fn install_sysvinit(&self) -> bool {
let path = self.path(SYSVINIT_INITTAB);
let inittab = match fs::read_to_string(&path) {
Ok(inittab) => inittab,
Err(e) => {
tracing::warn!(error = %e, "failed to read /etc/inittab");
return false;
}
};
let wanted = with_sysvinit_entry(&inittab);
if wanted != inittab
&& let Err(e) = write_file(&path, &wanted, 0o644)
{
tracing::warn!(error = %e, "failed to add the boot-done entry to /etc/inittab");
return false;
}
tracing::info!("installed the sysvinit boot-completion hook");
true
}
}
fn write_file(path: &Path, body: &str, mode: u32) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let staged = path.with_extension("arcbox-tmp");
let result = stage(&staged, body, mode).and_then(|()| fs::rename(&staged, path));
if result.is_err() {
let _ = fs::remove_file(&staged);
}
result
}
fn stage(staged: &Path, body: &str, mode: u32) -> std::io::Result<()> {
let mut file = fs::File::create(staged)?;
file.write_all(body.as_bytes())?;
file.sync_all()?;
fs::set_permissions(staged, fs::Permissions::from_mode(mode))
}
#[cfg(test)]
mod tests {
use super::*;
fn image() -> (tempfile::TempDir, Layout) {
let dir = tempfile::tempdir().expect("tempdir");
let layout = Layout {
root: dir.path().to_path_buf(),
};
(dir, layout)
}
fn touch(layout: &Layout, absolute: &str) {
let path = layout.path(absolute);
fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
fs::write(&path, "").expect("write");
}
fn hook() -> Hook {
Hook {
primary_interface: Some("eth0".to_owned()),
}
}
#[test]
fn an_unrecognized_init_installs_nothing_and_promises_nothing() {
let (_dir, layout) = image();
assert!(!layout.install(&hook()));
assert!(!layout.hook_installed());
}
#[test]
fn the_script_removes_only_a_duplicated_provisional_route() {
let body = hook_script_body(&hook());
assert!(body.starts_with("#!/bin/sh\n"), "{body}");
assert!(
body.contains("ip -4 route show default dev eth0 | $bb wc -l)\" -gt 1 ]"),
"{body}"
);
assert!(
body.contains(&format!(
"ip route del default dev eth0 proto {AGENT_DHCP_ROUTE_PROTO}"
)),
"{body}"
);
assert!(
body.trim_end()
.ends_with(&format!("{BOOT_ID} > {SENTINEL}")),
"{body}"
);
let without = hook_script_body(&Hook::default());
assert!(!without.contains("route"), "{without}");
assert!(without.contains(SENTINEL), "{without}");
}
#[test]
fn a_systemd_image_gets_a_unit_presets_cannot_disable() {
let (_dir, layout) = image();
touch(&layout, "/usr/lib/systemd/systemd");
assert!(layout.install(&hook()));
assert!(layout.hook_installed());
let drop_in = fs::read_to_string(layout.path(SYSTEMD_TARGET_DROP_IN)).expect("drop-in");
assert!(
drop_in.contains("Wants=arcbox-boot-done.service"),
"{drop_in}"
);
let unit = fs::read_to_string(layout.path(SYSTEMD_UNIT)).expect("unit");
assert!(!unit.contains("[Install]"), "{unit}");
assert!(unit.contains(&format!("ExecStart={HOOK_SCRIPT}")), "{unit}");
let mode = fs::metadata(layout.path(HOOK_SCRIPT))
.expect("script")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o755, "every init execs the script directly");
}
#[test]
fn an_openrc_image_gets_a_service_in_the_default_runlevel() {
let (_dir, layout) = image();
touch(&layout, "/sbin/openrc");
assert!(layout.install(&hook()));
assert!(layout.hook_installed());
let runlevel = layout.path(OPENRC_RUNLEVEL);
assert_eq!(
fs::read_link(&runlevel).expect("symlink"),
Path::new(OPENRC_SERVICE)
);
let mode = fs::metadata(layout.path(OPENRC_SERVICE))
.expect("service")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o755, "openrc runs the service as a program");
}
#[test]
fn a_sysvinit_image_gets_one_inittab_entry_after_the_runlevels() {
let (_dir, layout) = image();
touch(&layout, SYSVINIT_RC);
let inittab = layout.path(SYSVINIT_INITTAB);
fs::write(
&inittab,
"id:2:initdefault:\nl2:2:wait:/etc/init.d/rc 2\nl3:3:wait:/etc/init.d/rc 3",
)
.expect("write");
assert!(layout.install(&hook()));
assert!(layout.install(&hook()));
assert!(layout.hook_installed());
let lines: Vec<String> = fs::read_to_string(&inittab)
.expect("inittab")
.lines()
.map(str::to_owned)
.collect();
assert_eq!(lines.len(), 4, "{lines:?}");
assert_eq!(lines[2], "l3:3:wait:/etc/init.d/rc 3");
assert_eq!(lines[3], sysvinit_entry());
}
#[test]
fn a_stale_sysvinit_entry_is_replaced_in_place() {
let (_dir, layout) = image();
touch(&layout, SYSVINIT_RC);
let inittab = layout.path(SYSVINIT_INITTAB);
fs::write(
&inittab,
"l2:2:wait:/etc/init.d/rc 2\nabd:2345:wait:/bin/sh -c 'cat x > y'\nz6:6:respawn:/sbin/sulogin\n",
)
.expect("write");
assert!(layout.install(&hook()));
let lines: Vec<String> = fs::read_to_string(&inittab)
.expect("inittab")
.lines()
.map(str::to_owned)
.collect();
assert_eq!(lines.len(), 3, "{lines:?}");
assert_eq!(lines[1], sysvinit_entry());
assert_eq!(lines[2], "z6:6:respawn:/sbin/sulogin");
}
#[test]
fn a_busybox_inittab_alone_is_not_sysvinit() {
let (_dir, layout) = image();
touch(&layout, SYSVINIT_INITTAB);
assert!(!layout.install(&hook()));
assert!(!layout.hook_installed());
}
#[test]
fn a_failed_enable_rolls_the_unit_back() {
let (_dir, layout) = image();
touch(&layout, "/usr/lib/systemd/systemd");
touch(&layout, "/etc/systemd/system/multi-user.target.d");
assert!(!layout.install_systemd());
assert!(!layout.path(SYSTEMD_UNIT).exists());
assert!(!layout.hook_installed());
}
#[test]
fn a_failed_write_leaves_no_hook_behind() {
let (_dir, layout) = image();
touch(&layout, "/usr/lib/systemd/systemd");
let staged = layout.path(SYSTEMD_UNIT).with_extension("arcbox-tmp");
fs::create_dir_all(&staged).expect("mkdir");
assert!(!layout.install(&hook()));
assert!(!layout.path(SYSTEMD_UNIT).exists());
assert!(!layout.hook_installed());
}
#[test]
fn boot_complete_reads_the_sentinel_against_this_boot() {
let (_dir, layout) = image();
fs::create_dir_all(layout.path(SENTINEL).parent().expect("parent")).expect("mkdir");
fs::create_dir_all(layout.path(BOOT_ID).parent().expect("parent")).expect("mkdir");
fs::write(layout.path(BOOT_ID), "boot-2\n").expect("write");
assert!(!layout.boot_complete(), "no sentinel yet");
fs::write(layout.path(SENTINEL), "boot-1\n").expect("write");
assert!(!layout.boot_complete(), "sentinel from the previous boot");
fs::write(layout.path(SENTINEL), "boot-2\n").expect("write");
assert!(layout.boot_complete());
}
#[test]
fn the_openrc_body_survives_format_escaping() {
let body = openrc_service_body();
assert!(body.contains("depend() {\n after *\n}"), "{body}");
assert!(body.contains("start() {\n"), "{body}");
assert!(!body.contains("{{") && !body.contains("}}"), "{body}");
}
#[test]
fn a_sentinel_from_another_boot_does_not_count() {
assert!(!sentinel_matches("stale-boot-id", "current-boot-id"));
}
#[test]
fn the_trailing_newline_the_hook_writes_is_tolerated() {
assert!(sentinel_matches("boot-id\n", "boot-id\n"));
}
#[test]
fn an_empty_boot_id_never_matches() {
assert!(!sentinel_matches("", ""));
assert!(!sentinel_matches("anything", ""));
}
}