init: PID 1 becomes a role -- schinit splits, schinitd supervises
docs/plan/init.md milestones I0 and I1.
**The split.** schinit is PID 1 and does four things: bootstrap the
filesystems, fork schinitd, fork jobs on schinitd's behalf, reap children. No
manifest parsing, no filesystem walking after boot, no allocation. It is the
one program on the machine with no supervisor, so it is written to be too
boring to fail.
schinitd is PID 2 and holds everything else -- /etc/schinit, respawn policy,
throttling -- and is restartable, which is where the bugs are allowed to be.
**Jobs are forked by PID 1, not by schinitd.** That is the load-bearing
detail. schinitd decides and asks; PID 1 acts. So every job is a child of PID
1 at all times, and a supervisor that dies orphans nothing, reparents nothing
and restarts nothing.
**/proc/schinit** is the job table, written by PID 1 whenever it changes. The
kernel holds the text and does not interpret it: a procfs generator runs in
the kernel, and walking PID 1's memory to answer a read would put a userspace
program's format into the kernel, where it would rot.
Verified the way the plan asks for -- `kill 3`, then look:
schinitd: adopting os.schos.console-shell
schinitd: adopting os.schos.tty2-shell
/proc/schinit byte-identical across the restart, same pids, same cookies, same
start times, and the shell being typed into never noticed.
The first attempt got I1 wrong in the instructive way: PID 1 published the
table and schinitd never read it back, so the restarted supervisor started
everything a second time -- two console shells on one terminal, which is the
split-brain this plan rejects the two-init scheme for, reached by a different
road. Publishing state is half of it; coming back and believing it is the
other half.
The protocol is include/schinit.h: fixed-size messages, versioned in the
hello, entirely internal. Fixed-size so PID 1 never allocates, and a
supervisor that has gone mad cannot make it run a parser.
Still to come: I2 (heartbeat both ways, so a hang is as detectable as a death)
and I3 (proc_reincarnate, the actual law change).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
6ff84e5bf4
commit
c80b7bf3b2
9 changed files with 1095 additions and 416 deletions
|
|
@ -1,38 +1,58 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// schinit — SchOS process 1.
|
||||
// schinit -- PID 1.
|
||||
//
|
||||
// Shaped after launchd rather than sysvinit, and the reason is not the config
|
||||
// format. It is *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.
|
||||
// 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.
|
||||
//
|
||||
// This is milestone 1: manifests, RunAtLoad, KeepAlive with backoff, and
|
||||
// reaping. Sockets are parsed and remembered but not yet held; that is
|
||||
// milestone 2, and the manifest format already has a place to say it.
|
||||
// The reason is docs/plan/init.md, and it is worth restating because the
|
||||
// smallness looks like an accident otherwise:
|
||||
//
|
||||
// Constraints worth knowing before editing. This program 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 below are constants rather than growable.
|
||||
// **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" // TIOCSCTTY, TIOCSPGRP
|
||||
#include "posix.h"
|
||||
#include "socket.h"
|
||||
#include "schinit.h"
|
||||
|
||||
#define MAXJOBS 32
|
||||
#define MAXARGS 16
|
||||
#define MAXLABEL 64
|
||||
#define MAXPATH 128
|
||||
#define MAXLINE 256
|
||||
#define JOBDIR "/etc/schinit"
|
||||
#define SUPERVISOR "/bin/schinitd"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Small string helpers. No libc.
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 unsigned int generation = 1; // bumped by proc_reincarnate() later
|
||||
|
||||
static int
|
||||
slen(const char *s)
|
||||
|
|
@ -104,341 +124,28 @@ outn(int v)
|
|||
// Jobs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define MAXENVV 16 // Environment lines honoured per job
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
// Two formatters, because publish() needs them and pulling in a printf would
|
||||
// put a format-string parser inside PID 1.
|
||||
static int
|
||||
truthy(const char *v)
|
||||
fmt_str(char *dst, const char *s)
|
||||
{
|
||||
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;
|
||||
while (s[n]) { dst[n] = s[n]; n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
static int
|
||||
load_job(const char *path)
|
||||
fmt_num(char *dst, int v)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Starting and supervising
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void
|
||||
redirect(const char *path, int fd, int flags)
|
||||
{
|
||||
if (!path[0]) return;
|
||||
int f = open((char *)path, flags);
|
||||
if (f < 0) return;
|
||||
if (f != fd) { dup2(f, fd); close(f); }
|
||||
}
|
||||
|
||||
static void
|
||||
start_job(struct job *j)
|
||||
{
|
||||
int pid = fork();
|
||||
if (pid < 0) {
|
||||
out("schinit: fork failed for "); out(j->label); out("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Its own session, so a job cannot reach back and signal the rest of
|
||||
// the system through a shared controlling terminal.
|
||||
setsid();
|
||||
|
||||
redirect(j->stdin_path, 0, O_RDWR);
|
||||
redirect(j->stdout_path, 1, O_RDWR);
|
||||
redirect(j->stderr_path, 2, O_RDWR);
|
||||
|
||||
// A job that reads the console has to *own* it, and setsid() is what
|
||||
// makes that necessary: a fresh session has no controlling terminal,
|
||||
// and the console only delivers input to the foreground process group
|
||||
// it is tracking. Without this the shell came up, echoed every
|
||||
// keystroke through the line discipline, and executed none of them --
|
||||
// which looks like a hung shell and is really a terminal that is not
|
||||
// listening to it.
|
||||
//
|
||||
// Off by default. A daemon has no business seizing the console, and
|
||||
// launchd's model is that the console is a thing a job asks for.
|
||||
if (j->own_tty) {
|
||||
int pg = getpid(); // after setsid(), pgid == pid
|
||||
ioctl(0, TIOCSCTTY, 0);
|
||||
ioctl(0, TIOCSPGRP, &pg);
|
||||
}
|
||||
|
||||
// exec.c enables the Linux ABI automatically for musl binaries. Do NOT
|
||||
// call enable_linux_abi() here: it would renumber the syscalls this
|
||||
// child still has to make.
|
||||
execve(j->program, j->argv, j->envn ? j->envp : 0);
|
||||
|
||||
out("schinit: cannot exec "); out(j->program);
|
||||
out(" for "); out(j->label); out("\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
j->pid = pid;
|
||||
j->last_start = uptime();
|
||||
j->starts++;
|
||||
}
|
||||
|
||||
static struct job *
|
||||
job_by_pid(int pid)
|
||||
{
|
||||
for (int i = 0; i < njobs; i++)
|
||||
if (jobs[i].pid == pid)
|
||||
return &jobs[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bootstrap: the device nodes and mounts that must exist before anything runs.
|
||||
//
|
||||
// Not jobs. A job needs /dev/console to have somewhere to send its output, so
|
||||
// this cannot itself be expressed as one without a bootstrapping problem.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void
|
||||
bootstrap(void)
|
||||
{
|
||||
|
|
@ -496,87 +203,319 @@ bootstrap(void)
|
|||
mkdir("/run");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The job table, for /proc/schinit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Publish what PID 1 is holding.
|
||||
//
|
||||
// docs/plan/init.md part 2: the state 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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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;
|
||||
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_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(void)
|
||||
{
|
||||
if (getpid() != 1)
|
||||
exit();
|
||||
|
||||
bootstrap();
|
||||
|
||||
out("schinit: SchOS process 1\n");
|
||||
|
||||
load_jobs();
|
||||
|
||||
if (njobs == 0) {
|
||||
// Nothing to supervise. Rather than idle usefully-looking, say so and
|
||||
// fall back to a shell -- an unbootable-looking machine because a
|
||||
// manifest was missing would be a poor trade for purity.
|
||||
out("schinit: no jobs found; falling back to /bin/sh\n");
|
||||
struct job *j = &jobs[njobs++];
|
||||
job_defaults(j);
|
||||
scopy(j->label, "os.schos.fallback-shell", MAXLABEL);
|
||||
scopy(j->program, "/bin/sh", MAXPATH);
|
||||
split_args(j, "sh");
|
||||
scopy(j->stdin_path, "/dev/console", MAXPATH);
|
||||
scopy(j->stdout_path, "/dev/console", MAXPATH);
|
||||
scopy(j->stderr_path, "/dev/console", MAXPATH);
|
||||
j->run_at_load = 1;
|
||||
j->keep_alive = 1;
|
||||
j->own_tty = 1;
|
||||
}
|
||||
|
||||
out("schinit: "); outn(njobs); out(" job");
|
||||
if (njobs != 1) out("s");
|
||||
out(" loaded\n");
|
||||
|
||||
for (int i = 0; i < njobs; i++) {
|
||||
struct job *j = &jobs[i];
|
||||
if (j->disabled || !j->run_at_load) continue;
|
||||
out("schinit: starting "); out(j->label); out("\n");
|
||||
start_job(j);
|
||||
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");
|
||||
|
||||
// Supervision loop.
|
||||
//
|
||||
// wait() reaps whatever exited, including processes reparented here when
|
||||
// their own parent died -- those are not jobs and job_by_pid returns 0 for
|
||||
// them, which is the whole handling they need.
|
||||
for (;;) {
|
||||
int pid = wait();
|
||||
if (pid < 0) {
|
||||
// Nothing left to wait for. Should not happen while any job is
|
||||
// alive; sleep rather than spin so a mistake here costs a idle
|
||||
// machine and not a hot CPU.
|
||||
sleep(100);
|
||||
continue;
|
||||
// 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);
|
||||
}
|
||||
|
||||
struct job *j = job_by_pid(pid);
|
||||
if (!j) continue; // an adopted orphan; already reaped
|
||||
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;
|
||||
}
|
||||
|
||||
j->pid = 0;
|
||||
|
||||
if (!j->keep_alive) {
|
||||
out("schinit: "); out(j->label); out(" exited\n");
|
||||
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.
|
||||
}
|
||||
|
||||
// Throttle. A job that exits immediately and forever would otherwise
|
||||
// spin the machine; launchd's ThrottleInterval exists for exactly this
|
||||
// and defaults to 10 seconds. One is enough here, where the common
|
||||
// case is a shell someone just exited on purpose and expects back.
|
||||
int since = uptime() - j->last_start;
|
||||
int ticks = j->throttle * 100; // uptime() counts 10ms ticks
|
||||
if (since < ticks)
|
||||
sleep(ticks - since);
|
||||
|
||||
start_job(j);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue