Files
Asuro/src/boot/boot.mgr.pas
T
Aaron c0c56a9c84 Add initial ext2 read/write and formatting support
Register ext2 at boot, integrate mode-aware VFS opens and volume cleanup, and queue asynchronous transfers through a persistent worker.

Validation: Pascal compiler check and git diff --check passed. Full build blocked by Docker engine HTTP 500; runtime and Linux interoperability remain unverified.
2026-10-31 19:49:36 +00:00

567 lines
18 KiB
ObjectPascal

{
boot.mgr - Boot Initialization Manager.
Provides a self-registering, dependency-ordered boot sequence. Units
register themselves during their Pascal initialization sections by calling
registerBoot() with a name, an init procedure, a splash-screen status
message, and an optional dependency name. boot.mgr.run() performs a
topological sort on the entries and executes them in dependency order,
updating the splash screen as it goes.
Because registration happens before kmain (and therefore before the
heap exists), all storage is statically allocated.
@author(Kieron Morris <[email protected]>)
}
unit boot.mgr;
interface
const
{ Maximum number of boot entries that can be registered. This is a
static upper bound sized for the number of kernel units. If exceeded,
registration halts the system (fatal misconfiguration). }
MAX_BOOT_ENTRIES = 128;
const
{ BARRIER_NAMES }
BOOT_MGR_BARRIER_IMMEDIATE = 'immediate';
BOOT_MGR_BARRIER_EARLY = 'early';
BOOT_MGR_BARRIER_MID = 'middle';
BOOT_MGR_BARRIER_STORAGE = 'storage';
BOOT_MGR_BARRIER_DEVICE = 'device';
BOOT_MGR_BARRIER_BUS = 'bus';
BOOT_MGR_BARRIER_BUS_LATE = 'bus.late';
BOOT_MGR_BARRIER_LATE = 'late';
BOOT_MGR_BARRIER_FINAL = 'final';
type
{ Parameterless procedure pointer for boot init callbacks }
TBootProc = procedure;
PBootProc = ^TBootProc;
{ A single boot registration entry }
TBootEntry = record
Name : PChar; { Unique identifier, e.g. 'arch.x86.gdt' }
InitProc : TBootProc; { Procedure to call during boot }
Status : PChar; { Splash screen status text }
DependsOn : PChar; { Name of the entry this depends on, or nil }
Barrier : Boolean; { If true, deferred until all non-barrier entries
at this dependency level have been emitted }
end;
PBootEntry = ^TBootEntry;
{ Register a boot entry. Called from unit initialization sections.
- Name : unique identifier for this entry (must be a string literal / constant)
- InitProc : parameterless procedure to call during boot
- Status : splash screen progress text shown while this entry runs
- DependsOn : name of another entry that must run first, or nil for no dependency }
procedure registerBoot(Name: PChar; InitProc: TBootProc; Status: PChar; DependsOn: PChar);
{ Register a barrier entry. Same as registerBoot but the entry is deferred
until all non-barrier entries at the same dependency level have been
emitted. Use this for phase markers that define ordering boundaries
(e.g. 'immediate', 'early', 'middle', 'late'). }
procedure registerBarrier(Name: PChar; InitProc: TBootProc; Status: PChar; DependsOn: PChar);
{ Execute all registered boot entries in dependency order.
Updates syslog and the splash screen as each entry completes. }
procedure run;
implementation
uses
io.syslog, boot.splash,
// Silo units that must be included here to compile
driver.storage.boot,
test,
core,
app.wasm,
driver.storage.ctl.ahci,
core.enc.base64,
core.panic,
driver.video.bga,
arch.x86.bda,
arch.x86.panic,
core.ds.cfifo,
core.ds.cfifols,
core.ds.circ,
core.gfx.color,
arch.x86.proc.sched,
arch.x86.cpu,
driver.video.desktop,
driver.video.doublebuffer,
driver.mgr,
driver.net.dev.e1000,
driver.bus.usb.ehci,
driver.storage.fs.fat32,
driver.storage.fs.ext2,
arch.x86.fault,
core.ds.fifo,
driver.storage.fs.mgr,
driver.storage.fs.flatfs,
core.gfx.fonts,
arch.x86.gdt,
driver.video.gpu,
svc.gfxd,
core.ds.hashmap,
arch.x86.idt,
arch.x86.isr.ioapic,
arch.x86.irq,
driver.storage.fs.iso9660,
arch.x86.isr,
arch.x86.isr.mgr,
driver.hid.keyboard,
core.ds.lifo,
core.ds.lists,
memory.heap,
driver.video.lvgl,
core.ds.maxh,
core.enc.md5,
core.ds.minh,
driver.hid.mouse,
arch.x86.multiboot,
driver.net,
driver.bus.usb.ohci,
app.partcmd,
driver.bus.pci,
arch.x86.memory.physical,
core.ds.prio,
proc.mgr,
app.mgr,
driver.hid.ps2.keyboard,
driver.hid.ps2.mouse,
core.rand,
driver.timer.rtc,
driver.io.serial,
io.stdio,
driver.storage.mgr,
driver.storage.test,
core.strings,
driver.exp.testdriver,
proc.testprocs,
arch.x86.isr.tmr0,
debug.tracer,
driver.bus.usb.uhci,
app.uidebug,
driver.bus.usb,
driver.hid.usb.keyboard,
driver.hid.usb.mouse,
driver.storage.ctl.usb,
driver.bus.usb.core,
svc.usbd,
driver.bus.usb.hub,
driver.bus.usb.types,
core.util,
arch.x86.util,
arch.x86.v86,
driver.video.vesa,
driver.storage.vfs,
driver.video,
arch.x86.memory.virtual,
core.enc.fnv1a,
core.enc.djb2,
core.ds.bloom,
app.volcmd,
driver.storage.vol.mgr,
app.vterminal,
wasm,
wasm.vm.io,
wasm.test,
wasm.test.framework,
driver.video.windows,
driver.bus.usb.xhci,
core.fmt.json;
type
{ Resolved execution order — indices into Entries[] }
TOrderArray = array[0..MAX_BOOT_ENTRIES - 1] of uint32;
var
{ Registration table — filled during initialization sections }
Entries : array[0..MAX_BOOT_ENTRIES - 1] of TBootEntry;
EntryCount : uint32 = 0;
{ Shared scratch space for topoSort results — kept at module scope
rather than on the stack because this 512-byte array would otherwise
live on the 16 KB kernel stack for the entire duration of run(),
leaving too little headroom for the deeply-nested driver init
chains and their ISRs. }
SortedOrder : TOrderArray;
{ ---- Internal helpers ---- }
{ Null-safe PChar equality test. Does not allocate memory. }
function pcharEqual(a, b: PChar): Boolean;
var
i: uint32;
begin
if (a = nil) and (b = nil) then exit(true);
if (a = nil) or (b = nil) then exit(false);
i := 0;
while (a[i] <> #0) and (b[i] <> #0) do begin
if a[i] <> b[i] then exit(false);
Inc(i);
end;
pcharEqual := (a[i] = #0) and (b[i] = #0);
end;
{ Return the length of a null-terminated PChar string. }
function pcharLen(s: PChar): uint32;
var
i: uint32;
begin
i := 0;
while s[i] <> #0 do Inc(i);
pcharLen := i;
end;
{ Check whether a DependsOn string contains any '*' characters. }
function isGlob(pattern: PChar): Boolean;
var
i: uint32;
begin
if pattern = nil then exit(false);
i := 0;
while pattern[i] <> #0 do begin
if pattern[i] = '*' then exit(true);
Inc(i);
end;
isGlob := false;
end;
{ Match a name against a glob pattern. '*' matches zero or more of any
character and may appear anywhere in the pattern, including multiple
times (e.g. 'arch.*.memory.*').
Uses a greedy two-cursor algorithm with backtracking:
ni — current position in name
pi — current position in pattern
starP / starN — saved positions after the most recent '*' for retry
No heap allocation. Runs in O(n*m) worst case where n = name length
and m = pattern length, but typical patterns are short. }
function globMatches(name, pattern: PChar): Boolean;
var
ni, pi : uint32;
starP, starN: uint32;
nameLen, patLen: uint32;
begin
if (name = nil) or (pattern = nil) then exit(false);
nameLen := pcharLen(name);
patLen := pcharLen(pattern);
ni := 0;
pi := 0;
starP := $FFFFFFFF; { no star seen yet }
starN := 0;
while ni < nameLen do begin
if (pi < patLen) and (pattern[pi] = '*') then begin
{ Record star position and advance pattern past the '*' }
starP := pi;
starN := ni;
Inc(pi);
end
else if (pi < patLen) and (pattern[pi] = name[ni]) then begin
{ Characters match — advance both cursors }
Inc(ni);
Inc(pi);
end
else if starP <> $FFFFFFFF then begin
{ Mismatch but we have a prior '*' — backtrack:
let that '*' consume one more character from name }
pi := starP + 1;
Inc(starN);
ni := starN;
end
else begin
{ Mismatch with no '*' to fall back on }
exit(false);
end;
end;
{ Consume any trailing '*' characters in the pattern }
while (pi < patLen) and (pattern[pi] = '*') do
Inc(pi);
globMatches := (pi = patLen);
end;
{ Check whether all entries matching a glob pattern have been emitted.
Returns true if every entry whose Name matches the pattern has Emitted[]=true.
Also returns true if no entries match (vacuous truth — nothing to wait for). }
function allGlobEmitted(pattern: PChar; var Emitted: array of Boolean): Boolean;
var
i: uint32;
begin
for i := 0 to EntryCount - 1 do begin
if globMatches(Entries[i].Name, pattern) and (not Emitted[i]) then
exit(false);
end;
allGlobEmitted := true;
end;
{ Look up an entry index by name. Returns index, or $FFFFFFFF if not found. }
function findEntry(Name: PChar): uint32;
var
i: uint32;
begin
for i := 0 to EntryCount - 1 do begin
if pcharEqual(Entries[i].Name, Name) then exit(i);
end;
findEntry := $FFFFFFFF;
end;
{ Check whether entry i is eligible for emission (all dependencies met). }
function isEligible(i: uint32; var Emitted: array of Boolean): Boolean;
var
depIdx: uint32;
begin
if Emitted[i] then exit(false);
{ No dependency — always eligible }
if Entries[i].DependsOn = nil then exit(true);
{ Glob dependency — eligible when all matching entries are emitted }
if isGlob(Entries[i].DependsOn) then
exit(allGlobEmitted(Entries[i].DependsOn, Emitted));
{ Exact dependency — eligible when that specific entry is emitted }
depIdx := findEntry(Entries[i].DependsOn);
if depIdx = $FFFFFFFF then exit(false); { unresolved dependency }
isEligible := Emitted[depIdx];
end;
{ Perform a topological sort of Entries[] into Order[].
Uses a repeated-scan approach that is O(n^2) but requires only a
small static boolean array — no heap allocation needed.
Supports two kinds of DependsOn values:
- Exact name (e.g. 'io.syslog') — waits for that one entry.
- Glob pattern (e.g. 'arch.*', 'arch.*.memory.*') — waits for ALL
entries whose names match the pattern. '*' matches zero or more
of any character and may appear multiple times.
Barrier entries are deferred within each pass: non-barrier entries are
always emitted before barrier entries at the same dependency level.
This means that if 'immediate' is a barrier and 'mydriver' depends on
'immediate', 'mydriver' will be scheduled before the next barrier
('early') even though both become eligible at the same time.
Returns the number of entries placed into Order[]. If this is less
than EntryCount, there is a cycle or a missing dependency. }
function topoSort(var Order: TOrderArray): uint32;
var
Emitted : array[0..MAX_BOOT_ENTRIES - 1] of Boolean;
count : uint32;
i : uint32;
progress : Boolean;
nonBarrierProgress : Boolean;
begin
for i := 0 to EntryCount - 1 do
Emitted[i] := false;
count := 0;
repeat
progress := false;
{ Sub-pass 1: emit all eligible NON-barrier entries }
nonBarrierProgress := true;
while nonBarrierProgress do begin
nonBarrierProgress := false;
for i := 0 to EntryCount - 1 do begin
if Entries[i].Barrier then continue;
if not isEligible(i, Emitted) then continue;
Order[count] := i;
Inc(count);
Emitted[i] := true;
nonBarrierProgress := true;
progress := true;
end;
end;
{ Sub-pass 2: emit exactly ONE eligible barrier, then return to
sub-pass 1 so that non-barrier entries unlocked by this barrier
get a chance to run before the next barrier fires. Without this
break, barriers cascade (immediate -> early -> middle -> late all
in one shot) and non-barriers never interleave. }
for i := 0 to EntryCount - 1 do begin
if not Entries[i].Barrier then continue;
if not isEligible(i, Emitted) then continue;
Order[count] := i;
Inc(count);
Emitted[i] := true;
progress := true;
break;
end;
until (count = EntryCount) or (not progress);
topoSort := count;
end;
{ ---- Public API ---- }
{ Shared registration logic used by both registerBoot and registerBarrier. }
procedure internalRegister(Name: PChar; InitProc: TBootProc; Status: PChar; DependsOn: PChar; IsBarrier: Boolean);
begin
if EntryCount >= MAX_BOOT_ENTRIES then begin
{ Fatal: too many boot entries — halt immediately.
We cannot use syslog/panic here as they may not be initialized. }
asm
cli
hlt
end;
end;
Entries[EntryCount].Name := Name;
Entries[EntryCount].InitProc := InitProc;
Entries[EntryCount].Status := Status;
Entries[EntryCount].DependsOn := DependsOn;
Entries[EntryCount].Barrier := IsBarrier;
Inc(EntryCount);
end;
procedure registerBoot(Name: PChar; InitProc: TBootProc; Status: PChar; DependsOn: PChar);
begin
internalRegister(Name, InitProc, Status, DependsOn, false);
end;
procedure registerBarrier(Name: PChar; InitProc: TBootProc; Status: PChar; DependsOn: PChar);
begin
internalRegister(Name, InitProc, Status, DependsOn, true);
end;
{ Append a PChar string to a buffer at position pos.
Does not write past bufLen. Returns updated pos. }
function bufAppend(var buf: array of char; pos, bufLen: uint32; s: PChar): uint32;
var
i: uint32;
begin
if s = nil then exit(pos);
i := 0;
while (s[i] <> #0) and (pos < bufLen - 1) do begin
buf[pos] := s[i];
Inc(pos);
Inc(i);
end;
buf[pos] := #0;
bufAppend := pos;
end;
{ Return the dependency depth of an entry by following its DependsOn chain
iteratively. Glob dependencies are treated as depth 1 (group boundary).
Cap at 16 to guard against cycles. }
function entryDepth(idx: uint32): uint32;
var
depIdx, depth: uint32;
begin
depth := 0;
while (depth < 16) and (Entries[idx].DependsOn <> nil) do begin
if isGlob(Entries[idx].DependsOn) then begin
Inc(depth);
break;
end;
depIdx := findEntry(Entries[idx].DependsOn);
if depIdx = $FFFFFFFF then break;
Inc(depth);
idx := depIdx;
end;
entryDepth := depth;
end;
procedure printBootEntriesTree;
const
LINE_BUF_SIZE = 256;
var
Sorted : uint32;
i : uint32;
idx : uint32;
depth : uint32;
d : uint32;
line : array[0..LINE_BUF_SIZE - 1] of char;
pos : uint32;
begin
Sorted := topoSort(SortedOrder);
io.syslog.logln('BOOT', '--- Boot Entries Tree ---');
for i := 0 to Sorted - 1 do begin
idx := SortedOrder[i];
depth := entryDepth(idx);
pos := 0;
{ Indent by depth — two spaces per level }
if depth > 0 then
for d := 0 to depth - 1 do
pos := bufAppend(line, pos, LINE_BUF_SIZE, ' ');
{ Barrier marker }
if Entries[idx].Barrier then
pos := bufAppend(line, pos, LINE_BUF_SIZE, '[B] ')
else
pos := bufAppend(line, pos, LINE_BUF_SIZE, ' ');
{ Entry name }
pos := bufAppend(line, pos, LINE_BUF_SIZE, Entries[idx].Name);
io.syslog.logln('BOOT', @line[0]);
end;
io.syslog.logln('BOOT', '--- End Boot Entries ---');
end;
procedure run;
var
Sorted : uint32;
i : uint32;
idx : uint32;
begin
{ No entries to run — exit early. }
if EntryCount = 0 then exit;
{ Phase barriers form the main chain. The "after" barriers are
registered between the phase they gate and the next phase so that
when multiple barriers become eligible simultaneously, the for-loop
in topoSort picks the lower-indexed one first — giving "after"
barriers priority over the next phase marker. }
registerBarrier(BOOT_MGR_BARRIER_IMMEDIATE, nil, 'Immediate Section', nil);
registerBarrier(BOOT_MGR_BARRIER_EARLY, nil, 'Early Section', BOOT_MGR_BARRIER_IMMEDIATE);
registerBarrier(BOOT_MGR_BARRIER_MID, nil, 'Middle Section', BOOT_MGR_BARRIER_EARLY);
registerBarrier(BOOT_MGR_BARRIER_STORAGE, nil, 'Storage Section', BOOT_MGR_BARRIER_MID);
registerBarrier(BOOT_MGR_BARRIER_BUS, nil, 'Bus Section', BOOT_MGR_BARRIER_STORAGE);
registerBarrier(BOOT_MGR_BARRIER_DEVICE, nil, 'Device Section', BOOT_MGR_BARRIER_BUS);
registerBarrier(BOOT_MGR_BARRIER_BUS_LATE, nil, 'Late Bus Section', BOOT_MGR_BARRIER_DEVICE);
registerBarrier(BOOT_MGR_BARRIER_LATE, nil, 'Late Section', BOOT_MGR_BARRIER_BUS_LATE);
registerBarrier(BOOT_MGR_BARRIER_FINAL, nil, 'Final Section', BOOT_MGR_BARRIER_LATE);
{ Sort entries into dependency order }
Sorted := topoSort(SortedOrder);
{ Execute each entry in order.
Log AFTER calling InitProc so that io.syslog is already initialized
by the time we first try to write through it. }
for i := 0 to Sorted - 1 do begin
idx := SortedOrder[i];
io.syslog.logln('BOOT', Entries[idx].Name);
if Entries[idx].InitProc <> nil then begin
boot.splash.update((100 * i) div Sorted, Entries[idx].Status);
Entries[idx].InitProc();
end;
end;
end;
initialization
{ EntryCount lives in BSS and is zeroed by the bootloader's memory
clear. We intentionally do NOT reset it here because the INITFINAL
table calls initialization sections in alphabetical order — units
whose names sort before 'boot.mgr' (e.g. arch.x86.gdt, asuro) will
have already registered by the time this section runs, and resetting
EntryCount would silently discard their registrations. }
registerBoot('boot.mgr.debug', @printBootEntriesTree, 'Debug: Print Boot Entries Tree', 'io.syslog');
end.