-- Copyright (C) 1984  by Xerox Corporation. All rights reserved. 
-- BootServerFileTajo.mesa, HGM,  4-Jun-84 22:39:29 

DIRECTORY
  Ascii USING [CR],
  Checksum USING [ComputeChecksum],
  Environment USING [bytesPerPage, wordsPerPage],
  File USING [nullFile],
  Inline USING [LowHalf],
  MFile USING [
    AcquireTemp, AddNotifyProc, CopyFileHandle, Error, GetLength, GetProperties,
    GetTimes, Handle, ReadOnly, Release, ReleaseChoice, Rename, RemoveNotifyProc,
    SetAccess, SetTimes, SwapNames],
  MSegment USING [Address, Create, Handle, Delete],
  MStream USING [Copy, Create, GetLength, Handle, WriteOnly],
  Put USING [Text],
  Space USING [Map, nullInterval, Unmap],
  SpecialMFile USING [GetCapaWithAccess, LeaderPages],
  Stream USING [Block, Delete, GetBlock, Handle, PutBlock, SetPosition],
  String USING [AppendChar, AppendString, Equivalent],
  Time USING [Append, AppendCurrent, Current, Unpack],

  BootServer USING [AppendBFN],
  BootServerFriends USING [
    BootFile, EnumerateBootTable, pagesPerSwapUnit, ReleaseFile, timeNotKnown],
  PupWireFormat USING [
    BcplLongNumber, BcplToMesaLongNumber, MesaToBcplLongNumber];

