-- Transport Mechanism Mail Server - Mailbox management --

-- [Indigo]<Grapevine>MS>MailboxAlloc.mesa

-- Andrew Birrell	September 14, 1982 2:14 pm
-- Randy Gobbel		19-May-81 20:47:16
-- Mark Johnson		19-May-81 13:43:58

DIRECTORY
Ascii		USING[ CR, TAB ],
BitMapDefs	USING[ Map, FindFree, Clear, Test, Set ],
BodyDefs	USING[ ItemHeader, ItemLength, maxRNameLength, oldestTime,
		       PackedTime, RName, RNameSize, Timestamp],
BTreeDefs	USING[ BTreeHandle, Call, Delete, EnumerateFrom, Insert,
		       Lookup ],
EnquiryDefs	USING[ Handle ],
HeapDefs	USING[ GetReaderOffset, HeapAbandonWrite, HeapEndRead,
		       HeapEndWrite,
		       HeapReadData, HeapReadRName, HeapStartRead,
		       HeapStartWrite, HeapWriteData, HeapWriteRName,
		       ObjectNumber, ReaderHandle, ReadItemHeader, SendObj,
		       SetReaderOffset, WriterHandle ],
Inline		USING[ COPY ],
LogDefs		USING[ DisplayNumber, WriteChar, WriteLogEntry,
		       WriteString ],
MailboxDefs,
ObjectDirDefs	USING[ noObject, FreeObject, UseObject ],
PolicyDefs	USING[ ArchiverPause, EndOperation, WaitOperation ],
Process		USING[ InitializeMonitor ],
ProtocolDefs	USING[ AppendTimestamp, Handle, Remark, SendItemHeader,
		       SendTimestamp ],
ReturnDefs	USING[ CopyItem ],
SLDefs		USING[ SLHeader, SLWrite ],
SpillDefs	USING[ AddBody, ArchReader, CopyBody,
		       CopyText, Delete, DeleteEntry, EndWrite, EndReading,
		       OpenToRead, RecoverBody, ServerCrash,
		       StartWrite, TestDeletion ],
String		USING[ AppendDecimal, AppendString, LowerCase ],
Storage		USING[ Node ],
Time		USING[ Append, Current, Unpack ],
VMDefs		USING[ Page, ReadPage, UsePage, PageNumber,
		       PageIndex, Release, FileHandle, MarkStartWait,
		       LookAheadCount ];

MailboxAlloc: MONITOR
   IMPORTS BitMapDefs, BodyDefs, BTreeDefs, HeapDefs, Inline, LogDefs,
           ObjectDirDefs, PolicyDefs, Process, ProtocolDefs,
           ReturnDefs, SLDefs, SpillDefs,
           String, Storage, Time, VMDefs
   EXPORTS EnquiryDefs, MailboxDefs
   SHARES  MailboxDefs =
BEGIN

Allocate: PROCEDURE[CARDINAL]RETURNS[POINTER] = Storage.Node;

MBXName:	TYPE = VMDefs.PageNumber;

MBXHandle:	PUBLIC TYPE = POINTER TO MBXData;

-- representation of state of mailbox reading --
MBXData:	TYPE = MONITORED RECORD[
    lock:      MBXName, -- page number for mailbox interlock bit --
    latest:    MBXName, --address of latest page of mbx--
    where:     MBXName, --address of this page--
    current:   CARDINAL, --number of next page to be read--
    messages:  DESCRIPTOR FOR ARRAY OF HeapDefs.ObjectNumber, --messages in this page--
    nextMsg:   CARDINAL, -- index into array of messages --
    object:    HeapDefs.ObjectNumber, -- current element of "messages" --
    bodyObj:   HeapDefs.ObjectNumber, --"object" without TOC indirection --
    archObj:   HeapDefs.ObjectNumber, -- current "archived" object --
    archCount: CARDINAL, -- count relative to start of archive file --
    archFile:  SpillDefs.ArchReader,
    archOpen:  BOOLEAN,	
    remainder: CARDINAL, -- number of messages left in the mailbox --
    next:      MBXHandle, -- for free chain of 'MBXData's--
    page:      VMDefs.Page --current page buffer--];

-- header in disk representation of each mailbox page --
MBXHeader:	TYPE = RECORD[
		    chain:  MBXName, --previous MBX page of this MBX--
		    first:  VMDefs.PageIndex, --first entry in this page--
		    free:   VMDefs.PageIndex,
		    number: CARDINAL --page number within this MBX-- ];

MBXEnd:    MBXHandle = NIL;
MBXChain:  MBXHandle ← MBXEnd;

handle:    VMDefs.FileHandle;
lookAhead: VMDefs.LookAheadCount = 0;

pageMap:   BitMapDefs.Map;

mailboxes: CARDINAL ← 0;


-- punning for R-Names --

RNameDesc: PROCEDURE[ name: BodyDefs.RName ]
             RETURNS[ DESCRIPTOR FOR ARRAY OF WORD ] =
   BEGIN
   index: CARDINAL;
   FOR index IN [0 .. name.length)
   DO name[index] ← String.LowerCase[name[index]] ENDLOOP;
   IF name.length MOD 2 # 0 THEN name[name.length] ← '@;
   RETURN[ DESCRIPTOR[ @(name.text), (1+name.length)/2 ] ]
   END;


-- Management of Btree entries --

tree: BTreeDefs.BTreeHandle;

TreeRec: TYPE = RECORD[SELECT t: * FROM
                    found => [where, lock: MBXName,
                              oldest: BodyDefs.Timestamp],
                    empty => [last: BodyDefs.Timestamp],
                  ENDCASE];

FindInTree: INTERNAL PROCEDURE[ who: BodyDefs.RName ] RETURNS[ TreeRec ] =
   BEGIN
   value: TreeRec;
   length:CARDINAL = BTreeDefs.Lookup[tree,
                                      RNameDesc[who],
                                      DESCRIPTOR[@value,SIZE[TreeRec]] ];
   IF length=177777B
   THEN -- not in tree --
        RETURN[ [empty[BodyDefs.oldestTime]] ]
   ELSE RETURN[value]
   END;

ChangeInTree: INTERNAL PROCEDURE[ who: BodyDefs.RName,
                         where, lock: MBXName,
                         oldest: BodyDefs.Timestamp ] =
   BEGIN
   value: TreeRec ← [found[where:where, lock:lock, oldest:oldest]];
   BTreeDefs.Insert[tree,
                    RNameDesc[who],
                    DESCRIPTOR[@value,SIZE[TreeRec]] ];
   END;

EmptyInTree: ENTRY PROCEDURE[ who: BodyDefs.RName,
                                  last: BodyDefs.Timestamp ] =
   BEGIN
   value: TreeRec ← [empty[last]];
   BTreeDefs.Insert[tree,
                    RNameDesc[who],
                    DESCRIPTOR[@value,SIZE[TreeRec]] ];
   mailboxes ← mailboxes - 1;
   END;

PurgeFromTree: ENTRY PROCEDURE[ who: BodyDefs.RName] =
   BEGIN
   value: TreeRec = FindInTree[who];
   WITH value SELECT FROM
     empty => BTreeDefs.Delete[tree, RNameDesc[who] ];
   ENDCASE => NULL -- someone else may have reinstated it --;
   END;

EnumerateTree: ENTRY PROC[ work: PROC[BodyDefs.RName,found TreeRec] ] =
   BEGIN
   -- beware: calls "work" with the monitor locked --
   Action: INTERNAL BTreeDefs.Call =
      BEGIN
      value: POINTER TO TreeRec = LOOPHOLE[BASE[v]];
      name: BodyDefs.RName = [BodyDefs.maxRNameLength];
      more ← TRUE; dirty ← FALSE; -- results of "Action" --
      IF LENGTH[k] = 0 THEN RETURN;
      name.length ← 2 * LENGTH[k];
      Inline.COPY[from:BASE[k], to:@(name.text), nwords:LENGTH[k]];
      IF name.length > 0 AND name[name.length-1] = '@
      THEN name.length ← name.length-1 -- undo padding kludge --;
      WITH res: value SELECT FROM
        found => work[name, res];
      ENDCASE => NULL;
      END;
   BTreeDefs.EnumerateFrom[tree, DESCRIPTOR[NIL,0], Action !
                           UNWIND => NULL];
   END;


-- Interlocks on mailboxes --

-- the lock map is used for single writer/multiple reader synchronization
-- Any potential writer tests the lock; readers claim it.  Both always
-- have the monitor lock while doing this.

lockMap:   BitMapDefs.Map;
conflictMap: BitMapDefs.Map;

MBXFree: CONDITION;

AcquireMBX: INTERNAL PROCEDURE[ who: BodyDefs.RName ]
                       RETURNS[ found: BOOLEAN, mbx, lock: MBXName ] =
   BEGIN
   DO value: TreeRec = FindInTree[who];
      WITH res: value SELECT FROM
        empty => { found ← FALSE; EXIT };
        found =>
          IF LockedMBX[res.lock]
          THEN WAIT MBXFree
          ELSE BEGIN
               found←TRUE; mbx←res.where; lock ← res.lock;
               BitMapDefs.Set[lockMap, lock];
               EXIT
               END;
      ENDCASE => ERROR;
   ENDLOOP;
   END;

LockedMBX: INTERNAL PROCEDURE[ lock: MBXName ] RETURNS[locked: BOOLEAN] =
   BEGIN
   locked ← BitMapDefs.Test[lockMap, lock];
   IF locked THEN BitMapDefs.Set[conflictMap, lock];
   END;

FreeMBX: INTERNAL PROCEDURE[ lock: MBXName ] =
   BEGIN
   BitMapDefs.Clear[lockMap, lock];
   IF BitMapDefs.Test[conflictMap, lock]
   THEN { BitMapDefs.Clear[conflictMap, lock]; NoteUnlocked[] };
   BROADCAST MBXFree;
   END;

unlockedMBX: BOOLEAN ← TRUE;
unlockedMBXCond: CONDITION;

WaitForUnlocked: PUBLIC ENTRY PROC =
   BEGIN
   UNTIL unlockedMBX DO WAIT unlockedMBXCond ENDLOOP;
   unlockedMBX ← FALSE;
   END;

NoteUnlocked: INTERNAL PROC = INLINE
   { unlockedMBX ← TRUE; NOTIFY unlockedMBXCond };


-- Access to mailboxes --

nextVirginPage:  VMDefs.PageNumber; -- size of mailbox file --
WrongNewMBXPage: ERROR = CODE;

mailboxContents: LONG CARDINAL ← 0;

MBXWrite: PUBLIC ENTRY PROCEDURE[ who: BodyDefs.RName,
                                  obj: HeapDefs.ObjectNumber,
                                  postmark: BodyDefs.Timestamp ]
            RETURNS[ done: BOOLEAN ] =
   BEGIN
   ENABLE UNWIND => NULL;
   addPage: BOOLEAN ← FALSE;
   page: VMDefs.Page;
   header: POINTER TO MBXHeader;
   number: CARDINAL; -- number of a new page within this mailbox --
   mbx: TreeRec =  FindInTree[who: who];
   WITH tree:mbx SELECT FROM
     empty =>
        BEGIN
        IF tree.last = postmark
        THEN { LogDefs.WriteChar['E]; LogDefs.WriteChar['!];
               RETURN[TRUE] --duplicate-- };
        number ← 0; addPage ← TRUE;
        -- no interlock to check --
        END;
     found =>
        BEGIN
        page ← VMDefs.ReadPage[[handle,tree.where], lookAhead ];
        header ← LOOPHOLE[page,POINTER]+FIRST[VMDefs.PageIndex];
        IF header.first # header.free
        THEN -- test for duplicate elimination --
             BEGIN
             x: POINTER TO HeapDefs.ObjectNumber = LOOPHOLE[page,POINTER] +
                               header.free-SIZE[HeapDefs.ObjectNumber];
             IF x↑ = obj
             THEN BEGIN
                  VMDefs.Release[page];
                  LogDefs.WriteChar['E];
                  RETURN[TRUE] --duplicate--
                  END;
             END;
        IF LockedMBX[tree.lock]
        THEN BEGIN
             VMDefs.Release[page];
             RETURN[FALSE]
             END
      --ELSE we are protected by our monitor lock --;
        IF header.free+SIZE[HeapDefs.ObjectNumber] > LAST[VMDefs.PageIndex]
        THEN -- will need to expand to another page --
             BEGIN
             number ← header.number + 1;
             VMDefs.Release[page];
             addPage ← TRUE;
             END;
        IF tree.oldest = BodyDefs.oldestTime AND NOT addPage
        THEN ChangeInTree[who: who,
                          where: tree.where, lock: tree.lock,
                          oldest: postmark];
        END;
   ENDCASE => ERROR;
   IF addPage
   THEN BEGIN -- create and initialise new page of mailbox --
        desc: DESCRIPTOR FOR ARRAY OF WORD =
                DESCRIPTOR[who, BodyDefs.RNameSize[who] ];
        new: MBXName = BitMapDefs.FindFree[pageMap];
        IF new >= nextVirginPage
        THEN IF new > nextVirginPage
             THEN ERROR WrongNewMBXPage[]
             ELSE nextVirginPage ← nextVirginPage+1;
        BitMapDefs.Set[pageMap,new];
        WITH tree:mbx SELECT FROM
          empty => BEGIN
                   ChangeInTree[who: who,
                                where: new, lock: new,
                                oldest: postmark];
                   mailboxes ← mailboxes + 1;
                   END;
          found => ChangeInTree[who: who,
                                where: new, lock: tree.lock,
                                oldest: IF tree.oldest = BodyDefs.oldestTime
                                        THEN postmark ELSE tree.oldest];
        ENDCASE => ERROR;
        page ← VMDefs.UsePage[[handle,new]];
        header ← LOOPHOLE[page,POINTER]+FIRST[VMDefs.PageIndex];
        header.free ← FIRST[VMDefs.PageIndex] + SIZE[MBXHeader];
        Inline.COPY[BASE[desc],LENGTH[desc],page+header.free];
        header.first ← header.free ← header.free+LENGTH[desc];
        header.chain ← WITH tree:mbx SELECT FROM found=>tree.where,
                       ENDCASE=>new -- end-of-chain --;
        header.number ← number;
        END;
   BEGIN
      x: POINTER TO HeapDefs.ObjectNumber =
                                 LOOPHOLE[page,POINTER]+header.free;
      x↑ ← obj;
   END;
   header.free ← header.free + SIZE[HeapDefs.ObjectNumber];
   ObjectDirDefs.UseObject[obj];
   mailboxContents ← mailboxContents + 1; -- global count --
   VMDefs.MarkStartWait[ page ];
   VMDefs.Release[ page ];
   RETURN[TRUE]
   END;

FreeMBXPage: ENTRY PROC[page: MBXName] =
   { BitMapDefs.Clear[pageMap, page] };

Poll: PUBLIC ENTRY PROCEDURE[ who: BodyDefs.RName ]
                        RETURNS[found: BOOLEAN ] =
   BEGIN
   value: TreeRec = FindInTree[who];
   found ← WITH value SELECT FROM found => TRUE, ENDCASE => FALSE;
   END;

Open: PUBLIC ENTRY PROCEDURE[ who: BodyDefs.RName ]
                     RETURNS[found: BOOLEAN, count: CARDINAL,
                             mbx: MBXHandle] =
   BEGIN
   IF MBXChain = MBXEnd
   THEN mbx ← Allocate[SIZE[MBXData]]
   ELSE BEGIN mbx ← MBXChain; MBXChain ← MBXChain.next END;
   [found, mbx.latest, mbx.lock] ← AcquireMBX[who: who];
   IF found
   THEN BEGIN
        Process.InitializeMonitor[@mbx.LOCK]; SetupMBX[mbx];
        count ← mbx.remainder;
        END
   ELSE BEGIN
        LogAction[who, nothing];
        mbx.next ← MBXChain; MBXChain ← mbx;
        count ← 0; mbx ← NIL;
        END;
   END;

Destroy: ENTRY PROC[mbx: MBXHandle] =
   BEGIN
   VMDefs.Release[mbx.page];
   FreeMBX[mbx.lock];
   mbx.next ← MBXChain; MBXChain ← mbx;
   END;

RestartMBX: PROC[mbx: MBXHandle] =
   BEGIN
   -- re-positions to start of mailbox without releasing lock --
   CloseArch[mbx];
   VMDefs.Release[mbx.page];
   SetupMBX[mbx];
   END;

SetupMBX: PROC[mbx: MBXHandle] =
   BEGIN
   mbx.current ← 0;
   mbx.page ← VMDefs.ReadPage[[handle,mbx.where←mbx.latest],lookAhead];
   mbx.remainder ← GetPage[mbx];
   mbx.archObj ← ObjectDirDefs.noObject;
   mbx.archOpen ← FALSE;
   END;

GetPage: PROCEDURE[ mbx: MBXHandle ] RETURNS[ count: CARDINAL ] =
   BEGIN
   header: POINTER TO MBXHeader;
   count ← 0;
   IF mbx.where # mbx.latest
   THEN BEGIN -- finish with previous page --
        VMDefs.Release[mbx.page];
        mbx.page ←
              VMDefs.ReadPage[[handle,mbx.where←mbx.latest],lookAhead];
        END;
   DO BEGIN
      header ← LOOPHOLE[mbx.page,POINTER] + FIRST[VMDefs.PageIndex];
      count ← count + (header.free-header.first)/SIZE[HeapDefs.ObjectNumber];
      IF header.number = mbx.current THEN EXIT;
      BEGIN
         next: MBXName = header.chain; -- that store is about to go --
         VMDefs.Release[mbx.page];
         mbx.where ← next;
      END;
      mbx.page ← VMDefs.ReadPage[ [handle,mbx.where], lookAhead ];
      END;
   ENDLOOP;
   mbx.messages ← DESCRIPTOR[mbx.page+header.first,
               (header.free-header.first)/SIZE[HeapDefs.ObjectNumber] ];
   mbx.nextMsg ← 0; mbx.current ← mbx.current+1;
   END;

NextMessage: PUBLIC PROC[ mbx: MBXHandle ]
                 RETURNS[ msgExists, archived, deleted: BOOLEAN ] =
   { [msgExists, archived, deleted] ← RealNextMessage[mbx, yes] };

RealNextMessage: PROC[ mbx: MBXHandle, testArchDeletion: {yes, no} ]
                 RETURNS[ msgExists, archived, deleted: BOOLEAN ] =
   BEGIN
   IF mbx.remainder = 0 THEN RETURN[ FALSE, FALSE, FALSE ];
   IF mbx.nextMsg = LENGTH[mbx.messages] THEN [] ← GetPage[mbx];
   IF LENGTH[mbx.messages] = 0 THEN ERROR;
   mbx.nextMsg ← mbx.nextMsg + 1;
   mbx.remainder ← mbx.remainder - 1;
   mbx.object ← mbx.messages[mbx.nextMsg-1];
   mbx.bodyObj ← GetBodyObj[mbx.object];
   archived ← (mbx.bodyObj.type = archived);
   IF archived
   THEN BEGIN
        ENABLE 
           BEGIN
           SpillDefs.ServerCrash => ERROR InaccessibleArchive[];
           UNWIND => mbx.archOpen ← FALSE;
           END;
        IF mbx.bodyObj = mbx.archObj
        THEN mbx.archCount ← mbx.archCount + 1
        ELSE { CloseArch[mbx]; mbx.archObj ← mbx.bodyObj;
               mbx.archCount ← 0 };
        IF testArchDeletion = yes
        THEN BEGIN
             OpenArch[mbx]; -- for deletion test! --
             deleted ← SpillDefs.TestDeletion[mbx.archFile, mbx.archCount];
             END
        ELSE deleted ← FALSE --don't care--
        END
   ELSE BEGIN
        deleted ← (mbx.bodyObj = ObjectDirDefs.noObject);
        IF NOT deleted -- deleted msgs were skipped when archiving --
        THEN { CloseArch[mbx]; mbx.archObj ← ObjectDirDefs.noObject };
        END;
   msgExists ← TRUE;
   END;

ReadTOC: PUBLIC PROC[ mbx: MBXHandle, text: ProtocolDefs.Remark ] =
   BEGIN
   IF mbx.object.type # TOC
   THEN text.length ← 0
   ELSE BEGIN
        reader: HeapDefs.ReaderHandle = HeapDefs.HeapStartRead[mbx.object];
        obj: HeapDefs.ObjectNumber;
        [] ← HeapDefs.HeapReadData[reader,
                                   [@obj,SIZE[HeapDefs.ObjectNumber] ] ];
--CHEAT-- [] ← HeapDefs.HeapReadRName[reader, text];
        HeapDefs.HeapEndRead[reader];
        END;
   END;

GetBodyObj: PROC[given: HeapDefs.ObjectNumber]
               RETURNS[body: HeapDefs.ObjectNumber] =
   BEGIN
   IF given.type = TOC
   THEN BEGIN
        tocReader: HeapDefs.ReaderHandle = HeapDefs.HeapStartRead[given];
        [] ← HeapDefs.HeapReadData[tocReader,
                                   [@body,SIZE[HeapDefs.ObjectNumber] ] ];
        HeapDefs.HeapEndRead[tocReader];
        END
   ELSE body ← given;
   END;

InaccessibleArchive: PUBLIC ERROR = CODE;

SendBody: PUBLIC PROC[ mbx: MBXHandle, str: ProtocolDefs.Handle ] =
   BEGIN
   IF mbx.bodyObj.type = archived
   THEN BEGIN
        ENABLE 
           BEGIN
           SpillDefs.ServerCrash => ERROR InaccessibleArchive[];
           UNWIND => mbx.archOpen ← FALSE;
           END;
        OpenArch[mbx];
        SpillDefs.CopyBody[mbx.archFile, mbx.archCount, str];
        CountArchived[];
        END
   ELSE BEGIN
        reader: HeapDefs.ReaderHandle =
                        HeapDefs.HeapStartRead[mbx.bodyObj];
        header: BodyDefs.ItemHeader = HeapDefs.ReadItemHeader[reader];
        postmark: BodyDefs.Timestamp;
        IF header.type # PostMark THEN ERROR;
        [] ← HeapDefs.HeapReadData[reader,
                                   [@postmark,SIZE[BodyDefs.Timestamp]]];
        ConsiderDelay[postmark];
        ProtocolDefs.SendItemHeader[str, header];
        ProtocolDefs.SendTimestamp[str, postmark];
        HeapDefs.SendObj[reader, str];
        END;
   END;

SendText: PUBLIC PROC[ mbx: MBXHandle,
            sendLength: PROC[BodyDefs.ItemLength],
            str: UNSPECIFIED,
            sendBytes: PROC[UNSPECIFIED,POINTER,CARDINAL] ] =
   BEGIN
   IF mbx.bodyObj.type = archived
   THEN BEGIN
        ENABLE 
           BEGIN
           SpillDefs.ServerCrash => ERROR InaccessibleArchive[];
           UNWIND => mbx.archOpen ← FALSE;
           END;
        OpenArch[mbx];
        SpillDefs.CopyText[mbx.archFile, mbx.archCount,
                           sendLength, str, sendBytes];
        CountArchived[];
        END
   ELSE BEGIN
        reader: HeapDefs.ReaderHandle =
                       HeapDefs.HeapStartRead[mbx.bodyObj];
        DO ENABLE UNWIND => HeapDefs.HeapEndRead[reader];
           header: BodyDefs.ItemHeader = HeapDefs.ReadItemHeader[reader];
           SELECT header.type FROM
             PostMark =>
                BEGIN
                postmark: BodyDefs.Timestamp;
                [] ← HeapDefs.HeapReadData[reader,
                                   [@postmark,SIZE[BodyDefs.Timestamp]]];
                ConsiderDelay[postmark];
                END;
             Text =>
                BEGIN
                sendLength[header.length];
                ReturnDefs.CopyItem[reader, header.length, sendBytes, str];
                EXIT
                END;
             LastItem => EXIT --no text--;
           ENDCASE => HeapDefs.SetReaderOffset[reader,
                  HeapDefs.GetReaderOffset[reader] + (header.length+1)/2 ];
        ENDLOOP;
        HeapDefs.HeapEndRead[reader];
        END;
   END;

OpenArch: PROC[ mbx: MBXHandle ] =
   BEGIN
   IF NOT mbx.archOpen
   THEN BEGIN
        mbx.archFile ← SpillDefs.OpenToRead[mbx.archObj];
        mbx.archOpen ← TRUE;
        END;
   END;

CloseArch: PROC[ mbx: MBXHandle ] =
   BEGIN
   IF mbx.archOpen
   THEN SpillDefs.EndReading[mbx.archFile !
                             SpillDefs.ServerCrash => CONTINUE];
   mbx.archOpen ← FALSE;
   END;

WriteTOC: PUBLIC PROC[ mbx: MBXHandle, text: ProtocolDefs.Remark ] =
   BEGIN
   Accept: PROC[obj: HeapDefs.ObjectNumber] =
      BEGIN
      ObjectDirDefs.UseObject[obj];
      mbx.messages[mbx.nextMsg-1] ← obj;
      VMDefs.MarkStartWait[mbx.page];
      IF mbx.object.type = TOC THEN ObjectDirDefs.FreeObject[mbx.object];
      mbx.object ← obj;
      END;
   body: HeapDefs.ObjectNumber ← mbx.bodyObj;
   IF text.length = 0
   THEN BEGIN
        -- no TOC required --
        IF mbx.object.type = TOC
        THEN { Accept[body]; ObjectDirDefs.FreeObject[body] };
        END
   ELSE BEGIN
        writer: HeapDefs.WriterHandle = HeapDefs.HeapStartWrite[TOC];
        HeapDefs.HeapWriteData[writer, [@body,SIZE[HeapDefs.ObjectNumber] ] ];
--CHEAT-- HeapDefs.HeapWriteRName[writer, text];
        HeapDefs.HeapEndWrite[writer, Accept];
        END;
   END;

DeleteMessage: PUBLIC PROC[ mbx: MBXHandle ] =
   BEGIN
   IF mbx.bodyObj.type=archived
   THEN BEGIN
        ENABLE 
           BEGIN
           SpillDefs.ServerCrash => ERROR InaccessibleArchive[];
           UNWIND => mbx.archOpen ← FALSE;
           END;
        IF mbx.object.type = TOC
        THEN BEGIN
             mbx.messages[mbx.nextMsg-1] ← mbx.bodyObj;
             VMDefs.MarkStartWait[mbx.page];
             END;
        OpenArch[mbx];
        SpillDefs.DeleteEntry[mbx.archFile, mbx.archCount];
        END
   ELSE BEGIN
        mbx.messages[mbx.nextMsg-1] ← ObjectDirDefs.noObject;
        VMDefs.MarkStartWait[mbx.page];
        ObjectDirDefs.FreeObject[mbx.bodyObj];
        mbx.bodyObj ← ObjectDirDefs.noObject;
        END;
   IF mbx.object.type = TOC
   THEN BEGIN
        ObjectDirDefs.FreeObject[mbx.object];
        mbx.object ← mbx.bodyObj;
        END;
   END;

Close: PUBLIC PROCEDURE[ mbx: MBXHandle ] =
   BEGIN
   LogAction[LOOPHOLE[mbx.page,POINTER]+
             FIRST[VMDefs.PageIndex]+SIZE[MBXHeader], no];
   InnerClose[mbx];
   END;

InnerClose: PROC[ mbx: MBXHandle ] =
   BEGIN
   CloseArch[mbx];
   Destroy[mbx];
   END;

ReadPostmark: PROC[body: HeapDefs.ObjectNumber]
           RETURNS[postmark: BodyDefs.Timestamp] = 
   BEGIN
   reader: HeapDefs.ReaderHandle = HeapDefs.HeapStartRead[body];
   header: BodyDefs.ItemHeader = HeapDefs.ReadItemHeader[reader];
   IF header.type # PostMark THEN ERROR;
   [] ← HeapDefs.HeapReadData[reader, [@postmark,SIZE[BodyDefs.Timestamp]]];
   HeapDefs.HeapEndRead[reader];
   END;

FlushAndClose: PUBLIC PROCEDURE[ mbx: MBXHandle ] =
   BEGIN
   -- Beware: this procedure works without the monitor lock, because there
   -- may be unbounded delays while we delete archive files.  The order of
   -- events is quite important!  We must also take care that everything
   -- will look ok if we crash in the middle of flushing a multi-page
   -- mailbox.
   buffer: DESCRIPTOR FOR ARRAY OF HeapDefs.ObjectNumber;
   base: VMDefs.PageIndex = FIRST[VMDefs.PageIndex];
   header: POINTER TO MBXHeader ← LOOPHOLE[mbx.page,POINTER]+base;
   msgCount: CARDINAL ← 0;
   archiveFailure: BOOLEAN ← FALSE;
   lastPost: BodyDefs.Timestamp ← BodyDefs.oldestTime;
                  -- postmark of latest message --
   DeleteArch: PROC = -- delete archive file --
      BEGIN
      IF mbx.archObj # ObjectDirDefs.noObject
      THEN BEGIN
           SpillDefs.Delete[mbx.archObj ! SpillDefs.ServerCrash =>
                            { archiveFailure←TRUE; CONTINUE } ];
           ObjectDirDefs.FreeObject[mbx.archObj];
           mbx.archObj ← ObjectDirDefs.noObject;
           END;
      END;
   IF mbx.where # mbx.latest
   THEN BEGIN -- find first page --
        VMDefs.Release[mbx.page];
        mbx.page ←
              VMDefs.ReadPage[[handle,mbx.where←mbx.latest],lookAhead];
        END;
   CloseArch[mbx];  mbx.archObj ← ObjectDirDefs.noObject;
   DO who: BodyDefs.RName = LOOPHOLE[mbx.page,POINTER]+base+SIZE[MBXHeader];
      header ← LOOPHOLE[mbx.page,POINTER]+base;
      buffer ← DESCRIPTOR[mbx.page+header.first,
                  (header.free-header.first)/SIZE[HeapDefs.ObjectNumber] ];
      msgCount ← msgCount + LENGTH[buffer];
      BEGIN -- write MBX to disk as empty, in case of crash --
         header.free ← header.first;
         VMDefs.MarkStartWait[ mbx.page ];
      END;
      IF mbx.where = mbx.latest
      THEN BEGIN
           -- this mbx page contains last mbx message --
           lastObj: HeapDefs.ObjectNumber = buffer[LENGTH[buffer]-1];
           IF lastObj # ObjectDirDefs.noObject
           THEN BEGIN
                body: HeapDefs.ObjectNumber = GetBodyObj[lastObj];
                IF body.type # archived
                THEN lastPost ← ReadPostmark[body]
             -- ELSE lastPost is irrelevant! --;
                END;
           END;
      -- decrement reference counts --
      FOR index: CARDINAL DECREASING IN [0..LENGTH[buffer])
      DO obj: HeapDefs.ObjectNumber = buffer[index];
         IF obj # ObjectDirDefs.noObject
         THEN BEGIN
              body: HeapDefs.ObjectNumber = GetBodyObj[obj];
              IF obj.type = TOC THEN ObjectDirDefs.FreeObject[obj];
              IF body # mbx.archObj
              THEN BEGIN
                   DeleteArch[];
                   IF body.type=archived -- record for later deletion --
                   THEN{ mbx.archObj ← body; ObjectDirDefs.UseObject[body]};
                   END;
              ObjectDirDefs.FreeObject[body];
              END;
         mailboxContents ← mailboxContents-1;
      ENDLOOP;
      -- Clearing bit in pageMap allows page to be re-used --
      IF header.number = 0 -- we're at the end of the earliest page --
      THEN BEGIN
           EmptyInTree[who, lastPost];
           FreeMBXPage[mbx.where]; -- must not access page after here --
           EXIT
           END;
      BEGIN
         next: MBXName = header.chain; --that store is about to go--
         FreeMBXPage[mbx.where]; -- must not access page after here --
         VMDefs.Release[mbx.page];
         mbx.where ← next;
         mbx.page ← VMDefs.ReadPage[[handle,mbx.where],lookAhead];
      END;
   ENDLOOP;
   DeleteArch[];
   IF archiveFailure
   THEN LogDefs.WriteLogEntry["Failed to delete mailbox archive file"L];
   LogAction[LOOPHOLE[mbx.page,POINTER]+base+SIZE[MBXHeader],
             yes, msgCount];
   Destroy[mbx];
   END;

AdjustOldest: ENTRY PROC[who: BodyDefs.RName, time: BodyDefs.Timestamp]=
   BEGIN
   -- alter "oldest" stamp after archiving --
   prev: TreeRec = FindInTree[who: who];
   WITH tree: prev SELECT FROM
     empty => ERROR; -- silly: we have an interlock on it! --
     found => ChangeInTree[who: who,
                           where: tree.where, lock: tree.lock,
                           oldest: time];
   ENDCASE => ERROR;
   END;

Archive: PUBLIC PROC[who: BodyDefs.RName, keep: CARDINAL] =
   BEGIN
   mbx: MBXHandle;
   contents: CARDINAL;
   found: BOOLEAN;
   [found, contents, mbx] ← Open[who];
   IF found AND contents > keep
   THEN BEGIN
        ENABLE UNWIND => Close[mbx];
        msgExists, archived, deleted: BOOLEAN;
        archStart: CARDINAL ← 0;
        WHILE archStart < contents-keep -- skip to non-archived message --
        DO [msgExists, archived, deleted] ← RealNextMessage[mbx,no];
           IF NOT msgExists THEN ERROR;
           IF NOT archived AND NOT deleted THEN EXIT;
           archStart ← archStart+1;
        ENDLOOP;
        IF archStart < contents-keep
        THEN BEGIN
             spillCount: CARDINAL = (contents-keep)-archStart;
             archObj: HeapDefs.ObjectNumber;
             Backup: PROC =
                BEGIN
                RestartMBX[mbx];
                THROUGH [0..archStart]
                DO [msgExists, archived, deleted] ← RealNextMessage[mbx,no];
                ENDLOOP;
                END;
             BEGIN
                ENABLE SpillDefs.ServerCrash =>
                   { ObjectDirDefs.FreeObject[archObj]; Backup[]; RETRY };
                archObj ← SpillDefs.StartWrite[who, spillCount !
                   SpillDefs.ServerCrash => ERROR InaccessibleArchive[] ];
                THROUGH [0.. spillCount)
                DO IF NOT msgExists OR archived THEN ERROR;
                   IF NOT deleted
                   THEN  SpillDefs.AddBody[mbx.bodyObj];
                   [msgExists, archived, deleted] ← NextMessage[mbx];
                ENDLOOP;
                SpillDefs.EndWrite[];
             END;
             Backup[];
             THROUGH [0..spillCount)
             DO IF NOT deleted
                THEN BEGIN
                     mbx.messages[mbx.nextMsg-1] ← archObj;
                     ObjectDirDefs.UseObject[archObj];
                     VMDefs.MarkStartWait[mbx.page];
                     IF mbx.object.type = TOC
                     THEN ObjectDirDefs.FreeObject[mbx.object];
                     ObjectDirDefs.FreeObject[mbx.bodyObj];
                     END;
                [msgExists, archived, deleted] ← NextMessage[mbx];
             ENDLOOP;
             AdjustOldest[who, IF msgExists
                               THEN ReadPostmark[mbx.bodyObj]
                               ELSE BodyDefs.oldestTime];
             ObjectDirDefs.FreeObject[archObj];
             LogAction[who, arch, spillCount];
             END;
        END;
   IF found THEN InnerClose[mbx];
   END;

defaultDays: CARDINAL = 7;
archiveDays: CARDINAL ← defaultDays;

ReadArchiveDays: ENTRY PROC RETURNS[days: CARDINAL] = INLINE
   { days ← archiveDays; archiveDays ← defaultDays };

SetArchiveDays: PUBLIC ENTRY PROC[days: CARDINAL] =
   { archiveDays ← days };
 
Archiver: PROC =
   BEGIN
   DO PolicyDefs.ArchiverPause[];
      PolicyDefs.WaitOperation[archiver];
      BEGIN
      writer: HeapDefs.WriterHandle = HeapDefs.HeapStartWrite[temp];
      now: BodyDefs.PackedTime = Time.Current[];
      old: BodyDefs.PackedTime = now - 
                                LONG[ReadArchiveDays[]] * 24 * 60 * 60;
      count: CARDINAL ← 0;
      Work: PROC[who: BodyDefs.RName, tree: found TreeRec] =
         BEGIN
         IF tree.oldest # BodyDefs.oldestTime
         AND tree.oldest.time < old
         THEN { HeapDefs.HeapWriteRName[writer, who]; count←count+1 };
         END;
      EnumerateTree[Work];
      IF count = 0
      THEN HeapDefs.HeapAbandonWrite[writer]
      ELSE BEGIN
           reader: HeapDefs.ReaderHandle;
           GetReader: PROC[obj: HeapDefs.ObjectNumber] =
              { reader ← HeapDefs.HeapStartRead[obj] };
           HeapDefs.HeapEndWrite[writer, GetReader];
           THROUGH [ 1..count ]
           DO who: BodyDefs.RName = [BodyDefs.maxRNameLength];
              BEGIN
                 [] ← HeapDefs.HeapReadRName[reader, who];
                 Archive[who, 0 ! InaccessibleArchive => GOTO failed];
              EXITS
                 failed => { LogAction[who, archFail]; EXIT };
              END;
           ENDLOOP;
           HeapDefs.HeapEndRead[reader];
           END;
      END;
      PolicyDefs.EndOperation[archiver];
   ENDLOOP;
   END;
      

Remail: PUBLIC PROC[who: BodyDefs.RName, valid: BOOLEAN] =
   BEGIN
   RemailMessage: PROC[body: HeapDefs.ObjectNumber] =
      BEGIN
      log: STRING = [64];
      slHeader: SLDefs.SLHeader;
      slHeader.created ← ReadPostmark[body];
      slHeader.received ← [0,0,Time.Current[]];
      String.AppendString[log, "Remailing "L];
      ProtocolDefs.AppendTimestamp[log, slHeader.created];
      String.AppendString[log, " as "L];
      slHeader.created.time ← slHeader.received.time;
      ProtocolDefs.AppendTimestamp[log, slHeader.created];
      LogDefs.WriteLogEntry[log];
      slHeader.server ← NIL;
      BEGIN -- write steering list --
         sl: HeapDefs.WriterHandle = HeapDefs.HeapStartWrite[SLinput];
         HeapDefs.HeapWriteData[sl, [@slHeader, SIZE[SLDefs.SLHeader]] ];
         HeapDefs.HeapWriteRName[sl, who];
         SLDefs.SLWrite[body: body, SL: sl, queue: input];
      END;
      END;
   found: BOOLEAN;
   count: CARDINAL;
   mbx: MBXHandle;
   [found, count, mbx] ← Open[who];
   IF found
   THEN BEGIN
        IF valid
        THEN BEGIN
             ENABLE UNWIND => Close[mbx];
             LogAction[who, remail];
             THROUGH [1..count]
             DO msgExists, archived, deleted: BOOLEAN;
                [msgExists, archived, deleted] ← NextMessage[mbx];
                IF NOT msgExists THEN ERROR;
                IF deleted THEN LOOP;
                BEGIN
                  ENABLE UNWIND => PolicyDefs.EndOperation[remailing];
                  PolicyDefs.WaitOperation[remailing];
                  IF mbx.bodyObj.type = archived
                  THEN BEGIN
                       ENABLE SpillDefs.ServerCrash =>
                         { mbx.archOpen ← FALSE; GOTO inaccessible };
                       OpenArch[mbx];
                       HeapDefs.HeapEndWrite[
                         SpillDefs.RecoverBody[mbx.archFile,mbx.archCount],
                         RemailMessage];
                       EXITS inaccessible =>
                         { ERROR InaccessibleArchive[] };
                       END
                  ELSE RemailMessage[mbx.bodyObj];
                  DeleteMessage[mbx];
                  PolicyDefs.EndOperation[remailing];
                END;
             ENDLOOP;
             END
        ELSE LogAction[who,dead];
        FlushAndClose[mbx];
        END;
   PurgeFromTree[who];
   END;

s: STRING = "Birrell.pa"; --for use from the debugger--

LogAction: PROCEDURE[who: BodyDefs.RName,
                     action: {yes,no,nothing,arch,archFail,remail,dead},
                     data: CARDINAL ← 0] =
   BEGIN
   log: STRING = [128];
   String.AppendString[log, "Mailbox "L];
   String.AppendString[log, who];
   String.AppendString[log, SELECT action FROM
                                  nothing => ": already empty"L,
                                  no => ": abandoned reading"L,
                                  yes => ": emptied, "L,
                                  arch => ": archived "L,
                                  archFail => ": archive failed"L,
                                  remail => ": remailing"L,
                                  dead => ": dead"L,
                                ENDCASE => ERROR];
   SELECT action FROM
     yes, arch =>
       BEGIN
       String.AppendDecimal[log, data];
       String.AppendString[log, " messages"L];
       END;
   ENDCASE => NULL;
   LogDefs.WriteLogEntry[log];
   IF action = remail
   THEN { LogDefs.WriteString[" Remail "L]; LogDefs.WriteString[who] };
   END;

MailboxCount: PUBLIC PROC[str: EnquiryDefs.Handle] =
   BEGIN
   OPEN str;
   encountered: LONG CARDINAL ← 0;
   freePageCount: CARDINAL ← 0;
   WriteChar[Ascii.CR];
   FOR pageNumber: CARDINAL IN [0..nextVirginPage)
   DO page: VMDefs.Page = VMDefs.ReadPage[ [handle,pageNumber], 0];
      BEGIN
      ENABLE UNWIND => VMDefs.Release[page];
      header: POINTER TO MBXHeader = LOOPHOLE[page];
      IF header.first = header.free
      THEN freePageCount ← freePageCount+1
      ELSE BEGIN
           name: BodyDefs.RName = LOOPHOLE[page,POINTER]+SIZE[MBXHeader];
           count: CARDINAL =
                   (header.free-header.first)/SIZE[HeapDefs.ObjectNumber];
           objects: DESCRIPTOR FOR ARRAY OF HeapDefs.ObjectNumber =
                  DESCRIPTOR[ LOOPHOLE[page,POINTER]+header.first, count];
           tocCount: CARDINAL ← 0;
           delCount: CARDINAL ← 0;
           archCount: CARDINAL ← 0;
           WriteString[name];
           WriteChar['(]; WriteDecimal[header.number]; WriteChar[')];
           WriteString[": "L]; WriteDecimal[count];
           encountered ← encountered + count;
           WriteString[" messages"L];
           FOR this: CARDINAL IN [0..count)
           DO SELECT objects[this].type FROM
                TOC => tocCount ← tocCount+1;
                archived => archCount ← archCount+1;
                gap => delCount ← delCount+1;
              ENDCASE => NULL;
           ENDLOOP;
           IF tocCount # 0
           THEN{ WriteString["; "L];
                 WriteDecimal[tocCount]; WriteString[" TOC's"L] };
           IF delCount#0
           THEN{ WriteString["; "L];
                 WriteDecimal[delCount]; WriteString[" deleted"L] };
           IF archCount#0
           THEN{ WriteString["; "L];
                 WriteDecimal[archCount]; WriteString[" archived"L] };
           IF header.number = 0
           THEN BEGIN
                LookInTree: ENTRY PROC RETURNS[TreeRec] = INLINE
                   { RETURN[FindInTree[name]] };
                mbx: TreeRec = LookInTree[];
                WriteString["; "L];
                WITH tree:mbx SELECT FROM
                  empty => WriteString[" *** not in tree!"L];
                  found =>
                    IF tree.oldest = BodyDefs.oldestTime
                    THEN WriteString["no date"L]
                    ELSE BEGIN
                         t: STRING = [18];-- 29-Feb-99 24:23:22 --
                         Time.Append[t,
                                    Time.Unpack[LOOPHOLE[tree.oldest.time]] ];
                         WriteString[t];
                         END;
                ENDCASE => WriteString[" *** illegal tree entry"L];
                END;
           WriteChar[Ascii.CR];
           END;
      END;
      VMDefs.Release[page];
      IF DelTyped[] THEN GOTO del;
   ENDLOOP;
   WriteDecimal[freePageCount]; WriteString[" empty pages"L];
   WriteChar[Ascii.CR];
   WriteString["mailboxContents = "L];
   WriteLongDecimal[mailboxContents]; WriteChar[Ascii.CR];
