-- GateFTPServer.mesa,  Edit: HGM March 20, 1981  6:39 PM
-- Please don't forget to update the herald....

DIRECTORY
  Process USING [Detach],
  Runtime USING [IsBound],
  Storage USING [CopyString, Free, FreeString, FreeStringNil, Node],
  String USING [
    AppendChar, AppendString, AppendDecimal, AppendNumber, EquivalentString,
    InvalidNumber, StringToOctal, SubString, SubStringDescriptor],
  Time USING [AppendCurrent],

  CmFile USING [Close, GetNextToken, NextItem, OpenSection],
  Event USING [Item, Reason, AddNotifier],
  Put USING [Line],
  Tool USING [Create, MakeSWsProc, MakeMsgSW],
  ToolWindow USING [TransitionProcType],
  Window USING [Handle],

  File USING [Capability, nullCapability],
  George USING [DeleteFileFromDisk, LookupExistingFile, NameToCapability],
  Indirect USING [GetParmFileName],
  Lock USING [LockDiskAndWait, UnlockDisk],
  Password USING [Check, Encrypted, Status, ValidMemberOfGroup],
  GateDefs USING [GetVersionText],
  Slosh USING [
    AddProcs, Arrived, Check, CopyFile, Failed, RecvStatus, RejectThisTrash,
    Release, Why],
  FTPDefs,
  StatsDefs USING [StatCounterIndex, StatIncr, StatsStringToIndex],
  PupDefs;

