-- Copyright (C) 1984, 1985  by Xerox Corporation. All rights reserved. 
-- HostWatcher.mesa, HGM,  7-Oct-85 18:42:40
-- Please don't forget to update the herald....

DIRECTORY
  Ascii USING [CR, SP],
  CmFile USING [Handle, TableError],
  Event USING [aboutToSwap],
  EventTypes USING [aboutToBoot, aboutToBootPhysicalVolume],
  FormSW USING [
    ClientItemsProcType, ProcType, AllocateItemDescriptor, newLine, CommandItem,
    BooleanItem, StringItem, FindItem, Display, DisplayItem],
  Heap USING [Create, systemZone],
  Inline USING [LowHalf],
  MFile USING [AddNotifyProc, Handle, RemoveNotifyProc],
  MsgSW USING [Post],
  Process USING [Pause, SecondsToTicks, Ticks],
  Put USING [Char, CR, Text, Line, LongDecimal],
  String USING [
    AppendString, AppendChar, Equivalent, AppendNumber, AppendDecimal,
    AppendLongNumber],
  StringLookUp USING [noMatch, TableDesc],
  Supervisor USING [
    AddDependency, AgentProcedure, CreateSubsystem, RemoveDependency,
    SubsystemHandle],
  System USING [GetGreenwichMeanTime, gmtEpoch, GreenwichMeanTime],
  Time USING [AppendCurrent, Append, Unpack, Current],
  Token USING [Boolean, Filtered, FreeTokenString, Item, Line],
  Tool USING [Create, MakeSWsProc, MakeMsgSW, MakeFormSW, MakeFileSW],
  ToolWindow USING [TransitionProcType],
  Window USING [Handle],

  Buffer USING [AccessHandle, DestroyPool, GetBuffer, MakePool, ReturnBuffer],
  HostWatcherOps USING [
    Info, InfoObject, Mode, State, PokeGateway, PokeChat, PokeFtp, PokeMail,
    PokeLibrarian, PokeSpruce, PokeEftp, PokePopCorn, UpDown],
  Indirect USING [Close, GetParmFileName, NextValue, OpenSection],
  Mailer USING [Level, SendGVMail],
  NameServerDefs USING [
    BumpCacheSize, PupDirServerOn, PupNameServerOn, PupDirServerOff,
    PupNameServerOff],
  PupDefs USING [
    PupPackageMake, PupPackageDestroy, AppendHostName, AppendErrorPup,
    PupBuffer, PupSocket, PupSocketDestroy,
    PupSocketMake, SecondsToTocks, SetPupContentsBytes, GetPupContentsBytes,
    EnumeratePupAddresses, PupNameTrouble],
  PupRouterDefs USING [
    RoutingTableEntry, GetRoutingTableEntry, PupGateInfo, maxHop],
  PupTypes USING [
    eftpReceiveSoc, fillInPupAddress, fillInSocketID, ftpSoc, gatewaySoc,
    librarianSoc, mailSoc, PupAddress, PupType, PupSocketID, spruceStatusSoc,
    telnetSoc];