BootServerFileTajo: MONITOR
  IMPORTS
    Checksum, Inline, MFile, MSegment, MStream, Put, Space, SpecialMFile, Stream,
    String, Time, BootServer, BootServerFriends, PupWireFormat
  EXPORTS BootServerFriends =
  BEGIN

  BootFile: TYPE = BootServerFriends.BootFile;
  FileHandle: PUBLIC TYPE = MFile.Handle;

  FileAlreadyActive: ERROR = CODE;
  FileNotLocked: ERROR = CODE;
  FileLocked: ERROR = CODE;
  FileBusy: ERROR = CODE;
  ConfusionInFlushOneSpace: ERROR = CODE;
  PageCountUnderflow: ERROR = CODE;

  pagesInVM: PUBLIC CARDINAL ← 0;
  maxPagesInVM: PUBLIC CARDINAL ← 4000; -- SmallTalk is using 3000 page boot files!

  ActivateFileSystem: PUBLIC PROCEDURE =
    BEGIN
    MFile.AddNotifyProc[Arrived, ["*", null, readOnly], NIL];
    MFile.AddNotifyProc[Died, ["*", null, writeOnly], NIL];
    END;

  DeactivateFileSystem: PUBLIC PROCEDURE =
    BEGIN
    MFile.RemoveNotifyProc[Arrived, ["*", null, readOnly], NIL];
    MFile.RemoveNotifyProc[Died, ["*", null, writeOnly], NIL];
    END;

  ActivateFile: PUBLIC ENTRY PROCEDURE [bf: BootFile] =
    BEGIN
    IF bf.handle # NIL THEN ERROR FileAlreadyActive;
    bf.handle ← MFile.ReadOnly[
      bf.fileName, [PleaseReleaseFile, bf] ! MFile.Error => CONTINUE];
    IF bf.handle = NIL THEN RETURN;
    BlessBootFile[bf];
    IF bf.handle = NIL THEN RETURN;
    bf.unknown ← FALSE;
    bf.file ← SpecialMFile.GetCapaWithAccess[bf.handle];
    bf.bytes ← MFile.GetLength[bf.handle];
    bf.pages ← Inline.LowHalf[
      (bf.bytes + Environment.bytesPerPage - 1)/Environment.bytesPerPage];
    END;

  BlessBootFile: PROCEDURE [bf: BootServerFriends.BootFile] =
    BEGIN
    IF bf.pup THEN BEGIN BlessPupBootFile[bf]; RETURN; END;
    IF ~TestFileChecksum[bf.handle] THEN
      BEGIN
      text: STRING = [100];
      Time.AppendCurrent[text];
      String.AppendString[text, "  "L];
      String.AppendString[text, bf.fileName];
      String.AppendString[text, " has a bad file checksum"L];
      LogString[text];
      MFile.Release[bf.handle];
      bf.handle ← NIL;
      bf.file ← File.nullFile;
      bf.unknown ← TRUE;
      RETURN;
      END;
    bf.create ← MFile.GetTimes[bf.handle].create;
    END;

  DeactivateFile: PUBLIC ENTRY PROCEDURE [bf: BootFile] =
    BEGIN
    IF bf.space # Space.nullInterval THEN ERROR FileBusy;
    IF bf.useCount # 0 THEN ERROR FileBusy;
    IF bf.handle = NIL THEN RETURN;
    bf.file ← File.nullFile;
    MFile.Release[bf.handle];
    bf.handle ← NIL;
    END;

  ReadStreamFromBootFile: PUBLIC ENTRY PROCEDURE [bf: BootFile]
    RETURNS [Stream.Handle] =
    BEGIN
    IF bf.useCount = 0 THEN ERROR FileNotLocked;
    RETURN[
      MStream.Create[
        MFile.CopyFileHandle[bf.handle, []], [PleaseReleaseStream, bf]]];
    END;

  SetupSpace: PUBLIC ENTRY PROCEDURE [bf: BootFile] =
    BEGIN
    IF bf.useCount = 0 THEN ERROR FileNotLocked;
    IF bf.space # Space.nullInterval THEN RETURN;
    UNTIL (pagesInVM + bf.pages) < maxPagesInVM DO
      IF pagesInVM = 0 THEN EXIT; -- In case SmallTalk gets even bigger!!
      FlushOneSpace[bf];
      ENDLOOP;
    bf.space ← Space.Map[
      window: [bf.file, SpecialMFile.LeaderPages[], bf.pages],
      class: file,
      access: readOnly,
      life: alive,
      swapUnits: [uniform[BootServerFriends.pagesPerSwapUnit]] ];
    pagesInVM ← pagesInVM + bf.pages;
    END;

  FlushOneSpace: INTERNAL PROCEDURE [first: BootFile] =
    BEGIN
    hit: BootFile ← NIL;
    FOR bf: BootFile ← first, bf.next UNTIL bf = NIL DO
      IF bf.useCount > 0 THEN LOOP;
      IF bf.space # Space.nullInterval THEN hit ← bf;
      ENDLOOP;
    IF hit = NIL THEN ERROR ConfusionInFlushOneSpace;
    KillSpaceInternal[hit];
    END;

  KillSpace: PUBLIC ENTRY PROCEDURE [bf: BootFile] =
    BEGIN IF bf.useCount # 0 THEN ERROR FileLocked; KillSpaceInternal[bf]; END;

  KillSpaceInternal: INTERNAL PROCEDURE [bf: BootFile] =
    BEGIN
    IF bf.pages > pagesInVM THEN ERROR PageCountUnderflow;
    [] ← Space.Unmap[bf.space.pointer, return];
    bf.space ← Space.nullInterval;
    pagesInVM ← pagesInVM - bf.pages;
    END;


  -- Routines used to receive new boot files from other serers

  CreateTempFile: PUBLIC PROCEDURE RETURNS [fh: MFile.Handle, sh: Stream.Handle] =
    BEGIN
    fh ← NIL;  -- In case volume full (or whatever)
    sh ← NIL;
    fh ← MFile.AcquireTemp[binary, 200*LONG[Environment.bytesPerPage] ! MFile.Error => CONTINUE];
    IF fh = NIL THEN RETURN;
    sh ← MStream.Create[MFile.CopyFileHandle[fh, []], []];
    END;

  MakeTempFileIntoBootFile: PUBLIC PROCEDURE [bf: BootFile, fh: MFile.Handle]
    RETURNS [ok: BOOLEAN] =
    BEGIN
    -- ** Check to see if we like it
    ok ← TRUE;
    IF TRUE THEN  -- BUGS AND KROCKS   ********************
      BEGIN
      to, from: Stream.Handle;
      to ← MStream.WriteOnly[bf.fileName, [], binary];
      from ← MStream.Create[MFile.CopyFileHandle[fh, [], readOnly], []];
      [] ← MStream.Copy[from: from, to: to, bytes: LAST[LONG CARDINAL]];
      Stream.Delete[from];
      Stream.Delete[to];
      MFile.Release[fh];
      RETURN;
      END;
    MFile.SetAccess[fh, rename];
    IF bf.handle = NIL THEN
      BEGIN
      MFile.Rename[fh, bf.fileName]  -- This doesn't work yet....
      END
    ELSE
      BEGIN
      old: MFile.Handle ← MFile.CopyFileHandle[bf.handle, []];
      MFile.SetAccess[
        old, rename ! MFile.Error => BEGIN ok ← FALSE; CONTINUE; END];
      IF ok THEN MFile.SwapNames[fh, old];
      MFile.Release[old];
      END;
    MFile.Release[fh];
    END;

  DeleteTempFile: PUBLIC PROCEDURE [fh: MFile.Handle] =
    BEGIN MFile.Release[fh]; END;


  -- Routines to process files retrieved via FTP or stored via FTPServe
  -- (also used indirectly if we retrieve it ourselves)
  -- Beware, these should not use the same LOCK or things will get tangled

  PleaseReleaseFile: PROCEDURE [file: MFile.Handle, instanceData: BootFile]
    RETURNS [MFile.ReleaseChoice] =
    BEGIN
    BEGIN
    name: STRING = [100];
    [] ← MFile.GetProperties[file, name];
    Hack[name, "ReleaseFile"L];
    END;
    BootServerFriends.ReleaseFile[instanceData];
    RETURN[goAhead];
    END;

  PleaseReleaseStream: PROCEDURE [stream: MStream.Handle, instanceData: BootFile]
    RETURNS [MFile.ReleaseChoice] =
    BEGIN
    BEGIN
    name: STRING = [100];
    [] ← MFile.GetProperties[instanceData.handle, name];
    Hack[name, "ReleaseStream"L];
    END;
    RETURN[later];
    END;

  Arrived: PROCEDURE [
    name: LONG STRING, file: MFile.Handle, clientInstanceData: LONG POINTER]
    RETURNS [BOOLEAN] =
    BEGIN
    Check: PROCEDURE [bf: BootFile] =
      BEGIN
      IF ~bf.inTransit AND ~bf.unknown THEN RETURN;
      IF ~String.Equivalent[name, bf.fileName] THEN RETURN;
      ActivateFile[bf];
      bf.inTransit ← FALSE;
      IF bf.handle = NIL THEN RETURN;
      AnnounceArrival[bf];
      END;
    BootServerFriends.EnumerateBootTable[Check];
    RETURN[FALSE];
    END;

  AnnounceArrival: PROCEDURE [bf: BootFile] =
    BEGIN
    text: STRING = [150];
    Time.AppendCurrent[text];
    String.AppendString[text, "  "L];
    String.AppendString[text, bf.fileName];
    String.AppendString[text, " (#"L];
    BootServer.AppendBFN[text, bf.code];
    String.AppendString[text, ") was created on "L];
    IF bf.create = BootServerFriends.timeNotKnown THEN
      String.AppendString[text, "???"L]
    ELSE Time.Append[text, Time.Unpack[bf.create]];
    LogString[text];
    END;

  Died: PROCEDURE [
    name: LONG STRING, file: MFile.Handle, clientInstanceData: LONG POINTER]
    RETURNS [BOOLEAN] =
    BEGIN
    Check: PROCEDURE [bf: BootFile] =
      BEGIN
      IF ~bf.inTransit THEN RETURN;
      IF ~String.Equivalent[name, bf.fileName] THEN RETURN;
      bf.inTransit ← FALSE;
      END;
    IF file = NIL THEN
      BEGIN  -- File was deleted
      BootServerFriends.EnumerateBootTable[Check];
      END
    ELSE NULL;  -- File exists, Arrived will notice it
    RETURN[FALSE];
    END;

  Hack: PROCEDURE [name: LONG STRING, s: STRING] =
    BEGIN Message[s, " "L, name]; END;

  -- Leftover Pup stuff

  --For more on boot files see the BuildBoot documentation

  -- Boot files created after late Dec 78 have a time stamp stored in words 3+4.
  -- OutLd leaves it zero.  GateControl smashes it to "now".

  --There are two kinds of boot file.

  --B-Files produced by BuildBoot.run:
  --	File page 1:	DiskBoot loader
  --	File page 2:	locations #0-#377
  --	File page 3:	locations #1000 - #1377
  --	File page 4:	locations #1400 - #1777
  --	...
  --	File page n:	locations #(n-1)B7 - #(n-1)B7+#377
  --B-Files have 0 in the second data word
  --B-Files are started by jmp @0 when loading is complete

  --S-Files produced by Swat OutLd:
  --	File page 1:	Special loader
  --	File page 2:	locations #1000 - #1377
  --	File page 3:	locations #1400 - #1777
  --	...
  --	File page 253:	locations #176400-176777
  --	File page 254:	locations #400 - #777
  --	File page 255:	locations #0 - #377
  --S-Files have a non-zero value in the second data word
  --Some S-Files can be started by jmp @0.
  --This is the kind we use, but we re-format them first

  BootFileHeader: TYPE = RECORD [  -- disk boot loader is first page
    other: ARRAY [0..3) OF WORD, timeStamp: PupWireFormat.BcplLongNumber];

  BlessPupBootFile: PROCEDURE [bf: BootServerFriends.BootFile] =
    BEGIN
    old, new: MStream.Handle ← NIL;
    buffer: ARRAY [0..Environment.wordsPerPage) OF WORD;
    finger: Stream.Block = [LOOPHOLE[LONG[@buffer]], 0, Environment.bytesPerPage];
    bfh: POINTER TO BootFileHeader = LOOPHOLE[@buffer];
    old ← MStream.Create[MFile.CopyFileHandle[bf.handle, []], []];
    BEGIN
    bytes: CARDINAL;
    [bytes, ] ← Stream.GetBlock[old, finger];
    IF bytes # Environment.bytesPerPage THEN GOTO Empty;
    bf.bytes ← MStream.GetLength[old];
    bf.pages ← Inline.LowHalf[
      (bf.bytes + Environment.bytesPerPage - 1)/Environment.bytesPerPage];
    bf.create ← LOOPHOLE[PupWireFormat.BcplToMesaLongNumber[bfh.timeStamp]];
    IF bf.create # MFile.GetTimes[bf.handle].create THEN
      BEGIN
      text: STRING = [200];
      Time.AppendCurrent[text];
      String.AppendString[text, "  Fixing Create Time for "L];
      String.AppendString[text, bf.fileName];
      String.AppendString[text, ".  Was: "L];
      Time.Append[text, Time.Unpack[MFile.GetTimes[bf.handle].create]];
      String.AppendString[text, ",  Will be: "L];
      Time.Append[text, Time.Unpack[bf.create]];
      LogString[text];
      MFile.SetTimes[file: bf.handle, create: bf.create];
      END;
    IF buffer[1] # 0 THEN
      BEGIN  -- fixup S-format file
      text: STRING = [100];
      scratch: MFile.Handle ← MFile.AcquireTemp[
        binary, bf.pages ! MFile.Error => GOTO DiskFull];
      MFile.SetTimes[file: scratch, create: bf.create];
      Stream.Delete[old];
      Time.AppendCurrent[text];
      String.AppendString[text, "  Reformatting "L];
      String.AppendString[text, bf.fileName];
      LogString[text];
      old ← MStream.Create[MFile.CopyFileHandle[bf.handle, [], readOnly], []];
      new ← MStream.Create[MFile.CopyFileHandle[scratch, [], writeOnly], []];
      [] ← MStream.Copy[from: old, to: new, bytes: LAST[LONG CARDINAL]];
      Stream.Delete[new];
      Stream.Delete[old];
      new ← MStream.Create[MFile.CopyFileHandle[scratch, [], readOnly], []];
      old ← MStream.Create[MFile.CopyFileHandle[bf.handle, [], writeOnly], []];
      [bytes, ] ← Stream.GetBlock[new, finger];
      IF bytes # Environment.bytesPerPage THEN GOTO Empty;
      buffer[1] ← 0;
      BEGIN  -- smash crufty time stamp to now
      -- Note that our clock may be wrong at this point.  If it is fast, other boot servers will ignore our file.  If it is slow, we will go get a new boot file.  I think that will work out ok.
      bfh.timeStamp ← PupWireFormat.MesaToBcplLongNumber[Time.Current[]];
      bf.create ← LOOPHOLE[PupWireFormat.BcplToMesaLongNumber[bfh.timeStamp]];
      END;
      Stream.PutBlock[old, finger];  -- garbage DiskBoot loader
      THROUGH [2..255] DO
        [bytes, ] ← Stream.GetBlock[new, finger];
        IF bytes # Environment.bytesPerPage THEN GOTO Short;
        ENDLOOP;
      Stream.PutBlock[old, finger];  -- locations 0B to 377B
      Stream.SetPosition[new, Environment.bytesPerPage];
      THROUGH [2..253] DO
        [] ← Stream.GetBlock[new, finger]; Stream.PutBlock[old, finger]; ENDLOOP;
      Stream.Delete[new];
      -- It may have shrunk if there was trash on the end of Swatee when creating a Mesa boot file
      Stream.Delete[old];  -- Truncate file
      old ← MStream.Create[MFile.CopyFileHandle[bf.handle, []], []];
      bf.bytes ← MStream.GetLength[old];
      bf.pages ← Inline.LowHalf[
        (bf.bytes + Environment.bytesPerPage - 1)/Environment.bytesPerPage];
      END;
    EXITS
      DiskFull =>
        BEGIN
        text: STRING = [100];
        Time.AppendCurrent[text];
        String.AppendString[text, "  Disk full while copying over "L];
        String.AppendString[text, bf.fileName];
        LogString[text];
        MFile.Release[bf.handle];
        bf.handle ← NIL;
        bf.file ← File.nullFile;
        bf.unknown ← TRUE;
        END;
      Empty,
      Short =>
        BEGIN
        text: STRING = [100];
        Time.AppendCurrent[text];
        String.AppendString[text, "  "L];
        String.AppendString[text, bf.fileName];
        IF new = NIL THEN String.AppendString[text, " is empty."L]
        ELSE String.AppendString[text, " is SHORT.   ******"L];
        LogString[text];
        IF new # NIL THEN Stream.Delete[new];
        MFile.Release[bf.handle];
        bf.handle ← NIL;
        bf.file ← File.nullFile;
        bf.unknown ← TRUE;
        END;
    END;
    Stream.Delete[old];
    END;

  Message: PROCEDURE [s1, s2, s3, s4: LONG STRING ← NIL] =
    BEGIN
    text: STRING = [200];
    String.AppendString[text, "BootServer: "L];
    String.AppendString[text, s1];
    IF s2 # NIL THEN String.AppendString[text, s2];
    IF s3 # NIL THEN String.AppendString[text, s3];
    IF s4 # NIL THEN String.AppendString[text, s4];
    LogString[text];
    END;

  LogString: PROCEDURE [text: LONG STRING] =
    BEGIN
    String.AppendChar[text, '.];
    String.AppendChar[text, Ascii.CR];
    Put.Text[NIL, text];
    END;


  TestFileChecksum: PROCEDURE [file: MFile.Handle] RETURNS [ok: BOOLEAN] =
    BEGIN
    seg: MSegment.Handle;
    words: LONG CARDINAL;
    buffer: LONG POINTER;
    cs: WORD;
    words ← (MFile.GetLength[file] + 1)/2;
    seg ← MSegment.Create[MFile.CopyFileHandle[file, []], [], 0];
    buffer ← MSegment.Address[seg];
    BEGIN
    loc: LONG POINTER ← buffer + words - 1;
    IF words > 100*Environment.wordsPerPage THEN
      BEGIN  -- 64K limit on microcode
      hunk: CARDINAL ← 20*Environment.wordsPerPage;
      finger: LONG POINTER ← buffer;
      left: LONG CARDINAL ← words;
      cs ← 0;
      UNTIL left = 0 DO
        IF left < hunk THEN hunk ← Inline.LowHalf[left];
        cs ← Checksum.ComputeChecksum[cs, hunk, finger];
        left ← left - hunk;
        finger ← finger + hunk;
        ENDLOOP;
      END
    ELSE cs ← Checksum.ComputeChecksum[0, Inline.LowHalf[words], buffer];
    ok ← cs = 0;
    END;
    MSegment.Delete[seg];
    END;


  END.