-- File: HostWatcher.mesa,  Last Edit: HGM  March 28, 1981  3:27 PM
-- Please don't forget to update the herald....

DIRECTORY
  Ascii USING [CR, SP],
  Inline USING [LowHalf],
  Process USING [SetTimeout, SecondsToTicks, Yield],
  Runtime USING [IsBound],
  Storage USING [Node, String, Free, FreeString, FreeStringNil],
  String USING [
    AppendString, AppendChar, EquivalentString, AppendNumber, AppendDecimal,
    AppendLongNumber],
  System USING [GetGreenwichMeanTime, gmtEpoch, GreenwichMeanTime],
  Time USING [AppendCurrent, Append, Unpack, Current],

  CmFile USING [OpenSection, NextItem, Close],
  Event USING [Item, Reason, AddNotifier],
  FormSW USING [
    ClientItemsProcType, ProcType, AllocateItemDescriptor, newLine, CommandItem,
    BooleanItem, StringItem, FindItem, Display, DisplayItem],
  MsgSW USING [Post],
  Put USING [Char, CR, Text, Line, LongDecimal, LongNumber],
  Tool USING [Create, MakeSWsProc, MakeMsgSW, MakeFormSW, MakeFileSW],
  ToolWindow USING [TransitionProcType],
  Window USING [Handle],

  File USING [Capability],
  Indirect USING [GetParmFileName],
  Mailer USING [Level, SendMail],
  Slosh USING [AddProcs, Why],
  NameServerDefs USING [
    BumpCacheSize, PupDirServerOn, PupNameServerOn, PupDirServerOff,
    PupNameServerOff],
  PupRouterDefs USING [
    RoutingTableEntry, GetRoutingTableEntry, PupGateInfo, maxHop],
  PupDefs USING [
    PupPackageMake, PupPackageDestroy, AppendHostName, AppendErrorPup,
    GetFreePupBuffer, ReturnFreePupBuffer, PupBuffer, PupSocket, PupSocketDestroy,
    PupSocketMake, SecondsToTocks, SetPupContentsBytes, GetPupContentsBytes,
    EnumeratePupAddresses, PupNameTrouble],
  PupTypes USING [
    eftpReceiveSoc, fillInPupAddress, fillInSocketID, ftpSoc, gatewaySoc,
    librarianSoc, mailSoc, PupAddress, PupType, PupSocketID, spruceStatusSoc,
    telnetSoc],
  HostWatcherOps USING [
    Info, InfoObject, Mode, State, PokeGateway, PokeChat, PokeFtp, PokeMail,
    PokeLibrarian, PokeSpruce, PokeEftp, UpDown];

HostWatcher: MONITOR
  IMPORTS
    Inline, Process, Runtime, Storage, String, System, Time, CmFile, Event,
    FormSW, MsgSW, Put, Tool, Indirect, Mailer, Slosh, NameServerDefs,
    PupRouterDefs, PupDefs, HostWatcherOps
  EXPORTS HostWatcherOps =
  BEGIN OPEN PupDefs, PupTypes;

  herald: STRING = "Host Watcher of March 28, 1981";

  msg, form, log: Window.Handle ← NIL;
  eventItem: Event.Item ← [eventMask: 177777B, eventProc: Broom];
  running, scanning, pleaseStop, debug: BOOLEAN ← FALSE;
  probing: STRING ← NIL;
  useCount: CARDINAL ← 0;

  watcher: PROCESS;
  first: Info ← NIL;
  troubles: STRING ← NIL;
  pause: CONDITION;
  seconds: CARDINAL = 15*60;
  wordsPerCacheEntry: CARDINAL = 25;

  Mode: TYPE = HostWatcherOps.Mode;
  modeSoc: ARRAY Mode OF PupSocketID = [
    [31415, 9265], telnetSoc, ftpSoc, mailSoc, librarianSoc, spruceStatusSoc,
    eftpReceiveSoc];

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

  Info: TYPE = HostWatcherOps.Info;

  LastGatewayVanished: ERROR = CODE;

  HostWatcherOn: PROCEDURE =
    BEGIN
    BumpUseCount: ENTRY PROCEDURE RETURNS [BOOLEAN] = INLINE
      BEGIN RETURN[(useCount ← useCount + 1) = 1]; END;
    IF BumpUseCount[] THEN BEGIN running ← TRUE; Starter[]; END;
    UpdatePicture[];
    END;

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

  HostWatcherOff: PROCEDURE =
    BEGIN
    UnbumpUseCount: ENTRY PROCEDURE RETURNS [BOOLEAN] = INLINE
      BEGIN RETURN[useCount # 0 AND (useCount ← useCount - 1) = 0]; END;
    IF UnbumpUseCount[] THEN BEGIN running ← FALSE; Stopper[]; END;
    UpdatePicture[];
    END;

  Stopper: PROCEDURE =
    BEGIN
    StopperLocked: ENTRY PROCEDURE = INLINE BEGIN NOTIFY pause; END;
    pleaseStop ← TRUE;
    StopperLocked[];
    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[" is "L];
      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, " is "L];
    String.AppendString[text, stateText[info.state]];
    IF info.text.length # 0 THEN
      BEGIN
      String.AppendString[text, ": "L];
      String.AppendString[text, info.text];
      END;
    String.AppendString[text, "."L];
    LogString[text];
    END;

  FindTargets: ENTRY PROCEDURE RETURNS [BOOLEAN] =
    BEGIN
    modeStrings: ARRAY Mode OF STRING = [
      "Gateway"L, "Chat"L, "FTP"L, "Mail"L, "Librarian"L, "Spruce"L, "EFTP"L];
    AddTarget: INTERNAL PROCEDURE [server: STRING, mode: Mode] =
      BEGIN
      temp: STRING = [200];
      AddPair: INTERNAL PROCEDURE [tag, val: 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];
      Put.Line[NIL, temp];
      AppendItem[arg, to, cc, full, mode];
      END;
    parmFileName: STRING ← NIL;
    sectionName: STRING = "HostWatcher"L;
    token, arg: STRING ← NIL;
    to, cc, full: STRING ← NIL;
    IF Runtime.IsBound[Indirect.GetParmFileName] THEN
      parmFileName ← Indirect.GetParmFileName[];
    IF parmFileName = NIL THEN parmFileName ← "HostWatcher.txt"L;
    IF ~CmFile.OpenSection[parmFileName, sectionName] THEN
      BEGIN Problem["Can't find [HostWatcher] section."L]; RETURN[FALSE]; END;
    Announce["Starting "L, herald];
    DO
      [token, arg] ← CmFile.NextItem[];
      SELECT TRUE FROM
        token = NIL => EXIT;
        String.EquivalentString[token, "Troubles"L] =>
          BEGIN
          CheckForRegistry[arg];
          Storage.FreeString[troubles];
          troubles ← arg;
          Announce["In case of trouble, mail will be returned to: "L, troubles];
          END;
        String.EquivalentString[token, "To"L] =>
          BEGIN
          CheckForRegistry[arg];
          DeleteString[to];
          to ← FindString[arg];
          END;
        String.EquivalentString[token, "cc"L] =>
          BEGIN
          CheckForRegistry[arg];
          DeleteString[cc];
          cc ← FindString[arg];
          END;
        String.EquivalentString[token, "Full"L] =>
          BEGIN
          CheckForRegistry[arg];
          DeleteString[full];
          full ← FindString[arg];
          END;
        String.EquivalentString[token, "Debug"L] =>
          BEGIN
          debug ← String.EquivalentString[arg, "TRUE"L];
          Storage.FreeString[arg];
          END;
        String.EquivalentString[token, "Gateway"L] => AddTarget[arg, gate];
        String.EquivalentString[token, "Chat"L] => AddTarget[arg, chat];
        String.EquivalentString[token, "FTP"L] => AddTarget[arg, ftp];
        String.EquivalentString[token, "Mail"L] => AddTarget[arg, mail];
        String.EquivalentString[token, "Librarian"L] => AddTarget[arg, librarian];
        String.EquivalentString[token, "Printer"L] => AddTarget[arg, spruce];
        String.EquivalentString[token, "EFTP"L] => AddTarget[arg, eftp];
        ENDCASE =>
          BEGIN
          IF token[0] # '; THEN Problem["Unknown keyword: "L, token];
          Storage.FreeString[arg];
          END;
      Storage.FreeString[token];
      ENDLOOP;
    CmFile.Close[parmFileName];
    IF first = NIL THEN BEGIN Problem["Oops, no targets"L]; RETURN[FALSE]; END;
    IF troubles = NIL THEN
      Problem["Please specify somebody in case of TROUBLES"L];
    DeleteString[to];
    DeleteString[cc];
    DeleteString[full];
    RETURN[TRUE];
    END;

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

  Problem: PROCEDURE [one, two, three: STRING ← NIL] =
    BEGIN
    text: STRING = [100];
    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: STRING, mode: Mode] =
    BEGIN
    info: Info ← Storage.Node[SIZE[HostWatcherOps.InfoObject]];
    info↑ ← [
      name: server, to: to, cc: cc, full: full,
      address: [[0], [0], modeSoc[mode]], mode: mode, text: Storage.String[100],
      next: NIL];
    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;
    NameServerDefs.BumpCacheSize[wordsPerCacheEntry];
    END;

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

  ForgetTargets: ENTRY PROCEDURE =
    BEGIN
    info: Info ← first;
    UNTIL first = NIL DO
      info ← first; first ← first.next; DeleteItem[info]; ENDLOOP;
    troubles ← Storage.FreeStringNil[troubles];
    END;

  DeleteItem: INTERNAL PROCEDURE [info: Info] =
    BEGIN
    Storage.FreeString[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];
    Storage.FreeString[info.text];
    Storage.Free[info];
    NameServerDefs.BumpCacheSize[-wordsPerCacheEntry];
    END;

  DeleteString: INTERNAL PROCEDURE [s: 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;
    Storage.FreeString[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: ENTRY PROCEDURE =
      BEGIN
      sleep: CARDINAL ← LAST[CARDINAL];
      WHILE sleep > seconds DO
        start ← start + seconds;
        sleep ← Inline.LowHalf[seconds - (System.GetGreenwichMeanTime[] - start)];
        ENDLOOP;
      Process.SetTimeout[@pause, Process.SecondsToTicks[sleep]];
      WAIT pause;
      END;
    -- Give NameServer extra time to be sure it has started
    THROUGH [0..1000) DO Process.Yield[]; 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.Yield[]; ENDLOOP;
        ENDLOOP;
      scanning ← FALSE;
      probing ← NIL;
      UpdatePicture[];
      PostWithTime["End of scan."L];
      IF ~pleaseStop THEN WatcherWait[];
      ENDLOOP;
    END;

  WatcherPoke: ENTRY 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];
        Process.SetTimeout[@pause, Process.SecondsToTicks[180]];
        WAIT pause;
        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];
        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 = up AND oldState = up AND info.lastLineChanged);
    IF interesting OR info.state = full THEN LogState[info];
    IF interesting AND info.to # NIL THEN SendStatus[info.to, info];
    IF info.state = full AND info.full # NIL THEN SendStatus[info.full, info];
    IF info.state = up THEN
      BEGIN info.lastUp ← Time.Current[]; info.noPath ← FALSE; END;
    END;

  WatcherWait: ENTRY PROCEDURE [start: LONG CARDINAL] =
    BEGIN
    sleep: CARDINAL ← LAST[CARDINAL];
    sleep ← Inline.LowHalf[seconds - (System.GetGreenwichMeanTime[] - start)];
    WHILE sleep > seconds DO
      start ← start + seconds;
      sleep ← Inline.LowHalf[seconds - (System.GetGreenwichMeanTime[] - start)];
      ENDLOOP;
    Process.SetTimeout[@pause, Process.SecondsToTicks[sleep]];
    WAIT pause;
    END;

  FindLastGateway: PROCEDURE [info: Info] =
    BEGIN
    rte: PupRouterDefs.RoutingTableEntry;
    soc: PupSocket;
    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.network = NIL OR rte.hop > PupRouterDefs.maxHop THEN
      ERROR LastGatewayVanished;
    hops ← rte.hop;
    thisGateway ← previousGateway ← [
      [rte.network.netNumber.b], 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 PupSocketDestroy[soc]; IF b # NIL THEN ReturnFreePupBuffer[b]; END;
    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 ← GetFreePupBuffer[];
        b.pupType ← gatewayRequest;
        SetPupContentsBytes[b, 0];
        b.pupID ← [id, id];
        soc.put[b];
        UNTIL (b ← soc.get[]) = NIL DO
          IF b.pupType = gatewayInfo AND b.pupID = [id, id] THEN
            BEGIN
            one: LONG POINTER TO PupRouterDefs.PupGateInfo ←
              LOOPHOLE[@b.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;
          ReturnFreePupBuffer[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 ← GetFreePupBuffer[];
        b.pupType ← gatewayRequest;
        SetPupContentsBytes[b, 0];
        b.pupID ← [id, id];
        soc.put[b];
        UNTIL (b ← soc.get[]) = NIL DO
          IF b.pupType = gatewayInfo AND b.pupID = [id, id] THEN
            BEGIN
            length: CARDINAL = GetPupContentsBytes[b];
            n: CARDINAL ← length/(2*SIZE[PupRouterDefs.PupGateInfo]);
            one: LONG POINTER TO PupRouterDefs.PupGateInfo ←
              LOOPHOLE[@b.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;
          ReturnFreePupBuffer[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 ← GetFreePupBuffer[];
      b.pupType ← gatewayRequest;
      SetPupContentsBytes[b, 0];
      b.pupID ← [id, id];
      soc.put[b];
      UNTIL (b ← soc.get[]) = NIL DO
        IF b.pupType = gatewayInfo AND b.pupID = [id, id] THEN
          BEGIN
          length: CARDINAL = GetPupContentsBytes[b];
          n: CARDINAL ← length/(2*SIZE[PupRouterDefs.PupGateInfo]);
          one: LONG POINTER TO PupRouterDefs.PupGateInfo ← LOOPHOLE[@b.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;
        ReturnFreePupBuffer[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];
    END;

  CheckLastGateway: PROCEDURE [info: Info] =
    BEGIN
    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
    soc ← PupSocketMake[
      PupTypes.fillInSocketID, info.lastGateway, SecondsToTocks[2]];
    THROUGH [0..10) UNTIL info.lastGatewayOk DO
      b ← GetFreePupBuffer[];
      b.pupType ← gatewayRequest;
      SetPupContentsBytes[b, 0];
      b.pupID ← [0, 0];
      soc.put[b];
      UNTIL (b ← soc.get[]) = NIL DO
        IF b.pupType = gatewayInfo THEN info.lastGatewayOk ← TRUE;
        ReturnFreePupBuffer[b];
        ENDLOOP;
      ENDLOOP;
    PupSocketDestroy[soc];
    END;

  UpdateUpDown: PROCEDURE [info: Info] =
    BEGIN
    upTable: ARRAY State OF BOOLEAN = [
      FALSE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE];
    downTable: ARRAY State OF BOOLEAN = [
      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 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: STRING, info: Info] =
    BEGIN
    subject: STRING = [100];
    body: STRING = [350];
    state: State;
    temp: STRING = [25];
    n: LONG CARDINAL;
    Info: PROCEDURE [s: STRING, level: Mailer.Level] = {LogString[s]; };
    String.AppendString[subject, info.name];
    String.AppendString[subject, " is "L];
    String.AppendString[subject, stateText[info.state]];
    String.AppendString[body, info.name];
    String.AppendString[body, " is "L];
    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.SendMail[
      "HostWatcher"L, subject, to, info.cc, body, troubles, NIL, 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: STRING] = BEGIN Put.Text[log, s]; END;

  WriteLine: PROCEDURE [s: 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: 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: 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: 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: 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: POINTER TO PupAddress, name: 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.network = 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;

  FlakeyNet: PROCEDURE [him: PupAddress] RETURNS [BOOLEAN] =
    BEGIN
    -- SLA, SLA2 or PR
    IF him.net = 7B OR him.net = 17B OR him.net = 24B THEN RETURN[TRUE];
    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: STRING ← NIL] =
    BEGIN OPEN String;
    text: STRING = [200];
    Time.AppendCurrent[text];
    AppendString[text, "  "L];
    AppendString[text, one];
    IF two # NIL THEN AppendString[text, two];
    AppendChar[text, '.];
    LogString[text];
    END;

  LogString: PROCEDURE [text: STRING] =
    BEGIN IF msg # NIL THEN Put.Line[msg, text]; Put.Line[NIL, text]; 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 ← "HostWatcher.txt"L;
    IF String.EquivalentString[parmFileName, fileName] AND running THEN
      BEGIN Stopper[]; Starter[]; END;
    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: PROCEDURE [why: Event.Reason] =
    BEGIN
    SELECT why FROM
      makeImage, makeCheck, stopMesa => IF running THEN Stopper[];
      startImage, restartCheck, continueCheck => IF running THEN Starter[];
      ENDCASE => NULL;
    END;



  -- Main Body

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