InstallerImpl.mesa
last edited by Levin on December 7, 1983 9:22 pm
DIRECTORY
Ascii USING [BS, ControlA, ControlQ, ControlW, ControlX, CR, DEL, Digit, Letter, SP],
BasicTime USING [GMT, nullGMT, Period],
Booting USING [RegisterProcs, RollbackProc, switches],
DFOperations
USING [
BringOver, ChoiceInteraction, ChoiceResponse, Choices, DFInfoInteraction, FileInteraction, InfoInteraction, InteractionProc, YesNoInteraction, YesNoResponse],
DFUtilities USING [DateToRope],
FS USING [ComponentPositions, Copy, Error, ExpandName, FileInfo, StreamOpen],
GermSwap USING [Switch],
IO USING [
card, Close, EndOfStream, Error, GetChar, GetTime, GetTokenRope, EraseChar, IDProc, PutChar, PutF, PutFR, PutRope, rope, STREAM],
Process USING [SecondsToTicks],
Rope USING [Cat, Concat, Equal, Fetch, FromChar, Length, ROPE, SkipTo, Substr],
SimpleTerminal USING [InputTimeout, SetInputTimeout, TurnOff, TurnOn],
SystemVersion USING [release],
Terminal USING [BlinkBWDisplay, Current],
UserCredentials USING [Get],
UserProfile USING [ListOfTokens, ProfileChanged];
InstallerImpl:
CEDAR PROGRAM
IMPORTS
Ascii, BasicTime, Booting, DFOperations, DFUtilities, FS, IO, Process, Rope, SimpleTerminal, SystemVersion, Terminal, UserCredentials, UserProfile =
BEGIN
OPEN DFOps: DFOperations, DFUtils: DFUtilities;
ROPE: TYPE = Rope.ROPE;
in, out: IO.STREAM;
User interaction stuff
GetChoice:
PROC [c:
REF DFOps.ChoiceInteraction]
RETURNS [choice:
NAT] = {
DO
default: ROPE = c.choices[choice ← c.default];
ans: ROPE ← NIL;
firstChar: BOOL ← TRUE;
FlushVisible:
PROC = {
r: ROPE = IF firstChar THEN default ELSE ans;
FOR i:
INT DECREASING IN [0..r.Length[])
DO
out.EraseChar[r.Fetch[i]];
ENDLOOP;
ans ← NIL;
choice ← c.default;
firstChar ← FALSE;
};
THROUGH [0..2*depth) DO out.PutChar[Ascii.SP]; ENDLOOP;
out.PutRope[c.message]; out.PutChar[Ascii.SP];
out.PutRope[default];
DO
oldAns: ROPE = ans;
char: CHAR;
char ← in.GetChar[ ! SimpleTerminal.InputTimeout => {char ← Ascii.CR; CONTINUE}];
SELECT char
FROM
Ascii.DEL => {out.PutRope[" XXX\N"]; GO TO startOver};
Ascii.CR => IF ans.Length[] = 0 THEN GO TO done;
Ascii.
BS, Ascii.ControlA =>
IF firstChar THEN FlushVisible[]
ELSE
IF ans.Length[] > 0
THEN {
out.EraseChar[ans.Fetch[ans.Length[]-1]];
ans ← ans.Substr[len: ans.Length[]-1];
};
Ascii.ControlQ, Ascii.ControlW, Ascii.ControlX => FlushVisible[];
'? => {
out.PutRope[" XXX\NChoices are: "];
FOR i:
NAT IN [0..c.choices.length)
DO
IF i ~= 0 THEN out.PutRope[", "];
out.PutRope[c.choices[i]];
ENDLOOP;
out.PutChar[Ascii.CR];
IF c.explanations ~=
NIL THEN
FOR i:
NAT IN [0..c.explanations.length)
DO
IF c.explanations[i] ~=
NIL THEN {
out.PutRope[c.explanations[i]];
out.PutChar[Ascii.CR];
};
ENDLOOP;
GO TO startOver
};
ENDCASE => {
state: {notFound, found, ambiguous} ← notFound;
IF firstChar THEN FlushVisible[];
ans ← ans.Concat[Rope.FromChar[char]];
FOR i:
NAT IN [0..c.choices.length)
DO
IF ans.Equal[s2: c.choices[i].Substr[len: ans.Length[]], case:
FALSE]
THEN {
IF ans.Length[] = c.choices[i].Length[] THEN {choice ← i; state ← found; EXIT};
SELECT state
FROM
notFound => {choice ← i; state ← found};
found => state ← ambiguous;
ENDCASE;
};
ENDLOOP;
SELECT state
FROM
notFound => {
Terminal.BlinkBWDisplay[Terminal.Current[]];
ans ← oldAns;
};
found => {
out.PutRope[c.choices[choice].Substr[start: oldAns.Length[]]];
GO TO done
};
ambiguous => out.PutChar[c.choices[choice].Fetch[oldAns.Length[]]];
ENDCASE;
};
REPEAT
startOver => NULL;
ENDLOOP;
ENDLOOP;
out.PutChar[Ascii.CR];
};
Confirm:
PROC [message:
ROPE, default:
BOOL ←
TRUE]
RETURNS [
BOOL] = {
c:
REF DFOps.ChoiceInteraction =
NEW[DFOps.ChoiceInteraction ← [
message: message,
choices: yesNo,
explanations: NIL,
default: default.ORD
]];
OpenTerminal[];
RETURN[VAL[GetChoice[c]]]
};
autoConfirm: BOOL ← TRUE;
depth: INT ← -1;
Interact: DFOps.InteractionProc = {
WITH interaction
SELECT FROM
info:
REF DFOps.InfoInteraction => {
prompt: BOOL ← TRUE;
THROUGH [0..2*depth) DO out.PutChar[Ascii.SP]; ENDLOOP;
SELECT info.class
FROM
info => prompt ← FALSE;
warning => out.PutRope["Warning: "];
error => out.PutRope["Error: "];
abort => NULL;
ENDCASE;
out.PutRope[info.message];
out.PutChar[Ascii.CR];
IF prompt
THEN {
IF Confirm["Shall I continue anyway?"] THEN RETURN;
out.PutRope["Giving up..."];
Die[];
};
};
info:
REF DFOps.DFInfoInteraction => {
SELECT info.action
FROM
start => {
depth ← depth.SUCC;
THROUGH [0..2*depth) DO out.PutChar[Ascii.SP]; ENDLOOP;
out.PutRope["BringOver of "];
out.PutRope[info.dfFile];
};
end => {
THROUGH [0..2*depth) DO out.PutChar[Ascii.SP]; ENDLOOP;
out.PutRope["End BringOver of "];
out.PutRope[info.dfFile];
depth ← depth.PRED;
};
abort => {
out.PutRope["BringOver of "];
out.PutRope[info.dfFile];
out.PutRope[" aborted"];
depth ← -1;
};
ENDCASE;
IF info.message ~= NIL THEN out.PutF[" (%g)", IO.rope[info.message]];
out.PutChar[Ascii.CR];
};
c:
REF DFOps.ChoiceInteraction =>
IF c.blunder
OR ~autoConfirm
THEN
RETURN[response: NEW[DFOps.ChoiceResponse ← [GetChoice[c]]]];
yn:
REF DFOps.YesNoInteraction =>
IF yn.blunder
OR ~autoConfirm
THEN {
c:
REF DFOps.ChoiceInteraction =
NEW[DFOps.ChoiceInteraction ← [
message: yn.message,
choices: ynqa,
explanations: NIL,
default: IF yn.default THEN YNQA.yes.ORD ELSE YNQA.no.ORD
]];
choice: YNQA = VAL[GetChoice[c]];
SELECT choice
FROM
$yes => RETURN[response: NEW[DFOps.YesNoResponse ← [TRUE]]];
$no => RETURN[response: NEW[DFOps.YesNoResponse ← [FALSE]]];
$quit => {
abort ← TRUE;
abortMessageForLog ← "(requested by user)";
};
$all => {
autoConfirm ← TRUE;
RETURN[response: NEW[DFOps.YesNoResponse ← [TRUE]]]
};
ENDCASE;
};
file:
REF DFOps.FileInteraction =>
IF autoConfirm
THEN {
THROUGH [0..2*depth) DO out.PutChar[Ascii.SP]; ENDLOOP;
out.PutF["%g %g %g {%g}%g\N",
IO.rope[file.localFile],
IO.rope[
SELECT file.action
FROM
$fetch => "<--", $store => "-->", $check => "<-->", ENDCASE => NIL],
IO.rope[file.remoteFile],
IO.rope[DFUtils.DateToRope[[$explicit, file.date]]],
IO.rope[
IF file.dateFormat = $explicit THEN NIL
ELSE IO.PutFR[" ('%g')",
SELECT file.dateFormat
FROM
$greaterThan => [character['>]],
$notEqual => [rope["~="]],
ENDCASE => [null[]]
]
]
];
};
ENDCASE;
};
Rubout: ERROR = CODE;
GetID:
PROC [default:
ROPE, echo:
BOOL ←
TRUE]
RETURNS [id:
ROPE] = {
OPEN Ascii;
firstTime: BOOL ← TRUE;
c: CHAR;
EraseAll:
PROC = {
IF echo
THEN
FOR i: INT DECREASING IN [0..id.Length[]) DO out.EraseChar[id.Fetch[i]]; ENDLOOP;
id ← NIL;
};
Done:
PROC [c:
CHAR]
RETURNS [
BOOL] =
INLINE {
IF firstTime
THEN {
SELECT c
FROM
ControlA, BS, ControlQ, ControlW, ControlX, CR, SP => NULL;
ENDCASE => EraseAll[];
firstTime ← FALSE;
};
RETURN[c = SP OR c = CR]
};
id ← default;
IF echo THEN out.PutRope[default];
c ← in.GetChar[ ! SimpleTerminal.InputTimeout => --RETURN--GO TO defaultReturn];
UNTIL Done[c]
DO
SELECT c
FROM
DEL => {out.PutRope[" XXX\N"]; ERROR Rubout};
ControlA,
BS => {
len: INT ← id.Length[];
IF len > 0
THEN {
len ← len - 1;
IF echo THEN out.EraseChar[id.Fetch[len]];
id ← id.Substr[len: len];
};
};
ControlW, ControlQ => {
text to be backed up is of the form ...<non-alpha><alpha><non-alpha>, the <alpha> and following <non-alpha> are to be removed.
alpha: BOOL ← FALSE;
FOR i:
INT DECREASING IN [0..id.Length[])
DO
ch: CHAR = id.Fetch[i];
IF Ascii.Letter[ch] OR Ascii.Digit[ch] THEN alpha ← TRUE
ELSE IF alpha THEN {id ← id.Substr[len: i + 1]; EXIT};
IF echo THEN out.EraseChar[ch];
REPEAT
FINISHED => id ← NIL;
ENDLOOP;
};
ControlX => EraseAll[];
ENDCASE => {id ← id.Concat[Rope.FromChar[c]]; IF echo THEN out.PutChar[c]};
c ← in.GetChar[ ! SimpleTerminal.InputTimeout => RESUME];
ENDLOOP;
EXITS
defaultReturn => NULL;
};
Installation Stamp
The format of the installation stamp file is as follows:
Essentials: name and date of (remote) boot essentials file used <CR>
?(Profile: name and date of (remote) user profile used <CR>)
(UserDF: name and date of (remote) user-specified DF and date <CR>) ...
FileDesc: TYPE = RECORD [name: ROPE ← NIL, date: BasicTime.GMT ← BasicTime.nullGMT];
DFList: TYPE = REF DFListObject;
DFListObject: TYPE = RECORD [head: LIST OF FileDesc ← NIL, tail: LIST OF FileDesc ← NIL];
systemID:
ROPE =
IO.PutFR["%g.%g.%g",
IO.card[SystemVersion.release.major],
IO.card[SystemVersion.release.minor],
IO.card[SystemVersion.release.patch]
];
stamp: FileDesc ← [];
longDialogue: BOOL ← Booting.switches[l];
development: BOOL ← Booting.switches[d];
haveEssentials: BOOL ← FALSE;
haveProfile: BOOL ← FALSE;
essentials: FileDesc ← [];
profile: FileDesc ← [];
profileDFList: DFList = NEW[DFListObject ← []];
newProfileDFList: DFList = NEW[DFListObject ← []];
ParseInstallationStamp:
PROC = {
ForgetStamp[];
stamp ← TryFile[Rope.Concat["InstallationStamp.", systemID]].desc;
IF stamp.date ~= BasicTime.nullGMT
THEN {
in: IO.STREAM = FS.StreamOpen[stamp.name ! FS.Error => GO TO noStamp];
GetFileDesc:
PROC RETURNS [desc: FileDesc] = {
desc.name ← StripVersion[in.GetTokenRope[IO.IDProc].token];
desc.date ← in.GetTime[];
};
DO
ENABLE {
IO.EndOfStream, IO.Error => GO TO parsingProblem;
FS.Error => IF error.group = $user THEN GO TO parsingProblem ELSE REJECT;
};
itemName: ROPE = in.GetTokenRope[IO.IDProc ! IO.EndOfStream => EXIT].token;
SELECT TRUE FROM
itemName.Equal["Essentials"] =>
IF essentials.name = NIL THEN essentials ← GetFileDesc[]
ELSE GO TO parsingProblem;
itemName.Equal["Profile"] =>
IF profile.name = NIL THEN profile ← GetFileDesc[]
ELSE GO TO parsingProblem;
itemName.Equal["UserDF"] => AddToDFList[profileDFList, GetFileDesc[]];
ENDCASE => GO TO parsingProblem;
REPEAT
parsingProblem => {in.Close[]; GO TO noStamp};
ENDLOOP;
in.Close[];
};
EXITS
noStamp => ForgetStamp[];
};
WriteStamp:
PROC = {
out:
IO.
STREAM =
FS.StreamOpen[StripVersion[stamp.name], $create
! FS.Error => GO TO noStamp];
PutFileDesc:
PROC [head:
ROPE, desc: FileDesc] = {
out.PutF["%g: %g %g\N",
IO.rope[head],
IO.rope[desc.name],
IO.rope[DFUtils.DateToRope[[$explicit, desc.date]]]
];
};
Note: If the stamp was originally missing or invalid and the user explicitly prohibited retrieval of boot essentials, 'essentials' will be garbage. In this case, we don't write a new "Essentials" entry, and the user will be hassled the next time he boots.
IF essentials.name ~=
NIL AND essentials.date ~= BasicTime.nullGMT
THEN
PutFileDesc["Essentials", essentials];
IF profile.name ~=
NIL AND profile.date ~= BasicTime.nullGMT
THEN
PutFileDesc["Profile", profile];
FOR l:
LIST OF FileDesc ← newProfileDFList.head, l.rest
UNTIL l =
NIL DO
IF l.first.date ~= BasicTime.nullGMT THEN PutFileDesc["UserDF", l.first];
ENDLOOP;
out.Close[];
stamp ← [];
};
ForgetStamp:
PROC = {
essentials ← profile ← [];
profileDFList^ ← [];
};
FindBootEssentials:
PROC = {
Upon return, if 'haveEssentials' is TRUE, the contents of 'essentials' are unpredictable. If 'haveEssentials' is FALSE, however, 'essentials' describes an extant file to which BringOver is to be applied.
DO
Loops only if 'development' changes from FALSE to TRUE in the middle.
IF longDialogue
OR development
THEN
haveEssentials ←
Confirm["Shall I assume the local files essential for booting are current?", FALSE];
IF ~haveEssentials
THEN {
name: ROPE = Rope.Cat["BootEssentials.", systemID, ".df"];
path: ROPE = Rope.Cat["[Indigo]<", IF development THEN "PreCedar" ELSE "Cedar", ">Top>"];
desc: FileDesc ← TryFile[name, path].desc;
SELECT TRUE FROM
desc.date ~= BasicTime.nullGMT =>
haveEssentials ← essentials.date = desc.date
AND
essentials.name.Equal[StripVersion[desc.name], FALSE] AND
(~(longDialogue
OR development)
OR ~Confirm[
desc.name.Concat[
" was brought over the last time; shall I do it again anyway?"],
TRUE
]);
~development
AND
(development ← Confirm["Did you mean to boot with the 'D' switch?"]) => {
Booting.switches[d] ← TRUE;
out.PutRope["(D-switch now set.)\N"];
LOOP
};
ENDCASE =>
WHILE Confirm["Do you want to specify a DF file for booting essentials?"]
DO
fileName: ROPE = FS.ExpandName[name, Rope.Cat["[Ivy]<", user, ">"]].fullFName;
out.PutRope["DF file for booting essentials: "];
desc ← TryFile[GetID[fileName ! Rubout => LOOP]].desc;
IF desc.date = BasicTime.nullGMT THEN out.PutRope["...not found\N"]
ELSE {out.PutChar[Ascii.CR]; EXIT};
REPEAT
FINISHED => {
out.PutRope["The files essential for booting can't be found. I give up.\N"];
Die[];
};
ENDLOOP;
essentials ← desc;
};
EXIT
ENDLOOP;
};
FindUserProfile:
PROC = {
Upon return, if 'haveProfile' is TRUE, no profile change notification is required. If 'haveProfile' is FALSE, however, any necessary file retrieval has occurred and, if the fields of 'profile' do not have their default values, they describe a remote file to be included in the stamp. Profile change notification should occur after the BringOver of the boot essentials is completed, so that the system default profile can be mentioned therein and noticed at the proper time.
localDesc: FileDesc;
attachedTo: ROPE;
[localDesc, attachedTo] ← TryFile[localProfile];
SELECT TRUE FROM
localDesc.date = BasicTime.nullGMT => {
There is no personal profile on the local disk. The user gets the opportunity to specify one; if he chooses not to, the system default will be used (we assume the files essential for booting include a default profile).
installIfTimeout: BOOL ← TRUE;
WHILE Confirm["Do you wish to install a personal profile?", installIfTimeout]
DO
Loops until remote file is located or user decides against using one.
profileName:
ROPE ←
IF profile.name ~= NIL THEN profile.name
ELSE Rope.Cat["[Ivy]<", user, ">", localProfile];
remote: FileDesc ← [];
out.PutRope[" Personal profile name: "];
remote ← TryFile[GetID[profileName ! Rubout => LOOP]].desc;
IF remote.date = BasicTime.nullGMT
THEN {
out.PutRope["...not found\N"];
installIfTimeout ← ~remote.name.Equal[profileName];
}
ELSE {
out.PutChar[Ascii.CR];
profile ← remote;
FS.Copy[from: profile.name, to: localProfile, keep: 2, attach: TRUE];
EXIT
};
ENDLOOP;
};
profile.name =
NIL => {
There is a local profile on the disk, but either there was no installation stamp or it failed to mention a profile.
haveProfile ← TRUE; -- assume the local disk is good enough.
IF attachedTo ~= NIL THEN profile ← [attachedTo, localDesc.date];
};
ENDCASE => {
Both a local profile and a stamp entry for a remote profile exist.
remote: FileDesc ← TryFile[StripVersion[profile.name]].desc;
IF ~(haveProfile ← profile.date = remote.date)
THEN
IF attachedTo ~=
NIL AND attachedTo.Equal[remote.name,
FALSE]
THEN {
haveProfile ← TRUE;
profile ← [attachedTo, localDesc.date];
}
ELSE IF remote.date ~= BasicTime.nullGMT
THEN {
msg:
ROPE =
IO.PutFR["Shall I install %g {%g} as your profile?",
IO.rope[remote.name],
IO.rope[DFUtils.DateToRope[[$explicit, remote.date]]]
];
IF Confirm[msg, BasicTime.Period[from: localDesc.date, to: remote.date] > 0]
THEN {
FS.Copy[from: remote.name, to: localProfile, keep: 2, attach: TRUE];
profile ← remote;
};
};
};
};
BringOverUserDFs:
PROC = {
dfNames: LIST OF ROPE ← UserProfile.ListOfTokens["Installation.BringOver"];
IF longDialogue
THEN
SELECT TRUE FROM
dfNames ~= NIL AND
Confirm["Shall I ignore the profile's list of DF files to bring over?",
FALSE] =>
dfNames ← NIL;
profileDFList.head ~= NIL AND
Confirm["Shall I ignore DF files previously brought over?",
FALSE] =>
profileDFList^ ← [];
ENDCASE;
newProfileDFList^ ← [];
FOR dfList:
LIST OF ROPE ← dfNames, dfList.rest
UNTIL dfList =
NIL DO
desc: FileDesc = TryFile[dfList.first].desc;
IF InDFList[profileDFList, desc]
THEN {
out.PutF["%g {%g} previously brought over.\N",
IO.rope[desc.name],
IO.rope[DFUtils.DateToRope[[$explicit, desc.date]]]
];
AddToDFList[newProfileDFList, desc];
}
ELSE {
e, w: INT;
CheckAutoConfirm[desc.name];
[errors: e, warnings: w] ←
DFOps.BringOver[dfFile: desc.name, filter: [filterB: $public], interact: Interact];
IF e + w = 0 THEN AddToDFList[newProfileDFList, desc];
};
ENDLOOP;
};
InDFList:
PROC [list: DFList, desc: FileDesc]
RETURNS [
BOOL ←
FALSE] = {
FOR l:
LIST OF FileDesc ← list.head, l.rest
UNTIL l =
NIL DO
IF l.first.date = desc.date
AND
StripVersion[desc.name].Equal[StripVersion[l.first.name], FALSE] THEN RETURN[TRUE];
ENDLOOP;
};
AddToDFList:
PROC [list: DFList, desc: FileDesc] = {
descL: LIST OF FileDesc = CONS[desc, NIL];
IF list.head = NIL THEN list.head ← descL ELSE list.tail.rest ← descL;
list.tail ← descL;
};
Miscellaneous
installedUser: ROPE;
user: ROPE;
localProfile: ROPE;
InitUserRelatedInfo:
PROC = {
installedUser ← UserCredentials.Get[].name;
user ← installedUser.Substr[len: installedUser.SkipTo[skip: "."]];
localProfile ← user.Concat[".profile"];
};
OpenTerminal:
PROC = {
IF in ~= NIL THEN RETURN;
[in, out] ← SimpleTerminal.TurnOn[];
out.PutChar[Ascii.CR];
};
CloseTerminal:
PROC = {
IF in = NIL THEN RETURN;
SimpleTerminal.TurnOff[];
in ← out ← NIL;
};
TryFile:
PROC [shortName, prefix:
ROPE ←
NIL]
RETURNS [desc: FileDesc ← [], attachedTo: ROPE ← NIL] = {
desc.name ← FS.ExpandName[name: shortName, wDir: prefix].fullFName;
[fullFName: desc.name, attachedTo: attachedTo, created: desc.date] ←
FS.FileInfo[name: desc.name ! FS.Error => CONTINUE];
};
StripVersion:
PROC [old:
ROPE]
RETURNS [new:
ROPE] = {
cp: FS.ComponentPositions;
[fullFName: new, cp: cp] ← FS.ExpandName[old];
new ← new.Substr[len: cp.ext.start+cp.ext.length];
};
CheckAutoConfirm:
PROC [df:
ROPE] = {
IF (longDialogue
OR development)
AND autoConfirm
THEN {
msg:
ROPE =
Rope.Cat["Do you wish to confirm retrieval of each file from ", df, " individually?"];
autoConfirm ← ~Confirm[msg, FALSE];
};
};
Die: PROC = {DO ENDLOOP};
Main body
NewUser: Booting.RollbackProc = {
IF ~UserCredentials.Get[].name.Equal[installedUser, FALSE] THEN DoRealWork[];
};
DoRealWork:
PROC = {
haveEssentials ← haveProfile ← FALSE;
IF longDialogue
AND ~Confirm["Long installation dialogue [confirm] "]
THEN {
out.PutRope["(L-switch now cleared.)\N"];
longDialogue ← Booting.switches[l] ← FALSE;
};
IF ~longDialogue THEN SimpleTerminal.SetInputTimeout[Process.SecondsToTicks[30]];
InitUserRelatedInfo[];
ParseInstallationStamp[];
FindBootEssentials[];
FindUserProfile[];
IF ~haveEssentials
THEN {
CheckAutoConfirm[essentials.name];
[] ← DFOps.BringOver[dfFile: essentials.name, interact: Interact, action: fetch];
};
IF ~haveProfile THEN UserProfile.ProfileChanged[rollBack];
BringOverUserDFs[];
WriteStamp[];
SimpleTerminal.SetInputTimeout[0];
CloseTerminal[];
};
YNQA: TYPE = {yes, no, quit, all};
ynqa: REF DFOps.Choices = NEW[DFOps.Choices[YNQA.LAST.ORD.SUCC - YNQA.FIRST.ORD]];
yesNo: REF DFOps.Choices = NEW[DFOps.Choices[BOOL.LAST.ORD.SUCC - BOOL.FIRST.ORD]];
yesNo[BOOL.TRUE.ORD] ← "Yes";
yesNo[BOOL.FALSE.ORD] ← "No";
ynqa[YNQA.yes.ORD] ← "Yes";
ynqa[YNQA.no.ORD] ← "No";
ynqa[YNQA.quit.ORD] ← "Quit";
ynqa[YNQA.all.ORD] ← "All";
IF Booting.switches ~= []
THEN {
OpenTerminal[];
out.PutRope["\NBoot switches:"];
FOR sw: GermSwap.Switch
IN GermSwap.Switch
DO
IF Booting.switches[sw]
THEN {
out.PutChar[Ascii.SP];
out.PutChar[VAL[(IF sw <= nine THEN '0 ELSE 'A-10)+sw.ORD]];
};
ENDLOOP;
out.PutChar[Ascii.CR];
};
DoRealWork[
!
FS.Error => {
OpenTerminal[];
out.PutF["\NFatal FS Error: %g\N", IO.rope[error.explanation]];
Die[];
}
];
installedUser ← UserCredentials.Get[].name;
Booting.RegisterProcs[r: NewUser];
END.