--   WriteString["encountered = "L];
--   WriteLongDecimal[encountered]; WriteChar[Ascii.CR];
   EXITS del => { str.Flush[]; str.WriteString[" XXX"L] };
   END;

HistBox: TYPE =
   { min17, min53, min160, hr8, day1, day3, day9, longer, archived};

histInterval: ARRAY HistBox OF CARDINAL =
   [ (8*60)/27, (8*60)/9, (8*60)/3, 8*60, 3*8*60, 9*8*60, 27*8*60,
     LAST[CARDINAL], LAST[CARDINAL] ];

histCount: ARRAY HistBox OF CARDINAL ← ALL[0];
histTotal: LONG CARDINAL ← 0;

ConsiderDelay: ENTRY PROC[postmark: BodyDefs.Timestamp] =
   BEGIN
   now: LONG CARDINAL = Time.Current[];
   FOR box: HistBox IN [FIRST[HistBox]..longer)
   DO limit: LONG CARDINAL = now - LONG[60] * histInterval[box];
      IF postmark.time > limit
      THEN { histCount[box] ← histCount[box] + 1; EXIT };
   REPEAT
   FINISHED => histCount[longer] ← histCount[longer] + 1
   ENDLOOP;
   histTotal ← histTotal + 1;
   END;

CountArchived: ENTRY PROC = INLINE
   { histCount[archived] ← histCount[archived] + 1;
     histTotal ← histTotal + 1 };

Histograms: PUBLIC PROC[str: EnquiryDefs.Handle] =
   BEGIN
   OPEN str;
   histCaption: ARRAY HistBox OF STRING =
      [ "17 min: "L,
        "53 min: "L,
        "2.7 hr: "L,
        "8 hrs:  "L,
        "24 hrs: "L,
        "3 days: "L,
        "9 days: "L,
        "longer: "L,
        "archiv: "L ];
   WriteChar[Ascii.CR];
   WriteString["Mail retrieval delays"L];
   WriteChar[Ascii.CR];
   FOR box: HistBox IN HistBox
   DO WriteString[histCaption[box]]; WriteDecimal[histCount[box]];
      WriteChar[Ascii.TAB];
      IF histTotal = 0
      THEN WriteChar['?]
      ELSE WriteLongDecimal[(histCount[box]*LONG[100])/histTotal];
      WriteChar['%];
      WriteChar[Ascii.CR];
   ENDLOOP;
   WriteString["total:  "L]; WriteLongDecimal[histTotal];
   END;

LogDefs.DisplayNumber["Mailboxes"L, [short[@mailboxes]] ];

END.