-- File: WalnutNotifierImpl.mesa
-- Contents: Notifier & restart code
-- created July, 1983 by Willie-Sue
-- Last edit by:
-- Willie-Sue on: December 20, 1983 1:46 pm
DIRECTORY
AlpineFS USING [ErrorFromStream],
Booting USING [RegisterProcs, CheckpointProc, RollbackProc],
BasicTime USING [OutOfRange, GMT],
Convert USING [RopeFromTime],
DB USING [Aborted, Error, Failure, InternalError,
AbortTransaction, CloseTransaction, DeclareSegment, TransactionOf],
FS USING [Error, ErrorDesc, ErrorFromStream],
IO,
Labels USING [Set],
WQueue USING [Action, DequeueAction, Flush, QueueClientAction],
Process USING [Detach],
Rope,
UserCredentials USING [Get],
UserProfile USING [Boolean, CallWhenProfileChanges, Number, ProfileChangedProc],
ViewerClasses USING [Viewer],
ViewerLocks USING [CallUnderWriteLock],
ViewerSpecs USING [openRightTopY],
ViewerOps,
WalnutControlMonitorImpl,
WalnutControlPrivate USING [doingCheckpoint, forceQuitMenu, lastStateReported, mailDBMenu,
maybeQuitMenu, mustQuitWalnut, nonMailDBMenu, previousUser,
readOnlyDBMenu, rollbackFinished, scavMenu, segmentName,
CloseDownWalnut, FixupCreateLine, InternalConfirm],
WalnutDB USING [activeMsgSet, NumInMsgSet],
WalnutDBLog USING [SchemaMismatch, SchemaVersionTime,
GetCopyInProgress, GetCurrentLogFile, GetExpectedDBLogPos,
GetExpectedLogLength, GetStartExpungePos, SetStartExpungePos],
WalnutMsgOps USING [BuildListOfMsgsViewer, FixUpMsgSetViewer, FixUpMsgViewer],
WalnutLog USING [AbortLogTransaction, CloseLogStream, CloseWalnutTransaction,
FinishExpunge, InitializeLog, LogLength, MarkWalnutTransaction,
OpenWalnutTransaction, UpdateFromLog],
WalnutExtras USING [ ChangeWalnutMenu, ClearMsgSetDisplayers, DoScavenge,
EnumWalnutViewers, NotifyIfAppropriate, TakeDownWalnutViewers],
WalnutRetrieve USING [CloseConnection, OpenConnection],
WalnutSendOps USING [userRName],
WalnutWindow USING [enableTailRewrite, excessBytesInLogFile, initialActiveIconic,
initialActiveOpen, initialActiveRight, logIsAlpineFile, mailNotifyLabel,
msgSetBorders, personalMailDB, readOnlyAccess, walnut, walnutLogName,
walnutMenu, walnutQueue, walnutRulerAfter, walnutSegmentFile,
workingMenu,
DestroyAllMsgSetButtons, DisplayMsgSet, Report, ReportRope,
ShowMsgSetButtons];
WalnutNotifierImpl: CEDAR MONITOR LOCKS walnutControlLock
IMPORTS
walnutControlLock: WalnutControlMonitorImpl,
BasicTime, Convert, AlpineFS, DB, FS,
Labels, WQueue,
Booting, IO, Process, Rope,
UserCredentials, UserProfile, ViewerLocks, ViewerOps,
WalnutControlPrivate, WalnutDB, WalnutDBLog, WalnutMsgOps, WalnutExtras,
WalnutLog, WalnutRetrieve, WalnutSendOps, WalnutWindow
EXPORTS WalnutControlPrivate, WalnutWindow
SHARES WalnutControlMonitorImpl, WalnutWindow =
BEGIN OPEN WalnutControlPrivate, WalnutExtras, WalnutWindow;
-- Walnut Viewers types and global data
ROPE: TYPE = Rope.ROPE;
Viewer: TYPE = ViewerClasses.Viewer;
failureRope: ROPE = "Failure: what: %g; info: %g";
abortRope: ROPE = " transaction was aborted ... restarting\n";
scavengeMsg: ROPE = "Click Scavenge or Quit";
forceQuitRope: ROPE = "You must quit out of Walnut; Click Quit when ready";
WaitForGVLabel: ROPE = "Waiting for Grapevine response...";
NoMailLabel: ROPE = "Cannot retrieve mail using this database";
ROAccessLabel: ROPE = "You only have Read access to this database";
LogNotOpen: SIGNAL = CODE;
-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
WalnutNotifier: PUBLIC PROC =
BEGIN OPEN WQueue, IO;
DO
BEGIN ENABLE
BEGIN
ABORTED =>
{ WQueue.Flush[walnutQueue, CheckForNotify];
ChangeWalnutMenu[walnutMenu];
LOOP
};
-- IO.Error replaces FileIOAlpine.Aborted & FileIOAlpine.Failure
DB.Aborted => { Report[" \nDatabase", abortRope]; GOTO aborted};
DB.Failure =>
{ Report["\nDB.", PutFR[failureRope, atom[what], rope[info]]]; GOTO failure};
IO.Error => IF ec = Failure THEN
{ ed: FS.ErrorDesc← GetErrorDesc[stream];
Report[ed.explanation];
IF ed.code = $transAborted THEN GOTO aborted ELSE GOTO failure;
}
ELSE --is this the right hing to do here???
{ WQueue.Flush[walnutQueue, CheckForNotify];
ChangeWalnutMenu[walnutMenu];
LOOP
};
END;
action: Action← DequeueAction[walnutQueue];
WITH action SELECT FROM
e1: Action.user =>
e1.proc[e1.parent, e1.clientData, e1.mouseButton, e1.shift, e1.control];
e2: Action.client =>
{ IF e2.proc = ClosingWalnut THEN
{ WQueue.Flush[walnutQueue, CheckForNotify]; RETURN};
e2.proc[e2.data];
};
ENDCASE => ERROR;
EXITS
aborted =>
{ ViewerOps.BlinkIcon[walnut];
WQueue.Flush[walnutQueue, CheckForNotify];
IF DB.TransactionOf[segmentName] # NIL THEN
DB.AbortTransaction[DB.TransactionOf[segmentName] !
DB.Failure, IO.Error => CONTINUE];
RestartWalnut[];
};
failure => FailureExit[];
END;
ENDLOOP;
END;
GetErrorDesc: PROC[stream: IO.STREAM] RETURNS[FS.ErrorDesc] =
BEGIN
SELECT IO.GetInfo[stream].class FROM
$AlpineFS => RETURN[AlpineFS.ErrorFromStream[stream]];
$FS => RETURN[FS.ErrorFromStream[stream]];
ENDCASE => ERROR;
END;
FailureExit: ENTRY PROC =
BEGIN ENABLE UNWIND => NULL;
ViewerOps.BlinkIcon[walnut];
DB.AbortTransaction[DB.TransactionOf[segmentName] ! DB.Failure, IO.Error => CONTINUE];
Report["Click Quit when ready for Walnut to quit"];
[]← InternalConfirm[forceQuitMenu];
CloseDownWalnut[TRUE];
END;
-- CheckForNotify can't be an ENTRY proc
CheckForNotify: PROC[action: WQueue.Action] =
BEGIN OPEN WQueue;
WITH action SELECT FROM
e1: Action.user => NULL;
e2: Action.client => IF e2.proc # ClosingWalnut THEN NotifyIfAppropriate[e2.data];
ENDCASE => ERROR;
END;
-- for WalnutWindowImpl to call
FlushWQueue: PUBLIC PROC = { WQueue.Flush[walnutQueue, CheckForNotify] };
-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
AbandonStartup: INTERNAL PROC[s: ROPE← NIL] RETURNS[BOOL] =
BEGIN
IF s # NIL THEN {ReportRope[s]; Report[" you only have Read access"]};
Report[forceQuitRope];
[]← InternalConfirm[forceQuitMenu];
CloseDownWalnut[FALSE];
RETURN[FALSE]
END;
RestartWalnut: PUBLIC ENTRY PROC =
-- StartOrRestartWalnut will deal with DB.Failure, IO.Error
BEGIN ENABLE UNWIND => NULL;
BEGIN
CloseTransactions[ TRUE ! DB.Failure, IO.Error => GOTO failure];
EXITS
failure => IF DB.TransactionOf[segmentName] # NIL THEN
DB.AbortTransaction[DB.TransactionOf[segmentName ! IO.Error, ABORTED => CONTINUE]];
END;
IF ~StartOrRestartWalnut[FALSE] THEN RETURN;
Report["Restart finished"];
END;
-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
CloseTransactions: PUBLIC INTERNAL PROC[doCommit: BOOL] =
BEGIN
BEGIN
ENABLE BEGIN
DB.Aborted, UNWIND => GOTO dbAborted;
IO.Error => GOTO ioErr;
END;
IF doCommit THEN WalnutLog.CloseWalnutTransaction[] -- can cause FileIOAlpine.Aborted
ELSE DB.AbortTransaction[DB.TransactionOf[segmentName]];
EXITS
dbAborted => DB.AbortTransaction[DB.TransactionOf[segmentName]];
ioErr =>
{ DB.AbortTransaction[DB.TransactionOf[segmentName]];
WalnutLog.AbortLogTransaction[ ! IO.Error => CONTINUE];
};
END;
-- db transaction is either Closed or Aborted, now for the log
BEGIN
ENABLE IO.Error, UNWIND => GOTO ioError;
WalnutLog.CloseLogStream[]; -- can cause FileIOAlpine.Aborted
EXITS
ioError => WalnutLog.AbortLogTransaction[ ! IO.Error, UNWIND => CONTINUE];
END;
END;
StartOrRestartWalnut:
PUBLIC INTERNAL PROC[firstTime: BOOL← FALSE, scavengeFirst: BOOL← FALSE]
RETURNS[BOOL] =
BEGIN OPEN WalnutLog, WalnutDBLog;
DO
BEGIN
ENABLE
BEGIN
UNWIND => GOTO mustQuit;
DB.Aborted => GOTO mustQuit;
DB.Failure =>
{ Report["\nDB.", IO.PutFR[failureRope, IO.atom[what], IO.rope[info]]]; GOTO mustQuit};
IO.Error => IF ec = Failure THEN
{ ed: FS.ErrorDesc← GetErrorDesc[stream];
Report[ed.explanation];
GOTO mustQuit
} ELSE GOTO mustQuit;
END;
transOK: BOOL← TRUE;
curVersion: BasicTime.GMT;
curLength, expectedLength: INT;
openTries: INTEGER← 0;
logFromDB: ROPE;
wasReadOnly: BOOL← readOnlyAccess;
logFileNotFound, noDB: BOOL← FALSE;
notFoundRope: ROPE = " not found and can't be created";
didScavenge: BOOL← FALSE;
readOnlyAccess← FALSE;
-- if log trans failed, segment file may be open
IF DB.TransactionOf[segmentName] # NIL THEN CloseTransactions[FALSE];
DB.DeclareSegment[filePath: walnutSegmentFile, segment: segmentName];
-- database may be hopelessly mangled from an earlier scavenge attempt
IF scavengeFirst THEN
{ Report["Doing scavenge"];
BEGIN
DoScavenge[startPos: 0 ! DB.Error => IF code = ProtectionViolation THEN GOTO pvError];
didScavenge← TRUE;
EXITS
pvError => { Report["Protection violation, aborting"]; RETURN[FALSE]};
END;
Report[" ...done"]
};
OpenWalnutTransaction[segmentName, NIL, FALSE ! -- does InitializeDBVars
DB.InternalError => {transOK← FALSE; CONTINUE};
WalnutDBLog.SchemaMismatch =>
{curVersion← schemaVersion.time; transOK← FALSE; CONTINUE};
DB.Aborted => {IF (openTries← openTries + 1) < 3 THEN RETRY ELSE REJECT};
DB.Error => IF code = ProtectionViolation THEN {readOnlyAccess← TRUE; CONTINUE}
ELSE IF code = FileNotFound THEN {noDB← TRUE; CONTINUE } ELSE REJECT;
];
IF noDB THEN
{ Report[" Database file ", walnutSegmentFile, notFoundRope];
RETURN[AbandonStartup[]]
};
IF readOnlyAccess THEN
{ DB.CloseTransaction[DB.TransactionOf[segmentName]]; -- ugh
DB.DeclareSegment[filePath: walnutSegmentFile, segment: segmentName,
readonly: TRUE];
OpenWalnutTransaction[segmentName, NIL, FALSE ! -- does InitializeDBVars
DB.InternalError => {transOK← FALSE; CONTINUE};
WalnutDBLog.SchemaMismatch =>
{curVersion← schemaVersion.time; transOK← FALSE; CONTINUE};
DB.Aborted => {IF (openTries← openTries + 1) < 3 THEN RETRY ELSE REJECT};
DB.Error =>
{ IF code = ProtectionViolation OR code = FileNotFound THEN
{noDB← TRUE; CONTINUE }
ELSE REJECT
};
];
walnutMenu← readOnlyDBMenu;
}
ELSE walnutMenu← IF personalMailDB THEN mailDBMenu ELSE nonMailDBMenu;
IF noDB THEN
{ Report[" ReadOnly Database ", walnutSegmentFile, notFoundRope];
RETURN[AbandonStartup[]]
};
IF wasReadOnly # readOnlyAccess THEN FixupCreateLine[];
IF ~transOK THEN
{ xx: ROPE;
xx← Convert.RopeFromTime[curVersion ! BasicTime.OutOfRange => CONTINUE];
IF xx # NIL THEN Report[
IO.PutFR["\nDatabase schema is of %g, but Walnut wants it to be %g",
IO.rope[xx], IO.time[WalnutDBLog.SchemaVersionTime]]]
ELSE Report["Database has wrong time format"];
IF readOnlyAccess THEN RETURN[AbandonStartup[]];
Report[scavengeMsg];
IF ~InternalConfirm[scavMenu] THEN { CloseDownWalnut[FALSE]; RETURN[FALSE]};
DoScavenge[startPos: 0];
scavengeFirst← FALSE;
didScavenge← TRUE;
};
logFromDB← GetCurrentLogFile[];
IF logFromDB.Length[] # 0 AND ~personalMailDB THEN walnutLogName← logFromDB;
BEGIN
curLength← InitializeLog[walnutLogName ! FS.Error =>
{IF error.group # user THEN {Report[error.explanation]; GOTO cantOpen};
IF error.code = $unKnownFile THEN
{logFileNotFound← TRUE; CONTINUE} ELSE REJECT;
}];
EXITS
cantOpen => RETURN[AbandonStartup[]];
END;
IF curLength < 0 THEN
{ SIGNAL LogNotOpen; RETURN[AbandonStartup[]]};
IF logFileNotFound AND readOnlyAccess THEN
{ Report["Log file ", walnutLogName, notFoundRope];
RETURN[AbandonStartup[]]
};
IF GetCopyInProgress[] THEN
{ IF readOnlyAccess THEN
RETURN[AbandonStartup["This database needs to finish a copyInProgress"]];
ReportRope[" Recovering from interrupted Expunge ..."];
WalnutLog.FinishExpunge[];
Report[" done"];
curLength← LogLength[FALSE];
};
expectedLength← GetExpectedLogLength[];
IF curLength < expectedLength THEN
{ ReportRope["Log length is less than expected; "];
IF readOnlyAccess THEN
RETURN[AbandonStartup[" .. A scavenge is necessary but .."]];
Report["You Must Scavenge"];
Report[scavengeMsg];
IF ~InternalConfirm[scavMenu] THEN { CloseDownWalnut[FALSE]; RETURN[FALSE]};
DoScavenge[startPos: 0];
didScavenge← TRUE;
};
IF personalMailDB THEN WalnutRetrieve.OpenConnection[WalnutSendOps.userRName];
Labels.Set[mailNotifyLabel,
IF personalMailDB THEN WaitForGVLabel ELSE
IF readOnlyAccess THEN ROAccessLabel ELSE NoMailLabel];
ChangeWalnutMenu[workingMenu];
IF (expectedLength← GetExpectedDBLogPos[]) # curLength THEN
{ IF readOnlyAccess THEN
RETURN[AbandonStartup["Log file is longer than expected; updating is necessary but "]];
ReportRope["Updating database from log file ..."];
IF expectedLength = 0 THEN {DoScavenge[startPos: 0]; didScavenge← TRUE}
ELSE
{ []← WalnutLog.UpdateFromLog[expectedLength];
WalnutLog.MarkWalnutTransaction[]; -- commit what's been read
};
};
ShowMsgSetButtons[];
IF ~walnut.iconic THEN ViewerOps.PaintViewer[walnut, client];
-- check how much space is left (if any) in the control window for a typescript)
IF firstTime THEN
{ dif: INTEGER;
wH: INTEGER← ViewerSpecs.openRightTopY/4;
startExpungePos: INT← GetStartExpungePos[];
endOfLog: INT← LogLength[doFlush: FALSE];
LockedSetHeight: PROC = {ViewerOps.SetOpenHeight[walnut, wH - dif]};
IF (dif← (wH-walnutRulerAfter.cy) - 64) # 0 THEN
{ ViewerLocks.CallUnderWriteLock[LockedSetHeight, walnut];
IF ~walnut.iconic THEN ViewerOps.ComputeColumn[walnut.column]
};
IF didScavenge THEN
{ Report["Do you want to set the StartExpungePos to the current end of your log?"];
IF InternalConfirm[] THEN
{ SetStartExpungePos[endOfLog];
MarkWalnutTransaction[];
Report[IO.PutFR["StartExpungePos set to %g", IO.int[endOfLog]]];
}
ELSE Report["StartExpungePos is at zero"];
}
ELSE IF logIsAlpineFile AND enableTailRewrite AND
(expectedLength← (curLength - startExpungePos)) > excessBytesInLogFile THEN
Report[IO.PutFR[
"There are %g bytes (%g pages) in the tail of your log file;\nconsider doing an expunge",
IO.int[expectedLength], IO.int[(expectedLength/512)+1]]];
};
FixUpWalnutViewers[];
IF initialActiveOpen AND firstTime THEN
{ IF personalMailDB OR WalnutDB.NumInMsgSet[WalnutDB.activeMsgSet] # 0 THEN
[]← DisplayMsgSet[WalnutDB.activeMsgSet]
};
ChangeWalnutMenu[walnutMenu];
doingCheckpoint← FALSE;
BROADCAST rollbackFinished;
walnut.inhibitDestroy← FALSE;
RETURN[TRUE];
EXITS
mustQuit =>
{ Report["Start or Restart failed; click Quit or Retry"];
IF InternalConfirm[maybeQuitMenu] THEN
{CloseDownWalnut[TRUE]; RETURN[FALSE]};
};
END;
ENDLOOP;
END;
FixUpWalnutViewers: PROC =
BEGIN
msgSetList, msgList, queryList: LIST OF Viewer;
mL: LIST OF ROPE;
v: Viewer;
fullName: ROPE;
[msgSetList, msgList, queryList]← EnumWalnutViewers[TRUE];
FOR vL: LIST OF Viewer← msgSetList, vL.rest UNTIL vL=NIL DO
fullName← NARROW[ViewerOps.FetchProp[v← vL.first, $Entity]];
WalnutMsgOps.FixUpMsgSetViewer[fullName, v];
ENDLOOP;
FOR vL: LIST OF Viewer← queryList, vL.rest UNTIL vL=NIL DO
mL← NARROW[ViewerOps.FetchProp[v← vL.first, $WalnutQuery]];
[]← WalnutMsgOps.BuildListOfMsgsViewer[mL, v.name, v];
ENDLOOP;
FOR vL: LIST OF Viewer← msgList, vL.rest UNTIL vL=NIL DO
fullName← NARROW[ViewerOps.FetchProp[v← vL.first, $Entity]];
WalnutMsgOps.FixUpMsgViewer[fullName, v];
ENDLOOP;
END;
-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
QuitWalnut: PUBLIC ENTRY PROC[ra: REF ANY ] =
BEGIN ENABLE UNWIND => NULL;
IF ra # NIL THEN
{ msg: ROPE← NARROW[ra];
IF walnut = NIL THEN RETURN; -- something funny
walnut.inhibitDestroy← TRUE;
TakeDownWalnutViewers[]; -- make the user notice
ViewerOps.BlinkIcon[walnut, IF walnut.iconic THEN 0 ELSE 1];
Report["**********", msg];
Report["You MUST quit out of Walnut; Click Quit when ready"];
[]← InternalConfirm[forceQuitMenu];
};
CloseDownWalnut[TRUE]
END;
ClosingWalnut: PUBLIC PROC[ra: REF ANY] = {NULL};
----------------------------
SetWalnutProfileVars: ENTRY UserProfile.ProfileChangedProc = CHECKED
BEGIN ENABLE UNWIND => NULL;
curUser: ROPE← UserCredentials.Get[].name;
enableTailRewrite← UserProfile.Boolean[key: "Walnut.EnableTailRewrite", default: FALSE];
initialActiveIconic← UserProfile.Boolean[key: "Walnut.InitialActiveIconic", default: FALSE];
initialActiveRight← UserProfile.Boolean[key: "Walnut.InitialActiveRight", default: TRUE];
initialActiveOpen← UserProfile.Boolean[key: "Walnut.InitialActiveOpen", default: FALSE];
msgSetBorders← UserProfile.Boolean[key: "Walnut.MsgSetButtonBorders", default: FALSE];
excessBytesInLogFile←
UserProfile.Number[key: "Walnut.ExcessBytesInLogFile", default: 300000];
IF walnut = NIL THEN {previousUser← curUser; RETURN};
IF ~Rope.Equal[previousUser, curUser, FALSE] THEN
{ mustQuitWalnut← "Logged-in user changed";
WQueue.Flush[walnutQueue, CheckForNotify];
IF reason # rollBack THEN
WQueue.QueueClientAction[walnutQueue, QuitWalnut, mustQuitWalnut];
};
previousUser← curUser;
END;
----------------------------
WalnutCheckpointProc: ENTRY Booting.CheckpointProc =
BEGIN ENABLE UNWIND => NULL;
IF walnut = NIL THEN RETURN;
WQueue.Flush[walnutQueue, CheckForNotify];
ChangeWalnutMenu[workingMenu];
Report["\nDoing Checkpoint ..."];
WalnutRetrieve.CloseConnection[];
lastStateReported← unknown;
doingCheckpoint← TRUE;
DestroyAllMsgSetButtons[];
ClearMsgSetDisplayers[];
BEGIN
CloseTransactions[ TRUE ! DB.Failure, IO.Error => GOTO failure];
EXITS
failure => IF DB.TransactionOf[segmentName] # NIL THEN
DB.AbortTransaction[DB.TransactionOf[segmentName ! IO.Error => CONTINUE]];
END;
END;
WalnutRollbackProc: ENTRY Booting.RollbackProc =
{ ENABLE UNWIND => NULL;
IF walnut = NIL THEN RETURN;
TRUSTED
{IF mustQuitWalnut#NIL THEN Process.Detach[FORK QuitWalnut[mustQuitWalnut]]
ELSE Process.Detach[FORK RestartWalnut[]];
};
};
-- clean up Walnut at checkpoint time
TRUSTED {Booting.RegisterProcs[c: WalnutCheckpointProc, r: WalnutRollbackProc]};
UserProfile.CallWhenProfileChanges[SetWalnutProfileVars];
END.