GateFTPServer: MONITOR
  IMPORTS
    Process, Runtime, Storage, String, Time, -- Mesa/Pilot
    CmFile, Event, Put, Tool, -- Tools
    George, Indirect, Lock, Password, GateDefs, Slosh, StatsDefs, FTPDefs,
    PupDefs =
  BEGIN OPEN FTPDefs;

  herald: STRING = "Gateway FTP Server of March 20, 1981";

  ftplistener: FTPListener;

  jimsFilePrimitives: FilePrimitives = SomeFilePrimitives[];

  myFilePrimitives: FilePrimitivesObject ←
    [CreateFileSystem: jimsFilePrimitives.CreateFileSystem,
      DestroyFileSystem: jimsFilePrimitives.DestroyFileSystem,
      DecomposeFilename: jimsFilePrimitives.DecomposeFilename,
      ComposeFilename: jimsFilePrimitives.ComposeFilename,
      InspectCredentials: InspectCredentials,
      EnumerateFiles: jimsFilePrimitives.EnumerateFiles, OpenFile: OpenFile,
      CloseFile: CloseFile, DeleteFile: DeleteFile, RenameFile: RenameFile,
      ReadFile: jimsFilePrimitives.ReadFile,
      WriteFile: jimsFilePrimitives.WriteFile];

  defaultRegistry: STRING ← NIL;
  myGroup: STRING ← NIL;

  firstPassword: POINTER TO PasswordBlock ← NIL;
  PasswordBlock: TYPE = RECORD [
    next: POINTER TO PasswordBlock, user: STRING, encrypted: Password.Encrypted];

  maxServers: CARDINAL = 2;
  servers: CARDINAL ← 0;
  scratchFileNumber: CARDINAL ← 0;

  tool, msg: Window.Handle ← NIL;
  eventItem: Event.Item ← [eventMask: 177777B, eventProc: Broom];
  useCount: CARDINAL ← 0;
  pleaseStop, running: BOOLEAN ← FALSE;

  filesReceived: StatsDefs.StatCounterIndex = StatsDefs.StatsStringToIndex[
    "Files Received by FTP Server"];
  filesSent: StatsDefs.StatCounterIndex = StatsDefs.StatsStringToIndex[
    "Files Sent by FTP Server"];

  FTPServerOn: PUBLIC ENTRY PROCEDURE =
    BEGIN
    IF (useCount ← useCount + 1) = 1 THEN BEGIN running ← TRUE; Starter[]; END;
    UpdatePicture[];
    END;

  Starter: PROCEDURE =
    BEGIN
    backstopServer: BackstopServer ← MyBackstopServer;
    pleaseStop ← FALSE;
    Announce["Starting "L, herald];
    Findparameters[];
    FTPInitialize[];
    FTPSetBufferSize[1]; -- saves memory
    FTPCatchUnidentifiedErrors[FALSE]; -- Krock for GetUserAddress
    ftplistener ← FTPCreateListener[
      files, @myFilePrimitives, NIL, PupCommunicationPrimitives[],
      @backstopServer, NIL, RejectExtras];
    END;

  Findparameters: PROCEDURE =
    BEGIN
    parmFileName: STRING ← NIL;
    sectionName: STRING = "FTPServer"L;
    token, arg: STRING ← NIL;
    IF Runtime.IsBound[Indirect.GetParmFileName] THEN
      parmFileName ← Indirect.GetParmFileName[];
    IF parmFileName = NIL THEN parmFileName ← "FTPServer.txt"L;
    myGroup ← Storage.CopyString[parmFileName];
    FOR i: CARDINAL DECREASING IN [0..myGroup.length) DO
      IF myGroup[i] = '. THEN myGroup.length ← i; ENDLOOP;
    BEGIN
    builtin: ARRAY [0..8) OF WORD =
      [172427B, 11553B, 61543B, 113154B, 161500B, 47164B, 173546B, 120250B];
    firstPassword ← Storage.Node[SIZE[PasswordBlock]];
    firstPassword↑ ← [NIL, Storage.CopyString["Magic"L], LOOPHOLE[builtin]];
    END;
    IF ~CmFile.OpenSection[parmFileName, sectionName] THEN RETURN;
    DO
      [token, arg] ← CmFile.NextItem[];
      SELECT TRUE FROM
	token = NIL => EXIT;
	token.length > 0 AND token[0] = '; => LOOP;
	String.EquivalentString[token, "My Group"L] =>
	  BEGIN
	  Storage.FreeString[myGroup];
	  myGroup ← arg;
	  arg ← NIL;
	  Message["My Group for FTP passwords is "L, myGroup];
	  END;
	String.EquivalentString[token, "Default Registry"L] =>
	  BEGIN
	  Storage.FreeString[defaultRegistry];
	  defaultRegistry ← arg;
	  arg ← NIL;
	  Message["The Default Registry for FTP passwords is "L, defaultRegistry];
	  END;
	String.EquivalentString[token, "Password"L] =>
	  BEGIN
	  user: STRING = [50];
	  pwd: STRING = [8*8];
	  e: Password.Encrypted;
	  p: POINTER ← @e;
	  ss: String.SubStringDescriptor;
	  temp: POINTER TO PasswordBlock;
	  IF arg # NIL THEN ss ← [base: arg, offset: 0, length: arg.length];
	  [] ← CmFile.GetNextToken[@ss, user];
	  FOR i: CARDINAL IN [0..SIZE[Password.Encrypted]) DO
	    (p + i)↑ ← GetOctal[@ss]; ENDLOOP;
	  FOR i: CARDINAL IN [0..SIZE[Password.Encrypted]) DO
	    IF i # 0 THEN String.AppendChar[pwd, ' ];
	    String.AppendNumber[pwd, (p + i)↑, 8];
	    ENDLOOP;
	  temp ← Storage.Node[SIZE[PasswordBlock]];
	  temp↑ ← [firstPassword, Storage.CopyString[user], e];
	  firstPassword ← temp;
	  Message["FTP's Password for "L, user, " is "L, pwd];
	  END;
	ENDCASE => BEGIN Message["Unknown keyword: "L, token]; END;
      Storage.FreeString[token];
      Storage.FreeString[arg];
      ENDLOOP;
    CmFile.Close[parmFileName];
    END;

  GetOctal: PROCEDURE [ss: String.SubString] RETURNS [CARDINAL] =
    BEGIN
    token: STRING = [100];
    BEGIN
    ENABLE String.InvalidNumber => Message["Octal number expected."L, token];
    IF ~CmFile.GetNextToken[ss, token] THEN SIGNAL String.InvalidNumber;
    RETURN[String.StringToOctal[token]];
    END;
    END;

  Forgetparameters: PROCEDURE =
    BEGIN
    UNTIL firstPassword = NIL DO
      temp: POINTER TO PasswordBlock ← firstPassword;
      firstPassword ← firstPassword.next;
      Storage.FreeString[temp.user];
      Storage.Free[temp];
      ENDLOOP;
    defaultRegistry ← Storage.FreeStringNil[defaultRegistry];
    myGroup ← Storage.FreeStringNil[myGroup];
    END;

  FTPServerOff: PUBLIC ENTRY PROCEDURE =
    BEGIN
    IF useCount # 0 AND (useCount ← useCount - 1) = 0 THEN
      BEGIN running ← FALSE; Stopper[]; END;
    END;

  StopperLocked: ENTRY PROCEDURE = INLINE { Stopper[] };
  Stopper: INTERNAL PROCEDURE =
    BEGIN
    pause: CONDITION;
    pleaseStop ← TRUE;
    UNTIL servers = 0 DO WAIT pause; ENDLOOP;
    FTPFinalize[];
    FTPDestroyListener[ftplistener, FALSE];
    Forgetparameters[];
    Announce["Killed "L, herald];
    END;

  UpdatePicture: PROCEDURE = BEGIN END;

  RejectExtras: PROCEDURE [who: STRING, what: Purpose] =
    BEGIN
    IF pleaseStop THEN
      ERROR FTPDefs.RejectThisConnection["Sorry, we are trying to go away"];
    IF servers = maxServers THEN
      ERROR FTPDefs.RejectThisConnection["Sorry, we are full now"];
    servers ← servers + 1;
    END;

  -- krock that should be in the interface

  GetUserAddress: SIGNAL RETURNS [STRING] = CODE;
  GetUserName: SIGNAL RETURNS [STRING] = CODE;
  SetPasswordOK: SIGNAL [code: FTPDefs.FtpError, user, reason: STRING] = CODE;
  CheckPassword: SIGNAL [what: STRING] = CODE;

  MyBackstopServer: PUBLIC PROCEDURE [
    backstopServerData: UNSPECIFIED, purpose: SingularPurpose,
    originOfRequest, localInsignia: STRING, server: PROCEDURE] =
    BEGIN
    him: PupDefs.PupAddress ← [, , [0, 0]];
    userAddress: STRING = [40];
    userName: STRING ← Storage.CopyString["??"L];
    rejectReason: STRING ← Storage.CopyString["??"L];
    why: FTPDefs.FtpError ← credentialsMissing;
    IF ~PupDefs.ParsePupAddressConstant[@him, originOfRequest] THEN ERROR;
    PupDefs.AppendHostName[userAddress, him];
    Announce["Creating FTP server for "L, userAddress];
    String.AppendString[localInsignia, " on "L];
    String.AppendString[localInsignia,GateDefs.GetVersionText[]];
    server[
      ! GetUserAddress => RESUME [userAddress];
      GetUserName => RESUME [userName];
      SetPasswordOK =>
	BEGIN
	IF userName # NIL THEN Storage.FreeString[userName];
	userName ← Storage.CopyString[user];
        IF reason#NIL THEN
          BEGIN
          IF rejectReason # NIL THEN Storage.FreeString[rejectReason];
          rejectReason ← Storage.CopyString[reason];
          END;
	why ← code;
	RESUME;
	END;
      CheckPassword =>
	BEGIN
        IF why#ok THEN
	  BEGIN
          AnnounceReject[what, userName, userAddress, rejectReason];
          FTPDefs.FTPError[why, rejectReason];
	  END;
        RESUME;
	END;
      UNWIND =>
	BEGIN
	Announce["Aborting FTP server for "L, userAddress];
	servers ← servers - 1;
	END;
      FTPError =>
	BEGIN
	SELECT ftpError FROM
	  IN CommunicationError, IN ProtocolError => CONTINUE;
	  IN UnidentifiedError => NULL;
	  ENDCASE => RESUME;
	END];
    Storage.FreeString[userName];
    Announce["Killing FTP server for "L, userAddress];
    servers ← servers - 1;
    END;

  InspectCredentials: PROCEDURE [
    fileSystem: FileSystem, status: Status, user, password: STRING] =
    BEGIN
    IF user=NIL OR password=NIL THEN
      BEGIN
      SetPasswordOK[credentialsMissing, user, "Name or Password missing"L];
      RETURN;
      END;
    -- check passwords from parameter file
    FOR finger: POINTER TO PasswordBlock ← firstPassword, finger.next UNTIL finger
      = NIL DO
      IF String.EquivalentString[finger.user, user] THEN
	BEGIN
        matched: BOOLEAN ← Password.Check[password, finger.encrypted];
	IF matched THEN SetPasswordOK[ok, user, "OK"L]
        ELSE
          SetPasswordOK[incorrectPrimaryPassword, user, "Password rejected by info from parameter file"L];
	RETURN;
	END;
      ENDLOOP;
    -- try Grapevine if that fails
    IF myGroup = NIL THEN
      SetPasswordOK[requestedAccessDenied, user, "Don't know our Grapevine group"L]
    ELSE
      BEGIN
      person: STRING ← MaybeAppend[user, defaultRegistry];
      machine: STRING ← MaybeAppend[myGroup, "internet"L];
      status: Password.Status;
      status ← Password.ValidMemberOfGroup[person, password, machine];
      SELECT status FROM
        yes => SetPasswordOK[ok, person, NIL];
        nil => SetPasswordOK[credentialsMissing, person, "Confusion about NIL"L];
        allDown => SetPasswordOK[requestedAccessDenied, person, "All Grapevine servers appear to be down"L];
        notFound => SetPasswordOK[noSuchPrimaryUser, person, "Grapevine doesn't like your name"L];
        badPwd => SetPasswordOK[incorrectPrimaryPassword, person, "Grapevine doesn't like your password"L];
        group => SetPasswordOK[credentialsMissing, person, "Grapevine thinks you are a group"L];
        no => SetPasswordOK[requestedAccessDenied, person, "You are not in the appropiate group"L];
        notGroup => SetPasswordOK[requestedAccessDenied, person, "Grapevine doesn't recognize this machine's group"L];
        error => SetPasswordOK[requestedAccessDenied, person, "Error from GrapevineUser package"L];
        ENDCASE => ERROR;
      Storage.FreeString[machine];
      Storage.FreeString[person];
      END;
    END;

  MaybeAppend: PROCEDURE [string, tail: STRING] RETURNS [new: STRING] =
    BEGIN
    IF tail = NIL THEN RETURN[Storage.CopyString[string]];
    FOR i: CARDINAL IN [0..string.length) DO
      IF string[i] = '. THEN RETURN[Storage.CopyString[string]]; ENDLOOP;
    new ← Storage.CopyString[string, 1 + tail.length];
    String.AppendChar[new, '.];
    String.AppendString[new, tail];
    END;


  -- There are 3 interesting cases for OpenFile+CloseFile
  --  1) read:  oldFile#NIL, scratchFile=NIL, write=FALSE
  --  2) write:  oldFile#NIL, scratchFile#NIL, write=TRUE
  --  3) new:  oldFile=NIL, scratchFile#NIL, write=TRUE
  -- New is like write except the copyover is avoided

  -- We should only see modes read and write

  OpenFile: PROCEDURE [
    fileSystem: FileSystem, file: STRING, mode: Mode, fileTypePlease: BOOLEAN,
    info: FileInfo] RETURNS [fileHandle: FileHandle, fileType: FileType] =
    BEGIN
    write: BOOLEAN ←
      SELECT mode FROM read => FALSE, write => TRUE, ENDCASE => ERROR;
    scratchName: STRING = [20];
    oldFile, scratchFile: File.Capability ← File.nullCapability;
    IF write THEN
      BEGIN OPEN String;
      CheckPassword["store"L];
      IF String.EquivalentString[file, "Gateway.image"L] THEN
	file ← "NewGateway.image"L;
      AppendString[scratchName, "FTP-scratch"L];
      AppendDecimal[scratchName, (scratchFileNumber ← scratchFileNumber + 1)];
      AppendChar[scratchName, '$];
      END;
    -- No Read protection
    BEGIN
    oldFile ← George.LookupExistingFile[file];
    [fileHandle, fileType] ← jimsFilePrimitives.OpenFile[
      fileSystem,
      IF write AND oldFile # File.nullCapability THEN scratchName ELSE file, mode,
      fileTypePlease, info];
    IF write THEN
      scratchFile ← George.NameToCapability[
	IF oldFile = File.nullCapability THEN file ELSE scratchName, 100];
    END;
    AssignSlot[file, scratchName, oldFile, scratchFile, write, fileHandle];
    Lock.LockDiskAndWait[file, IF write THEN write ELSE read];
    IF write THEN AnnounceOp["Receiving "L, file, " from "L]
    ELSE AnnounceOp["Sending "L, file, " to "L];
    END;

  CloseFile: PROCEDURE [
    fileSystem: FileSystem, fileHandle: FileHandle, aborted: BOOLEAN] =
    BEGIN
    oldFile, scratchFile: File.Capability;
    fileName: STRING;
    trouble, rejected, write: BOOLEAN ← FALSE;
    slot: Slot ← FindSlot[fileHandle];
    oldFile ← slot.oldFile;
    write ← slot.write;
    scratchFile ← slot.scratchFile;
    fileName ← slot.oldName;
    IF aborted THEN scratchFile ← File.nullCapability; -- Jim deletes the file
    MarkSlotClosed[slot];
    jimsFilePrimitives.CloseFile[fileSystem, fileHandle, aborted];
    IF ~aborted AND write THEN
      BEGIN
      Slosh.Check[fileName, scratchFile ! Slosh.RejectThisTrash => GOTO Rejected];
      IF oldFile = File.nullCapability THEN
	BEGIN -- new, don't delete the scratch file because it is the real thing
	Slosh.Arrived[fileName, scratchFile];
	scratchFile ← File.nullCapability;
	END
      ELSE
	BEGIN
	Slosh.Release[fileName, oldFile];
	trouble ← ~CopyFile[oldFile, scratchFile];
	-- Delete the scratch copy before calling Arrived to avoid having a third copy on the disk if the BootServer decides to reformat it.
	George.DeleteFileFromDisk[scratchFile];
	scratchFile ← File.nullCapability;
	IF ~trouble THEN Slosh.Arrived[fileName, oldFile]
	ELSE Slosh.Failed[fileName, oldFile]
	END;
      EXITS Rejected => rejected ← TRUE;
      END;
    IF scratchFile # File.nullCapability THEN
      George.DeleteFileFromDisk[scratchFile];
    Lock.UnlockDisk[fileName];
    FreeSlot[slot];
    BEGIN
    ENABLE UNWIND => Storage.FreeString[fileName];
    IF aborted AND write THEN
      AnnounceOp["Trouble writing "L, fileName, " for "L];
    IF rejected THEN AnnounceOp["Rejecting"L, fileName, " from "L];
    IF rejected THEN FTPError[fileDataError, "New file rejected"L];
    IF trouble THEN
      AnnounceOp[
	"Disk filled up while copying into "L, fileName, " for "L];
    IF trouble THEN FTPError[noRoomForFile, "Disk filled up during copyover"L];
    END;
    StatsDefs.StatIncr[IF write THEN filesReceived ELSE filesSent];
    Storage.FreeString[fileName];
    END;

  DeleteFile: PROCEDURE [fileSystem: FileSystem, file: STRING] =
    BEGIN
    CheckPassword["delete"L];
    AnnounceOp["Deleting "L, file, " for "L];
    jimsFilePrimitives.DeleteFile[
      fileSystem, file !
      UNWIND => AnnounceOp["Failed deleting"L, file, " for "L]];
    END;

  RenameFile: PROCEDURE [fileSystem: FileSystem, currentFile, newFile: STRING] =
    BEGIN
    CheckPassword["rename"L];
    AnnounceRename[currentFile, newFile];
    jimsFilePrimitives.RenameFile[
      fileSystem, currentFile, newFile !
      UNWIND =>
	AnnounceOp["Failed renaming"L, currentFile, " for "L]];
    END;

  CopyFile: PROCEDURE [to, from: File.Capability] RETURNS [ok: BOOLEAN] =
    BEGIN RETURN[Slosh.CopyFile[to: to, from: from] = statusStoreOk]; END;

  Announce: PROCEDURE [one, two: STRING] =
    BEGIN OPEN String;
    text: STRING = [200];
    Time.AppendCurrent[text];
    AppendString[text, "  "L];
    AppendString[text, one];
    AppendString[text, two];
    AppendChar[text, '.];
    LogString[text];
    END;

  AnnounceOp: PROCEDURE [what, file, noise: STRING ← NIL] =
    BEGIN OPEN String;
    text: STRING = [200];
    Time.AppendCurrent[text];
    AppendString[text, "  "L];
    AppendString[text, what];
    AppendString[text, file];
    AppendString[text, noise];
    AppendString[text, GetUserName[]];
    AppendString[text, " on "L];
    AppendString[text, GetUserAddress[]];
    AppendChar[text, '.];
    LogString[text];
    END;

  AnnounceReject: PROCEDURE [what, who, where, why: STRING] =
    BEGIN OPEN String;
    text: STRING = [200];
    Time.AppendCurrent[text];
    AppendString[text, "  Rejecting attempt to "L];
    AppendString[text, what];
    AppendString[text, " a file by "L];
    AppendString[text, who];
    AppendString[text, " on "L];
    AppendString[text, where];
    AppendString[text, " because "L];
    AppendString[text, why];
    AppendChar[text, '.];
    LogString[text];
    END;

  AnnounceRename: PROCEDURE [old, new: STRING] =
    BEGIN OPEN String;
    text: STRING = [100];
    AppendString[text, old];
    AppendString[text, " to be "L];
    AppendString[text, new];
    AnnounceOp["Renaming "L, text, " for "L];
    END;

  Message: PROCEDURE [one, two, three, four: STRING ← NIL] =
    BEGIN
    text: STRING = [100];
    String.AppendString[text, one];
    IF two # NIL THEN String.AppendString[text, two];
    IF three # NIL THEN String.AppendString[text, three];
    IF four # NIL THEN String.AppendString[text, four];
    String.AppendChar[text, '.];
    LogString[text];
    END;

  MessageDecimal: PROCEDURE [one: STRING, two: UNSPECIFIED, three: STRING] =
    BEGIN
    text: STRING = [100];
    String.AppendString[text, one];
    String.AppendDecimal[text, two];
    String.AppendString[text, three];
    String.AppendChar[text, '.];
    LogString[text];
    END;

  LogString: PROCEDURE [text: STRING] =
    BEGIN IF msg # NIL THEN Put.Line[msg, text]; Put.Line[NIL, text]; END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN msg ← Tool.MakeMsgSW[window: window, lines: 5]; END;

  ClientTransition: ToolWindow.TransitionProcType =
    BEGIN IF new = inactive THEN msg ← NIL; END;

  Checker: PROCEDURE [
    why: Slosh.Why, fileName: STRING, file: File.Capability] =
    BEGIN
    parmFileName: STRING ← NIL;
    IF why # arrived THEN RETURN;
    IF Runtime.IsBound[Indirect.GetParmFileName] THEN
      parmFileName ← Indirect.GetParmFileName[];
    IF parmFileName = NIL THEN parmFileName ← "FTPServer.txt"L;
    IF running AND String.EquivalentString[parmFileName, fileName]  THEN
      BEGIN
      ForkRestarter: ENTRY PROCEDURE =
        BEGIN
        IF restarting THEN RETURN;
        restarting ← TRUE;
        AnnounceOp[
  	"FTPServer restarting because a new version of "L, parmFileName,
  	" arrived from "L];
        Process.Detach[FORK Restart[]];
        END;
      ForkRestarter[];
      END;
    END;

  restarting: BOOLEAN ← FALSE;
  Restart: ENTRY PROCEDURE =
    BEGIN
    Stopper[];
    restarting ← FALSE;
    Starter[];
    END;

  Broom: PROCEDURE [why: Event.Reason] =
    BEGIN
    SELECT why FROM
      makeImage, makeCheck, stopMesa => IF running THEN StopperLocked[];
      startImage, restartCheck, continueCheck => IF running THEN Starter[];
      ENDCASE => NULL;
    END;

  -- File cooridination things

  SlotSize: TYPE = [0..2*maxServers); -- rename uses 2 at once
  Slot: TYPE = POINTER TO SlotObject;
  SlotObject: TYPE = RECORD [
    oldName, scratchName: STRING,
    oldFile, scratchFile: File.Capability,
    write: BOOLEAN,
    handle: UNSPECIFIED];
  slots: ARRAY SlotSize OF SlotObject ← ALL[
    [NIL, NIL, File.nullCapability, File.nullCapability, FALSE, NIL]];

  -- An empty slot is indicated by oldName=NIL

  AssignSlot: ENTRY PROCEDURE [
    fileName, scratchName: STRING, oldFile, scratchFile: File.Capability,
    write: BOOLEAN, fileHandle: UNSPECIFIED] =
    BEGIN
    slot: Slot;
    FOR s: SlotSize IN SlotSize DO
      IF slots[s].oldName = NIL THEN BEGIN slot ← @slots[s]; EXIT; END;
      REPEAT FINISHED => ERROR;
      ENDLOOP;
    slot↑ ←
      [Storage.CopyString[fileName], Storage.CopyString[scratchName], oldFile,
	scratchFile, write, fileHandle];
    END;

  FindSlot: ENTRY PROCEDURE [fileHandle: UNSPECIFIED]
    RETURNS [slot: Slot] =
    BEGIN
    FOR s: SlotSize IN SlotSize DO
      IF slots[s].handle = fileHandle THEN RETURN[@slots[s]];
      REPEAT FINISHED => ERROR;
      ENDLOOP;
    END;

  -- This is atomic so it doesn't need to be an ENTRY
  MarkSlotClosed: PROCEDURE [slot: Slot] = INLINE BEGIN slot.handle ← NIL; END;

  FreeSlot: ENTRY PROCEDURE [slot: Slot] =
    BEGIN
    -- Caller must free oldName
    Storage.FreeString[slot.scratchName];
    slot.oldName ← NIL;
    END;

  -- Initialization

  tool ← Tool.Create[
    name: herald, makeSWsProc: MakeSWs, clientTransition: ClientTransition];
  Event.AddNotifier[@eventItem];
  Slosh.AddProcs[Checker];
  FTPServerOn[];
  END.