-- Copyright (C) 1983, 1985  by Xerox Corporation. All rights reserved. 
-- LineWatcher.mesa, HGM, 15-Nov-85  2:12:01

DIRECTORY
  Context USING [Create, Data, Find, Type, UniqueType],
  Display USING [Bitmap, Invert, replaceFlags, White],
  Exec USING [AddCommand, ExecProc, FreeTokenString, GetToken],
  Format USING [],  -- Needed by Put.Number
  FormSW USING [
    ClientItemsProcType, ProcType, AllocateItemDescriptor, newLine, CommandItem,
    StringItem, NumberItem],
  FileSW USING [GetFile, IsIt, SetFile],
  Heap USING [systemZone],
  MFile USING [Acquire, Error, GetLength, Handle, Release, SetTimes],
  MsgSW USING [Post],
  MStream USING [GetFile, GetLength, Handle, IsIt, Log, SetAccess, SetLength, SetLogReadLength],
  Process USING [Detach, MsecToTicks, Pause, Ticks],
  Put USING [Char, CR, Line, Number, Text],
  Runtime USING [GetBcdTime],
  Stream USING [GetByteProcedure, GetWordProcedure, GetProcedure, Handle],
  String USING [AppendLongNumber, AppendNumber, AppendString, Equivalent],
  System USING [
    AdjustGreenwichMeanTime, GreenwichMeanTime, GetGreenwichMeanTime, nullHostNumber],
  Time USING [Append, AppendCurrent, Unpack],
  Tool USING [
    Create, MakeSWsProc, UnusedLogName, MakeMsgSW, MakeFormSW, MakeFileSW,
    AddThisSW],
  ToolWindow USING [CreateSubwindow, DisplayProcType, nullBox, SetTinyName, TransitionProcType],
  Window USING [Handle, Box],

  Buffer USING [AccessHandle, DestroyPool, GetBuffer, MakePool, ReturnBuffer],
  EthernetDriverFriends USING [EtherStatsInfo],
  EthernetFormat USING [EtherStatsEntry, ethernetStatsReply, etherVersion],
  GateControlDefs USING [pupStatsAck, pupStatsNak, pupStatsSend],
  PhoneNetFriends USING [PhoneNetInfo],
  PhoneNetExtras USING [pupDupsFiltered, leafDupsFiltered, nsDupsFiltered],
  PupRouterDefs USING [GetRoutingTableEntry, maxHop, RoutingTableEntry],
  PupDefs USING [
    AppendPupAddress, PupPackageMake, PupPackageDestroy, PupBuffer,
    PupAddress, EnumeratePupAddresses,
    PupNameTrouble, SetPupContentsWords, PupSocket, PupSocketDestroy,
    PupSocketMake, MsToTocks],
  PupTypes USING [fillInSocketID, statSoc],
  PupWireFormat USING [BcplToMesaLongNumber],
  SlaFormat USING [LineState, RoutingTableEntry, SlaStatsEntry, slaStatsReply, slaVersion],
  Driver;

LineWatcher: MONITOR
  IMPORTS
    Context, Display, Exec, FileSW, FormSW, Heap, MFile, MsgSW, MStream, Process, Put,
    Runtime, String, System, Time, Tool, ToolWindow,
    Buffer, PupDefs, PupRouterDefs, PupWireFormat
  SHARES Driver =
  BEGIN

  contextType: PUBLIC Context.Type ← Context.UniqueType[];

  Data: TYPE = LONG POINTER TO DataRecord;
  DataRecord: TYPE = RECORD [
    tool, msg, form, boxes, log: Window.Handle ← NIL,
    running: BOOLEAN ← FALSE,
    pleaseStop: BOOLEAN ← FALSE,
    indicator: {left, right, off} ← off,
    seconds: CARDINAL ← 300,
    target: LONG STRING ← Heap.systemZone.NEW[StringBody[30]],
    netNumber: CARDINAL ← 0,
    where: PupDefs.PupAddress ← [[0], [0], PupTypes.statSoc],
    pool: Buffer.AccessHandle ← NIL,
    soc: PupDefs.PupSocket ← NIL,
    oldInfo: LONG POINTER TO LineInfo ← Heap.systemZone.NEW[LineInfo],
    newInfo: LONG POINTER TO LineInfo ← Heap.systemZone.NEW[LineInfo],
    new: BOOL ← FALSE, -- Old GateStats ignores new querrys
    cyclesPerSecond: LONG CARDINAL ← 0,
    thisID: CARDINAL ← 0 ];
  
  overheadPerPacket: CARDINAL = 4; -- Flag, CRC, CRC, flag
  -- Arg. Can't distinguish 3MB nets from 10MB nets
  ethernetOneOverheadPerPacket: CARDINAL = 2; -- CRC, CRC (and 1 preamble bit)
  ethernetOverheadPerPacket: CARDINAL = 8 + 4 + 12; -- preamble, CRC, gap (9.6 microseconds)

  Start: FormSW.ProcType =
    BEGIN
    data: Data = Context.Find[contextType, sw];
    oldLogFileName: LONG STRING ← FileSW.GetFile[data.log].name;
    newLogFileName: STRING = [100];
    netNumberString: STRING = [10];
    IF data.running THEN { MsgSW.Post[data.msg, "Somebody is already running..."L]; RETURN; };
    String.AppendNumber[netNumberString, data.netNumber, 8];
    String.AppendString[newLogFileName, data.target];
    String.AppendString[newLogFileName, "-"L];
    String.AppendString[newLogFileName, netNumberString];
    String.AppendString[newLogFileName, ".data"L];
    ToolWindow.SetTinyName[data.tool, data.target, netNumberString];
    IF ~String.Equivalent[oldLogFileName, newLogFileName] THEN
      BEGIN -- All this to get append mode. Yetch.
      fh: MFile.Handle ← NIL;
      bytes: LONG CARDINAL ← 0;
      sh: MStream.Handle;
      fh ← MFile.Acquire[newLogFileName, anchor, [] ! MFile.Error => CONTINUE];
      IF fh # NIL THEN {
        bytes ← MFile.GetLength[fh];
        MFile.Release[fh]; };
      sh ← MStream.Log[newLogFileName, []];
      IF bytes # 0 THEN {
        getByte: Stream.GetByteProcedure = sh.getByte;
        getWord: Stream.GetWordProcedure = sh.getWord;
        get: Stream.GetProcedure = sh.get;
        MStream.SetAccess[sh, writeOnly];
        MStream.SetLength[sh, bytes];
        MStream.SetAccess[sh, log];
        sh.getByte ← getByte;
        sh.getWord ← getWord;
        sh.get ← get; };
      FileSW.SetFile[data.log, newLogFileName, sh];
      IF fh = NIL THEN Put.Line[data.log, -- New File: Insert header for ChartPlot (and people)
"
*                                   Packets             Bytes      Bits/Sec     Errors
Date       Time      Sec  CPU P-Sent P-Recv   B-Sent   B-Recv   Sent   Recv  Dup Cong Miss  CRC
"L];
      END;
    Put.CR[data.log];
    Put.Text[data.log, "*  Watching net "L];
    O[data.log, data.netNumber];
    Put.Text[data.log, " on "L];
    IF ~FindPath[data] THEN RETURN;
    data.running ← TRUE;
    Process.Detach[FORK Watch[data, data.seconds]];
    END;

  Stop: FormSW.ProcType =
    BEGIN
    data: Data = Context.Find[contextType, sw];
    Off[data];
    END;

  Off: PROCEDURE [data: Data] =
    BEGIN
    IF ~data.running THEN RETURN;
    data.pleaseStop ← TRUE;
    WHILE data.running DO Process.Pause[1]; ENDLOOP;
    data.pleaseStop ← FALSE;
    END;

  FindPath: PROCEDURE [data: Data] RETURNS [BOOLEAN] =
    BEGIN
    Put.Text[data.log, data.target];
    Put.Char[data.log, '=];
    MyGetPupAddress[
      @data.where, data.target !
      PupDefs.PupNameTrouble =>
        BEGIN MsgSW.Post[data.msg, e]; Put.Line[data.log, e]; GOTO Trouble; END];
    PrintPupAddress[data.log, data.where];
    Put.Line[data.log, "."L];
    RETURN[TRUE];
    EXITS Trouble => RETURN[FALSE];
    END;

  MyGetPupAddress: PROCEDURE [
    him: LONG POINTER TO PupDefs.PupAddress, name: LONG STRING] =
    BEGIN
    SkipFlakeyNets: PROCEDURE [her: PupDefs.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 PupDefs.EnumeratePupAddresses[name, SkipFlakeyNets] THEN RETURN;
    ERROR PupDefs.PupNameTrouble["No Route to that Host"L, noRoute];
    END;

  FlakeyNet: PROCEDURE [him: PupDefs.PupAddress] RETURNS [BOOLEAN] =
    BEGIN
    IF him.net > 277B THEN RETURN[TRUE];
    IF him.net = 106B THEN RETURN[TRUE]; -- Aylesbury/Welwyn-Link
    IF him.net = 141B THEN RETURN[TRUE]; -- CP10/DLOS-Link
    IF him.net = 202B THEN RETURN[TRUE]; -- FortXerox/CP10-Link
    RETURN[FALSE];
    END;

  Watch: PROCEDURE [data: Data, seconds: CARDINAL] =
    BEGIN
    startTime, stopTime, targetTime: System.GreenwichMeanTime;
    actual: LONG CARDINAL;
    pause: Process.Ticks;
    MakeConnection[data];
    SetupBoxes[data];
    pause ← Process.MsecToTicks[500];
    UNTIL data.pleaseStop DO -- Wait until even multiple of seconds
      targetTime ← System.GetGreenwichMeanTime[];
      IF (targetTime MOD seconds) = 0 THEN EXIT;
      Process.Pause[pause];
      ENDLOOP;
    startTime ← targetTime;
    GetInfo[data];
    data.oldInfo↑ ← data.newInfo↑;
    Put.CR[data.log];
    UNTIL data.pleaseStop DO
      WHILE System.GetGreenwichMeanTime[] - targetTime >= data.seconds DO
        IF targetTime > System.GetGreenwichMeanTime[] THEN EXIT;  -- Clock set backwards ==> hang
        -- Oops, it took us more than a cycle to process everything.
        -- Maybe we were sitting in the debugger.
        targetTime ← System.AdjustGreenwichMeanTime[targetTime, data.seconds];
        ENDLOOP;
      UNTIL data.pleaseStop OR (System.GetGreenwichMeanTime[] - targetTime) >= data.seconds DO
        Process.Pause[pause];
        ENDLOOP;
      IF data.pleaseStop THEN EXIT;
      GetInfo[data];
      stopTime ← System.GetGreenwichMeanTime[];
      actual ← stopTime - startTime;
      IF data.pleaseStop THEN EXIT;
      PrintInfo[data, actual];
      data.oldInfo↑ ← data.newInfo↑;
      startTime ← stopTime;
      targetTime ← System.AdjustGreenwichMeanTime[targetTime, seconds];
      ENDLOOP;
    SetDownBoxes[data];
    KillConnection[data];
    data.running ← FALSE;
    END;

  Cpu: TYPE = RECORD [cycles, cyclesPerSecond: LONG CARDINAL];
  State: TYPE = {down, up, looped, unknown};
  LineInfo: TYPE = RECORD [
    cycles: LONG CARDINAL,
    packetsSent: LONG CARDINAL,
    packetsRecv: LONG CARDINAL,
    bytesSent: LONG CARDINAL,
    bytesRecv: LONG CARDINAL,
    filtered: LONG CARDINAL,
    congestion: LONG CARDINAL,
    missed: LONG CARDINAL,
    badCrc: LONG CARDINAL,
    state: State];

  GetInfo: PROCEDURE [data:Data] =
    BEGIN
    b: PupDefs.PupBuffer;
    try: CARDINAL ← 0;
    data.thisID ← data.thisID + 1;
    DO  -- until we get the answer
      b ← Buffer.GetBuffer[pup, data.pool, send];
      b.pup.pupID ← [data.thisID, data.thisID];
      b.pup.pupType ← GateControlDefs.pupStatsSend;
      b.pup.pupWords[0] ← data.netNumber;
      b.pup.pupWords[1] ← 1;
      PupDefs.SetPupContentsWords[b, 2];
      IF ~data.new AND try > 10 THEN PupDefs.SetPupContentsWords[b, 1];
      data.soc.put[b];
      FlipBoxes[data];
      try ← try + 1;
      DO
        b ← data.soc.get[];
        IF b = NIL THEN EXIT;
        IF b.pup.pupType = error OR b.pup.pupType = GateControlDefs.pupStatsNak
          OR b.pup.pupID.b # data.thisID THEN BEGIN Buffer.ReturnBuffer[b]; LOOP; END;
        IF b.pup.pupType # GateControlDefs.pupStatsAck THEN ERROR;
	SELECT b.pup.pupWords[0] FROM
        SlaFormat.slaStatsReply => {
          SELECT b.pup.pupWords[1] FROM
	    SlaFormat.slaVersion => CopySlaData[data, b];
	    SlaFormat.slaVersion+1 => CopyPhoneData[data, b];
	    ENDCASE => ERROR;
          EXIT; };
        EthernetFormat.ethernetStatsReply => {
          SELECT b.pup.pupWords[1] FROM
            EthernetFormat.etherVersion => CopyOldEtherData[data, b];
            EthernetFormat.etherVersion+1 => CopyNewEtherData[data, b];
	    ENDCASE => ERROR;
          EXIT; };
        ENDCASE => ERROR;
	ENDLOOP;
      IF b # NIL THEN EXIT;
      IF data.pleaseStop THEN RETURN;
      LOOP;
      ENDLOOP;
    FlipBoxes[data];
    Buffer.ReturnBuffer[b];
    END;

  CopyPhoneData: PROCEDURE [data: Data, b: PupDefs.PupBuffer] =
    BEGIN
    cpu: LONG POINTER TO Cpu ← LOOPHOLE[@b.pup.pupWords[2]];
    psi: LONG POINTER TO PhoneNetFriends.PhoneNetInfo ← LOOPHOLE[cpu + SIZE[Cpu]];
    state: State;
    data.new ← TRUE;
    data.cyclesPerSecond ← cpu.cyclesPerSecond;
    SELECT psi.remoteHostNumber FROM
      System.nullHostNumber => state ← down;
      -- don't know his NS host number to check for looped
      ENDCASE => state ← up;
    data.newInfo↑ ← [
      cycles: cpu.cycles,
      packetsSent: psi.stats[pktsSent],
      packetsRecv: psi.stats[pktsReceived],
      bytesSent: psi.stats[bytesSent],
      bytesRecv: psi.stats[bytesReceived],
      filtered:
        psi.stats[PhoneNetExtras.pupDupsFiltered] +
        psi.stats[PhoneNetExtras.leafDupsFiltered] +
        psi.stats[PhoneNetExtras.nsDupsFiltered],
      congestion: psi.stats[congestion] + psi.stats[connTooGreedy],
      missed: psi.stats[rcvErrorNoGet],
      badCrc: psi.stats[rcvErrorCRC],
      state: state ];
    END;

  CopySlaData: PROCEDURE [data: Data, b: PupDefs.PupBuffer] =
    BEGIN
    lastHost: CARDINAL ← b.pup.pupWords[2];
    rte: LONG POINTER TO SlaFormat.RoutingTableEntry ←
      LOOPHOLE[@b.pup.pupWords[3] + lastHost*SIZE[SlaFormat.RoutingTableEntry]];
    sse: LONG POINTER TO SlaFormat.SlaStatsEntry ← LOOPHOLE[rte+1];
    state: State;
    SELECT sse.state FROM
      loopedBack => state ← looped;
      up => state ← up;
      down => state ← down;
      ENDCASE => state ← unknown;
    data.newInfo↑ ← [
      cycles: 0,
      packetsSent: PupWireFormat.BcplToMesaLongNumber[sse.packetsSent],
      packetsRecv: PupWireFormat.BcplToMesaLongNumber[sse.packetsRecv],
      bytesSent: PupWireFormat.BcplToMesaLongNumber[sse.bytesSent],
      bytesRecv: PupWireFormat.BcplToMesaLongNumber[sse.bytesRecv],
      filtered: 0,
      congestion: 0,
      missed: sse.syncErrors,
      badCrc: sse.badCrc,
      state: state ];
    END;

  CopyNewEtherData: PROCEDURE [data: Data, b: PupDefs.PupBuffer] =
    BEGIN
    cpu: LONG POINTER TO Cpu ← LOOPHOLE[@b.pup.pupWords[2]];
    esi: LONG POINTER TO EthernetDriverFriends.EtherStatsInfo ← LOOPHOLE[cpu + SIZE[Cpu]];
    data.new ← TRUE;
    data.cyclesPerSecond ← cpu.cyclesPerSecond;
    data.newInfo↑ ← [
      cycles: cpu.cycles,
      packetsSent: esi.packetsSent,
      packetsRecv: esi.packetsRecv,
      bytesSent: 2*esi.wordsSent,
      bytesRecv: 2*esi.wordsRecv,
      filtered: 0,
      congestion: 0,
      missed: esi.packetsMissed,
      badCrc: esi.badCrc + esi.crcAndBadAlignment,
      state: up ];
    END;

  CopyOldEtherData: PROCEDURE [data: Data, b: PupDefs.PupBuffer] =
    BEGIN
    ese: LONG POINTER TO EthernetFormat.EtherStatsEntry ← LOOPHOLE[@b.pup.pupWords[1]];
    data.newInfo↑ ← [
      cycles: 0,
      packetsSent: PupWireFormat.BcplToMesaLongNumber[ese.packetsSent],
      packetsRecv: PupWireFormat.BcplToMesaLongNumber[ese.packetsRecv],
      bytesSent: 0,
      bytesRecv: 0,
      filtered: 0,
      congestion: 0,
      missed: PupWireFormat.BcplToMesaLongNumber[ese.inputOff],
      badCrc: PupWireFormat.BcplToMesaLongNumber[ese.badRecvStatus],
      state: up ];
    END;

  PrintInfo: PROCEDURE [data: Data, actual: LONG CARDINAL] =
    BEGIN
    restarted: BOOL ← FALSE;
    temp: LineInfo ← [
      cycles: data.newInfo.cycles - data.oldInfo.cycles,
      packetsSent: data.newInfo.packetsSent - data.oldInfo.packetsSent,
      packetsRecv: data.newInfo.packetsRecv - data.oldInfo.packetsRecv,
      bytesSent: data.newInfo.bytesSent - data.oldInfo.bytesSent,
      bytesRecv: data.newInfo.bytesRecv - data.oldInfo.bytesRecv,
      filtered: data.newInfo.filtered - data.oldInfo.filtered,
      congestion: data.newInfo.congestion - data.oldInfo.congestion,
      missed: data.newInfo.missed - data.oldInfo.missed,
      badCrc: data.newInfo.badCrc - data.oldInfo.badCrc,
      state: up ];
    PrintTime[data.log];
    Put.Text[data.log, " "L];
    IF LOOPHOLE[temp.packetsSent, LONG INTEGER] < 0 THEN { restarted ← TRUE; temp ← data.newInfo↑; };
    LD5[data.log, actual];
    IF data.cyclesPerSecond = 0 THEN Put.Text[data.log, "    0"L]
    ELSE {
      used: LONG CARDINAL ← temp.cycles*100/data.cyclesPerSecond/actual;
      IF used > 100 THEN used ← 100;
      LD5[data.log, 100-used]; };
    LD7[data.log, temp.packetsSent];
    LD7[data.log, temp.packetsRecv];
    LD9[data.log, temp.bytesSent];
    LD9[data.log, temp.bytesRecv];
    LD7[data.log, (temp.bytesSent + overheadPerPacket*temp.packetsSent)*8/actual];
    LD7[data.log, (temp.bytesRecv + overheadPerPacket*temp.packetsRecv)*8/actual];
    IF (temp.filtered + temp.congestion + temp.missed + temp.badCrc) > 0 OR restarted THEN {
      LD5[data.log, temp.filtered];
      LD5[data.log, temp.congestion];
      LD5[data.log, temp.missed];
      LD5[data.log, temp.badCrc];
      IF restarted THEN Put.Text[data.log, " *R"L]; };
    Put.CR[data.log];
    ForceOutInfo[data.log];
    END;

  PrintTime: PROCEDURE [log: Window.Handle] =
    BEGIN
    text: STRING = [20];
    Time.AppendCurrent[text];
    Put.Text[log, text]
    END;

  PrintPupAddress: PROCEDURE [log: Window.Handle, a: PupDefs.PupAddress] =
    BEGIN
    text: STRING = [20];
    PupDefs.AppendPupAddress[text, a];
    Put.Text[log, text];
    END;

  O: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Number[log, n, [8, FALSE, TRUE, 0]];
    END;

  O2: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Char[log, ' ];
    Put.Number[log, n, [8, FALSE, TRUE, 1]];
    END;

  O3: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Char[log, ' ];
    Put.Number[log, n, [8, FALSE, TRUE, 2]];
    END;

  D: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Number[log, n, [10, FALSE, TRUE, 0]];
    END;

  D2: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Char[log, ' ];
    Put.Number[log, n, [10, FALSE, TRUE, 1]];
    END;

  D3: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Char[log, ' ];
    Put.Number[log, n, [10, FALSE, TRUE, 2]];
    END;

  D4: PROCEDURE [log: Window.Handle, n: CARDINAL] =
    BEGIN
    Put.Char[log, ' ];
    Put.Number[log, n, [10, FALSE, TRUE, 3]];
    END;

  LD2: PROCEDURE [log: Window.Handle, n: LONG INTEGER] =
    BEGIN
    s: STRING = [20];
    Put.Char[log, ' ];
    String.AppendLongNumber[s, n, 10];
    THROUGH [s.length..1) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, s];
    END;

  LD3: PROCEDURE [log: Window.Handle, n: LONG INTEGER] =
    BEGIN
    s: STRING = [20];
    Put.Char[log, ' ];
    String.AppendLongNumber[s, n, 10];
    THROUGH [s.length..2) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, s];
    END;

  LD5: PROCEDURE [log: Window.Handle, n: LONG INTEGER] =
    BEGIN
    s: STRING = [20];
    Put.Char[log, ' ];
    String.AppendLongNumber[s, n, 10];
    THROUGH [s.length..4) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, s];
    END;

  LD7: PROCEDURE [log: Window.Handle, n: LONG INTEGER] =
    BEGIN
    s: STRING = [20];
    Put.Char[log, ' ];
    String.AppendLongNumber[s, n, 10];
    THROUGH [s.length..6) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, s];
    END;

  LD9: PROCEDURE [log: Window.Handle, n: LONG INTEGER] =
    BEGIN
    s: STRING = [20];
    String.AppendLongNumber[s, n, 10];
    Put.Char[log, ' ];
    THROUGH [s.length..8) DO Put.Char[log, ' ]; ENDLOOP;
    Put.Text[log, s];
    END;

  MakeConnection: PROCEDURE [data: Data] =
    BEGIN
    data.pool ← Buffer.MakePool[send: 1, receive: 10];
    data.soc ← PupDefs.PupSocketMake[
      PupTypes.fillInSocketID, data.where, PupDefs.MsToTocks[5000]];
    END;

  KillConnection: PROCEDURE [data: Data] =
    BEGIN
    PupDefs.PupSocketDestroy[data.soc];
    Buffer.DestroyPool[data.pool];
    END;

  indicatorBox: Window.Box = [[25, 10], [16, 16]];
  DisplayBoxes: ToolWindow.DisplayProcType =
    BEGIN
    data: Data = Context.Find[contextType, window];
    pattern: ARRAY [0..1] OF ARRAY [0..8) OF WORD;
    left: WORD = 177400B;
    right: WORD = 000377B;
    SELECT data.indicator FROM
      left => pattern ← [ALL[left], ALL[right]];
      right => pattern ← [ALL[right], ALL[left]];
      off => pattern ← [ALL[0], ALL[0]];
      ENDCASE;
    Display.Bitmap[window, indicatorBox, [@pattern, 0, 0], 16, Display.replaceFlags]
    END;

  SetupBoxes: PROCEDURE [data: Data] =
    BEGIN
    data.indicator ← left;
    DisplayBoxes[data.boxes];
    END;

  FlipBoxes: PROCEDURE [data: Data] =
    BEGIN
    SELECT data.indicator FROM
      left => data.indicator ← right;
      off, right => data.indicator ← left;
      ENDCASE;
    Display.Invert[data.boxes, indicatorBox];
    END;

  SetDownBoxes: PROCEDURE [data: Data] =
    BEGIN
    data.indicator ← off;
    Display.White[data.boxes, indicatorBox];
    END;

  MakeBoxesSW: PROCEDURE [data: Data, window: Window.Handle] =
    BEGIN
    box: Window.Box ← ToolWindow.nullBox;
    box.dims.h ← 36;
    data.boxes ← ToolWindow.CreateSubwindow[
      parent: window,
      display: DisplayBoxes,
      box: box];
    Context.Create[type: contextType, data: data, proc: DropIt, window: data.boxes];
    Tool.AddThisSW[window: window, sw: data.boxes, swType: vanilla];
    END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN
    logFileName: STRING = [40];
    data: Data ← Context.Find[contextType, window];
    IF data = NIL THEN {
      data ← SIGNAL FindMyContext[];
      Context.Create[type: contextType, data: data, proc: DropIt, window: window]; };
    data.msg ← Tool.MakeMsgSW[window: window, lines: 5];
    BEGIN ENABLE FindMyContext => RESUME[data];
    data.form ← Tool.MakeFormSW[window: window, formProc: MakeForm];
    END;
    Context.Create[type: contextType, data: data, proc: DropIt, window: data.form];
    MakeBoxesSW[data, window];
    Tool.UnusedLogName[logFileName, "LineWatcher.log$"L];
    data.log ← Tool.MakeFileSW[window: window, name: logFileName, allowTypeIn: FALSE];
    END;

  MakeForm: FormSW.ClientItemsProcType =
    BEGIN
    nParams: CARDINAL = 5;
    data: Data = SIGNAL FindMyContext[];
    items ← FormSW.AllocateItemDescriptor[nParams];
    items[0] ← FormSW.CommandItem[
      tag: "Stop"L, proc: Stop, place: FormSW.newLine];
    items[1] ← FormSW.CommandItem[tag: "Start"L, proc: Start];
    items[2] ← FormSW.NumberItem[tag: "Seconds"L, value: @data.seconds, default: 60];
    items[3] ← FormSW.NumberItem[
      tag: "NetNumber"L, value: @data.netNumber, default: 7, radix: octal];
    items[4] ← FormSW.StringItem[tag: "Target"L, string: @data.target];
    RETURN[items, TRUE];
    END;

  ClientTransition: ToolWindow.TransitionProcType =
    BEGIN
    data: Data ← Context.Find[contextType, window];
    IF data = NIL THEN {
      data ← SIGNAL FindMyContext[];
      Context.Create[type: contextType, data: data, proc: DropIt, window: window]; };
    SELECT TRUE FROM
      old = inactive =>
        BEGIN
	IF data.target.length = 0 THEN String.AppendString[data.target, "ME"L];
	[] ← PupDefs.PupPackageMake[];
	END;
      new = inactive =>
        BEGIN IF data.running THEN Off[data]; PupDefs.PupPackageDestroy[]; END;
      ENDCASE;
    END;

  ForceOutInfo: ENTRY PROCEDURE [wh: Window.Handle] =
    -- Beware: This gets an SV lockup and then runs out of VM for Resident Memory
    BEGIN
    sh: Stream.Handle;
    IF ~FileSW.IsIt[wh] THEN RETURN;
    sh ← FileSW.GetFile[wh].s;
    IF ~MStream.IsIt[sh] THEN RETURN;
    Process.Pause[Process.MsecToTicks[5000]]; -- Let others play before we thrash
    MStream.SetLogReadLength[sh, MStream.GetLength[sh]];
    MFile.SetTimes[file: MStream.GetFile[sh], create: System.GetGreenwichMeanTime[]];
    END;

  FindMyContext: SIGNAL RETURNS [Data] = CODE;
  LineWatcherCommand: Exec.ExecProc =
    BEGIN
    tryIt: BOOLEAN ← FALSE;
    token: LONG STRING ← NIL;
    data: Data = Heap.systemZone.NEW[DataRecord ← []];
    herald: STRING = [100];
    String.AppendString[herald, "LineWatcher of  "L];
    Time.Append[herald, Time.Unpack[Runtime.GetBcdTime[]]];
    [token, ] ← h.GetToken[];
    IF token # NIL THEN {
      data.netNumber ← 0;
      FOR i: CARDINAL IN [0..token.length) DO
        c: CHAR = token[i];
	IF ~(c IN ['0..'7]) THEN EXIT;
	data.netNumber ← data.netNumber*10B + c-'0;
        ENDLOOP;
      token ← Exec.FreeTokenString[token]; };
    [token, ] ← h.GetToken[];
    IF token # NIL THEN {
      tryIt ← TRUE;
      String.AppendString[data.target, token];
      token ← Exec.FreeTokenString[token]; };
    BEGIN ENABLE FindMyContext => RESUME[data];
    data.tool ← Tool.Create[
      name: herald,
      cmSection: "LineWatcher"L,
      makeSWsProc: MakeSWs,
      clientTransition: ClientTransition];
    END;
    IF Context.Find[contextType, data.tool] = NIL THEN
      Context.Create[type: contextType, data: data, proc: DropIt, window: data.tool];
    IF tryIt THEN Start[data.form];
    END;

  DropIt: PROCEDURE [Data, Window.Handle] =
    BEGIN
    END;

  Exec.AddCommand["LineWatcher.~"L, LineWatcherCommand, NIL, NIL];

  END.