schinit: a launchd-shaped process 1
Milestone 1 of three: manifests, RunAtLoad, KeepAlive with throttling,
and reaping. Replaces the hardcoded fork-a-shell loop in cmd/init, which
stays in the tree for now.
Jobs are sectioned key=value files in /etc/schinit/*.job, kept in the
repo under etc/ and installed into the sysroot -- sysroot/ is generated
and gitignored, so anything written only there is lost on a fresh clone,
which is already true of /etc/passwd and should not become true of the
boot configuration. Unknown keys and sections are ignored rather than
fatal: a manifest written for a newer schinit must not stop this one
booting the machine.
Socket sections are parsed and recorded but not yet held. That is
milestone 2 and it is the whole reason for copying launchd rather than
sysvinit -- the supervisor holds the listener and starts the job on first
connect, which inverts the dependency graph and makes runlevels a
non-concept. The key exists now so manifests need not change later.
getdents64 is exposed to userland for this. The kernel has had it since
the Linux ABI landed, but nothing declared it in user.h or lib/usys.S, so
first-party programs had no way to list a directory at all.
Two things found by testing rather than reasoning:
- setsid() gives a job its own session, and a session has no
controlling terminal. The console then delivers input only to the
foreground process group it tracks, so the shell came up, echoed
every keystroke through the line discipline, and executed none of
them -- indistinguishable from a hung shell. ControllingTerminal=yes
claims it with TIOCSCTTY/TIOCSPGRP; off by default, since a daemon
has no business seizing the console.
- the first bootstrap() dropped /dev/input/event0-1 and most of /proc,
because it was transcribed from the part of the old init that had
been read rather than all of it. -Evdev caught it.
build/usbstick.img is built by `all` now: without it QEMU refuses the
-drive and the entire -Usb run produces no output, which reads as a
broken USB driver.
Seven suites green: base 27, Tty 51, Ps 32, Sock 64, Fb 36, Usb 31,
Evdev 29. make schfs-check passes.
This commit is contained in:
parent
08a1a9033a
commit
baede13847
10 changed files with 664 additions and 7 deletions
|
|
@ -16,7 +16,7 @@ one group. Verified rendering correctly on 2026-08-08.
|
||||||
counter-intuitive part and it is a timing fact, not a preference: the kernel does
|
counter-intuitive part and it is a timing fact, not a preference: the kernel does
|
||||||
not reach `vga_init()` until roughly the last 300ms of a ~2.8s boot, so a
|
not reach `vga_init()` until roughly the last 300ms of a ~2.8s boot, so a
|
||||||
kernel-drawn logo appeared for about one frame before `console_takeover()` wiped
|
kernel-drawn logo appeared for about one frame before `console_takeover()` wiped
|
||||||
it. Drawn from `splash()` in `boot/bios/stage2main.c` it is up for the whole boot.
|
it. Drawn from `splash()` in `boot/stage2main.c` it is up for the whole boot.
|
||||||
`kernel/dev/vga.c` keeps the same drawing code against the same mask
|
`kernel/dev/vga.c` keeps the same drawing code against the same mask
|
||||||
(`vga_draw_logo`, `vga_logo_centered`) for things that want the mark *after*
|
(`vga_draw_logo`, `vga_logo_centered`) for things that want the mark *after*
|
||||||
boot — the crash screen, eventually `schinit`. Don't "fix" the duplication by
|
boot — the crash screen, eventually `schinit`. Don't "fix" the duplication by
|
||||||
|
|
@ -35,7 +35,7 @@ for the one rule. 4 bits rather than 8 because the header is included by the
|
||||||
**`STAGE2_SECTORS` was encoded in five places, not the three `docs/layout.md`
|
**`STAGE2_SECTORS` was encoded in five places, not the three `docs/layout.md`
|
||||||
claimed.** Raising it 24 → 48 to fit the 8 KiB mask proved it: the `dd seek=25`
|
claimed.** Raising it 24 → 48 to fit the 8 KiB mask proved it: the `dd seek=25`
|
||||||
offset stayed behind and the kernel got written straight over stage2's tail.
|
offset stayed behind and the kernel got written straight over stage2's tail.
|
||||||
The five were `boot/bios/bootasm.S`, `boot/bios/stage2main.c`, the `dd seek=`, and *twice*
|
The five were `boot/bootasm.S`, `boot/stage2main.c`, the `dd seek=`, and *twice*
|
||||||
inside `tools/mkiso.sh` (its size assertion and `-boot-load-size`). It now lives
|
inside `tools/mkiso.sh` (its size assertion and `-boot-load-size`). It now lives
|
||||||
only in the root Makefile, which passes `-DSTAGE2_SECTORS` and exports it to
|
only in the root Makefile, which passes `-DSTAGE2_SECTORS` and exports it to
|
||||||
mkiso; the `#define`s left in the boot sources are `#ifndef` fallbacks. Hard
|
mkiso; the `#define`s left in the boot sources are `#ifndef` fallbacks. Hard
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ protected mode before it reads the kernel. That rules out both El Torito
|
||||||
emulation modes (hard-disk emulation exists only through the BIOS) and floppy
|
emulation modes (hard-disk emulation exists only through the BIOS) and floppy
|
||||||
emulation (2.88 MB cap). So:
|
emulation (2.88 MB cap). So:
|
||||||
|
|
||||||
- `boot/bios/cdboot.S` — a separate stage 1 for optical media. `tools/mkiso.sh`
|
- `boot/cdboot.S` — a separate stage 1 for optical media. `tools/mkiso.sh`
|
||||||
passes `-boot-load-size 25`, so the BIOS loads cdboot *and* all 24 sectors of
|
passes `-boot-load-size 25`, so the BIOS loads cdboot *and* all 24 sectors of
|
||||||
stage2 before anything of ours runs, putting stage2 at 0x7E00 exactly where
|
stage2 before anything of ours runs, putting stage2 at 0x7E00 exactly where
|
||||||
`bootasm.S` would have. cdboot only records the medium and jumps.
|
`bootasm.S` would have. cdboot only records the medium and jumps.
|
||||||
|
|
|
||||||
21
Makefile
21
Makefile
|
|
@ -86,7 +86,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
|
||||||
# Third-party software does NOT go here — see ports/.
|
# Third-party software does NOT go here — see ports/.
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
|
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
|
||||||
CMDS = init fbinfo shmtest socktest inputtest
|
CMDS = init schinit fbinfo shmtest socktest inputtest
|
||||||
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
@ -325,7 +325,10 @@ endif
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Top-level targets
|
# Top-level targets
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
all: schos.img
|
# usbstick.img is built here rather than on demand because bootcheck -Usb
|
||||||
|
# cannot start QEMU without it, and a missing test image looks exactly like a
|
||||||
|
# broken USB driver: QEMU refuses the -drive and the whole run produces nothing.
|
||||||
|
all: schos.img $(BUILD)/usbstick.img
|
||||||
|
|
||||||
schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img
|
schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img
|
||||||
dd if=/dev/zero of=schos.img count=10000
|
dd if=/dev/zero of=schos.img count=10000
|
||||||
|
|
@ -684,7 +687,19 @@ $(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h
|
||||||
# zero -- and it is stated out loud rather than silently skipped, because a
|
# zero -- and it is stated out loud rather than silently skipped, because a
|
||||||
# port that quietly does not rebuild is exactly the kind of thing that wastes
|
# port that quietly does not rebuild is exactly the kind of thing that wastes
|
||||||
# an afternoon later.
|
# an afternoon later.
|
||||||
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS))
|
# schinit's job manifests. Kept under etc/ in the repo and installed here,
|
||||||
|
# because sysroot/ is generated and gitignored -- anything only ever written
|
||||||
|
# straight into it is lost on a fresh clone, which is already true of
|
||||||
|
# /etc/passwd and /etc/motd and should not become true of the boot
|
||||||
|
# configuration as well.
|
||||||
|
SCHINIT_JOBS = $(wildcard etc/schinit/*.job) etc/schinit/README
|
||||||
|
|
||||||
|
etc-install: $(SCHINIT_JOBS)
|
||||||
|
@mkdir -p $(SYSROOT)/etc/schinit
|
||||||
|
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
|
||||||
|
@echo "installed $(words $(SCHINIT_JOBS)) schinit manifest file(s)"
|
||||||
|
|
||||||
|
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install
|
||||||
ifdef WINHOST
|
ifdef WINHOST
|
||||||
@echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)"
|
@echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)"
|
||||||
@test -f $(SYSROOT)/bin/sh || { echo "ports: $(SYSROOT)/bin/sh is missing and cannot be built here; build ports on a Linux host once" >&2; exit 1; }
|
@test -f $(SYSROOT)/bin/sh || { echo "ports: $(SYSROOT)/bin/sh is missing and cannot be built here; build ports on a Linux host once" >&2; exit 1; }
|
||||||
|
|
|
||||||
557
cmd/schinit/main.c
Normal file
557
cmd/schinit/main.c
Normal file
|
|
@ -0,0 +1,557 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
// schinit — SchOS process 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 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.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "types.h"
|
||||||
|
#include "user.h"
|
||||||
|
#include "fcntl.h"
|
||||||
|
#include "posix.h" // TIOCSCTTY, TIOCSPGRP
|
||||||
|
|
||||||
|
#define MAXJOBS 32
|
||||||
|
#define MAXARGS 16
|
||||||
|
#define MAXLABEL 64
|
||||||
|
#define MAXPATH 128
|
||||||
|
#define MAXLINE 256
|
||||||
|
#define JOBDIR "/etc/schinit"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Small string helpers. No libc.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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, "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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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.
|
||||||
|
exec(j->program, j->argv);
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// Console first, as fd 0/1/2, so every message below has somewhere to go.
|
||||||
|
if (open("console", O_RDWR) < 0) {
|
||||||
|
mknod("console", 1, 1);
|
||||||
|
open("console", O_RDWR);
|
||||||
|
}
|
||||||
|
dup(0);
|
||||||
|
dup(0);
|
||||||
|
|
||||||
|
// Minor numbers under major 1 follow Linux's mem-major layout where it
|
||||||
|
// costs nothing (null=2, zero=5, full=7, random=8, urandom=9).
|
||||||
|
mkdir("/dev");
|
||||||
|
mknod("/dev/console", 1, 1);
|
||||||
|
mknod("/dev/null", 1, 2);
|
||||||
|
mknod("/dev/keyboard", 1, 3);
|
||||||
|
mknod("/dev/zero", 1, 5);
|
||||||
|
mknod("/dev/full", 1, 7);
|
||||||
|
mknod("/dev/random", 1, 8);
|
||||||
|
mknod("/dev/urandom", 1, 9);
|
||||||
|
mknod("/dev/fb0", 2, 0);
|
||||||
|
mknod("/dev/mouse", 3, 1);
|
||||||
|
mknod("/dev/tty", 1, 1); // alias for console -- must pass TCGETS
|
||||||
|
mknod("/dev/ttyS0", 4, 64);
|
||||||
|
mknod("/dev/stdin", 1, 1);
|
||||||
|
mknod("/dev/stdout", 1, 1);
|
||||||
|
mknod("/dev/stderr", 1, 1);
|
||||||
|
|
||||||
|
// Raw input events, Linux evdev layout (kernel/dev/input.c). /dev/keyboard
|
||||||
|
// and /dev/mouse above are the old character-and-position interfaces;
|
||||||
|
// these are the ones with key releases and button transitions, which is
|
||||||
|
// what a game or a compositor needs and neither of the others express.
|
||||||
|
mkdir("/dev/input");
|
||||||
|
mknod("/dev/input/event0", 7, 0); // keyboard
|
||||||
|
mknod("/dev/input/event1", 7, 1); // mouse
|
||||||
|
|
||||||
|
// /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 mknod'd global files still happen and are simply covered by the
|
||||||
|
// mount, so a failure there degrades to the old behaviour rather than
|
||||||
|
// losing /proc entirely.
|
||||||
|
mkdir("/proc");
|
||||||
|
mount("proc", "/proc", "proc", 0, 0);
|
||||||
|
mknod("/proc/meminfo", 6, 1);
|
||||||
|
mknod("/proc/uptime", 6, 2);
|
||||||
|
mknod("/proc/cpuinfo", 6, 3);
|
||||||
|
mknod("/proc/version", 6, 4);
|
||||||
|
mknod("/proc/stat", 6, 5);
|
||||||
|
mknod("/proc/loadavg", 6, 6);
|
||||||
|
mknod("/proc/mounts", 6, 7);
|
||||||
|
mknod("/proc/filesystems", 6, 8);
|
||||||
|
mknod("/proc/schfs", 6, 9);
|
||||||
|
mknod("/proc/sysrq", 6, 10); // echo c > here to crash on purpose
|
||||||
|
mknod("/proc/pci", 6, 11);
|
||||||
|
mknod("/proc/interrupts", 6, 12);
|
||||||
|
mknod("/proc/devices", 6, 13);
|
||||||
|
|
||||||
|
mkdir("/sys");
|
||||||
|
mkdir("/run");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct job *j = job_by_pid(pid);
|
||||||
|
if (!j) continue; // an adopted orphan; already reaped
|
||||||
|
|
||||||
|
j->pid = 0;
|
||||||
|
|
||||||
|
if (!j->keep_alive) {
|
||||||
|
out("schinit: "); out(j->label); out(" exited\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
27
etc/schinit/README
Normal file
27
etc/schinit/README
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
Job manifests for schinit, SchOS process 1.
|
||||||
|
|
||||||
|
Every *.job file in this directory is loaded at boot. Format is sectioned
|
||||||
|
key = value; # starts a comment, keys are case-insensitive.
|
||||||
|
|
||||||
|
Label a reverse-DNS name, as launchd uses. Identity, not a path.
|
||||||
|
Program the binary to execute
|
||||||
|
Args argv, space separated. argv[0] is conventionally the
|
||||||
|
basename, so `Args = sh` for `/bin/sh`.
|
||||||
|
RunAtLoad start it when schinit loads
|
||||||
|
KeepAlive restart it when it exits
|
||||||
|
Disabled load it but do not start it
|
||||||
|
ThrottleInterval seconds between respawns (default 1)
|
||||||
|
StandardIn \
|
||||||
|
StandardOut > paths to open as fd 0, 1, 2
|
||||||
|
StandardError /
|
||||||
|
|
||||||
|
[Socket <name>]
|
||||||
|
Path a unix socket schinit will hold on the job's behalf and
|
||||||
|
start the job on first connect
|
||||||
|
|
||||||
|
Socket sections are parsed but not yet acted on -- that is milestone 2, and
|
||||||
|
demand launching is the whole reason this is shaped after launchd rather than
|
||||||
|
sysvinit. The key exists now so manifests do not have to change later.
|
||||||
|
|
||||||
|
Unknown keys and unknown sections are ignored rather than fatal: a manifest
|
||||||
|
written for a newer schinit must not stop this one booting the machine.
|
||||||
25
etc/schinit/console-shell.job
Normal file
25
etc/schinit/console-shell.job
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# The interactive shell on the system console.
|
||||||
|
#
|
||||||
|
# In launchd terms this is a RunAtLoad + KeepAlive daemon: started when
|
||||||
|
# schinit loads, and restarted whenever it exits -- which for a shell is
|
||||||
|
# every time someone types `exit`.
|
||||||
|
|
||||||
|
Label = os.schos.console-shell
|
||||||
|
Program = /bin/sh
|
||||||
|
Args = sh
|
||||||
|
RunAtLoad = yes
|
||||||
|
KeepAlive = yes
|
||||||
|
|
||||||
|
# This job reads the console, so it has to own it: schinit puts every job in
|
||||||
|
# its own session, and a session with no controlling terminal is not delivered
|
||||||
|
# terminal input at all. Without this the shell echoes and executes nothing.
|
||||||
|
ControllingTerminal = yes
|
||||||
|
|
||||||
|
StandardIn = /dev/console
|
||||||
|
StandardOut = /dev/console
|
||||||
|
StandardError = /dev/console
|
||||||
|
|
||||||
|
# Seconds between respawns. Guards against a shell that dies instantly
|
||||||
|
# taking the machine with it; launchd calls this ThrottleInterval and
|
||||||
|
# defaults to 10. One is enough for a shell someone just exited on purpose.
|
||||||
|
ThrottleInterval = 1
|
||||||
|
|
@ -95,6 +95,24 @@ int getppid(void);
|
||||||
int rt_sigaction(int, void*, void*, uint64);
|
int rt_sigaction(int, void*, void*, uint64);
|
||||||
int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data);
|
int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data);
|
||||||
|
|
||||||
|
// Directory reading. The kernel has had getdents64 since the Linux ABI landed,
|
||||||
|
// but nothing declared it here, so first-party programs had no way to list a
|
||||||
|
// directory at all -- which is what schinit needs to find its job files.
|
||||||
|
//
|
||||||
|
// Records are packed: step by d_reclen, never by sizeof, because d_name is a
|
||||||
|
// flexible array and each record is only as long as its name needs.
|
||||||
|
struct dirent64 {
|
||||||
|
uint64 d_ino;
|
||||||
|
int64 d_off;
|
||||||
|
ushort d_reclen;
|
||||||
|
uchar d_type;
|
||||||
|
char d_name[];
|
||||||
|
};
|
||||||
|
#define DT_DIR 4
|
||||||
|
#define DT_REG 8
|
||||||
|
|
||||||
|
int getdents64(int fd, void *buf, int count);
|
||||||
|
|
||||||
// Sockets. `struct sockaddr` and the AF_*/SOCK_* constants are in socket.h;
|
// Sockets. `struct sockaddr` and the AF_*/SOCK_* constants are in socket.h;
|
||||||
// include it as well as this header to use them.
|
// include it as well as this header to use them.
|
||||||
struct sockaddr;
|
struct sockaddr;
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@ exit:
|
||||||
syscall
|
syscall
|
||||||
jmp exit
|
jmp exit
|
||||||
|
|
||||||
|
// The kernel's very first exec. /bin/init remains as the older, hardcoded
|
||||||
|
// fork-a-shell process 1; /bin/schinit is the supervisor that replaced it and
|
||||||
|
// reads its jobs from /etc/schinit.
|
||||||
init:
|
init:
|
||||||
.string "/bin/init"
|
.string "/bin/schinit"
|
||||||
|
|
||||||
.p2align 3
|
.p2align 3
|
||||||
argv:
|
argv:
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,4 @@ SYSCALL(socketpair)
|
||||||
SYSCALL(setsockopt)
|
SYSCALL(setsockopt)
|
||||||
SYSCALL(getsockopt)
|
SYSCALL(getsockopt)
|
||||||
SYSCALL(mount)
|
SYSCALL(mount)
|
||||||
|
SYSCALL(getdents64)
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,13 @@ $commands += 'kill -TERM 1'
|
||||||
$commands += 'echo INIT-SURVIVED'
|
$commands += 'echo INIT-SURVIVED'
|
||||||
$commands += 'ps | head -3'
|
$commands += 'ps | head -3'
|
||||||
|
|
||||||
|
# KeepAlive. Exiting the shell must bring a new one back -- that is the
|
||||||
|
# supervision loop doing its job, and it is the only part of schinit that
|
||||||
|
# cannot be checked by reading its startup output.
|
||||||
|
$commands += 'echo BEFORE-EXIT'
|
||||||
|
$commands += 'exit'
|
||||||
|
$commands += 'echo SHELL-RESPAWNED'
|
||||||
|
|
||||||
if ($Fb) {
|
if ($Fb) {
|
||||||
# /dev/fb0. The geometry has to come back from the mode the bootloader
|
# /dev/fb0. The geometry has to come back from the mode the bootloader
|
||||||
# actually programmed -- three places in the kernel used to assume
|
# actually programmed -- three places in the kernel used to assume
|
||||||
|
|
@ -441,6 +448,10 @@ Expect 'vga: display controller' 'the display driver found its aperture'
|
||||||
# point would appear at all -- but check explicitly, so a failure names the
|
# point would appear at all -- but check explicitly, so a failure names the
|
||||||
# cause instead of showing up as "the shell stopped answering".
|
# cause instead of showing up as "the shell stopped answering".
|
||||||
Expect 'INIT-SURVIVED' 'init survives SIGINT, SIGKILL and SIGTERM'
|
Expect 'INIT-SURVIVED' 'init survives SIGINT, SIGKILL and SIGTERM'
|
||||||
|
Expect 'schinit: ready \(pid 1\)' 'schinit came up as process 1'
|
||||||
|
Expect 'job.? loaded' 'schinit read its manifests from /etc/schinit'
|
||||||
|
Expect 'starting os\.schos\.' 'a job started by Label, not by hardcoded path'
|
||||||
|
Expect 'SHELL-RESPAWNED' 'KeepAlive restarted the shell after it exited'
|
||||||
Expect '(?m)^\s*1\s' 'pid 1 is still in the process table afterwards'
|
Expect '(?m)^\s*1\s' 'pid 1 is still in the process table afterwards'
|
||||||
|
|
||||||
Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem'
|
Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue