B4: the disk remembers whether the last start arrived

The loader marks the superblock before it hands over and the system clears
the mark when it reaches its prompt, so a system that crashes on the way
there leaves it set. The loader finding it still set next time is how a
machine that will not start says so to the only thing in a position to do
anything about it. Without that, pointing boot.cfg at something that dies
before the shell is a machine that can never be told anything again - the
shell is the only way to change the file, and the file is what stops the
shell from starting.

Three states rather than two, and the third is the one worth having:

  0 settled    the last start arrived; use the configuration
  1 trying     handed over, and nothing came back to say it got there
  2 fell back  a try failed and the fallback was used, until settled

With only 0 and 1 the machine alternates for ever: fall back, reach a
prompt, clear the mark, retry the broken system, crash, fall back. State 2
stops that. A system known not to start is not tried again until somebody
says the situation has changed.

REACHING THE PROMPT IS A DELIBERATE THRESHOLD. It is not a claim that the
system works - a shell can be reached by something broken in every other
way. It is the point where a person can type, which is exactly what the
fallback exists to give back: anything wrong past there is fixable from the
prompt and nothing wrong before it is fixable at all.

The routines live in sbfs.asm because both the loader and the system read
and write this byte, and two pieces of code with their own idea of where a
byte lives is what this format has two implementations and a byte for byte
comparison to avoid.

And the trap this system documents in its own manual caught me anyway: the
first version handed the state back in A, which CALL restores, so every
read got whatever the caller happened to be holding. It comes back in
memory now, and the comment says why.

Three disks differing only in the state on them, so the tests read as three
consecutive starts of one machine while none depends on another running.
This commit is contained in:
Anachronaut
2026-08-27 16:51:39 -04:00
parent 546f336823
commit dc74149321
14 changed files with 368 additions and 3 deletions
+52
View File
@@ -58,6 +58,7 @@ typedef struct {
uint16_t freeBlocks;
uint16_t bootBlocks; // Per slot. Zero on a disk that cannot be booted.
uint8_t bootSlot; // Which of the two is live.
uint8_t bootState; // How the last start went. See sbfs.h.
} Superblock;
// Reads block 0 and checks it really is one of ours. Without the magic a blank image and
@@ -97,6 +98,7 @@ static int readSuperblock(FILE *image, Superblock *super) {
super->freeBlocks = readWord(block + SBFS_SUPER_FREE);
super->bootBlocks = readWord(block + SBFS_SUPER_BOOTBLOCKS);
super->bootSlot = block[SBFS_SUPER_BOOTSLOT];
super->bootState = block[SBFS_SUPER_BOOTSTATE];
// The boot area and the directory's position describe the same fact from two sides,
// so they have to agree or one of them is wrong and there is no way to tell which.
@@ -130,6 +132,7 @@ static int writeSuperblock(FILE *image, const Superblock *super) {
writeWord(block + SBFS_SUPER_FREE, super->freeBlocks);
writeWord(block + SBFS_SUPER_BOOTBLOCKS, super->bootBlocks);
block[SBFS_SUPER_BOOTSLOT] = super->bootSlot;
block[SBFS_SUPER_BOOTSTATE] = super->bootState;
return writeBlock(image, 0, block);
}
@@ -481,6 +484,7 @@ static int commandFormat(const char *path, uint16_t blocks, uint16_t directoryBl
super.freeBlocks = (uint16_t)(blocks - overhead);
super.bootBlocks = bootBlocks;
super.bootSlot = 0;
super.bootState = SBFS_BOOT_SETTLED;
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
@@ -782,6 +786,51 @@ static int commandBootSlot(const char *path, long slot) {
return 0;
}
// ---- How the last start went, from the host ----
//
// Shown with no argument and set with one. Setting it is how a disk that fell back is told
// to try again, which is a decision rather than a repair: the thing that did not start has
// to be fixed first, or the next start marks it and falls back once more.
static const char *bootStateName(uint8_t state) {
switch (state) {
case SBFS_BOOT_SETTLED: return "settled, so the next start will use the configuration";
case SBFS_BOOT_TRYING: return "trying, so the last start never arrived";
case SBFS_BOOT_FELLBACK: return "fell back, and will keep doing so until settled";
default: return "a number this does not recognise";
}
}
static int commandBootState(const char *path, const char *setting) {
FILE *image = openImage(path, setting ? "r+b" : "rb");
if (image == NULL) {
return 1;
}
Superblock super;
if (readSuperblock(image, &super)) {
fclose(image);
return 1;
}
if (setting == NULL) {
printf("%u: %s\n", super.bootState, bootStateName(super.bootState));
fclose(image);
return 0;
}
long wanted = strtol(setting, NULL, 0);
if (wanted < 0 || wanted > SBFS_BOOT_FELLBACK) {
fprintf(stderr, "Error: The boot state is 0, 1 or 2.\n");
fclose(image);
return 1;
}
super.bootState = (uint8_t)wanted;
if (writeSuperblock(image, &super)) {
fclose(image);
return 1;
}
fclose(image);
printf("%ld: %s\n", wanted, bootStateName((uint8_t)wanted));
return 0;
}
static int commandPut(const char *path, const char *hostFile, const char *asName) {
FILE *source = fopen(hostFile, "rb");
if (source == NULL) {
@@ -1183,6 +1232,9 @@ int main(int argc, char *argv[]) {
}
return commandBoot(path, argv[3], (argc > 4) ? strtol(argv[4], NULL, 0) : 0);
}
if (strcmp(command, "bootstate") == 0) {
return commandBootState(path, (argc > 3) ? argv[3] : NULL);
}
if (strcmp(command, "bootslot") == 0) {
if (argc < 4) {
fprintf(stderr, "Error: bootslot needs the slot to start from.\n");
+24 -1
View File
@@ -53,7 +53,8 @@
// 12 2 Free blocks, a cache rather than the authority
// 14 2 Blocks in each boot slot, or zero for a disk that cannot be booted
// 16 1 Which boot slot is live, 0 or 1
// 17 Reserved to the end of the block
// 17 1 How the last start went. See below.
// 18 Reserved to the end of the block
#define SBFS_SUPER_VERSION 4
#define SBFS_SUPER_DISK 6
@@ -86,6 +87,28 @@
// 1 .. bootBlocks slot 0
// bootBlocks+1 .. 2*bootBlocks slot 1
// directoryStart .. the directory, and then files
// ---- How the last start went ----
//
// Written by the loader before it hands over and cleared by the system once it is running,
// so that a system which never gets that far leaves a mark saying so. THE MARK IS WHAT
// MAKES A NEW SYSTEM SAFE TO TRY: without it, pointing boot.cfg at something that crashes
// before the prompt is a machine that cannot be told anything ever again.
//
// What clears it is reaching the shell, and that is a deliberate choice of threshold. It
// does not mean the system works - a shell can be reached by something that is broken in
// every other way. It means A PERSON HAS CONTROL AGAIN, which is exactly what the fallback
// exists to restore and therefore exactly when it has done its job.
//
// 0 Settled. The last start finished. Start what the configuration says.
// 1 Trying. The loader handed over and nothing came back to say it arrived.
// 2 Fell back. A try failed and the fallback was used instead. Stays until somebody
// settles it, so that a system which crashes is not retried every other
// boot for ever.
#define SBFS_SUPER_BOOTSTATE 17
#define SBFS_BOOT_SETTLED 0
#define SBFS_BOOT_TRYING 1
#define SBFS_BOOT_FELLBACK 2
#define SBFS_BOOT_SLOTS 2
#define SBFS_FIRST_BOOT_BLOCK 1