The site and several documents still described the system as it was before virtual terminals, the new schinit, the hybrid work and descriptor passing. docs/architecture/init.md described the wrong program. It documented a single schinit that parses manifests and supervises jobs, which has not been true since PID 1 was split: PID 1 forks a supervisor, spawns jobs on its behalf and reaps; the job table lives in /proc/schinit; a heartbeat runs both ways; and proc_reincarnate() re-execs PID 1 in place rather than panicking. Rewritten around what exists, keeping the reasoning that is still load-bearing -- the five failures of the sibling-init scheme, the escalation ladder, and the three bugs in the reincarnation path that each had to happen before they were obvious. docs/plan/init.md is deleted, which is the tree's own rule for finished work and had not been applied. Fourteen source comments referenced it by part number, so they are repointed at the architecture doc by section name rather than left dangling -- a deleted document nobody notices is worse than a stale one. docs/architecture/devices.md gained the section it has been missing since /dev/pci existed: what a driver needs from a kernel, why mmap is checked against the caller's claims, why DMA is a call and not a mapping, and why the write back after an interrupt is not optional. Also that a released device does not come back by itself, which is true and was nowhere. docs/architecture/networking.md gained a descriptor-passing section, and docs/architecture/README.md's stack diagram gained the two layers and the third seam it was missing. "Two seams hold the whole thing together" has not been true since /dev/pci. docs/plan/README.md said the hybrid work was deliberately last, on the argument that an interface with no consumer is wrong in ways nobody can see. The argument was right; the conclusion was not, and the document now says why it changed -- the alternative to a driver compatibility layer is writing a driver for every device that exists, and the original objection is answered rather than ignored because SchOS's own drivers are the consumers. The site: a Hybrid section with the eviction transcript, cards for virtual terminals, schinit and drivers outside the kernel, and the numbers remeasured rather than estimated -- 46,961 lines, 207 syscalls. The boot log in app.js is from a boot taken today, and the lines are trimmed to what the console can show without clipping, each still a verbatim prefix of real output. Two small rendering fixes found by looking at the pages rather than assuming: .console pre's min-height is now scoped to the hero, so a static transcript sizes to its content instead of reserving eight lines it does not have; and the docs reader's contents panel strips inline marks from headings, so a heading written `proc_reincarnate()` no longer carries its backticks into the sidebar. Every intra-doc link checked; none broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
689 lines
23 KiB
C
689 lines
23 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// schinit -- PID 1.
|
|
//
|
|
// This program used to be the whole init system: it parsed manifests, walked
|
|
// /etc/schinit, decided respawn policy, and supervised every job. All of that
|
|
// now lives in schinitd (PID 2), and what is left here is deliberately, almost
|
|
// aggressively small.
|
|
//
|
|
// The reason is docs/architecture/init.md, and it is worth restating because the
|
|
// smallness looks like an accident otherwise:
|
|
//
|
|
// **PID 1 is the one program on the machine with no supervisor.** Nothing
|
|
// restarts it, nothing notices it hang, and until proc_reincarnate() lands
|
|
// its death is a panic. A program in that position should not contain a
|
|
// parser, should not walk a filesystem, and should not allocate. So it does
|
|
// none of those things. Its entire job is to be too boring to fail.
|
|
//
|
|
// What it does:
|
|
//
|
|
// - bootstrap the filesystems, because something must and it happens first
|
|
// - fork schinitd and restart it if it dies
|
|
// - fork jobs *on schinitd's behalf*, so every job is a child of PID 1
|
|
// - reap children and say so
|
|
//
|
|
// That third one is the load-bearing one. schinitd decides and asks; PID 1
|
|
// acts. So schinitd crashing 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 jobs could not have that.
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "posix.h"
|
|
#include "socket.h"
|
|
#include "schinit.h"
|
|
|
|
#define SUPERVISOR "/bin/schinitd"
|
|
|
|
// Jobs PID 1 is holding for schinitd. This is a table of pids and labels, not
|
|
// a table of policy: PID 1 does not know what a job *is*, only that it forked
|
|
// one and what to call it in /proc/schinit.
|
|
#define MAXJOBS 32
|
|
|
|
struct child {
|
|
int pid; // 0 = free slot
|
|
unsigned int cookie; // schinitd's handle for it
|
|
char label[SCHINIT_MAXLABEL];
|
|
int started; // uptime() ticks
|
|
};
|
|
|
|
static struct child children[MAXJOBS];
|
|
static int sock = -1; // to schinitd
|
|
static int supervisor_pid;
|
|
static int last_pong; // uptime() when schinitd last answered
|
|
static int last_ping; // uptime() when we last asked
|
|
static unsigned int generation = 1; // bumped by proc_reincarnate() later
|
|
|
|
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));
|
|
}
|
|
|
|
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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Two formatters, because publish() needs them and pulling in a printf would
|
|
// put a format-string parser inside PID 1.
|
|
static int
|
|
fmt_str(char *dst, const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) { dst[n] = s[n]; n++; }
|
|
return n;
|
|
}
|
|
|
|
static int
|
|
fmt_num(char *dst, int v)
|
|
{
|
|
char tmp[12];
|
|
int i = 0, n = 0;
|
|
if (v < 0) { dst[n++] = '-'; v = -v; }
|
|
if (v == 0) { dst[n++] = '0'; return n; }
|
|
while (v > 0) { tmp[i++] = '0' + (v % 10); v /= 10; }
|
|
while (i-- > 0) dst[n++] = tmp[i];
|
|
return n;
|
|
}
|
|
|
|
static void
|
|
bootstrap(void)
|
|
{
|
|
// /dev before the console, because the console now lives in it.
|
|
//
|
|
// The old order was the other way round -- open a console first so every
|
|
// message below had somewhere to go -- and the fallback below is what keeps
|
|
// that guarantee. If the mount fails there is no /dev/console and no way to
|
|
// say so, so the original path stays: a node called `console` in the root
|
|
// directory, which is where this used to put one.
|
|
//
|
|
// /dev is a filesystem now, not a pile of nodes. Every major and minor it
|
|
// contains is the kernel's -- devsw[] in kernel/dev/cdev.c is what they
|
|
// dispatch on -- and this program used to hold a second copy of that
|
|
// mapping with nothing checking the two agreed.
|
|
//
|
|
// It was also slow in a way that only showed on a fresh image: each mknod
|
|
// was one SchFS transaction, and the twenty of them cost 2600 ms of the
|
|
// first boot before this program printed its first line. On a second boot
|
|
// the nodes already existed and the same code took 0 ms, so the cost was
|
|
// invisible to anyone not building images -- which is to say, invisible to
|
|
// everyone except whoever works on this.
|
|
mkdir("/dev");
|
|
mount("dev", "/dev", "devtmpfs", 0, 0);
|
|
|
|
if (open("/dev/console", O_RDWR) < 0) {
|
|
// No devtmpfs. Fall back to a real node on the root filesystem, which
|
|
// is what every boot did before this.
|
|
if (open("console", O_RDWR) < 0) {
|
|
mknod("console", 1, 1);
|
|
open("console", O_RDWR);
|
|
}
|
|
}
|
|
dup(0);
|
|
dup(0);
|
|
|
|
// /proc is a real filesystem (kernel/fs/procfs.c), not a handful of nodes:
|
|
// it has to be, because one directory per process cannot be created ahead
|
|
// of time. That is what makes ps and top work.
|
|
//
|
|
// The thirteen mknod() calls that used to follow this mount are gone. They
|
|
// ran *after* it, so every one of them was resolving a path into a
|
|
// read-only synthetic filesystem and failing -- dead weight kept as a
|
|
// fallback for a mount that has not failed in a long time, exactly as the
|
|
// note in procfs.c said it was.
|
|
mkdir("/proc");
|
|
mount("proc", "/proc", "proc", 0, 0);
|
|
|
|
// Mount points, and nothing mounts on them yet. They are created by the
|
|
// image builder (see sysroot-install), so these are a fallback for an image
|
|
// that does not have them -- a mkdir that fails because the directory is
|
|
// already there costs a path walk and no writes, where creating one costs a
|
|
// whole SchFS transaction.
|
|
mkdir("/sys");
|
|
mkdir("/run");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The job table, for /proc/schinit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Publish what PID 1 is holding.
|
|
//
|
|
// docs/architecture/init.md, "State outside the process": it lives outside
|
|
// whichever process is
|
|
// supervising, so a restarted supervisor can read the world back instead of
|
|
// rebuilding it. It is written by PID 1 because PID 1 is the process that
|
|
// cannot lose it.
|
|
//
|
|
// One line per running job: cookie, pid, start tick, label. Written whenever
|
|
// the table changes rather than on read, because a procfs generator runs in
|
|
// the kernel and the kernel has no business walking this program's memory.
|
|
static void
|
|
publish(void)
|
|
{
|
|
char buf[MAXJOBS * (SCHINIT_MAXLABEL + 40) + 64];
|
|
int n = 0;
|
|
|
|
// Hand-rolled rather than snprintf, for the same reason as everything else
|
|
// here: no allocation, no format parser, no libc surprise in PID 1.
|
|
n += fmt_str(buf + n, "generation ");
|
|
n += fmt_num(buf + n, (int)generation);
|
|
n += fmt_str(buf + n, "\nsupervisor ");
|
|
n += fmt_num(buf + n, supervisor_pid);
|
|
n += fmt_str(buf + n, "\n");
|
|
|
|
for (int i = 0; i < MAXJOBS; i++) {
|
|
if (!children[i].pid)
|
|
continue;
|
|
n += fmt_num(buf + n, (int)children[i].cookie);
|
|
n += fmt_str(buf + n, " ");
|
|
n += fmt_num(buf + n, children[i].pid);
|
|
n += fmt_str(buf + n, " ");
|
|
n += fmt_num(buf + n, children[i].started);
|
|
n += fmt_str(buf + n, " ");
|
|
n += fmt_str(buf + n, children[i].label);
|
|
n += fmt_str(buf + n, "\n");
|
|
}
|
|
|
|
int fd = open("/proc/schinit", O_WRONLY);
|
|
if (fd < 0)
|
|
return; // no procfs yet, or it is read-only
|
|
write(fd, buf, n);
|
|
close(fd);
|
|
}
|
|
|
|
// Read the table back after a reincarnation.
|
|
//
|
|
// This is the one parse in PID 1, and it is worth being uneasy about. It is
|
|
// bounded: a fixed buffer, four integers and a label per line, no allocation,
|
|
// and it reads a format this same program wrote. It happens once, at startup,
|
|
// only when the kernel has just re-exec'd us. The alternative is forgetting
|
|
// every running job, which is worse than a twenty-line scan of known text.
|
|
static void
|
|
reclaim(void)
|
|
{
|
|
char buf[MAXJOBS * (SCHINIT_MAXLABEL + 40) + 64];
|
|
int fd = open("/proc/schinit", O_RDONLY);
|
|
int n, slot = 0;
|
|
|
|
if (fd < 0)
|
|
return;
|
|
n = read(fd, buf, (int)sizeof(buf) - 1);
|
|
close(fd);
|
|
if (n <= 0)
|
|
return;
|
|
buf[n] = 0;
|
|
|
|
char *p = buf;
|
|
while (*p && slot < MAXJOBS) {
|
|
char *line = p;
|
|
while (*p && *p != '\n') p++;
|
|
if (*p == '\n') *p++ = 0;
|
|
|
|
unsigned int cookie = 0;
|
|
int pid = 0, started = 0, digits = 0;
|
|
char *q = line;
|
|
|
|
if (*q < '0' || *q > '9') continue; // a header line
|
|
while (*q >= '0' && *q <= '9') { cookie = cookie * 10 + (unsigned)(*q++ - '0'); digits++; }
|
|
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 (!digits || pid <= 0 || !*q) continue;
|
|
|
|
children[slot].pid = pid;
|
|
children[slot].cookie = cookie;
|
|
children[slot].started = started;
|
|
scopy(children[slot].label, q, SCHINIT_MAXLABEL);
|
|
slot++;
|
|
}
|
|
if (slot) {
|
|
out("schinit: reclaimed ");
|
|
outn(slot);
|
|
out(" running job(s) from /proc/schinit\n");
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Spawning, on request
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void
|
|
redirect(const char *path, int fd, int flags)
|
|
{
|
|
if (!path || !path[0])
|
|
return;
|
|
close(fd);
|
|
if (open((char *)path, flags) != fd) {
|
|
// Could not put it on the right descriptor. Say nothing -- this runs
|
|
// in the child, between fork and exec, and the console may be exactly
|
|
// the thing that failed to open.
|
|
}
|
|
}
|
|
|
|
// Fork and exec one job. Returns the pid, or a negative number.
|
|
//
|
|
// Everything this needs arrives in the message. It does not read a file, does
|
|
// not look at a manifest, and does not consult any policy: schinitd worked all
|
|
// of that out and sent the answer.
|
|
static int
|
|
spawn(struct schinit_spawn *s)
|
|
{
|
|
// Rebuild argv and envp from the flat buffer. Pointers cannot cross a
|
|
// process boundary, so the message carries offsets and they are turned
|
|
// back into pointers here, on this stack, where they are valid.
|
|
char *argv[SCHINIT_MAXARGS + 1];
|
|
char *envp[SCHINIT_MAXENVV + 1];
|
|
unsigned int na = s->nargs, ne = s->nenvs;
|
|
|
|
if (na > SCHINIT_MAXARGS) na = SCHINIT_MAXARGS;
|
|
if (ne > SCHINIT_MAXENVV) ne = SCHINIT_MAXENVV;
|
|
|
|
// A hostile or broken offset must not walk off the buffer. The strings are
|
|
// NUL-terminated inside strbuf and the last byte is forced to NUL below,
|
|
// so a bad offset yields a short string rather than a read past the end.
|
|
s->strbuf[SCHINIT_STRBUF - 1] = 0;
|
|
s->program[SCHINIT_MAXPATH - 1] = 0;
|
|
s->label[SCHINIT_MAXLABEL - 1] = 0;
|
|
|
|
for (unsigned int i = 0; i < na; i++) {
|
|
unsigned short off = s->args[i];
|
|
argv[i] = (off < SCHINIT_STRBUF) ? &s->strbuf[off] : (char *)"";
|
|
}
|
|
argv[na] = 0;
|
|
for (unsigned int i = 0; i < ne; i++) {
|
|
unsigned short off = s->envs[i];
|
|
envp[i] = (off < SCHINIT_STRBUF) ? &s->strbuf[off] : (char *)"";
|
|
}
|
|
envp[ne] = 0;
|
|
|
|
if (!argv[0])
|
|
argv[0] = s->program;
|
|
|
|
int pid = fork();
|
|
if (pid < 0)
|
|
return -1;
|
|
|
|
if (pid == 0) {
|
|
// A session of its own, so terminal input reaches it. A session with
|
|
// no controlling terminal is not delivered terminal input at all --
|
|
// without this the shell echoes and executes nothing.
|
|
setsid();
|
|
|
|
redirect(s->stdin_path, 0, O_RDWR);
|
|
redirect(s->stdout_path, 1, O_RDWR);
|
|
redirect(s->stderr_path, 2, O_RDWR);
|
|
|
|
if (s->own_tty)
|
|
ioctl(0, TIOCSCTTY, 0);
|
|
|
|
// The socket to schinitd is PID 1's and nobody else's.
|
|
close(sock);
|
|
|
|
execve(s->program, argv, ne ? envp : 0);
|
|
|
|
out("schinit: cannot exec ");
|
|
out(s->program);
|
|
out("\n");
|
|
exit();
|
|
}
|
|
return pid;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The supervisor
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static int
|
|
start_supervisor(void)
|
|
{
|
|
int sv[2];
|
|
|
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
|
|
out("schinit: socketpair failed\n");
|
|
return -1;
|
|
}
|
|
|
|
int pid = fork();
|
|
if (pid < 0) {
|
|
close(sv[0]);
|
|
close(sv[1]);
|
|
return -1;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
close(sv[0]);
|
|
// A fixed descriptor rather than an argument: schinitd is exec'd, so
|
|
// there is nowhere to pass a number, and a well-known one is one less
|
|
// thing for the two of them to disagree about.
|
|
if (sv[1] != SCHINIT_SOCK_FD) {
|
|
close(SCHINIT_SOCK_FD);
|
|
dup2(sv[1], SCHINIT_SOCK_FD);
|
|
close(sv[1]);
|
|
}
|
|
char *argv[] = { (char *)"schinitd", 0 };
|
|
execve((char *)SUPERVISOR, argv, 0);
|
|
out("schinit: cannot exec " SUPERVISOR "\n");
|
|
exit();
|
|
}
|
|
|
|
close(sv[1]);
|
|
sock = sv[0];
|
|
supervisor_pid = pid;
|
|
last_pong = last_ping = uptime(); // it has not missed anything yet
|
|
return pid;
|
|
}
|
|
|
|
static void
|
|
send_reply(unsigned int op, unsigned int cookie, int pid, int status)
|
|
{
|
|
struct schinit_reply r;
|
|
r.op = op;
|
|
r.cookie = cookie;
|
|
r.pid = pid;
|
|
r.status = status;
|
|
// A supervisor that has stopped reading must not wedge PID 1. The socket
|
|
// buffer is far larger than anything this sends in a burst, and if the
|
|
// write fails the supervisor is already lost -- it will be restarted, and
|
|
// it will read /proc/schinit rather than this message.
|
|
write(sock, &r, sizeof(r));
|
|
}
|
|
|
|
// One message from schinitd. Returns 0 if the socket has closed.
|
|
static int
|
|
serve(void)
|
|
{
|
|
union schinit_msg m;
|
|
int n = read(sock, &m, sizeof(m));
|
|
|
|
if (n <= 0)
|
|
return 0; // supervisor gone
|
|
|
|
switch (m.op) {
|
|
case SCHINIT_HELLO: {
|
|
struct schinit_hello h;
|
|
h.op = SCHINIT_WELCOME;
|
|
h.proto = SCHINIT_PROTO;
|
|
h.generation = generation;
|
|
h.reserved = 0;
|
|
write(sock, &h, sizeof(h));
|
|
break;
|
|
}
|
|
|
|
case SCHINIT_PONG:
|
|
last_pong = uptime();
|
|
break;
|
|
|
|
case SCHINIT_SPAWN: {
|
|
int pid = spawn(&m.spawn);
|
|
if (pid > 0) {
|
|
for (int i = 0; i < MAXJOBS; i++) {
|
|
if (children[i].pid)
|
|
continue;
|
|
children[i].pid = pid;
|
|
children[i].cookie = m.spawn.cookie;
|
|
children[i].started = uptime();
|
|
scopy(children[i].label, m.spawn.label, SCHINIT_MAXLABEL);
|
|
break;
|
|
}
|
|
publish();
|
|
}
|
|
send_reply(SCHINIT_SPAWNED, m.spawn.cookie, pid, 0);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
// An op PID 1 does not know. Ignore it rather than guess: this is the
|
|
// only process that cannot afford to act on a message it misread.
|
|
break;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
// argv[1] is the generation, and it is only there when the kernel put it
|
|
// there -- proc_reincarnate() re-execs this program in place after PID 1
|
|
// has died. A normal boot has no argument and is generation 1.
|
|
if (argc > 1) {
|
|
unsigned int g = 0;
|
|
for (char *q = argv[1]; *q >= '0' && *q <= '9'; q++)
|
|
g = g * 10 + (unsigned int)(*q - '0');
|
|
if (g)
|
|
generation = g;
|
|
}
|
|
|
|
bootstrap();
|
|
|
|
// A reincarnated PID 1 has the children but not the table.
|
|
//
|
|
// exec() replaced this program's memory and kept everything else -- the
|
|
// pid, the open files, and crucially the *children*, which are still
|
|
// parented here because nothing killed them. What did not survive is the
|
|
// array describing them, so it is read back from where it was published
|
|
// precisely so that it could be.
|
|
//
|
|
// Without this the next publish() overwrites the record with an empty
|
|
// table, the supervisor reads that, believes nothing is running and starts
|
|
// everything a second time. Same split brain as before, one level up.
|
|
if (generation > 1) {
|
|
// exec() keeps open files, which is what lets this program still have
|
|
// a console to complain to -- and also means the previous life's
|
|
// socket to the old supervisor is still open here. Leaving it open
|
|
// holds the old supervisor's channel up, so it never sees a hangup,
|
|
// never exits, and sits there reporting that PID 1 has stopped
|
|
// pinging it. Which is true, and no longer its business.
|
|
for (int fd = 3; fd < 32; fd++)
|
|
close(fd);
|
|
reclaim();
|
|
}
|
|
|
|
out("schinit: SchOS process 1");
|
|
if (generation > 1) {
|
|
out(", generation ");
|
|
outn((int)generation);
|
|
out(" (reincarnated)");
|
|
}
|
|
out("\n");
|
|
|
|
// Generation 4 is the safety net: a manifest that kills init must not kill
|
|
// it for ever, so this generation does not read /etc/schinit at all. One
|
|
// shell, on the console, and nothing else -- enough to log in and fix
|
|
// whatever has been taking the machine down.
|
|
if (generation >= 4) {
|
|
out("schinit: safe mode -- console shell only, /etc/schinit ignored\n");
|
|
if (fork() == 0) {
|
|
setsid();
|
|
ioctl(0, TIOCSCTTY, 0);
|
|
char *sh[] = { (char *)"-zsh", 0 };
|
|
execve((char *)"/bin/zsh", sh, 0);
|
|
char *fb[] = { (char *)"sh", 0 };
|
|
execve((char *)"/bin/sh", fb, 0);
|
|
exit();
|
|
}
|
|
publish();
|
|
for (;;) {
|
|
int st, pid;
|
|
while ((pid = waitpid(-1, &st, WNOHANG)) > 0)
|
|
;
|
|
sleep(20);
|
|
}
|
|
}
|
|
|
|
if (start_supervisor() < 0) {
|
|
// No supervisor. Rather than idle usefully-looking, fall back to a
|
|
// shell so the machine is still usable and the failure is visible.
|
|
out("schinit: no supervisor; falling back to a shell\n");
|
|
if (fork() == 0) {
|
|
setsid();
|
|
char *argv[] = { (char *)"sh", 0 };
|
|
execve((char *)"/bin/sh", argv, 0);
|
|
exit();
|
|
}
|
|
}
|
|
|
|
publish();
|
|
out("schinit: ready (pid 1)\n");
|
|
|
|
for (;;) {
|
|
// Two things can happen -- a request arrives, or a child exits -- and
|
|
// both of them block. poll() with a timeout plus WNOHANG turns that
|
|
// into one loop with no signal handler, which is the simplest thing
|
|
// that works and therefore the right thing for this program.
|
|
struct pollfd pf;
|
|
pf.fd = sock;
|
|
pf.events = POLLIN;
|
|
pf.revents = 0;
|
|
|
|
if (sock >= 0 && poll(&pf, 1, 200) > 0 && (pf.revents & POLLIN)) {
|
|
if (!serve()) {
|
|
close(sock);
|
|
sock = -1;
|
|
}
|
|
} else if (sock < 0) {
|
|
sleep(10);
|
|
}
|
|
|
|
// The heartbeat. A supervisor that has stopped answering is stuck,
|
|
// and a stuck supervisor is indistinguishable from a working one by
|
|
// any means except asking it something -- which is the whole reason
|
|
// this exists. See docs/architecture/init.md, "The heartbeat".
|
|
if (sock >= 0) {
|
|
int now = uptime();
|
|
if (now - last_ping >= SCHINIT_PING_SECS * 100) {
|
|
struct schinit_hello p;
|
|
p.op = SCHINIT_PING;
|
|
p.proto = SCHINIT_PROTO;
|
|
p.generation = generation;
|
|
p.reserved = 0;
|
|
write(sock, &p, sizeof(p));
|
|
last_ping = now;
|
|
}
|
|
if (now - last_pong >= SCHINIT_DEADLINE_SECS * 100) {
|
|
out("schinit: supervisor stopped answering; replacing it\n");
|
|
|
|
// Ask it to die, then replace it *without waiting*.
|
|
//
|
|
// kill() sets a flag the target checks the next time it runs,
|
|
// and the whole premise here is that it is not running. A
|
|
// supervisor stuck in a loop will notice at its next trap; one
|
|
// blocked forever never will. Waiting for a corpse that may
|
|
// never be produced is how a recovery path becomes a second
|
|
// hang -- which is exactly what the first version of this did.
|
|
//
|
|
// Two supervisors briefly existing is safe, and safe in the
|
|
// way that matters: the old one's socket is closed here, so
|
|
// the moment it runs again its first write fails and it exits.
|
|
// It cannot spawn anything, because it cannot ask.
|
|
kill(supervisor_pid);
|
|
close(sock);
|
|
sock = -1;
|
|
int old = supervisor_pid;
|
|
if (start_supervisor() > 0)
|
|
out("schinit: new supervisor started\n");
|
|
publish(); // the table names the live supervisor
|
|
(void)old;
|
|
}
|
|
}
|
|
|
|
int status;
|
|
int pid;
|
|
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
if (pid == supervisor_pid) {
|
|
out("schinit: supervisor died; restarting\n");
|
|
if (sock >= 0) { close(sock); sock = -1; }
|
|
supervisor_pid = 0;
|
|
// The jobs are untouched. They are children of this process,
|
|
// not of the supervisor, so nothing was orphaned and nothing
|
|
// needs restarting -- which is the entire reason jobs are
|
|
// forked here. The new supervisor reads /proc/schinit and
|
|
// picks up where the old one stopped.
|
|
sleep(100); // one second, so a crash loop is slow
|
|
start_supervisor();
|
|
publish();
|
|
continue;
|
|
}
|
|
|
|
for (int i = 0; i < MAXJOBS; i++) {
|
|
if (children[i].pid != pid)
|
|
continue;
|
|
unsigned int cookie = children[i].cookie;
|
|
children[i].pid = 0;
|
|
children[i].label[0] = 0;
|
|
publish();
|
|
if (sock >= 0)
|
|
send_reply(SCHINIT_EXITED, cookie, pid, status);
|
|
break;
|
|
}
|
|
// A pid in no slot is an orphan the kernel reparented here. It has
|
|
// been reaped, which was the whole obligation.
|
|
}
|
|
}
|
|
}
|