NumericalOS

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