boot/numinit.sh
back to source
#!/bin/sh
# NumericalOS init - shell PID-1 path.
# Walks BootPhase records by ordinal, resolves the unit DAG, runs units.
set -u
NUMOS_STATE_FILE=""
numos_die() {
echo "numos: HALT: $*" >&2
exit 1
}
numos_load_state() {
NUMOS_STATE_FILE="$1"
[ -f "$NUMOS_STATE_FILE" ] || numos_die "state not found: $1"
}
numos_phase_ordinals() {
grep '^P ' "$NUMOS_STATE_FILE" | cut -d' ' -f2 | sort -n
}
numos_phase_field() {
# numos_phase_field <ordinal> <name|on_failure|required_units>
line="$(grep "^P $1 " "$NUMOS_STATE_FILE" | head -n 1)"
[ -n "$line" ] || numos_die "no phase with ordinal $1"
case "$2" in
name) echo "$line" | cut -d' ' -f3 ;;
on_failure) echo "$line" | cut -d' ' -f4 ;;
required_units) echo "$line" | cut -d' ' -f5 | tr ',' ' ' | sed 's/^-$//' ;;
*) numos_die "unknown phase field: $2" ;;
esac
}
numos_unit_field() {
# numos_unit_field <name> <field>. exec is last on the line, so it keeps spaces.
line="$(grep "^U $1 " "$NUMOS_STATE_FILE" | head -n 1)"
[ -n "$line" ] || numos_die "unknown unit: $1"
case "$2" in
kind) echo "$line" | cut -d' ' -f3 ;;
restart) echo "$line" | cut -d' ' -f4 ;;
backoff_ms) echo "$line" | cut -d' ' -f5 ;;
backoff_max_ms) echo "$line" | cut -d' ' -f6 ;;
requires) echo "$line" | cut -d' ' -f9 | tr ',' ' ' | sed 's/^-$//' ;;
after) echo "$line" | cut -d' ' -f10 | tr ',' ' ' | sed 's/^-$//' ;;
exec) echo "$line" | cut -d' ' -f11- ;;
*) numos_die "unknown unit field: $2" ;;
esac
}
# Emit units in dependency order (Kahn's algorithm).
#
# Deliberately iterative, not recursive: POSIX sh has no `local`, so a
# recursive helper's variables are global and the recursive call clobbers the
# caller's loop variable. Two passes instead: expand the dependency closure,
# then repeatedly emit whichever units have all their deps already emitted.
numos_unit_deps() {
# numos_unit_deps <name>. Fails (nonzero) if the unit is unknown; caller
# must check the status, since exit inside a $(...) only kills the subshell.
reqs="$(numos_unit_field "$1" requires)" || return 1
afters="$(numos_unit_field "$1" after)" || return 1
echo "$reqs $afters"
}
numos_resolve_order() {
closure=""
pending="$*"
while [ -n "$(echo $pending)" ]; do
nextwave=""
for node in $pending; do
case " $closure " in
*" $node "*) continue ;;
esac
closure="$closure $node"
deps="$(numos_unit_deps "$node")" || numos_die "unit $node: dependency lookup failed"
nextwave="$nextwave $deps"
done
pending="$nextwave"
done
emitted=""
remaining="$closure"
while [ -n "$(echo $remaining)" ]; do
progress=0
stillwaiting=""
for node in $remaining; do
ready=1
deps="$(numos_unit_deps "$node")" || numos_die "unit $node: dependency lookup failed"
for dep in $deps; do
case " $emitted " in
*" $dep "*) ;;
*) ready=0 ;;
esac
done
if [ "$ready" = "1" ]; then
echo "$node"
emitted="$emitted $node"
progress=1
else
stillwaiting="$stillwaiting $node"
fi
done
remaining="$stillwaiting"
[ "$progress" = "1" ] || numos_die "unresolvable dependency order in: $remaining"
done
}
# Execute a unit's exec line synchronously and return its exit status.
# The NUMOS_DRY_RUN seam lives here, so every path that would execute
# something -- the phase walk and the supervisor alike -- reports instead.
numos_exec_unit() {
name="$1"
if [ "${NUMOS_DRY_RUN:-0}" = "1" ]; then
echo "RUN $name"
return 0
fi
cmd="$(numos_unit_field "$name" exec)" || numos_die "unit $name: exec lookup failed"
sh -c "$cmd"
}
# Start a unit according to its kind.
#
# oneshot and target run to completion: the phase walk needs their exit
# status to apply the phase's on_failure policy. A longrun unit is a daemon
# -- it does not return -- so PID 1 must not wait on one. It is started in
# the background and reported successful once started; the seeded `join`
# unit is exactly this shape and would otherwise block the phase walk at
# phase 50 forever. Watching a started longrun unit and restarting it is
# steady-state supervision (numos_supervise), which Spec 1 does not wire
# into the boot path -- see README and design spec section 11.
numos_run_unit() {
name="$1"
kind="$(numos_unit_field "$name" kind)" || numos_die "unit $name: kind lookup failed"
case "$kind" in
oneshot|target)
numos_exec_unit "$name"
;;
longrun)
if [ "${NUMOS_DRY_RUN:-0}" = "1" ]; then
# Nothing is executed in dry run, so nothing can block: report
# synchronously to keep RUN lines in dependency order.
numos_exec_unit "$name"
return 0
fi
numos_exec_unit "$name" &
return 0
;;
*)
numos_die "unit $name: unknown kind: $kind"
;;
esac
}
# Delays double from initial up to the ceiling, then hold there.
numos_backoff_sequence() {
delay="$1"
ceiling="$2"
count="$3"
[ "$delay" -le "$ceiling" ] || delay="$ceiling"
n=0
while [ "$n" -lt "$count" ]; do
echo "$delay"
delay=$((delay * 2))
[ "$delay" -le "$ceiling" ] || delay="$ceiling"
n=$((n + 1))
done
}
# Compute the sleep(1) argument for a millisecond delay, without sleeping.
# Split out from numos_sleep_ms so tests can assert on the computed value
# without waiting on a real sleep. A nonzero delay must never compute to an
# argument that means "no wait" -- the fractional form below always carries
# a nonzero fractional part when ms is not an exact multiple of 1000, and
# numos_sleep_ms's whole-second fallback (below) separately guards the case
# where the fractional form is rejected by the running sleep(1).
numos_sleep_arg() {
ms="$1"
[ "$ms" -gt 0 ] || { echo 0; return 0; }
secs=$((ms / 1000))
rem=$((ms % 1000))
frac="$(printf '%03d' "$rem")"
echo "$secs.$frac"
}
numos_sleep_ms() {
[ "${NUMOS_NO_SLEEP:-0}" = "1" ] && return 0
ms="$1"
[ "$ms" -gt 0 ] || return 0
arg="$(numos_sleep_arg "$ms")"
# Prefer the fractional form (GNU coreutils and busybox both accept it).
# POSIX only guarantees whole-second sleep, so fall back rather than
# assume -- and round the fallback UP so a sub-second delay never
# collapses to "sleep 0" (a crash-looping unit would otherwise spin at
# full speed with zero throttling).
sleep "$arg" 2>/dev/null && return 0
whole=$(( (ms + 999) / 1000 ))
[ "$whole" -gt 0 ] || whole=1
sleep "$whole"
}
# Restart a unit under its own policy, waiting on each attempt.
#
# NOT CALLED BY numos_main: this is steady-state supervision, which Spec 1
# implements and tests but does not wire into the boot path. It runs each
# attempt through numos_exec_unit (synchronous) rather than numos_run_unit,
# because a supervisor's whole job is to wait for the process to exit and
# then decide whether to restart it -- backgrounding here would make every
# attempt report success immediately.
numos_supervise() {
name="$1"
max_attempts="$2"
policy="$(numos_unit_field "$name" restart)" || numos_die "unit $name: restart lookup failed"
init_backoff="$(numos_unit_field "$name" backoff_ms)" || numos_die "unit $name: backoff_ms lookup failed"
ceiling_backoff="$(numos_unit_field "$name" backoff_max_ms)" || numos_die "unit $name: backoff_max_ms lookup failed"
case "$init_backoff" in
''|*[!0-9]*) numos_die "unit $name: backoff_ms is not a non-negative integer: $init_backoff" ;;
esac
case "$ceiling_backoff" in
''|*[!0-9]*) numos_die "unit $name: backoff_max_ms is not a non-negative integer: $ceiling_backoff" ;;
esac
delays="$(numos_backoff_sequence "$init_backoff" "$ceiling_backoff" "$max_attempts")" || numos_die "unit $name: backoff sequence generation failed"
attempt=0
last_ok=0
for delay in $delays; do
attempt=$((attempt + 1))
echo "ATTEMPT $attempt"
if numos_exec_unit "$name"; then
last_ok=1
[ "$policy" = "always" ] || return 0
else
last_ok=0
[ "$policy" = "never" ] && return 1
fi
[ "$attempt" -lt "$max_attempts" ] || break
numos_sleep_ms "$delay"
done
[ "$last_ok" = "1" ] && return 0
return 1
}
numos_run_phase() {
ordinal="$1"
required="$(numos_phase_field "$ordinal" required_units)" || numos_die "phase $ordinal: required_units lookup failed"
[ -n "$required" ] || return 0
order="$(numos_resolve_order $required)" || numos_die "phase $ordinal: dependency resolution failed"
for name in $order; do
numos_run_unit "$name" || return 1
done
return 0
}
NUMOS_DEGRADED=0
numos_degraded() {
echo "$NUMOS_DEGRADED"
}
numos_mark_degraded() {
NUMOS_DEGRADED=1
}
# A phase failed. What happens next is the phase's own on_failure policy.
numos_handle_phase_failure() {
ordinal="$1"
name="$(numos_phase_field "$ordinal" name)" || numos_die "phase $ordinal: name lookup failed"
policy="$(numos_phase_field "$ordinal" on_failure)" || numos_die "phase $ordinal: on_failure lookup failed"
case "$policy" in
halt)
numos_die "phase $name failed and is on_failure=halt"
;;
degrade)
numos_mark_degraded
echo "numos: DEGRADED: $name"
;;
continue)
echo "numos: WARN: phase $name failed, continuing"
;;
*)
numos_die "phase $name has an unknown on_failure policy"
;;
esac
}
numos_main() {
[ -n "${NUMOS_STATE:-}" ] || numos_die "NUMOS_STATE is unset"
numos_load_state "$NUMOS_STATE"
for ordinal in $(numos_phase_ordinals); do
if ! numos_run_phase "$ordinal"; then
numos_handle_phase_failure "$ordinal"
fi
done
echo "numos: boot complete degraded=$(numos_degraded)"
}
if [ "${NUMOS_SOURCE_ONLY:-0}" != "1" ]; then
numos_main "$@"
fi