# `schinit` — PID 1 Shaped after launchd rather than sysvinit. The reason is not the configuration format. --- ## Why launchd **Demand launching.** The supervisor holds the listening socket or watches the path, and starts a job when something actually arrives. That inverts the dependency graph. A job stops needing to declare what must be running before it, because the thing it depends on answers on first use — and runlevels stop being a concept at all. SchOS has the two pieces that makes possible: `AF_UNIX` sockets and a real `/proc`. **This milestone is the supervisor; demand launching is the next one.** Socket sections are parsed and remembered but not yet held. ## Constraints on the program itself `cmd/schinit/main.c` links against `lib/`, which is raw syscall stubs. **There is no libc here** — no `malloc`, no `printf`, no `strlen`. Everything is static storage and hand-rolled, which is also why the limits are constants rather than growable: | | | |---|---| | `MAXJOBS` | 32 | | `MAXARGS` | 16 | | manifest size | 4 KB, read into one static buffer | ## Bootstrap Before any job runs, PID 1 creates the device nodes and mounts that a job needs in order to have somewhere to send its output. **This cannot itself be expressed as a job** without a bootstrapping problem, which is why it is a function and not a manifest. Console first, as fd 0/1/2, so every message after it has somewhere to go. Then the `/dev` nodes — minor numbers under major 1 follow Linux's mem-major layout where it costs nothing (`null=2`, `zero=5`, `full=7`, `random=8`, `urandom=9`) — then `/dev/input/event0` and `event1`, the raw disks, and `/proc`. Two details that look like carelessness and are not: **The nodes are created unconditionally, including for absent hardware.** Opening `/dev/hdb` with no drive present fails with `ENXIO`, which is a better answer than a missing path. It matters more than it looks: with no node, `echo x > /dev/hdb` silently creates an ordinary *file* of that name and appears to succeed. **The `mknod`'d `/proc` files still happen and are then covered by the mount.** A failure to mount degrades to the old behaviour rather than losing `/proc` entirely. ## Job manifests Every `*.job` file in `/etc/schinit` is loaded at boot, found with `getdents64` rather than a hardcoded list — so dropping a file in the directory is all it takes to add a job. Sectioned `key = value`, `#` starts a comment, keys are case-insensitive (a manifest may say `KeepAlive`, `keepalive` or `KEEPALIVE`; keys are for humans and this is not a language). | Key | | |---|---| | `Label` | A reverse-DNS name, as launchd uses. Identity, not a path. | | `Program` | The binary to execute | | `Args` | argv, space separated. argv[0] is conventionally the basename, so `Args = sh` for `/bin/sh` | | `RunAtLoad` | Start it when schinit loads | | `KeepAlive` | Restart it when it exits | | `Disabled` | Load it but do not start it | | `ThrottleInterval` | Seconds between respawns (default 1) | | `ControllingTerminal` | Claim the console | | `StandardIn` / `StandardOut` / `StandardError` | Paths to open as fd 0, 1, 2 | | `[Socket ]` → `Path` | Parsed, recorded, not yet honoured | **Unknown keys and unknown sections are ignored rather than fatal.** A manifest written for a newer schinit must not stop this one booting the machine. ## Starting a job Each job gets **its own session** (`setsid`), so it cannot reach back and signal the rest of the system through a shared controlling terminal. That is exactly what makes `ControllingTerminal` necessary, and the interaction is the least obvious thing in the program: > A fresh session has no controlling terminal, and the console only delivers > input to the foreground process group it is tracking. Without > `ControllingTerminal = yes`, the shell comes up, echoes every keystroke through > the line discipline, and executes none of them — which looks like a hung shell > and is really a terminal that is not listening to it. It is **off by default**. A daemon has no business seizing the console, and launchd's model is that the console is something a job asks for. `exec.c` enables the Linux ABI automatically for musl binaries, so a job must not call `enable_linux_abi()` itself — it would renumber the syscalls the child still has to make. ## Supervision One loop around `wait()`. **It reaps whatever exited, including processes reparented here when their own parent died.** Those are not jobs, `job_by_pid` returns 0 for them, and that is the whole handling they need. A `KeepAlive` job is restarted, subject to `ThrottleInterval` — a job that exits immediately and forever would otherwise spin the machine. launchd defaults to 10 seconds; 1 is enough here, where the common case is a shell someone just exited on purpose and expects back. When `wait()` returns -1 there is nothing left to wait for, which should not happen while any job is alive. It sleeps rather than spinning, so a mistake there costs an idle machine and not a hot CPU. ## The fallback With no manifests found, schinit says so and starts `/bin/sh` on the console anyway. > An unbootable-looking machine because a manifest was missing would be a poor > trade for purity. ## PID 1 cannot be killed Enforced in the kernel, not here. `init_ignores()` discards any signal whose disposition is the default action when the target is PID 1, **including `SIGKILL`** — see [kernel.md](kernel.md#signals) for why Ctrl-C is the input that finds that window. A handler init did install is still delivered. --- ## What it does not do - **Socket activation is not implemented.** The manifest key is parsed and the path remembered; nothing holds the socket, so demand launching — the entire reason for copying launchd — does not happen yet. - **No `schinitctl`.** Jobs cannot be started, stopped, reloaded or inspected at runtime. The only interface is rebooting with a different manifest. - **The job table is invisible.** Nothing reports what is loaded, what is running, or how many times something has respawned. - **No dependency ordering of any kind**, deliberately — demand launching is meant to make it unnecessary, and until that exists there is nothing in its place either. - **No log capture.** `StandardOut` is a path, so a job's output goes wherever the manifest says and nothing aggregates it. - **A crash in schinit is fatal to the machine**, because `exit()` panics on initproc. Fixing that is [../plan/init.md](../plan/init.md). - **32 jobs, 16 arguments, 4 KB manifests**, all compile-time.