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
607
cmd/schinitd/main.c
Normal file
607
cmd/schinitd/main.c
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
// 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/plan/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/plan/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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
(void)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;
|
||||
|
||||
// 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 docs/plan/init.md part 2 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;
|
||||
|
||||
// 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();
|
||||
out("schinitd: "); outn(njobs); out(" job(s) loaded, generation ");
|
||||
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;
|
||||
|
||||
for (int i = 0; i < njobs; i++) {
|
||||
struct job *j = &jobs[i];
|
||||
if (j->disabled || !j->run_at_load)
|
||||
continue;
|
||||
if (j->pid)
|
||||
continue; // adopted; it never stopped running
|
||||
out("schinitd: starting "); out(j->label); out("\n");
|
||||
ask_spawn(j);
|
||||
}
|
||||
|
||||
out("schinitd: ready\n");
|
||||
|
||||
// Everything from here is PID 1 telling us a child exited. There is no
|
||||
// wait() in this program and there cannot be: the children are not ours.
|
||||
for (;;) {
|
||||
struct schinit_reply r;
|
||||
int n = read(sock, &r, sizeof(r));
|
||||
if (n != sizeof(r)) {
|
||||
// PID 1 closed the channel. It is the only process that cannot
|
||||
// have gone away, so this means we are being replaced.
|
||||
out("schinitd: channel closed\n");
|
||||
exit();
|
||||
}
|
||||
if (r.op == SCHINIT_EXITED)
|
||||
note_exit(r.cookie, r.pid, r.status);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue