Two things.
**The F8 resolution now applies to the other displays as well.** It already
reached them once vgpud started -- that was the last commit but one -- but not
before: choosing 1024x768 changed the screen it was chosen on and left the
other at 1280x800, so the menu and then the splash appeared on it as a small
rectangle in the middle of a larger black one. Which is a fair reading of "the
resolution change is not changing resolution".
The resolution offered in that menu is a statement about the machine's display,
not about one adapter of it, so both stages before vgpud now follow it:
the loader mirrors_match() puts every mirror GOP into the primary's mode,
called from mirrors_show() so it cannot be forgotten at a new
call site. Two integer comparisons in the common case.
the kernel setup_scanout() takes gfx's dimensions over GET_DISPLAY_INFO's.
Same argument as vgpud's bring_up(): the host window's size is
a fact about QEMU, and the console's mode is the one somebody
chose.
**And the greeting is no longer a race.** "Welcome to SchOS!" was printed after
every job had been started, including the shell on the console -- so which of
the two reached the screen first was up to the scheduler, and it went both ways
about evenly. A boot that sometimes greets you above the prompt and sometimes
below it does not read as two orderings of one boot; it reads as a line printed
by mistake.
schinitd now holds back the job that owns the console -- `own_tty`, which is
exactly the job whose output lands there -- until after the greeting is
written. That is enough to settle it: this process writes the line, and only
then asks PID 1 to spawn the shell, so the fork has not happened yet. Nothing
waits on it that was not already waiting.
Verified: F8 -> Resolution -> 1024x768 -> boot, with the virtio head
screendumped over QMP at three points during startup and once at the prompt --
1024x768 throughout, and byte-identical to the stdvga head. Three consecutive
boots put the greeting at the same offset, ahead of the prompt every time.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
760 lines
25 KiB
C
760 lines
25 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;
|
|
|
|
static void
|
|
read_bootmode(void)
|
|
{
|
|
char b[16];
|
|
int fd = open("/proc/bootmode", O_RDONLY);
|
|
int n;
|
|
|
|
if (fd < 0)
|
|
return; // no procfs: assume quiet
|
|
n = read(fd, b, sizeof(b) - 1);
|
|
close(fd);
|
|
if (n > 0) {
|
|
b[n] = 0;
|
|
verbose_boot = (b[0] == 'v');
|
|
}
|
|
}
|
|
|
|
// 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];
|
|
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;
|
|
}
|
|
}
|
|
}
|