-- 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: ROPENIL] 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: BOOLFALSE, scavengeFirst: BOOLFALSE]
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: BOOLTRUE;
 curVersion: BasicTime.GMT;
 curLength, expectedLength: INT;
 openTries: INTEGER← 0;
 logFromDB: ROPE;
 wasReadOnly: BOOL← readOnlyAccess;
 logFileNotFound, noDB: BOOLFALSE;
 notFoundRope: ROPE = " not found and can't be created";
 didScavenge: BOOLFALSE;

 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: ROPENARROW[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
.