schdbgd ships Disabled, and its manifest explains why: a debugger that can read
the process table should not run on a machine whose owner never asked for one.
That default is right and has one bad consequence -- **the machines that most
need debugging are the ones you cannot get a shell on to enable it.** A laptop
with no serial port that stops before it can draw has nowhere to say why, and
`schinitd enable` needs a prompt that does not exist.
So `make DEBUG=1` adds a sixth entry to the F8 menu. Picking it toggles;
booting carries the choice through SCHBOOT_DEBUG_FLAG; the kernel publishes
/proc/bootdebug; schinitd starts the one job that flag may un-disable, with
the manifest's Args now naming 0.0.0.0 -- a debugger reachable only from the
machine being debugged is no use when that machine is the one that will not
talk to you.
Four decisions, since this is a network-reachable debugger:
compiled out, not hidden A release build cannot set the flag because the
code that would set it is not in the binary. A
menu item that is present and refuses is an
invitation to make it not refuse.
consent at the machine Somebody standing at the keyboard during boot is
the strongest consent this system can ask for and
the weakest one that is any use on hardware that
will not boot.
one job, named in schinitd Not a manifest key like EnableIfDebug: the general
version of "the boot menu starts things that ship
off" is worse than the specific one, and this way
what the flag can start is one readable line.
the kernel only publishes Nothing in the kernel behaves differently, so a
debug kernel and a release kernel are the same
kernel and a bug cannot be the flag's fault.
/proc/bootdebug is its own file rather than a second line in /proc/bootmode,
because bootmode's whole promise is that reading it is a string compare and
not a parser.
**Also: `tone` stopped lying.** On a machine with no sound card it reported
"the device will not take 16-bit samples", which sends you looking at the
format code instead of at the empty PCI slot -- /dev/dsp exists whether or not
hardware is behind it, and the ioctl is where the absence shows up. ENODEV is
now told apart from a format refusal.
Verified end to end: F8 -> item 6 -> boot -> schinitd says it is starting
schdbgd -> a client outside the VM connected to the forwarded port and ran
`dma`, getting the real arena dump back. The release build has no such entry
in the binary at all, and boots clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
788 lines
26 KiB
C
788 lines
26 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// schinitd -- the supervisor. PID 2, and restartable.
|
|
//
|
|
// Everything that used to make PID 1 complicated lives here: reading
|
|
// /etc/schinit, parsing manifests, deciding what runs, respawn policy and
|
|
// throttling. docs/architecture/init.md calls this "where the bugs will be", which is
|
|
// exactly why it is a separate program -- a bug here is a restart, not a panic.
|
|
//
|
|
// The one rule that makes the split worth having:
|
|
//
|
|
// **This program never forks a job.** It decides, and asks PID 1 to act. So
|
|
// every job on the machine is a child of PID 1 at all times, and this
|
|
// process dying orphans nothing, reparents nothing and restarts nothing. It
|
|
// comes back, reads /proc/schinit, and finds the world exactly as it left it.
|
|
//
|
|
// A supervisor that forked its own children could not be restarted without
|
|
// either losing them to reparenting or racing the new copy to adopt them --
|
|
// the split-brain failure docs/architecture/init.md rejects the two-init scheme for.
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "posix.h"
|
|
#include "socket.h"
|
|
#include "schinit.h"
|
|
|
|
#define JOBDIR "/etc/schinit"
|
|
|
|
// Sizes. These mirror the protocol's, deliberately: a job that parses here but
|
|
// will not fit in a spawn message is a job that fails at the last moment, and
|
|
// keeping the two the same makes that impossible rather than unlikely.
|
|
#define MAXJOBS 32
|
|
#define MAXARGS SCHINIT_MAXARGS
|
|
#define MAXENVV SCHINIT_MAXENVV
|
|
#define MAXLABEL SCHINIT_MAXLABEL
|
|
#define MAXPATH SCHINIT_MAXPATH
|
|
#define MAXLINE 256
|
|
|
|
static int
|
|
slen(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
return n;
|
|
}
|
|
|
|
static void
|
|
scopy(char *dst, const char *src, int cap)
|
|
{
|
|
int i = 0;
|
|
while (src[i] && i < cap - 1) { dst[i] = src[i]; i++; }
|
|
dst[i] = '\0';
|
|
}
|
|
|
|
static int
|
|
seq(const char *a, const char *b)
|
|
{
|
|
while (*a && *a == *b) { a++; b++; }
|
|
return *a == *b;
|
|
}
|
|
|
|
// Case-insensitive, so a manifest may say KeepAlive, keepalive or KEEPALIVE.
|
|
// Keys are for humans and this is not a language.
|
|
static int
|
|
sieq(const char *a, const char *b)
|
|
{
|
|
for (;;) {
|
|
char ca = *a, cb = *b;
|
|
if (ca >= 'A' && ca <= 'Z') ca += 32;
|
|
if (cb >= 'A' && cb <= 'Z') cb += 32;
|
|
if (ca != cb) return 0;
|
|
if (!ca) return 1;
|
|
a++; b++;
|
|
}
|
|
}
|
|
|
|
static int
|
|
ends_with(const char *s, const char *suffix)
|
|
{
|
|
int ls = slen(s), lf = slen(suffix);
|
|
if (lf > ls) return 0;
|
|
return seq(s + ls - lf, suffix);
|
|
}
|
|
|
|
static void
|
|
out(const char *s)
|
|
{
|
|
write(1, (void *)s, slen(s));
|
|
}
|
|
|
|
// How the machine was booted, read once.
|
|
//
|
|
// The routine progress lines below -- one per job started, plus the summary --
|
|
// are the userspace half of the same problem the kernel has: worth reading on
|
|
// a boot you are watching, and noise on the several thousand after it. They
|
|
// are not deleted, they are held back, and `make qemu` with F8 or any verbose
|
|
// boot brings all of them back.
|
|
//
|
|
// Failures are never held back. A job that could not start, a channel that
|
|
// closed, a pid 1 that stopped answering: those print either way, because a
|
|
// silent supervisor is worse than a chatty one.
|
|
static int verbose_boot;
|
|
|
|
// Whether the boot menu was asked for a network debugger. See
|
|
// SCHBOOT_DEBUG_FLAG in include/schboot.h -- the kernel only publishes it, and
|
|
// this is what acts on it, because what it turns on is a job.
|
|
static int debug_boot;
|
|
|
|
static void
|
|
read_bootmode(void)
|
|
{
|
|
char b[16];
|
|
int fd = open("/proc/bootmode", O_RDONLY);
|
|
int n;
|
|
|
|
if (fd >= 0) {
|
|
n = read(fd, b, sizeof(b) - 1);
|
|
close(fd);
|
|
if (n > 0) {
|
|
b[n] = 0;
|
|
verbose_boot = (b[0] == 'v');
|
|
}
|
|
}
|
|
|
|
fd = open("/proc/bootdebug", O_RDONLY);
|
|
if (fd >= 0) {
|
|
n = read(fd, b, sizeof(b) - 1);
|
|
close(fd);
|
|
if (n > 0) {
|
|
b[n] = 0;
|
|
debug_boot = (b[0] == 'o' && b[1] == 'n');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Progress, as opposed to news.
|
|
static void
|
|
vout(const char *s)
|
|
{
|
|
if (verbose_boot)
|
|
write(1, (void *)s, slen(s));
|
|
}
|
|
|
|
static void
|
|
outn(int v)
|
|
{
|
|
char buf[12];
|
|
int i = 11;
|
|
int neg = v < 0;
|
|
if (neg) v = -v;
|
|
buf[i--] = '\0';
|
|
if (v == 0) buf[i--] = '0';
|
|
while (v > 0) { buf[i--] = (char)('0' + v % 10); v /= 10; }
|
|
if (neg) buf[i--] = '-';
|
|
out(&buf[i + 1]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Jobs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct job {
|
|
char label[MAXLABEL];
|
|
char program[MAXPATH];
|
|
char *argv[MAXARGS + 1];
|
|
char argstore[MAXLINE];
|
|
|
|
// Environment, one "KEY=VALUE" per Environment line, repeatable. Stored
|
|
// as pointers into envstore so the job owns its own strings -- the parse
|
|
// buffer is gone by the time anything execs.
|
|
char *envp[MAXENVV + 1];
|
|
char envstore[MAXLINE * 2];
|
|
int envlen;
|
|
int envn;
|
|
|
|
char stdin_path[MAXPATH];
|
|
char stdout_path[MAXPATH];
|
|
char stderr_path[MAXPATH];
|
|
|
|
int run_at_load;
|
|
int keep_alive;
|
|
int disabled;
|
|
int throttle; // seconds between respawns
|
|
int own_tty; // claim the console as controlling terminal
|
|
|
|
// A socket the job wants schinit to hold. Parsed now, honoured in
|
|
// milestone 2 -- recorded here so the manifest format does not have to
|
|
// change when it is.
|
|
char sock_path[MAXPATH];
|
|
|
|
int pid; // 0 when not running
|
|
unsigned int cookie; // PID 1's handle for this job while it runs
|
|
int last_start; // uptime() ticks, for throttling
|
|
int starts;
|
|
};
|
|
|
|
static struct job jobs[MAXJOBS];
|
|
static int njobs;
|
|
|
|
static void
|
|
job_defaults(struct job *j)
|
|
{
|
|
j->label[0] = j->program[0] = '\0';
|
|
j->stdin_path[0] = j->stdout_path[0] = j->stderr_path[0] = '\0';
|
|
j->sock_path[0] = '\0';
|
|
j->argstore[0] = '\0';
|
|
j->argv[0] = 0;
|
|
j->run_at_load = 0;
|
|
j->keep_alive = 0;
|
|
j->disabled = 0;
|
|
j->throttle = 1;
|
|
j->own_tty = 0;
|
|
j->pid = 0;
|
|
j->last_start = 0;
|
|
j->starts = 0;
|
|
}
|
|
|
|
static int
|
|
truthy(const char *v)
|
|
{
|
|
return sieq(v, "yes") || sieq(v, "true") || sieq(v, "1") || sieq(v, "on");
|
|
}
|
|
|
|
static int
|
|
parse_int(const char *s)
|
|
{
|
|
int v = 0;
|
|
while (*s >= '0' && *s <= '9') { v = v * 10 + (*s - '0'); s++; }
|
|
return v;
|
|
}
|
|
|
|
// Split Args into argv, in place within the job's own storage. argv[0] is the
|
|
// program's name as it should see it, which is conventionally the basename and
|
|
// not the full path -- so a manifest says `Args = sh` for `/bin/sh`.
|
|
static void
|
|
split_args(struct job *j, const char *value)
|
|
{
|
|
scopy(j->argstore, value, MAXLINE);
|
|
int n = 0;
|
|
char *p = j->argstore;
|
|
while (*p && n < MAXARGS) {
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (!*p) break;
|
|
j->argv[n++] = p;
|
|
while (*p && *p != ' ' && *p != '\t') p++;
|
|
if (*p) *p++ = '\0';
|
|
}
|
|
j->argv[n] = 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Manifest parsing
|
|
//
|
|
// Sectioned key = value. Comments start with #. A [Socket name] section marks
|
|
// everything after it as belonging to that socket, which is why Path is read
|
|
// into sock_path rather than treated as a job key.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// One "KEY=VALUE" onto the job's environment. Repeatable, so a manifest reads
|
|
//
|
|
// Environment = TERM=linux
|
|
// Environment = PATH=/bin:/usr/bin
|
|
//
|
|
// rather than inventing a separator that a value might contain.
|
|
static void
|
|
add_env(struct job *j, const char *kv)
|
|
{
|
|
if (j->envn >= MAXENVV)
|
|
return;
|
|
int n = slen(kv);
|
|
if (j->envlen + n + 1 > (int)sizeof(j->envstore))
|
|
return;
|
|
char *dst = j->envstore + j->envlen;
|
|
scopy(dst, kv, n + 1);
|
|
j->envp[j->envn++] = dst;
|
|
j->envp[j->envn] = 0;
|
|
j->envlen += n + 1;
|
|
}
|
|
|
|
static char *
|
|
trim(char *s)
|
|
{
|
|
while (*s == ' ' || *s == '\t') s++;
|
|
int n = slen(s);
|
|
while (n > 0 && (s[n-1] == ' ' || s[n-1] == '\t' ||
|
|
s[n-1] == '\r' || s[n-1] == '\n'))
|
|
s[--n] = '\0';
|
|
return s;
|
|
}
|
|
|
|
static int
|
|
load_job(const char *path)
|
|
{
|
|
if (njobs >= MAXJOBS) {
|
|
out("schinit: too many jobs, ignoring "); out(path); out("\n");
|
|
return -1;
|
|
}
|
|
|
|
int fd = open((char *)path, O_RDONLY);
|
|
if (fd < 0) return -1;
|
|
|
|
// Whole file into one static buffer. A job manifest that does not fit in
|
|
// 4 KB is a manifest doing something it should not.
|
|
static char buf[4096];
|
|
int n = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0) return -1;
|
|
buf[n] = '\0';
|
|
|
|
struct job *j = &jobs[njobs];
|
|
job_defaults(j);
|
|
|
|
int in_socket = 0;
|
|
char *p = buf;
|
|
while (*p) {
|
|
char *line = p;
|
|
while (*p && *p != '\n') p++;
|
|
if (*p) *p++ = '\0';
|
|
|
|
// Strip comments before trimming, so trailing comments work too.
|
|
for (char *c = line; *c; c++)
|
|
if (*c == '#') { *c = '\0'; break; }
|
|
|
|
line = trim(line);
|
|
if (!*line) continue;
|
|
|
|
if (line[0] == '[') {
|
|
// [Socket name] -- the only section kind for now. Anything else is
|
|
// ignored rather than fatal: an unknown section from a newer
|
|
// schinit should not stop this one booting the machine.
|
|
in_socket = (line[1] == 'S' || line[1] == 's');
|
|
continue;
|
|
}
|
|
|
|
char *eq = line;
|
|
while (*eq && *eq != '=') eq++;
|
|
if (!*eq) continue; // not key = value; skip
|
|
*eq = '\0';
|
|
char *key = trim(line);
|
|
char *val = trim(eq + 1);
|
|
|
|
if (in_socket) {
|
|
if (sieq(key, "Path")) scopy(j->sock_path, val, MAXPATH);
|
|
continue;
|
|
}
|
|
|
|
if (sieq(key, "Label")) scopy(j->label, val, MAXLABEL);
|
|
else if (sieq(key, "Program")) scopy(j->program, val, MAXPATH);
|
|
else if (sieq(key, "Args")) split_args(j, val);
|
|
else if (sieq(key, "RunAtLoad")) j->run_at_load = truthy(val);
|
|
else if (sieq(key, "KeepAlive")) j->keep_alive = truthy(val);
|
|
else if (sieq(key, "Disabled")) j->disabled = truthy(val);
|
|
else if (sieq(key, "ThrottleInterval")) j->throttle = parse_int(val);
|
|
else if (sieq(key, "ControllingTerminal")) j->own_tty = truthy(val);
|
|
else if (sieq(key, "Environment")) add_env(j, val);
|
|
else if (sieq(key, "StandardIn")) scopy(j->stdin_path, val, MAXPATH);
|
|
else if (sieq(key, "StandardOut")) scopy(j->stdout_path, val, MAXPATH);
|
|
else if (sieq(key, "StandardError")) scopy(j->stderr_path, val, MAXPATH);
|
|
// Unknown keys are ignored on purpose, same reasoning as sections.
|
|
}
|
|
|
|
if (!j->program[0]) {
|
|
out("schinit: "); out(path); out(": no Program, ignoring\n");
|
|
return -1;
|
|
}
|
|
if (!j->label[0])
|
|
scopy(j->label, j->program, MAXLABEL);
|
|
if (!j->argv[0]) {
|
|
// No Args given: pass the program path as argv[0], which is what a
|
|
// program expects when nothing better is said.
|
|
scopy(j->argstore, j->program, MAXLINE);
|
|
j->argv[0] = j->argstore;
|
|
j->argv[1] = 0;
|
|
}
|
|
if (j->throttle <= 0)
|
|
j->throttle = 1;
|
|
|
|
njobs++;
|
|
return 0;
|
|
}
|
|
|
|
// Read every *.job in JOBDIR. getdents64 rather than a hardcoded list, so
|
|
// dropping a file in the directory is all it takes to add a job.
|
|
static void
|
|
load_jobs(void)
|
|
{
|
|
int fd = open(JOBDIR, O_RDONLY);
|
|
if (fd < 0) {
|
|
out("schinit: no " JOBDIR "\n");
|
|
return;
|
|
}
|
|
|
|
static char dbuf[4096];
|
|
for (;;) {
|
|
int n = getdents64(fd, dbuf, sizeof(dbuf));
|
|
if (n <= 0) break;
|
|
|
|
int off = 0;
|
|
while (off < n) {
|
|
struct dirent64 *d = (struct dirent64 *)(dbuf + off);
|
|
// Step by d_reclen: records are packed and variable length.
|
|
if (d->d_reclen == 0) break;
|
|
off += d->d_reclen;
|
|
|
|
if (d->d_name[0] == '.') continue;
|
|
if (!ends_with(d->d_name, ".job")) continue;
|
|
|
|
char path[MAXPATH];
|
|
scopy(path, JOBDIR "/", MAXPATH);
|
|
int at = slen(path);
|
|
scopy(path + at, d->d_name, MAXPATH - at);
|
|
load_job(path);
|
|
}
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Talking to PID 1
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static int sock = SCHINIT_SOCK_FD;
|
|
|
|
// Flatten a job into a spawn request. Strings are copied into one buffer and
|
|
// referred to by offset, because pointers do not survive the trip.
|
|
static int
|
|
pack(struct schinit_spawn *s, struct job *j, unsigned int cookie)
|
|
{
|
|
int used = 0;
|
|
|
|
for (unsigned int z = 0; z < sizeof(*s); z++) ((char *)s)[z] = 0;
|
|
s->op = SCHINIT_SPAWN;
|
|
s->cookie = cookie;
|
|
scopy(s->label, j->label, SCHINIT_MAXLABEL);
|
|
scopy(s->program, j->program, SCHINIT_MAXPATH);
|
|
scopy(s->stdin_path, j->stdin_path, SCHINIT_MAXPATH);
|
|
scopy(s->stdout_path, j->stdout_path, SCHINIT_MAXPATH);
|
|
scopy(s->stderr_path, j->stderr_path, SCHINIT_MAXPATH);
|
|
s->own_tty = j->own_tty;
|
|
|
|
for (int i = 0; i < MAXARGS && j->argv[i]; i++) {
|
|
int n = slen(j->argv[i]) + 1;
|
|
if (used + n > SCHINIT_STRBUF)
|
|
return -1; // too much text; refuse rather than trim
|
|
s->args[s->nargs++] = (unsigned short)used;
|
|
scopy(&s->strbuf[used], j->argv[i], n);
|
|
used += n;
|
|
}
|
|
for (int i = 0; i < j->envn; i++) {
|
|
int n = slen(j->envp[i]) + 1;
|
|
if (used + n > SCHINIT_STRBUF)
|
|
return -1;
|
|
s->envs[s->nenvs++] = (unsigned short)used;
|
|
scopy(&s->strbuf[used], j->envp[i], n);
|
|
used += n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Ask PID 1 to start a job. The reply may be preceded by exit notifications
|
|
// for other jobs, so this reads until it sees the answer to *this* cookie and
|
|
// handles anything else on the way past.
|
|
static void note_exit(unsigned int cookie, int pid, int status);
|
|
|
|
static unsigned int next_cookie = 1;
|
|
|
|
static int
|
|
ask_spawn(struct job *j)
|
|
{
|
|
struct schinit_spawn s;
|
|
unsigned int cookie = next_cookie++;
|
|
|
|
if (pack(&s, j, cookie) < 0) {
|
|
out("schinitd: "); out(j->label); out(": job too large to send\n");
|
|
return -1;
|
|
}
|
|
if (write(sock, &s, sizeof(s)) != sizeof(s))
|
|
return -1;
|
|
|
|
for (;;) {
|
|
struct schinit_reply r;
|
|
if (read(sock, &r, sizeof(r)) != sizeof(r))
|
|
return -1; // PID 1 is gone, which cannot happen
|
|
if (r.op == SCHINIT_SPAWNED && r.cookie == cookie) {
|
|
if (r.pid > 0) {
|
|
j->pid = r.pid;
|
|
j->cookie = cookie;
|
|
j->last_start = uptime();
|
|
j->starts++;
|
|
}
|
|
return r.pid;
|
|
}
|
|
if (r.op == SCHINIT_EXITED)
|
|
note_exit(r.cookie, r.pid, r.status);
|
|
}
|
|
}
|
|
|
|
static struct job *
|
|
job_by_cookie(unsigned int cookie)
|
|
{
|
|
for (int i = 0; i < njobs; i++)
|
|
if (jobs[i].cookie == cookie && jobs[i].pid)
|
|
return &jobs[i];
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
note_exit(unsigned int cookie, int pid, int status)
|
|
{
|
|
struct job *j = job_by_cookie(cookie);
|
|
if (!j)
|
|
return; // not ours, or already noticed
|
|
j->pid = 0;
|
|
j->cookie = 0;
|
|
|
|
if (!j->keep_alive)
|
|
return;
|
|
|
|
// A job that says it cannot run on this machine is believed. The status is
|
|
// wait(2)-encoded: the low seven bits are the signal that killed it, so a
|
|
// job that was killed or faulted never matches this and is restarted the
|
|
// way KeepAlive promises. See SCHINIT_EXIT_NORESTART.
|
|
if ((status & 0x7f) == 0 && ((status >> 8) & 0xff) == SCHINIT_EXIT_NORESTART) {
|
|
// Not a failure: the job looked, found the machine did not have what
|
|
// it drives, and said so properly. Worth reading once and not every
|
|
// boot on a machine that will never have one.
|
|
vout("schinitd: "); vout(j->label);
|
|
vout(" has nothing to do on this machine; not restarting\n");
|
|
return;
|
|
}
|
|
|
|
// Throttle. A job that exits immediately and forever would otherwise spin
|
|
// the machine; launchd's ThrottleInterval exists for exactly this.
|
|
int ticks = j->throttle * 100;
|
|
int since = uptime() - j->last_start;
|
|
if (since < ticks)
|
|
sleep(ticks - since);
|
|
|
|
out("schinitd: restarting "); out(j->label); out("\n");
|
|
ask_spawn(j);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Adopting what is already running
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Read /proc/schinit and claim the jobs PID 1 is already holding.
|
|
//
|
|
// This is the half of "State outside the process" that makes the other half worth
|
|
// anything. PID 1 publishes the table; without this, a restarted supervisor
|
|
// reads its manifests, sees nothing running *as far as it knows*, and starts
|
|
// everything a second time. The console shell then exists twice, both reading
|
|
// the same terminal, and every keystroke goes to whichever woke first -- the
|
|
// split-brain failure the plan rejects the two-init scheme for, arrived at by
|
|
// a different route.
|
|
//
|
|
// The state is outside the process precisely so that coming back is a *read*
|
|
// rather than a guess.
|
|
static unsigned int
|
|
adopt(void)
|
|
{
|
|
char buf[2048];
|
|
int fd = open("/proc/schinit", O_RDONLY);
|
|
unsigned int highest = 0;
|
|
|
|
if (fd < 0)
|
|
return 0;
|
|
int n = read(fd, buf, sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0)
|
|
return 0;
|
|
buf[n] = 0;
|
|
|
|
// Lines are "cookie pid started label". The two header lines do not match
|
|
// that shape and fall out of the parse on their own.
|
|
char *p = buf;
|
|
while (*p) {
|
|
char *line = p;
|
|
while (*p && *p != '\n') p++;
|
|
if (*p == '\n') *p++ = 0;
|
|
|
|
unsigned int cookie = 0;
|
|
int pid = 0, started = 0, f = 0;
|
|
char *q = line;
|
|
|
|
// cookie
|
|
if (*q < '0' || *q > '9') continue;
|
|
while (*q >= '0' && *q <= '9') { cookie = cookie * 10 + (*q++ - '0'); f++; }
|
|
while (*q == ' ') q++;
|
|
while (*q >= '0' && *q <= '9') { pid = pid * 10 + (*q++ - '0'); }
|
|
while (*q == ' ') q++;
|
|
while (*q >= '0' && *q <= '9') { started = started * 10 + (*q++ - '0'); }
|
|
while (*q == ' ') q++;
|
|
if (!f || !pid || !*q) continue;
|
|
|
|
for (int i = 0; i < njobs; i++) {
|
|
if (!seq(jobs[i].label, q))
|
|
continue;
|
|
jobs[i].pid = pid;
|
|
jobs[i].cookie = cookie;
|
|
jobs[i].last_start = started;
|
|
out("schinitd: adopting "); out(jobs[i].label); out("\n");
|
|
break;
|
|
}
|
|
if (cookie > highest)
|
|
highest = cookie;
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
struct schinit_hello h;
|
|
|
|
read_bootmode();
|
|
|
|
// Version handshake before anything else. A mismatch is a refusal rather
|
|
// than a best-effort interpretation -- see the header for why.
|
|
h.op = SCHINIT_HELLO;
|
|
h.proto = SCHINIT_PROTO;
|
|
h.generation = 0;
|
|
h.reserved = 0;
|
|
if (write(sock, &h, sizeof(h)) != sizeof(h)) {
|
|
out("schinitd: no channel to pid 1\n");
|
|
exit();
|
|
}
|
|
if (read(sock, &h, sizeof(h)) != sizeof(h) || h.op != SCHINIT_WELCOME) {
|
|
out("schinitd: pid 1 did not answer\n");
|
|
exit();
|
|
}
|
|
if (h.proto != SCHINIT_PROTO) {
|
|
out("schinitd: protocol mismatch with pid 1\n");
|
|
exit();
|
|
}
|
|
|
|
load_jobs();
|
|
vout("schinitd: "); if (verbose_boot) outn(njobs);
|
|
vout(" job(s) loaded, generation ");
|
|
if (verbose_boot) { outn((int)h.generation); out("\n"); }
|
|
|
|
// Anything PID 1 is already holding is ours again, not something to start.
|
|
unsigned int highest = adopt();
|
|
next_cookie = highest + 1;
|
|
|
|
// Everything except whatever owns the console. That one waits for the
|
|
// greeting below -- see the comment on it.
|
|
for (int i = 0; i < njobs; i++) {
|
|
struct job *j = &jobs[i];
|
|
// The one job a boot-menu choice can un-disable.
|
|
//
|
|
// Deliberately by label rather than by a manifest key like
|
|
// `EnableIfDebug`: a key would be a general mechanism, and the general
|
|
// version of "the boot menu can start things that ship switched off"
|
|
// is a worse idea than the specific one. One job, named here, where
|
|
// anybody reading this file can see which.
|
|
if (debug_boot && j->disabled && seq(j->label, "os.schos.schdbgd")) {
|
|
out("schinitd: remote debugging was chosen at boot; "
|
|
"starting schdbgd\n");
|
|
j->disabled = 0;
|
|
}
|
|
|
|
if (j->disabled || !j->run_at_load || j->own_tty)
|
|
continue;
|
|
if (j->pid)
|
|
continue; // adopted; it never stopped running
|
|
vout("schinitd: starting "); vout(j->label); vout("\n");
|
|
ask_spawn(j);
|
|
}
|
|
|
|
vout("schinitd: ready\n");
|
|
|
|
// The one line a working boot prints, and the last one.
|
|
//
|
|
// Here rather than in the kernel because "the machine is up" is not true
|
|
// until the jobs that make it usable have been started, and because
|
|
// anything printed before this point would come after it on screen. The
|
|
// kernel's whole boot is a kinfo() and is in the klog; this is the part a
|
|
// person is meant to read.
|
|
// Only the first supervisor of a boot says it. A replacement adopts
|
|
// running jobs rather than starting them, so greeting the user again would
|
|
// be announcing a boot that already happened -- and it printed twice for
|
|
// real, the day schinitd was replaced during startup.
|
|
if (!verbose_boot && h.generation <= 1)
|
|
out("\nWelcome to SchOS!"
|
|
" (F8 at boot for the full log, or dmesg once you are in)\n\n");
|
|
|
|
// *Now* the shell on the console.
|
|
//
|
|
// It used to be started with everything else, and then this greeting was
|
|
// printed -- which made the two a race that the shell won about half the
|
|
// time. A boot that sometimes reads
|
|
//
|
|
// Welcome to SchOS! (F8 at boot for the full log, ...)
|
|
// root@schos / »
|
|
//
|
|
// and sometimes reads the same two lines the other way round is not two
|
|
// orderings of one boot, it is a greeting that looks like it was printed
|
|
// by mistake.
|
|
//
|
|
// Holding it back until after the write is enough to settle it: this
|
|
// process writes the line, and only then asks PID 1 to spawn the shell, so
|
|
// the fork cannot even have happened yet. Nothing else waits on it -- the
|
|
// console shell is the last job to start either way, and starting it a
|
|
// few microseconds later costs nothing a person can see.
|
|
//
|
|
// `own_tty` rather than a name: the job that claims the console as its
|
|
// controlling terminal is exactly the job whose output lands here.
|
|
for (int i = 0; i < njobs; i++) {
|
|
struct job *j = &jobs[i];
|
|
if (j->disabled || !j->run_at_load || !j->own_tty || j->pid)
|
|
continue;
|
|
vout("schinitd: starting "); vout(j->label); vout("\n");
|
|
ask_spawn(j);
|
|
}
|
|
|
|
// From here on this program does two things: answer PID 1's heartbeat, and
|
|
// act on the exits it reports. There is no wait() in this program and
|
|
// there cannot be -- the children are not ours.
|
|
//
|
|
// The read is polled rather than blocking, so that a PID 1 which has
|
|
// stopped talking is noticed. A blocking read would sit there forever
|
|
// looking exactly like a quiet machine, which is the failure mode this
|
|
// whole heartbeat exists to make visible.
|
|
int last_ping = uptime();
|
|
|
|
for (;;) {
|
|
struct pollfd pf;
|
|
pf.fd = sock;
|
|
pf.events = POLLIN;
|
|
pf.revents = 0;
|
|
|
|
int pr = poll(&pf, 1, 1000);
|
|
|
|
// The channel is gone. This happens to the *old* supervisor when PID 1
|
|
// has replaced it: PID 1 closes its end, so poll reports a hangup and
|
|
// never reports readable again. Without this the replaced supervisor
|
|
// sits in the loop for ever, complaining that PID 1 has stopped
|
|
// pinging -- which it has, because it is no longer talking to us.
|
|
if (pr > 0 && (pf.revents & (POLLHUP | POLLERR | POLLNVAL))) {
|
|
out("schinitd: channel closed; this supervisor has been replaced\n");
|
|
exit();
|
|
}
|
|
|
|
if (pr > 0 && (pf.revents & POLLIN)) {
|
|
union schinit_msg m;
|
|
int n = read(sock, &m, sizeof(m));
|
|
if (n <= 0) {
|
|
// PID 1 closed the channel. It is the one process that cannot
|
|
// have gone away, so this means we are being replaced.
|
|
out("schinitd: channel closed\n");
|
|
exit();
|
|
}
|
|
if (m.op == SCHINIT_PING) {
|
|
struct schinit_hello p;
|
|
p.op = SCHINIT_PONG;
|
|
p.proto = SCHINIT_PROTO;
|
|
p.generation = 0;
|
|
p.reserved = 0;
|
|
write(sock, &p, sizeof(p));
|
|
last_ping = uptime();
|
|
} else if (m.op == SCHINIT_EXITED) {
|
|
note_exit(m.reply.cookie, m.reply.pid, m.reply.status);
|
|
}
|
|
}
|
|
|
|
// Silence from PID 1.
|
|
//
|
|
// There is nothing this program can do about it -- PID 1 cannot be
|
|
// killed and cannot be replaced from here -- so it says so, loudly,
|
|
// once. docs/architecture/init.md is explicit that this is the limit: a hang
|
|
// in PID 1 is only caught by noticing, and if both ends are wedged at
|
|
// once nothing catches it. There is no hardware watchdog.
|
|
//
|
|
// Saying so is still worth doing. The alternative is a machine that
|
|
// looks idle while its init is stuck, which is how this class of bug
|
|
// stays invisible for months.
|
|
static int complained;
|
|
if (!complained && uptime() - last_ping >= SCHINIT_DEADLINE_SECS * 100) {
|
|
out("schinitd: pid 1 has not pinged for "
|
|
"the deadline -- it may be wedged\n");
|
|
complained = 1;
|
|
}
|
|
}
|
|
}
|