HostWatcher: MONITOR
  IMPORTS
    CmFile, Event, FormSW, Heap, Inline, MFile, MsgSW, Process, Put,
    String, Supervisor, System, Time, Token, Tool, HostWatcherOps,
    Buffer, Indirect, Mailer, NameServerDefs, PupRouterDefs, PupDefs
  EXPORTS HostWatcherOps =
  BEGIN OPEN PupDefs, PupTypes;

  z: UNCOUNTED ZONE = Heap.Create[1];

  herald: LONG STRING = "Host Watcher of  7-Oct-85 18:42:35";

  msg, form, log: Window.Handle ← NIL;
  broom: Supervisor.SubsystemHandle = Supervisor.CreateSubsystem[Broom];
  running, scanning, pleaseStop, debug: BOOLEAN ← FALSE;
  probing: LONG STRING ← NIL;
  useCount: CARDINAL ← 0;
  parmFileName: LONG STRING ← Indirect.GetParmFileName[];

  watcher: PROCESS;
  first: Info ← NIL;
  troubles: LONG STRING ← NIL;
  seconds: CARDINAL = 15*60;
  thirtySeconds: Process.Ticks = Process.SecondsToTicks[30];
  wordsPerCacheEntry: CARDINAL = 25;

  Mode: TYPE = HostWatcherOps.Mode;
  modeSoc: ARRAY Mode OF PupSocketID = [
    gate: [31415, 9265],
    chat: telnetSoc,
    ftp: ftpSoc,
    mail: mailSoc,
    librarian: librarianSoc,
    spruce: spruceStatusSoc,
    eftp: eftpReceiveSoc,
    popCorn: [0, 0]];

  State: TYPE = HostWatcherOps.State;
  isText: ARRAY State OF STRING ← [
    inaccessible: " is ", up: " is ", restarted: " was ", full: " is ", down: " is ",
    rejecting: " is ", timeout: " is ", unknown: " is "];
  stateText: ARRAY State OF STRING ← [
    inaccessible: "inaccessible", up: "up", restarted: "restarted", full: "full", down: "down",
    rejecting: "rejecting", timeout: "not responding", unknown: "unknown"];

  Info: TYPE = HostWatcherOps.Info;

  LastGatewayVanished: ERROR = CODE;

  HostWatcherOn: ENTRY PROCEDURE =
    BEGIN
    IF (useCount ← useCount + 1) = 1 THEN
      BEGIN
      Supervisor.AddDependency[client: broom, implementor: Event.aboutToSwap];
      MFile.AddNotifyProc[Inspect, [parmFileName, null, readOnly], NIL];
      Starter[];
      END;
    UpdatePicture[];
    END;

  Starter: INTERNAL PROCEDURE =
    BEGIN
    IF ~FindTargets[] THEN BEGIN running ← FALSE; RETURN; END;
    running ← TRUE;
    [] ← PupPackageMake[];
    NameServerDefs.PupDirServerOn[];
    NameServerDefs.PupNameServerOn[];
    pleaseStop ← FALSE;
    watcher ← FORK Watcher[];
    END;

  HostWatcherOff: ENTRY PROCEDURE =
    BEGIN
    IF ~running THEN RETURN;  -- No parm file
    IF useCount # 0 AND (useCount ← useCount - 1) = 0 THEN
      BEGIN
      Stopper[];
      MFile.RemoveNotifyProc[Inspect, [parmFileName, null, readOnly], NIL];
      Supervisor.RemoveDependency[client: broom, implementor: Event.aboutToSwap];
      END;
    UpdatePicture[];
    END;

  Stopper: INTERNAL PROCEDURE =
    BEGIN
    running ← FALSE;
    pleaseStop ← TRUE;
    JOIN watcher[];
    NameServerDefs.PupDirServerOff[];
    NameServerDefs.PupNameServerOff[];
    PupPackageDestroy[];
    ForgetTargets[];
    Announce["Killed "L, herald];
    END;

  UpdatePicture: PROCEDURE =
    BEGIN
    IF form = NIL THEN RETURN;
    FormSW.FindItem[form, startIX].flags.invisible ← running;
    FormSW.FindItem[form, stopIX].flags.invisible ← ~running;
    FormSW.FindItem[form, probeIX].flags.invisible ← ~scanning;
    FormSW.Display[form];
    END;

  PrintSummary: ENTRY PROCEDURE =
    BEGIN
    state: State;
    n: LONG CARDINAL;
    WriteCR[];
    WriteCurrentDateAndTime[];
    WriteLine["  Current Status:"L];
    FOR info: Info ← first, info.next UNTIL info = NIL DO
      WriteString[info.name];
      WriteString[isText[info.state]];
      WriteString[stateText[info.state]];
      IF info.text.length # 0 THEN
        BEGIN WriteString[": "L]; WriteString[info.text]; END;
      WriteLine["."L];
      IF info.foundLastGateway AND info.lastHops # 0 THEN
        BEGIN
        text: STRING = [100];
        AppendGatewayInfo[text, info];
        WriteLine[text];
        END;
      IF info.lastLineChanged THEN
        BEGIN
        text: STRING = [100];
        AppendLineChangedInfo[text, info];
        WriteLine[text];
        END;
      IF info.mode = gate AND info.lastHopUsesPhoneLine THEN
        BEGIN WriteLine["The last hop uses a phone line."L]; END;
      IF info.state # up THEN
        BEGIN
        IF info.lastUp # System.gmtEpoch THEN
          BEGIN
          text: STRING = [100];
          AppendLastUp[text, info];
          WriteLine[text];
          END;
        END;
      FOR state IN State DO
        IF (n ← info.counters[state]) = 0 THEN LOOP;
        LD8[n];
        WriteString[" ("];
        WriteLongDecimal[n*100/info.probes];
        WriteString["%) "];
        WriteLine[stateText[state]];
        ENDLOOP;
      ENDLOOP;
    WriteCR[];
    END;

  LogState: PROCEDURE [info: Info] =
    BEGIN
    text: STRING = [200];
    Time.AppendCurrent[text];
    String.AppendString[text, "  "L];
    String.AppendString[text, info.name];
    String.AppendString[text, isText[info.state]];
    String.AppendString[text, stateText[info.state]];
    IF info.text.length # 0 THEN
      BEGIN
      String.AppendString[text, ": "L];
      String.AppendString[text, info.text];
      END;
    LogString[text];
    END;

  FindTargets: INTERNAL PROCEDURE RETURNS [BOOLEAN] =
    BEGIN
    modeStrings: ARRAY Mode OF STRING = [
      gate: "Gateway"L,
      chat: "Chat"L,
      ftp: "FTP"L,
      mail: "Mail"L,
      librarian: "Librarian"L,
      spruce: "Spruce"L,
      eftp: "EFTP"L,
      popCorn: "PopCorn"L];
    AddTarget: INTERNAL PROCEDURE [server: LONG STRING, mode: Mode] =
      BEGIN
      temp: STRING = [200];
      AddPair: INTERNAL PROCEDURE [tag, val: LONG STRING] =
        BEGIN
        IF val = NIL THEN RETURN;
        IF temp.length # 0 THEN String.AppendString[temp, ", "L];
        String.AppendString[temp, tag];
        String.AppendString[temp, ": "L];
        String.AppendString[temp, val];
        END;
      AddPair[modeStrings[mode], server];
      AddPair["To"L, to];
      AddPair["cc"L, cc];
      AddPair["Full"L, full];
      String.AppendChar[temp, Ascii.CR];
      Put.Text[NIL, temp];
      AppendItem[server, to, cc, full, mode];
      [] ← Token.FreeTokenString[server];
      END;
    to, cc, full: LONG STRING ← NIL;
    cmFile: CmFile.Handle;
    Option: TYPE = MACHINE DEPENDENT{
      troubles(0), to, cc, full, gate, chat, ftp, mail, librarian, spruce, eftp, popCorn,
      debug, noMatch(StringLookUp.noMatch)};
    DefinedOption: TYPE = Option [troubles..debug];
    CheckType: PROCEDURE [h: CmFile.Handle, table: StringLookUp.TableDesc]
      RETURNS [index: CARDINAL] = Indirect.NextValue;
    MyNextValue: PROCEDURE [
      h: CmFile.Handle,
      table: LONG DESCRIPTOR FOR ARRAY DefinedOption OF LONG STRING]
      RETURNS [index: Option] = LOOPHOLE[CheckType];
    optionTable: ARRAY DefinedOption OF LONG STRING ← [
      troubles: "Troubles"L, to: "to"L, cc: "cc"L, full: "Full"L,
      gate: "Gateway"L, chat: "Chat"L, ftp: "FTP"L, mail: "Mail"L,
      librarian: "Librarian"L, spruce: "Printer"L, eftp: "EFTP"L, popCorn: "PopCorn"L,
      debug: "Debug"L];
    cmFile ← Indirect.OpenSection["HostWatcher"L];
    IF cmFile = NIL THEN
      BEGIN
      Message["Can't find [HostWatcher] section in parameter file"L];
      RETURN[FALSE];
      END;
    Announce["Starting "L, herald];
    DO
      option: Option;
      option ← MyNextValue[cmFile, DESCRIPTOR[optionTable] !
        CmFile.TableError =>
          BEGIN
	  IF name[0] # '; THEN Message["Unrecognized parameter: ", name];
	  RETRY;
	  END];
      SELECT option FROM
        noMatch => EXIT;
        troubles =>
          BEGIN
	  temp: LONG STRING ← Token.Item[cmFile, FALSE];
          z.FREE[@troubles];
	  troubles ← z.NEW[StringBody[temp.length]];
	  String.AppendString[troubles, temp];
          CheckForRegistry[troubles];
	  [] ← Token.FreeTokenString[temp];
          END;
        to =>
          BEGIN
	  temp: LONG STRING ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
          DeleteString[to];
	  to ← z.NEW[StringBody[temp.length]];
	  String.AppendString[to, temp];
          CheckForRegistry[to];
	  [] ← Token.FreeTokenString[temp];
          END;
        cc =>
          BEGIN
	  temp: LONG STRING ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
          DeleteString[cc];
	  cc ← z.NEW[StringBody[temp.length]];
	  String.AppendString[cc, temp];
          CheckForRegistry[cc];
	  [] ← Token.FreeTokenString[temp];
          END;
        full =>
          BEGIN
	  temp: LONG STRING ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
          DeleteString[full];
	  full ← z.NEW[StringBody[temp.length]];
	  String.AppendString[full, temp];
          CheckForRegistry[full];
	  [] ← Token.FreeTokenString[temp];
          END;
        gate => AddTarget[Token.Item[cmFile, FALSE], gate];
        chat => AddTarget[Token.Item[cmFile, FALSE], chat];
        ftp => AddTarget[Token.Item[cmFile, FALSE], ftp];
        mail => AddTarget[Token.Item[cmFile, FALSE], mail];
        librarian => AddTarget[Token.Item[cmFile, FALSE], librarian];
        spruce => AddTarget[Token.Item[cmFile, FALSE], spruce];
        eftp => AddTarget[Token.Item[cmFile, FALSE], eftp];
	popCorn => AddTarget[Token.Item[cmFile, FALSE], popCorn];
        debug => debug ← Token.Boolean[cmFile];
        ENDCASE => ERROR;
      ENDLOOP;
    Indirect.Close[cmFile];
    IF first = NIL THEN BEGIN Message["Oops, no targets"L]; RETURN[FALSE]; END;
    IF troubles = NIL THEN
      Message["Please specify somebody in case of TROUBLES"L];
    DeleteString[to];
    DeleteString[cc];
    DeleteString[full];
    RETURN[TRUE];
    END;

  CheckForRegistry: PROCEDURE [s: LONG STRING] =
    BEGIN
    dot: BOOLEAN ← FALSE;
    FOR i: CARDINAL IN [0..s.length) DO
      SELECT s[i] FROM
        '. => dot ← TRUE;
        ', =>
          BEGIN
          IF ~dot THEN
            BEGIN Message["Registry expected in arg: "L, s]; RETURN; END;
          dot ← FALSE;
          END;
        ENDCASE => NULL;
      ENDLOOP;
    IF ~dot THEN BEGIN Message["Registry expected in arg: "L, s]; RETURN; END;
    END;

  Message: PROCEDURE [one, two, three: LONG STRING ← NIL] =
    BEGIN
    text: STRING = [250];
    Time.AppendCurrent[text];
    String.AppendString[text, "  HostWatcher: "L];
    String.AppendString[text, one];
    IF two # NIL THEN String.AppendString[text, two];
    IF three # NIL THEN String.AppendString[text, three];
    LogString[text];
    END;

  AppendItem: INTERNAL PROCEDURE [server, to, cc, full: LONG STRING, mode: Mode] =
    BEGIN
    info: Info ← z.NEW[HostWatcherOps.InfoObject];
    info↑ ← [
      name: , to: NIL, cc: NIL, full: NIL,
      address: [[0], [0], modeSoc[mode]], mode: mode, text: z.NEW[StringBody[100]],
      next: NIL];
    info.name ← z.NEW[StringBody[server.length]];
    String.AppendString[info.name, server];
    IF first = NIL THEN first ← info
    ELSE
      BEGIN
      where: Info;
      FOR where ← first, where.next UNTIL where.next = NIL DO ENDLOOP;
      where.next ← info;
      END;
    info.to ← FindString[to];
    info.cc ← FindString[cc];
    info.full ← FindString[full];
    NameServerDefs.BumpCacheSize[wordsPerCacheEntry];
    END;

  FindString: INTERNAL PROCEDURE [s: LONG STRING] RETURNS [t: LONG STRING] =
    BEGIN
    t ← s;
    FOR info: Info ← first, info.next UNTIL info = NIL DO
      SELECT TRUE FROM
        String.Equivalent[info.to, s] => BEGIN t ← info.to; EXIT; END;
        String.Equivalent[info.cc, s] => BEGIN t ← info.cc; EXIT; END;
        String.Equivalent[info.full, s] => BEGIN t ← info.full; EXIT; END;
        ENDCASE;
      ENDLOOP;
    END;

  ForgetTargets: INTERNAL PROCEDURE =
    BEGIN
    info: Info ← first;
    UNTIL first = NIL DO
      info ← first; first ← first.next; DeleteItem[info]; ENDLOOP;
    z.FREE[@troubles];
    END;

  DeleteItem: INTERNAL PROCEDURE [info: Info] =
    BEGIN
    z.FREE[@info.name];
    IF info.to # info.cc AND info.to # info.full THEN DeleteString[info.to];
    IF info.cc # info.full THEN DeleteString[info.cc];
    DeleteString[info.full];
    z.FREE[@info.text];
    z.FREE[@info];
    NameServerDefs.BumpCacheSize[-wordsPerCacheEntry];
    END;

  DeleteString: INTERNAL PROCEDURE [s: LONG STRING] =
    BEGIN
    FOR info: Info ← first, info.next UNTIL info = NIL DO
      IF info.to = s OR info.cc = s OR info.full = s THEN RETURN; ENDLOOP;
    z.FREE[@s];
    END;

  sequenceNumber: CARDINAL ← 0;

  NextSequenceNumber: PROCEDURE RETURNS [CARDINAL] = INLINE
    BEGIN RETURN[sequenceNumber ← sequenceNumber + 1]; END;

  Watcher: PROCEDURE =
    BEGIN
    start: LONG CARDINAL ← System.GetGreenwichMeanTime[];
    WatcherWait: PROCEDURE =
      BEGIN
      sleep: CARDINAL ← LAST[CARDINAL];
      WHILE sleep > seconds DO
        start ← start + seconds;
        sleep ← Inline.LowHalf[seconds - (System.GetGreenwichMeanTime[] - start)];
        ENDLOOP;
      FOR i: CARDINAL IN [0..sleep/30) UNTIL pleaseStop DO
        Process.Pause[Process.SecondsToTicks[30]]; ENDLOOP;
      END;
    -- Give NameServer extra time to be sure it has started
    THROUGH [0..1000) DO Process.Pause[1]; ENDLOOP;
    UNTIL pleaseStop DO
      scanning ← TRUE;
      UpdatePicture[];
      PostWithTime["Start of scan..."L];
      FOR info: Info ← first, info.next UNTIL info = NIL OR pleaseStop DO
        probing ← info.name;
        IF form # NIL THEN FormSW.DisplayItem[form, probeIX];
        WatcherPoke[info];
        THROUGH [0..100) UNTIL pleaseStop DO Process.Pause[1]; ENDLOOP;
        ENDLOOP;
      scanning ← FALSE;
      probing ← NIL;
      UpdatePicture[];
      PostWithTime["End of scan."L];
      IF ~pleaseStop THEN WatcherWait[];
      ENDLOOP;
    END;

  WatcherPoke: PROCEDURE [info: Info] =
    BEGIN
    tries: CARDINAL ← 0;
    oldState: State ← info.state;
    oldUpDown: HostWatcherOps.UpDown ← info.upDown;
    interesting: BOOLEAN;
    BEGIN
    ENABLE
      LastGatewayVanished, PupNameTrouble =>
        BEGIN
        text: STRING = [100];
        Time.AppendCurrent[text];
        String.AppendString[text, "  Troubles finding last Gateway to "L];
        String.AppendString[text, info.name];
        LogString[text];
	FOR i: CARDINAL IN [0..6) UNTIL pleaseStop DO
          Process.Pause[thirtySeconds]; ENDLOOP;
        tries ← tries + 1;
        IF ~pleaseStop AND tries < 2 THEN RETRY;
        info.state ← unknown;
        CONTINUE;
        END;
    info.state ← unknown;
    info.text.length ← 0;
    MyGetPupAddress[
      @info.address, info.name !
      PupNameTrouble =>
        BEGIN
        text: STRING = [100];
        String.AppendString[info.text, e];
        String.AppendString[text, info.name];
        String.AppendString[text, ": "L];
        String.AppendString[text, e];
        IF msg # NIL THEN MsgSW.Post[msg, text];
        info.state ← inaccessible;
        info.noPath ← TRUE;
        CONTINUE;
        END];
    IF info.state = inaccessible THEN
      BEGIN
      CheckLastGateway[info];
      IF ~info.lastGatewayOk THEN info.state ← unknown;
      END
    ELSE
      BEGIN
      FindLastGateway[info];
      SELECT info.mode FROM
        gate => HostWatcherOps.PokeGateway[info];
        chat => HostWatcherOps.PokeChat[info];
        ftp => HostWatcherOps.PokeFtp[info];
        mail => HostWatcherOps.PokeMail[info];
        librarian => HostWatcherOps.PokeLibrarian[info];
        spruce => HostWatcherOps.PokeSpruce[info];
        eftp => HostWatcherOps.PokeEftp[info];
        popCorn => HostWatcherOps.PokePopCorn[info];
        ENDCASE => ERROR;
      END;
    END;  -- of ENABLE
    IF pleaseStop THEN RETURN;
    info.counters[info.state] ← info.counters[info.state] + 1;
    info.probes ← info.probes + 1;
    UpdateUpDown[info];
    interesting ← InterestingStateChange[new: info.upDown, old: oldUpDown]
      OR info.state = restarted
      OR (info.state = up AND oldState = up AND info.lastLineChanged);
    IF interesting OR info.state = full THEN LogState[info];
    SELECT TRUE FROM
      interesting AND info.to # NIL => SendStatus[info.to, info];
      info.state = full AND info.full # NIL => SendStatus[info.full, info];
      ENDCASE => NULL;
    IF info.state = up THEN
      BEGIN info.lastUp ← Time.Current[]; info.noPath ← FALSE; END;
    END;

  FindLastGateway: PROCEDURE [info: Info] =
    BEGIN
    pool: Buffer.AccessHandle;
    soc: PupSocket;
    rte: PupRouterDefs.RoutingTableEntry;
    b: PupBuffer ← NIL;
    thisGateway, previousGateway: PupAddress;
    hops, id: CARDINAL;
    oldPhoneLine: BOOLEAN ← info.lastHopUsesPhoneLine;
    info.lastHopUsesPhoneLine ← info.lastLineChanged ← FALSE;
    rte ← PupRouterDefs.GetRoutingTableEntry[info.address.net];
    IF rte = NIL OR rte.context = NIL OR rte.hop > PupRouterDefs.maxHop THEN
      ERROR LastGatewayVanished;
    hops ← rte.hop;
    thisGateway ← previousGateway ← [
      [rte.context.pupNetNumber], rte.route, PupTypes.gatewaySoc];
    IF hops = 0 THEN
      BEGIN
      info.previousHops ← info.lastHops;
      info.lastHops ← hops;
      info.lastGateway ← thisGateway;
      info.lastGatewayOk ← TRUE;
      RETURN;
      END;
    BEGIN
    ENABLE
      UNWIND =>
        BEGIN
	IF b # NIL THEN Buffer.ReturnBuffer[b];
	PupSocketDestroy[soc];
        Buffer.DestroyPool[pool];
	END;
    pool ← Buffer.MakePool[send: 1, receive: 10];
    soc ← PupSocketMake[
      PupTypes.fillInSocketID, info.lastGateway, SecondsToTocks[2]];
    THROUGH [1..hops) DO
      hit: BOOLEAN ← FALSE;
      id ← NextSequenceNumber[];
      thisGateway ← GetReasonableAddress[thisGateway];
      soc.setRemoteAddress[thisGateway];
      THROUGH [0..10) DO
        b ← Buffer.GetBuffer[pup, pool, send];
        b.pup.pupType ← gatewayRequest;
        SetPupContentsBytes[b, 0];
        b.pup.pupID ← [id, id];
        soc.put[b];
        UNTIL (b ← soc.get[]) = NIL DO
          IF b.pup.pupType = gatewayInfo AND b.pup.pupID = [id, id] THEN
            BEGIN
            one: LONG POINTER TO PupRouterDefs.PupGateInfo ←
              LOOPHOLE[@b.pup.pupWords];
            length: CARDINAL = GetPupContentsBytes[b];
            n: CARDINAL ← length/(2*SIZE[PupRouterDefs.PupGateInfo]);
            FOR i: CARDINAL IN [0..n) DO
              IF one.net = info.address.net THEN
                BEGIN
                IF one.hop > PupRouterDefs.maxHop THEN ERROR LastGatewayVanished;
                previousGateway ← thisGateway;
                thisGateway ← [one.viaNet, one.viaHost, PupTypes.gatewaySoc];
                hit ← TRUE;
                EXIT;
                END;
              one ← one + SIZE[PupRouterDefs.PupGateInfo];
              ENDLOOP;
            END;
          Buffer.ReturnBuffer[b];
          b ← NIL;
          IF hit THEN EXIT;
          ENDLOOP;
        IF hit THEN EXIT;
        REPEAT FINISHED => ERROR LastGatewayVanished;
        ENDLOOP;
      ENDLOOP;
    IF info.mode = gate THEN
      BEGIN  -- Check for phone line  (only interesting if mode=gate)
      hit: BOOLEAN ← FALSE;
      me: PupAddress ← soc.getLocalAddress[];
      soc.setRemoteAddress[
        [info.address.net, info.address.host, PupTypes.gatewaySoc]];
      id ← NextSequenceNumber[];
      THROUGH [0..10) DO
        b ← Buffer.GetBuffer[pup, pool, send];
        b.pup.pupType ← gatewayRequest;
        SetPupContentsBytes[b, 0];
        b.pup.pupID ← [id, id];
        soc.put[b];
        UNTIL (b ← soc.get[]) = NIL DO
          IF b.pup.pupType = gatewayInfo AND b.pup.pupID = [id, id] THEN
            BEGIN
            length: CARDINAL = GetPupContentsBytes[b];
            n: CARDINAL ← length/(2*SIZE[PupRouterDefs.PupGateInfo]);
            one: LONG POINTER TO PupRouterDefs.PupGateInfo ←
              LOOPHOLE[@b.pup.pupWords];
            FOR i: CARDINAL IN [0..n) DO
              IF one.net = me.net THEN
                BEGIN
                IF one.viaNet = 7B THEN info.lastHopUsesPhoneLine ← TRUE;
                hit ← TRUE;
                END;
              one ← one + SIZE[PupRouterDefs.PupGateInfo];
              ENDLOOP;
            END;
          Buffer.ReturnBuffer[b];
          b ← NIL;
          IF hit THEN EXIT;
          ENDLOOP;
        IF hit THEN EXIT;
        REPEAT FINISHED => NULL;  -- It won't talk to us!
        ENDLOOP;
      END;
    BEGIN  -- Check for back door problem  (only interesting if mode=gate)
    hit: BOOLEAN ← FALSE;
    me: PupAddress ← soc.getLocalAddress[];
    thisGateway ← GetReasonableAddress[thisGateway];
    soc.setRemoteAddress[thisGateway];
    id ← NextSequenceNumber[];
    THROUGH [0..10) DO
      b ← Buffer.GetBuffer[pup, pool, send];
      b.pup.pupType ← gatewayRequest;
      SetPupContentsBytes[b, 0];
      b.pup.pupID ← [id, id];
      soc.put[b];
      UNTIL (b ← soc.get[]) = NIL DO
        IF b.pup.pupType = gatewayInfo AND b.pup.pupID = [id, id] THEN
          BEGIN
          length: CARDINAL = GetPupContentsBytes[b];
          n: CARDINAL ← length/(2*SIZE[PupRouterDefs.PupGateInfo]);
          one: LONG POINTER TO PupRouterDefs.PupGateInfo ← LOOPHOLE[@b.pup.pupWords];
          FOR i: CARDINAL IN [0..n) DO
            IF one.net = info.address.net THEN
              BEGIN
              IF info.address.net = one.viaNet AND info.address.host = one.viaHost
                THEN
                -- our best path to his net is via him,
                --   hence we are talking to him via his back door
                BEGIN
                IF info.mode # gate THEN ERROR;
                thisGateway ← previousGateway;
                hops ← hops - 1;
                END;
              hit ← TRUE;
              END;
            one ← one + SIZE[PupRouterDefs.PupGateInfo];
            ENDLOOP;
          END;
        Buffer.ReturnBuffer[b];
        b ← NIL;
        ENDLOOP;
      IF hit THEN
        BEGIN
        IF info.mode = gate AND (oldPhoneLine OR info.lastHopUsesPhoneLine)
          AND info.previousHops # info.lastHops AND info.lastGateway # thisGateway
          THEN info.lastLineChanged ← TRUE;
        info.previousHops ← info.lastHops;
        info.lastHops ← hops;
        info.lastGateway ← thisGateway;
        info.lastGatewayOk ← info.foundLastGateway ← TRUE;
        EXIT;
        END;
      REPEAT FINISHED => ERROR LastGatewayVanished;
      ENDLOOP;
    END;
    END;  -- of ENABLE
    PupSocketDestroy[soc];
    Buffer.DestroyPool[pool];
    END;

  CheckLastGateway: PROCEDURE [info: Info] =
    BEGIN
    pool: Buffer.AccessHandle;
    soc: PupSocket;
    b: PupBuffer;
    IF info.lastHops = 0 THEN
      BEGIN  -- directly connected, or never got off the ground
      info.lastGatewayOk ← info.foundLastGateway;
      RETURN;
      END;
    info.lastGatewayOk ← FALSE;
    IF info.lastGateway = fillInPupAddress THEN RETURN;  -- haven't found it yet
    pool ← Buffer.MakePool[send: 1, receive: 10];
    soc ← PupSocketMake[
      PupTypes.fillInSocketID, info.lastGateway, SecondsToTocks[2]];
    THROUGH [0..10) UNTIL info.lastGatewayOk DO
      b ← Buffer.GetBuffer[pup, pool, send];
      b.pup.pupType ← gatewayRequest;
      SetPupContentsBytes[b, 0];
      b.pup.pupID ← [0, 0];
      soc.put[b];
      UNTIL (b ← soc.get[]) = NIL DO
        IF b.pup.pupType = gatewayInfo THEN info.lastGatewayOk ← TRUE;
        Buffer.ReturnBuffer[b];
        ENDLOOP;
      ENDLOOP;
    PupSocketDestroy[soc];
    Buffer.DestroyPool[pool];
    END;

  UpdateUpDown: PROCEDURE [info: Info] =
    BEGIN
    upTable: ARRAY State OF BOOLEAN = [
      FALSE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE];
    downTable: ARRAY State OF BOOLEAN = [
      FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, FALSE];
    up: BOOLEAN ← upTable[info.state];
    down: BOOLEAN ← downTable[info.state];
    IF info.mode = gate AND info.state = inaccessible AND info.lastGatewayOk THEN
      down ← TRUE;
    IF info.mode = popCorn AND info.state = full THEN up ← FALSE;
    IF up THEN info.upDown ← up;
    IF down THEN info.upDown ← down;
    END;

  InterestingStateChange: PROCEDURE [new, old: HostWatcherOps.UpDown]
    RETURNS [BOOLEAN] =
    BEGIN
    IF old = unknown OR new = unknown THEN RETURN[FALSE];
    RETURN[new # old];
    END;

  SendStatus: PROCEDURE [to: LONG STRING, info: Info] =
    BEGIN
    subject: STRING = [100];
    body: STRING = [500];
    state: State;
    temp: STRING = [25];
    n: LONG CARDINAL;
    Info: PROCEDURE [s: LONG STRING, level: Mailer.Level] =
      BEGIN
      copy: LONG STRING ← Heap.systemZone.NEW[StringBody[s.length+2]];
      String.AppendString[copy, s];
      LogString[copy];
      Heap.systemZone.FREE[@copy];
      END;
    String.AppendString[subject, info.name];
    String.AppendString[subject, isText[info.state]];
    String.AppendString[subject, stateText[info.state]];
    String.AppendString[body, info.name];
    String.AppendString[body, isText[info.state]];
    String.AppendString[body, stateText[info.state]];
    IF info.text.length # 0 THEN
      BEGIN
      String.AppendString[body, ": "L];
      String.AppendString[body, info.text];
      END;
    String.AppendChar[body, '.];
    String.AppendChar[body, Ascii.CR];
    IF info.foundLastGateway AND info.lastHops # 0 THEN
      BEGIN AppendGatewayInfo[body, info]; String.AppendChar[body, Ascii.CR]; END;
    IF info.lastLineChanged THEN
      BEGIN
      AppendLineChangedInfo[body, info];
      String.AppendChar[body, Ascii.CR];
      END;
    IF info.lastUp # System.gmtEpoch THEN
      BEGIN AppendLastUp[body, info]; String.AppendChar[body, Ascii.CR]; END;
    FOR state IN State DO
      IF (n ← info.counters[state]) = 0 THEN LOOP;
      temp.length ← 0;
      String.AppendLongNumber[temp, n, 10];
      THROUGH [temp.length..8) DO String.AppendChar[body, Ascii.SP]; ENDLOOP;
      String.AppendLongNumber[body, n, 10];
      String.AppendString[body, " ("];
      String.AppendLongNumber[body, n*100/info.probes, 10];
      String.AppendString[body, "%) "];
      String.AppendString[body, stateText[state]];
      String.AppendChar[body, '.];
      String.AppendChar[body, Ascii.CR];
      ENDLOOP;
    [] ← Mailer.SendGVMail[
      subject, to, info.cc, body, troubles, Info];
    END;

  -- IO things  (Write* used only by PrintSummary)

  WriteChar: PROCEDURE [c: CHARACTER] = BEGIN Put.Char[log, c]; END;

  WriteCR: PROCEDURE = BEGIN Put.CR[log]; END;

  WriteString: PROCEDURE [s: LONG STRING] = BEGIN Put.Text[log, s]; END;

  WriteLine: PROCEDURE [s: LONG STRING] = BEGIN Put.Line[log, s]; END;

  WriteLongDecimal: PROCEDURE [n: LONG CARDINAL] =
    BEGIN Put.LongDecimal[log, n]; END;

  WriteDecimal: PROCEDURE [n: CARDINAL] = INLINE BEGIN WriteNumber[n, 10, 0]; END;

  WriteOctal: PROCEDURE [n: CARDINAL] = INLINE BEGIN WriteNumber[n, 8, 0]; END;

  WriteNumber: PROCEDURE [n, radix, width: CARDINAL] = INLINE
    BEGIN
    temp: STRING = [25];
    String.AppendNumber[temp, n, radix];
    THROUGH [temp.length..width) DO WriteChar[' ]; ENDLOOP;
    WriteString[temp];
    END;

  LD8: PROCEDURE [n: LONG CARDINAL] =
    BEGIN
    temp: STRING = [25];
    String.AppendLongNumber[temp, n, 10];
    THROUGH [temp.length..8) DO WriteChar[' ]; ENDLOOP;
    WriteString[temp];
    END;

  D8: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 10, 8]; END;

  O3: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;

  O6: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 3]; END;

  O9: PROCEDURE [n: CARDINAL] = BEGIN WriteNumber[n, 8, 9]; END;

  WriteCurrentDateAndTime: PROCEDURE =
    BEGIN time: STRING = [18]; Time.AppendCurrent[time]; WriteString[time]; END;

  PostWithTime: PROCEDURE [s: LONG STRING] =
    BEGIN
    text: STRING = [120];
    IF msg = NIL THEN RETURN;
    Time.AppendCurrent[text];
    String.AppendString[text, "  "L];
    String.AppendString[text, s];
    MsgSW.Post[msg, text];
    END;

  ShowErrorPup: PUBLIC PROCEDURE [b: PupBuffer] =
    BEGIN
    text: STRING = [200];
    IF msg = NIL THEN RETURN;
    PupDefs.AppendErrorPup[text, b];
    MsgSW.Post[msg, text];
    END;

  AppendGatewayInfo: PROCEDURE [text: LONG STRING, info: Info] =
    BEGIN
    String.AppendString[text, "The last gateway"L];
    String.AppendString[text, IF info.lastGatewayOk THEN " is "L ELSE " was "L];
    AppendHostName[text, info.lastGateway];
    String.AppendString[text, " which"L];
    String.AppendString[text, IF info.lastGatewayOk THEN " is "L ELSE " was "L];
    String.AppendDecimal[text, info.lastHops];
    String.AppendString[text, " hop"L];
    IF info.lastHops > 1 THEN String.AppendChar[text, 's];
    String.AppendString[text, " away."L];
    END;

  AppendLineChangedInfo: PROCEDURE [text: LONG STRING, info: Info] =
    BEGIN
    String.AppendString[text, "The last line has recently "L];
    String.AppendString[
      text,
      SELECT info.lastHops FROM
        > info.previousHops => "died"L,
        < info.previousHops => "recovered"L
        ENDCASE => "changed"L];
    String.AppendChar[text, '.];
    END;

  AppendLastUp: PROCEDURE [text: LONG STRING, info: Info] =
    BEGIN
    IF info.noPath THEN
      String.AppendString[text, "The last time we saw it up was "L]
    ELSE String.AppendString[text, "The last time it was up was "L];
    Time.Append[text, Time.Unpack[info.lastUp], TRUE];
    String.AppendChar[text, '.];
    END;

  MyGetPupAddress: PROCEDURE [
    him: LONG POINTER TO PupAddress, name: LONG STRING] =
    BEGIN
    SkipFlakeyNets: PROCEDURE [her: PupAddress] RETURNS [BOOLEAN] =
      BEGIN
      rte: PupRouterDefs.RoutingTableEntry;
      IF FlakeyNet[her] THEN RETURN[FALSE];
      rte ← PupRouterDefs.GetRoutingTableEntry[her.net];
      IF rte = NIL OR rte.context = NIL OR rte.hop > PupRouterDefs.maxHop THEN
        RETURN[FALSE];
      him.net ← her.net;
      him.host ← her.host;
      IF her.socket # [0, 0] THEN him.socket ← her.socket;
      RETURN[TRUE];
      END;
    IF EnumeratePupAddresses[name, SkipFlakeyNets] THEN RETURN;
    ERROR PupNameTrouble["No Route to that Host"L, noRoute];
    END;

  GetReasonableAddress: PROCEDURE [him: PupAddress] RETURNS [PupAddress] =
    BEGIN
    hisName: STRING = [40];
    IF ~FlakeyNet[him] THEN RETURN[him];
    AppendHostName[hisName, him];
    MyGetPupAddress[@him, hisName];
    RETURN[him];
    END;

  -- Beware: This needs be kept in sync with reality
  FlakeyNet: PROCEDURE [him: PupAddress] RETURNS [BOOLEAN] =
    BEGIN
    IF him.net = 7B OR him.net = 17B OR him.net = 145B THEN RETURN[TRUE];  -- SLA, second SLA or PR
    RETURN[FALSE];
    END;

  Start: FormSW.ProcType = BEGIN HostWatcherOn[]; END;

  Stop: FormSW.ProcType = BEGIN HostWatcherOff[]; END;

  Summary: FormSW.ProcType = BEGIN PrintSummary[]; END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN
    msg ← Tool.MakeMsgSW[window: window, lines: 5];
    form ← Tool.MakeFormSW[window: window, formProc: MakeForm];
    log ← Tool.MakeFileSW[window: window, name: "HostWatcher.log$"L];
    END;

  Announce: PROCEDURE [one, two: LONG STRING ← NIL] =
    BEGIN OPEN String;
    text: STRING = [200];
    Time.AppendCurrent[text];
    AppendString[text, "  "L];
    AppendString[text, one];
    IF two # NIL THEN AppendString[text, two];
    LogString[text];
    END;

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


  startIX: CARDINAL = 0;
  stopIX: CARDINAL = 1;
  runningIX: CARDINAL = 2;
  probeIX: CARDINAL = 4;
  MakeForm: FormSW.ClientItemsProcType =
    BEGIN
    nParams: CARDINAL = 5;
    items ← FormSW.AllocateItemDescriptor[nParams];
    items[0] ← FormSW.CommandItem[
      tag: "Start"L, proc: Start, place: FormSW.newLine, invisible: running];
    items[1] ← FormSW.CommandItem[
      tag: "Stop"L, proc: Stop, place: FormSW.newLine, invisible: ~running];
    items[2] ← FormSW.BooleanItem[
      tag: "Running"L, switch: @running, readOnly: TRUE];
    items[3] ← FormSW.CommandItem[tag: "Summary"L, proc: Summary];
    items[4] ← FormSW.StringItem[
      tag: "Probing"L, string: @probing, readOnly: TRUE, invisible: ~scanning];
    RETURN[items, TRUE];
    END;

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

  Broom: ENTRY Supervisor.AgentProcedure =
    BEGIN
    SELECT event FROM
      EventTypes.aboutToBoot, EventTypes.aboutToBootPhysicalVolume =>
        IF running THEN Stopper[];
      ENDCASE => NULL;
    END;

  Inspect: ENTRY PROCEDURE [
    name: LONG STRING, file: MFile.Handle, clientInstanceData: LONG POINTER]
    RETURNS [BOOLEAN] =
    BEGIN
    IF parmFileName = NIL THEN RETURN[FALSE];
    Message["Recycling because a new version of "L, parmFileName, " arrived"L];
    IF running THEN Stopper[];
    Starter[];
    RETURN[FALSE]
    END;
  

  -- Main Body
  [] ← Tool.Create[
    name: herald, makeSWsProc: MakeSWs, clientTransition: ClientTransition];
  HostWatcherOn[];
  END.