Start CosmOS with a script, and let a script hold its tongue
Three things scripts wanted, and they are one thing: a machine that can have a face. /System/Boot/startup.sh runs before anybody can type. Every way of reaching the prompt for the first time goes through it, including the one where there is no disk - in which case there is nothing to find and nothing is said. A MISSING one is ordinary and silent, because a clean install has none and a machine that complained every boot about a file nobody wrote would be teaching its owner to ignore it. One that is THERE and does not begin with #! is the other case entirely: somebody meant that to run. #quiet stops each line being echoed, #loud puts it back. The prompt and the echo go together, because together they are what makes a script look like typing, so a quiet script gets neither and what it prints is all that appears. A nested script inherits quiet - a build that asked for it meant its helpers too - and gets its own setting back when the helper returns. Anything else beginning with # is handed to the shell, which does not know it and stops the script, because a script that asked for something this shell cannot do should not carry on as though it had been given it. clear empties the screen, which the console has been able to do since before there was a screen to do it on. THE PROMPT IS NOW SAID BY WHOEVER SUPPLIES THE LINE. It used to be said at the top of the loop, which is a decision made before the line is read and an answer not known until after - and it was wrong at both ends. #quiet is itself a line, so its prompt went out before anything knew to stay silent; and the line after a quiet script's last one comes from the console, having already been denied one. Off by exactly one line in opposite directions. A first attempt at this remembered whether the prompt had been skipped, which worked and was a flag standing in for a structure. The monitor's assembler prints a prompt of its own, so it reads through shellReadRaw, which is the same source without one. One admission. Handing the console its prompt back when a quiet script ended was a real fix when I wrote it and stopped being one an hour later, because the restructure above means the console's own path prompts whatever the flag holds. The comment claimed it fixed something. Breaking it on purpose changed nothing, which is how that was found, and it is now a comment saying so instead of a line pretending to work. The startup fixture ends QUIET on purpose: nothing puts the flag back when the outermost script finishes, so a script ending #loud would have tested the easy half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
c28826df77
commit
553882d28d
@@ -95,7 +95,7 @@ boot:
|
||||
; 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
|
||||
BNQ bootStartup
|
||||
SETD.0 SbfsStateWas
|
||||
LDA.0
|
||||
INIB 0d1
|
||||
@@ -103,18 +103,18 @@ boot:
|
||||
BRQ bootArrived
|
||||
INIB 0d2
|
||||
XOR
|
||||
BNQ prompt
|
||||
BNQ bootStartup
|
||||
|
||||
; 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
|
||||
BRI bootStartup
|
||||
|
||||
bootArrived:
|
||||
RSTA
|
||||
CALL sbfsSetBootState
|
||||
BRI prompt
|
||||
BRI bootStartup
|
||||
bootNoDisk:
|
||||
RSTA
|
||||
STA.0
|
||||
@@ -122,6 +122,30 @@ bootNoDisk:
|
||||
CALL printString
|
||||
CALL newLine
|
||||
|
||||
; ---- Something to run before anybody types ----
|
||||
;
|
||||
; Every way of arriving at the prompt for the first time comes through here, which is the
|
||||
; point: a startup script should run whether the disk was marked, unmarked, or is not there
|
||||
; at all - and in the last case scriptOpen simply finds nothing.
|
||||
;
|
||||
; A MISSING ONE IS NOT A FAULT and says nothing, because a clean install has none and a
|
||||
; machine that complained at every boot about a file nobody wrote would be teaching its owner
|
||||
; to ignore it. A file that is THERE and is not a script is the other case entirely: somebody
|
||||
; meant it to run.
|
||||
bootStartup:
|
||||
SETD.0 StartupName
|
||||
CALL scriptOpen
|
||||
BRQ bootReady
|
||||
MVQA
|
||||
INIB 0x01
|
||||
CCF
|
||||
SUB
|
||||
BRQ bootReady ; There is none, which is ordinary.
|
||||
SETD.0 StartupNotOne
|
||||
CALL printString
|
||||
CALL newLine
|
||||
bootReady:
|
||||
|
||||
; ---- The loop ----
|
||||
;
|
||||
; The shell has two modes and one prompt that says which. Ordinary mode runs programs;
|
||||
@@ -154,31 +178,15 @@ prompt:
|
||||
CALL scriptAbandon
|
||||
|
||||
promptWhere:
|
||||
; Where you are, but only when that is not obvious. At the root the prompt is the one it
|
||||
; has always been, so a machine nobody has moved about on looks exactly as it did - and
|
||||
; every recorded test that never says "cd" keeps its recorded prompt.
|
||||
SETD.0 SbfsCwd
|
||||
LDA.0
|
||||
INCD.0
|
||||
LDB.0
|
||||
OR
|
||||
BRQ promptMode
|
||||
CALL shellPath
|
||||
SETD.0 CwdAt
|
||||
LDD.0.0
|
||||
CALL printString
|
||||
|
||||
promptMode:
|
||||
SETD.0 Mode
|
||||
LDA.0
|
||||
BRA promptPlain
|
||||
SETD.0 MonitorPrompt
|
||||
BRI promptSay
|
||||
promptPlain:
|
||||
SETD.0 PromptText
|
||||
promptSay:
|
||||
CALL printString
|
||||
|
||||
; ---- The prompt is said by whoever turns out to be supplying the line ----
|
||||
;
|
||||
; Not here, which is where it was and where it cannot be right. The decision has to be made
|
||||
; before the line is read and the answer is not known until after: a quiet script's #quiet
|
||||
; is itself a line, so the prompt for it went out before anybody knew to stay silent, and
|
||||
; the line AFTER a quiet script's last one comes from the console and had already been
|
||||
; denied its prompt. Both were off by exactly one line and in opposite directions.
|
||||
;
|
||||
; So shellReadLine says it. It is the one place that knows.
|
||||
SETD.0 CommandLine
|
||||
INIB 0d63
|
||||
CALL shellReadLine
|
||||
@@ -251,6 +259,11 @@ promptSay:
|
||||
CALL textSame
|
||||
BRQ doRename
|
||||
|
||||
SETD.0 CommandLine
|
||||
SETD.1 ClearName
|
||||
CALL textSame
|
||||
BRQ doClear
|
||||
|
||||
SETD.0 CommandLine
|
||||
SETD.1 EchoName
|
||||
CALL textSame
|
||||
@@ -313,6 +326,35 @@ promptSay:
|
||||
CALL textSame
|
||||
BRQ doAssemble
|
||||
|
||||
; Where you are, and then the prompt itself. A routine rather than a run of code in the loop,
|
||||
; because the loop is no longer the only thing that needs it - see shellReadTyped.
|
||||
sayPrompt:
|
||||
; Where you are, but only when that is not obvious. At the root the prompt is the one it
|
||||
; has always been, so a machine nobody has moved about on looks exactly as it did - and
|
||||
; every recorded test that never says "cd" keeps its recorded prompt.
|
||||
SETD.0 SbfsCwd
|
||||
LDA.0
|
||||
INCD.0
|
||||
LDB.0
|
||||
OR
|
||||
BRQ sayPromptMode
|
||||
CALL shellPath
|
||||
SETD.0 CwdAt
|
||||
LDD.0.0
|
||||
CALL printString
|
||||
|
||||
sayPromptMode:
|
||||
SETD.0 Mode
|
||||
LDA.0
|
||||
BRA sayPromptPlain
|
||||
SETD.0 MonitorPrompt
|
||||
BRI sayPromptSay
|
||||
sayPromptPlain:
|
||||
SETD.0 PromptText
|
||||
sayPromptSay:
|
||||
CALL printString
|
||||
RET
|
||||
|
||||
; ---- Where a line comes from ----
|
||||
;
|
||||
; The whole of what a script is. Everything below this - splitting the line, matching it
|
||||
@@ -329,13 +371,41 @@ shellReadLine:
|
||||
BNQ shellReadLine ; That one ended. Ask again: something under it may still be
|
||||
; running, and only depth reaching nought means the console.
|
||||
|
||||
; Echoed, so that a script working can be watched and a script failing says where. It is
|
||||
; printed after the prompt, so it reads exactly like somebody typing it.
|
||||
; The prompt and the echo are one thing - together they look like somebody typing - so a
|
||||
; quiet script gets neither, and everything else gets both in that order.
|
||||
SETD.1 ScriptQuiet
|
||||
LDA.1
|
||||
BNA shellReadQuiet
|
||||
CALL sayPrompt
|
||||
CALL printString
|
||||
CALL newLine
|
||||
shellReadQuiet:
|
||||
RET
|
||||
|
||||
shellReadTyped:
|
||||
CALL sayPrompt
|
||||
CALL readLine
|
||||
RET
|
||||
|
||||
; ---- The same, for a caller that prints a prompt of its own ----
|
||||
;
|
||||
; The monitor's assembler shows the address it is about to write to and then asks for a line,
|
||||
; so it must not also get the shell's. It still wants the line to come from the script when
|
||||
; there is one, which is what lets a script carry a block of assembly.
|
||||
shellReadRaw:
|
||||
SETD.1 ScriptDepth
|
||||
LDA.1
|
||||
BRA shellReadRawTyped
|
||||
CALL scriptLine
|
||||
BNQ shellReadRaw
|
||||
SETD.1 ScriptQuiet
|
||||
LDA.1
|
||||
BNA shellReadRawQuiet
|
||||
CALL printString
|
||||
CALL newLine
|
||||
shellReadRawQuiet:
|
||||
RET
|
||||
shellReadRawTyped:
|
||||
CALL readLine
|
||||
RET
|
||||
|
||||
@@ -1588,6 +1658,16 @@ fileComplain:
|
||||
CALL newLine
|
||||
BRI commandFailed
|
||||
|
||||
; ---- clear ----
|
||||
;
|
||||
; The console has done this since before there was a screen to do it on: writing 1 to the
|
||||
; command port. On a terminal it is what a terminal does with it, and on the Voyager it is
|
||||
; the screen going blank and the cursor going home.
|
||||
doClear:
|
||||
INIA 0x01
|
||||
OUTA 0x05
|
||||
BRI prompt
|
||||
|
||||
; ---- echo ----
|
||||
;
|
||||
; Says the rest of the line and nothing else. Say.sbx has done this since before there were
|
||||
@@ -3206,7 +3286,7 @@ assembleLine:
|
||||
|
||||
SETD.0 AsmLine
|
||||
INIB 0d40
|
||||
CALL shellReadLine
|
||||
CALL shellReadRaw
|
||||
|
||||
INA 0x01
|
||||
INIB 0x02 ; ENDED, so there is nothing more to assemble.
|
||||
@@ -3624,6 +3704,7 @@ PromptText:
|
||||
LineFailed:
|
||||
0x00
|
||||
|
||||
|
||||
LastStatus:
|
||||
0x00
|
||||
|
||||
@@ -3640,6 +3721,10 @@ ScriptNotOne:
|
||||
"do: that is not a script - it wants #! on the first line"
|
||||
ScriptTooDeep:
|
||||
"do: scripts are only four deep"
|
||||
StartupName:
|
||||
"/System/Boot/startup.sh"
|
||||
StartupNotOne:
|
||||
"startup.sh is there but does not begin with #!, so it was not run"
|
||||
ScriptStopped:
|
||||
"stopped: that line did not work"
|
||||
Unknown:
|
||||
@@ -3700,7 +3785,8 @@ mkdir <path> make a directory
|
||||
rmdir <path> remove an empty one"
|
||||
HelpScriptText:
|
||||
"do <file> run the lines in a file, which must start with #!
|
||||
echo [words] say them"
|
||||
echo [words] say them
|
||||
clear empty the screen"
|
||||
HelpMoreText:
|
||||
"delete <file> take it off the disk
|
||||
rename <file> <to> call it something else
|
||||
@@ -3828,6 +3914,8 @@ DoName:
|
||||
"do"
|
||||
EchoName:
|
||||
"echo"
|
||||
ClearName:
|
||||
"clear"
|
||||
LoadName:
|
||||
"load"
|
||||
RunName:
|
||||
|
||||
@@ -72,7 +72,7 @@ scriptSlotAt:
|
||||
DECA
|
||||
BRA scriptSlotDone
|
||||
scriptSlotStep:
|
||||
DPUP.3 0d70
|
||||
DPUP.3 0d71
|
||||
DECA
|
||||
BNA scriptSlotStep
|
||||
scriptSlotDone:
|
||||
@@ -80,7 +80,7 @@ scriptSlotDone:
|
||||
|
||||
; Seventy bytes, DP0 to DP1.
|
||||
scriptCopyState:
|
||||
INIB 0d70
|
||||
INIB 0d71
|
||||
scriptCopyByte:
|
||||
LDA.0
|
||||
STA.1
|
||||
@@ -136,8 +136,18 @@ scriptOpen:
|
||||
; the time the answer is known the caller's place is already gone.
|
||||
;
|
||||
; A still holds the depth from the check above: SUB writes Q and leaves it alone.
|
||||
BRA scriptOpenFirst
|
||||
BRA scriptOpenOutermost
|
||||
CALL scriptPush
|
||||
BRI scriptOpenFirst
|
||||
scriptOpenOutermost:
|
||||
; ---- The one place a stale quiet would matter ----
|
||||
;
|
||||
; A script started from the prompt begins loud whatever the last one left behind, which is
|
||||
; what makes this the only place the flag has to be put back. Nested scripts INHERIT
|
||||
; instead: a build that asked for quiet meant its helpers too.
|
||||
RSTA
|
||||
SETD.1 ScriptQuiet
|
||||
STA.1
|
||||
scriptOpenFirst:
|
||||
|
||||
SETD.1 ScriptName
|
||||
@@ -288,6 +298,45 @@ scriptLineDone:
|
||||
SUB
|
||||
BRQ scriptLineAgain
|
||||
|
||||
; ---- A directive, which is about the file rather than for the shell ----
|
||||
;
|
||||
; '#' the way the assembler means it. #quiet stops each line being echoed as it runs, for a
|
||||
; script whose own output is the point and which the prompts get in the way of; #loud puts
|
||||
; it back.
|
||||
;
|
||||
; ANYTHING ELSE BEGINNING WITH # IS HANDED TO THE SHELL, which does not know it and says
|
||||
; so and stops the script. That is deliberate and it is free: a script that asked for
|
||||
; something this shell cannot do should not carry on as though it had been given it, and
|
||||
; the machinery for saying so already exists.
|
||||
INIB 0d35 ; '#'
|
||||
CCF
|
||||
SUB
|
||||
BNQ scriptLineGive
|
||||
|
||||
SETD.1 QuietWord
|
||||
CALL textSame
|
||||
BRQ scriptLineQuiet
|
||||
SETD.0 ScriptInto
|
||||
LDD.0.0
|
||||
SETD.1 LoudWord
|
||||
CALL textSame
|
||||
BRQ scriptLineLoud
|
||||
SETD.0 ScriptInto
|
||||
LDD.0.0
|
||||
BRI scriptLineGive
|
||||
|
||||
scriptLineQuiet:
|
||||
INIA 0x01
|
||||
SETD.1 ScriptQuiet
|
||||
STA.1
|
||||
BRI scriptLineAgain
|
||||
scriptLineLoud:
|
||||
RSTA
|
||||
SETD.1 ScriptQuiet
|
||||
STA.1
|
||||
BRI scriptLineAgain
|
||||
|
||||
scriptLineGive:
|
||||
RSTA ; Q = 0: there is a line.
|
||||
RSTB
|
||||
CCF
|
||||
@@ -446,7 +495,17 @@ scriptClose:
|
||||
STA.1
|
||||
BRA scriptCloseNone
|
||||
CALL scriptPop
|
||||
RET
|
||||
|
||||
scriptCloseNone:
|
||||
; ---- Nothing to put back here ----
|
||||
;
|
||||
; The flag is only ever READ while a script is running: the console's own path says its
|
||||
; prompt whatever this holds, and the first script started from the prompt sets it to loud
|
||||
; on the way in. So a stale quiet cannot be observed, and clearing it here would be a line
|
||||
; no test could tell the difference about - which is how it was written the first time, with
|
||||
; a comment claiming it fixed something. It fixed something that a later change had already
|
||||
; made impossible.
|
||||
RET
|
||||
|
||||
; Every script ending at once, which is what a line that did not work means. A build whose
|
||||
@@ -459,13 +518,19 @@ scriptAbandon:
|
||||
|
||||
#Data
|
||||
|
||||
QuietWord:
|
||||
"#quiet"
|
||||
LoudWord:
|
||||
"#loud"
|
||||
|
||||
ScriptDepth:
|
||||
0x00
|
||||
|
||||
; ---- Seventy bytes, and they are next to each other on purpose ----
|
||||
;
|
||||
; Name, blocks left, next block, where in the block: the whole of where a script has got to.
|
||||
; Saving it is one copy because of this order, and nothing else may be put between them.
|
||||
; Name, blocks left, next block, where in the block, and whether it is echoing: the whole of
|
||||
; where a script has got to. Saving it is one copy because of this order, and nothing else
|
||||
; may be put between them.
|
||||
ScriptName:
|
||||
#Reserve 0d64
|
||||
ScriptBlocks:
|
||||
@@ -474,11 +539,17 @@ ScriptIndex:
|
||||
0x00 0x00
|
||||
ScriptAt:
|
||||
0x00 0x00
|
||||
; Saved with the rest, so that a quiet script calling a loud one gets its quiet back when
|
||||
; the loud one finishes. A new script INHERITS it rather than resetting, because a build
|
||||
; that asked for quiet meant its helpers too; only the first script started from the prompt
|
||||
; begins loud.
|
||||
ScriptQuiet:
|
||||
0x00
|
||||
|
||||
; Three would do - a save happens on the second script and not the first - but four costs
|
||||
; seventy bytes and removes an off-by-one from the only place it could hide.
|
||||
ScriptSaved:
|
||||
#Reserve 0d280
|
||||
#Reserve 0d284
|
||||
ScriptInto:
|
||||
0x00 0x00
|
||||
ScriptRoom:
|
||||
|
||||
Reference in New Issue
Block a user