diff --git a/Programs/Boot/stage2.asm b/Programs/Boot/stage2.asm index ff589d1..22fc068 100644 --- a/Programs/Boot/stage2.asm +++ b/Programs/Boot/stage2.asm @@ -111,16 +111,50 @@ systemChosen: STA.0 noFallbackSet: + ; ---- How the last start went ---- + ; + ; Settled means the last one arrived, so try what the configuration asks for. Trying + ; means the loader handed over last time and nothing came back to say it got there, so + ; the thing named is what broke the machine and the fallback is the way out. Fell back + ; means that already happened and nobody has settled it since, so do the same again + ; rather than retrying a system that is known not to start. + CALL sbfsBootState + SETD.0 SbfsStateWas + LDA.0 + BRA stateSettled + INIB 0d1 + XOR + BRQ stateFailedLast + SETD.0 StillBackText + RCAL say + BRI useFallback + +stateFailedLast: + SETD.0 DidNotArriveText + RCAL say + INIA 0d2 + CALL sbfsSetBootState + BRI useFallback + +stateSettled: + ; The mark goes down BEFORE the jump, because after it there is nothing here to do it. + ; What clears it is the system reaching its prompt, which is not a claim that the system + ; works - it is the moment a person can tell it otherwise. + INIA 0d1 + CALL sbfsSetBootState + SETD.0 BootName RCAL tryImage ; It did not start. Whatever went wrong has already said so, and if there is something ; else to try then trying it is the whole reason for having said it rather than stopping. + SETD.0 FallbackText + RCAL say + +useFallback: SETD.0 HaveFallback LDA.0 BRA noSystem - SETD.0 FallbackText - RCAL say SETD.0 FallbackName RCAL tryImage BRI noSystem @@ -509,6 +543,12 @@ KnownKeys: FallbackText: "trying the fallback " +DidNotArriveText: +"the last start did not arrive +" +StillBackText: +"still on the fallback: settle it to try again +" NothingText: "nothing to start " diff --git a/Programs/Boot/wedged.asm b/Programs/Boot/wedged.asm new file mode 100644 index 0000000..6301e95 --- /dev/null +++ b/Programs/Boot/wedged.asm @@ -0,0 +1,35 @@ +; wedged.asm +; A system that starts and never reaches a prompt. +; +; Not a broken program so much as a stand in for one: what matters is that it is handed the +; machine and never clears the mark the loader put on the disk, which is what every real +; way of failing before the shell has in common. +; +; Without that mark, pointing /System/Boot/boot.cfg at something like this would be a +; machine that could not be told anything ever again - the shell is the only way to change +; the file, and the file is what stops the shell from starting. +; +; Written by Anachronaut + +#Program + +start: + SETD.0 Dying +sayLoop: + LDA.0 + BRA gone + OUTA 0x00 + INCD.0 + BRI sayLoop +gone: + HALT + +#Data + +Dying: +"a system that never reaches a prompt +" + +#Vectors + + Boot start diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 571f2e9..9560d56 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -63,6 +63,38 @@ boot: BNQ bootNoDisk INIA 0x01 STA.0 + + ; ---- Saying that this start arrived ---- + ; + ; The loader marks the disk before handing over, and nothing clears it but this. A system + ; that crashes on the way here leaves the mark, and the loader finding it still set next + ; time is how a machine that will not start says so. + ; + ; HERE RATHER THAN LATER, and the threshold is the whole of what the mark means. It is + ; not a claim that anything works - a prompt can be reached by something broken in every + ; other way. It is the point where somebody can type, which is what the fallback exists + ; to give back: anything wrong past here can be fixed from the prompt, and nothing wrong + ; before it can be fixed at all. + CALL sbfsBootState + BNQ prompt + SETD.0 SbfsStateWas + LDA.0 + INIB 0d1 + XOR + BRQ bootArrived + INIB 0d2 + XOR + BNQ prompt + + ; Started by the fallback, so the system somebody asked for is not the one running. Said + ; once, here, because there is nowhere else it would be noticed. + SETD.0 OnFallback + CALL printString + BRI prompt + +bootArrived: + RSTA + CALL sbfsSetBootState BRI prompt bootNoDisk: RSTA @@ -3370,6 +3402,9 @@ Banner: "CosmOS" PromptText: "> " +OnFallback: +"this is the fallback: what boot.cfg asks for did not start +" NoDisk: "no filesystem on the disk" Unknown: diff --git a/Programs/CosmOS/Source/sbfs.asm b/Programs/CosmOS/Source/sbfs.asm index e7ae6d2..d8b8d74 100644 --- a/Programs/CosmOS/Source/sbfs.asm +++ b/Programs/CosmOS/Source/sbfs.asm @@ -2984,8 +2984,86 @@ sbfsSaveFailed: ADD RET +; ---- How the last start went ---- +; +; One byte of the superblock, written by the loader before it hands over and cleared by the +; system once it is running. A system that never gets that far leaves the mark set, and the +; loader seeing it still set next time is how a machine that cannot start says so to the +; only thing in a position to do anything about it. +; +; Kept here rather than in either of them because BOTH read and write it, and two pieces of +; code with their own idea of where a byte lives is the thing this filesystem has two +; implementations and a byte for byte comparison to avoid. +; +; sbfsBootState leaves the state in SbfsStateWas, and Q zero if the disk answered. +; +; NOT IN A, and the first version of this tried to. CALL restores A, so a routine cannot +; hand anything back in it: the value was set, the RET put the caller's own A back over it, +; and every state read as whatever the caller happened to be holding. It is the trap this +; system documents in its own manual and it still catches people. +sbfsBootState: + RSTA + SETD.0 SbfsBlock + STA.0 + INCD.0 + STA.0 + CALL sbfsReadBlock + BNQ sbfsBootStateNo + SETD.1 SbfsBuffer + CALL sbfsBufferOut + SETD.0 SbfsBuffer + DPUP.0 0d17 + LDA.0 + SETD.1 SbfsStateWas + STA.1 + RSTA + RSTB + CCF + ADD ; Q is zero: SbfsStateWas is what the disk says. + RET + +sbfsBootStateNo: + RSTA + SETD.0 SbfsStateWas + STA.0 + INIB 0d1 + CCF + ADD ; Q is not zero, and the state reads as settled. + RET + +; A is what to write. Q is zero if it went down. +; +; The superblock is read back before it is changed rather than kept from the mount, because +; everything between then and now has been reading other blocks over the buffer it was in. +sbfsSetBootState: + SETD.0 SbfsStateWants + STA.0 + RSTA + SETD.0 SbfsBlock + STA.0 + INCD.0 + STA.0 + CALL sbfsReadBlock + BNQ sbfsBootStateNo + SETD.1 SbfsBuffer + CALL sbfsBufferOut + SETD.0 SbfsStateWants + LDA.0 + SETD.1 SbfsBuffer + DPUP.1 0d17 + STA.1 + SETD.1 SbfsBuffer + CALL sbfsBufferIn + CALL sbfsWriteBlock + RET + #Data +SbfsStateWas: + 0x00 +SbfsStateWants: + 0x00 + SbfsMagic: "SBFS" diff --git a/README.md b/README.md index e5b7c83..f206633 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,7 @@ increment, decrement, addition, and subtraction when their inputs are known. | Command | What it does | | --- | --- | | `boot [slot]` | Write a file into a boot slot, padding the rest of it with zeroes. Slot 0 unless told otherwise. | +| `bootstate [0\|1\|2]` | Show how the last start went, or set it. Setting it to 0 is how a disk that fell back is told to try again. | | `bootslot ` | Choose which slot the machine starts from. One byte, on its own, so writing a slot and committing to it stay separate decisions. | | `format [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. | | `list [path]` | Show the whole disk, or one directory of it. | @@ -295,6 +296,43 @@ so a machine interrupted while updating its only boot slot would not boot at all the one failure on this disk with no way back. Writing the slot that is *not* live and then moving one byte in the superblock turns that into a machine that boots what it had before. +### Knowing Whether The Last Start Arrived: + +The loader marks the disk before it hands over, and the system clears the mark when it +reaches its prompt. **A system that crashes on the way there leaves the mark**, and 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. + +| State | Means | +| -- | -- | +| 0 | Settled. The last start arrived, so start what the configuration says. | +| 1 | Trying. The loader handed over and nothing came back to say it got there. | +| 2 | Fell back. A try failed and the fallback was used, and will be until this is settled. | + +Three starts of a machine whose configuration names something broken: + +``` +stage two +a system that never reaches a prompt <- marks the disk, dies + +stage two +the last start did not arrive <- finds the mark, uses the fallback +CosmOS +this is the fallback: what boot.cfg asks for did not start + +stage two +still on the fallback: settle it to try again <- does NOT retry +``` + +That third one is the part worth having. A system known not to start is not tried every +other boot for ever; it waits to be told the situation has changed. + +**Reaching the prompt is a deliberate choice of 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 somebody can type, which is exactly what the fallback exists to give back: +anything wrong past there can be fixed from the prompt, and nothing wrong before it can be +fixed at all. + `bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where they disagree is refused rather than guessed at. diff --git a/Source/DiskTool/SplitDisk.c b/Source/DiskTool/SplitDisk.c index 0972ac9..a773a52 100644 --- a/Source/DiskTool/SplitDisk.c +++ b/Source/DiskTool/SplitDisk.c @@ -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"); diff --git a/Source/DiskTool/sbfs.h b/Source/DiskTool/sbfs.h index f0ea391..69b71ab 100644 --- a/Source/DiskTool/sbfs.h +++ b/Source/DiskTool/sbfs.h @@ -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 diff --git a/Tests/disk.sh b/Tests/disk.sh index bc11dcd..901fa9d 100755 --- a/Tests/disk.sh +++ b/Tests/disk.sh @@ -220,6 +220,14 @@ d = open('boot.img','rb').read() slot = d[256:256 + 32 * 256] sys.exit(1 if b'aaaa' in slot else 0)" +# ---- How the last start went ---- +check "the boot state reads" "$TOOL" bootstate boot.img +check "and can be set" "$TOOL" bootstate boot.img 2 +refuses "but only to a state there is" "$TOOL" bootstate boot.img 7 +check "a fresh disk is settled" python3 -c " +import sys +sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)" + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS disk tool checks passed." diff --git a/Tests/expected/selfBootNoSystem.out b/Tests/expected/selfBootNoSystem.out index 3d5d6c0..d745e55 100644 --- a/Tests/expected/selfBootNoSystem.out +++ b/Tests/expected/selfBootNoSystem.out @@ -1,5 +1,6 @@ stage two no /System/Boot/cosmos.bin +trying the fallback nothing to start Execution halted. [exit 0] diff --git a/Tests/expected/wedgeFirst.out b/Tests/expected/wedgeFirst.out new file mode 100644 index 0000000..656ee0e --- /dev/null +++ b/Tests/expected/wedgeFirst.out @@ -0,0 +1,4 @@ +stage two +a system that never reaches a prompt +Execution halted. +[exit 0] diff --git a/Tests/expected/wedgeSecond.out b/Tests/expected/wedgeSecond.out new file mode 100644 index 0000000..a2e9ddc --- /dev/null +++ b/Tests/expected/wedgeSecond.out @@ -0,0 +1,8 @@ +stage two +the last start did not arrive +CosmOS +this is the fallback: what boot.cfg asks for did not start +> +halted +Execution halted. +[exit 0] diff --git a/Tests/expected/wedgeThird.out b/Tests/expected/wedgeThird.out new file mode 100644 index 0000000..7afc924 --- /dev/null +++ b/Tests/expected/wedgeThird.out @@ -0,0 +1,8 @@ +stage two +still on the fallback: settle it to try again +CosmOS +this is the fallback: what boot.cfg asks for did not start +> +halted +Execution halted. +[exit 0] diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 0f8c107..92f7cb0 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -446,3 +446,24 @@ printf 'system /System/Boot/bare.bin\n' > "$WORK/bare.cfg" printf 'fallback /System/Boot/cosmos.bin\n' } > "$WORK/fallback.cfg" "$TOOL" put "$DISKS/cfgfallback.img" "$WORK/fallback.cfg" /System/Boot/boot.cfg >/dev/null + +# ---- A system that never reaches a prompt ---- +# +# The case the boot state exists for. Pointing the configuration at something that dies +# before the shell is, without a mark on the disk, a machine that can never be told +# anything again - so the loader marks it before handing over and the system clears the +# mark on arrival, which makes "did not arrive" a thing the next start can see. +"$ROOT/Assembler" "$ROOT/Programs/Boot/wedged.asm" -o "$WORK/wedged.bin" >/dev/null +for stage in first second third; do + cp "$DISKS/selfboot.img" "$DISKS/wedge$stage.img" + "$TOOL" put "$DISKS/wedge$stage.img" "$WORK/wedged.bin" /System/Boot/wedged.bin >/dev/null + { + printf 'system /System/Boot/wedged.bin\n' + printf 'fallback /System/Boot/cosmos.bin\n' + } > "$WORK/wedge.cfg" + "$TOOL" put "$DISKS/wedge$stage.img" "$WORK/wedge.cfg" /System/Boot/boot.cfg >/dev/null +done +# Each disk begins where the one before it ended, so the three tests read as three +# consecutive starts of one machine without any of them depending on the others running. +"$TOOL" bootstate "$DISKS/wedgesecond.img" 1 >/dev/null +"$TOOL" bootstate "$DISKS/wedgethird.img" 2 >/dev/null diff --git a/Tests/manifest b/Tests/manifest index fae017c..76f1655 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -419,6 +419,20 @@ cfgBroken | Boot/stage1.asm | rom | - cfgFallback | Boot/stage1.asm | rom | selfBoot.in | 90000000 | disks/cfgfallback.img # And the bare image on its own, which has to keep assembling. bareMetal | Boot/bare.asm | assemble | - | - + +# ---- A system that never reaches a prompt ---- +# +# Three disks that differ only in the boot state on them, so the three tests read as three +# consecutive starts of one machine while none of them depends on another having run. +# +# wedgeFirst tries it and marks the disk. wedgeSecond finds the mark still set - nothing +# came back to say the last start arrived - and uses the fallback. wedgeThird finds that +# already happened and does the same again WITHOUT retrying, which is the part worth +# pinning down: a system known not to start should not be tried every other boot for ever. +wedgeFirst | Boot/stage1.asm | rom | - | 90000000 | disks/wedgefirst.img +wedgeSecond | Boot/stage1.asm | rom | - | 90000000 | disks/wedgesecond.img +wedgeThird | Boot/stage1.asm | rom | - | 90000000 | disks/wedgethird.img +wedgedImage | Boot/wedged.asm | assemble | - | - # Reading a disk that has directories on it. The machine can walk a path at this point but # cannot make a directory, so the disk is built by the host tool and read here - which is # the two implementations checking each other rather than either checking itself.