-- File: FixTimeServers.mesa,  Last Edit: HGM  March 19, 1981  6:51 PM
-- Please don't forget to update the herald....

DIRECTORY
  InlineDefs USING [BcplLongNumber, BcplToMesaLongNumber],
  Process USING [Detach, MsecToTicks, Pause, Yield],
  Storage USING [Node, String, Free, FreeString],
  String USING [AppendString, AppendNumber],
  System USING [
    AdjustGreenwichMeanTime, GreenwichMeanTime, GetGreenwichMeanTime],
  Time USING [Append, AppendCurrent, Unpack],

  Event USING [Item, Reason, AddNotifier],
  Format USING [], -- Needed by Put.Number and Put.Date
  FormSW USING [
    AllocateItemDescriptor, ClientItemsProcType, CommandItem, Display, FindItem,
    ItemHandle, ModifyEditable, newLine, NumberItem, ProcType, StringItem],
  MsgSW USING [Post],
  Profile USING [userName, userPassword],
  Put USING [Char, CR, Decimal, Text, Line, Number, LongDecimal, LongNumber],
  Tool USING [
    Create, MakeSWsProc, UnusedLogName, MakeMsgSW, MakeFormSW, MakeFileSW,
    AddThisSW],
  ToolWindow USING [TransitionProcType, DisplayProcType, CreateSubwindow],
  Window USING [Handle, Box, DisplayData, DisplayInvert, DisplayWhite],

  TimeServerDefs USING [
    PupTimeFormat, PupTimeServerOff, PupTimeServerOn, resetTimeReply,
    resetTimeRequest, timeStatsRequest, timeStatsReply],
  Clock USING [SetTime],
  Password USING [Status, ValidMemberOfGroup],
  PupDefs USING [
    PupPackageMake, PupPackageDestroy, GetFreePupBuffer, GetLocalPupAddress,
    ReturnFreePupBuffer, PupBuffer, PupSocket, PupSocketDestroy, PupSocketMake,
    defaultNumberOfNetworks, GetHopsToNetwork, SecondsToTocks,
    SetPupContentsWords, AppendPupAddress, AppendHostName, AppendErrorPup,
    GetPupAddress, PupNameTrouble],
  PupTypes USING [PupAddress, fillInSocketID, miscSrvSoc];

FixTimeServers: PROGRAM
  IMPORTS
    InlineDefs, Process, Storage, String, System, Time, Event, FormSW,
    MsgSW, Profile, Put, Tool, ToolWindow, Window,
    Clock, Password, PupDefs, TimeServerDefs =
  BEGIN OPEN PupDefs, PupTypes;

  herald: STRING = "Fix TimeServers of March 19, 1981";

  msg, form, boxes, log: Window.Handle;
  eventItem: Event.Item ← [eventMask: 177777B, eventProc: Broom];

  defaultMaxHops: CARDINAL = 3;
  pleaseStop, stopUpdating: BOOLEAN ← FALSE;
  running: BOOLEAN ← FALSE;
  indicator: {left, right, off} ← off;
  first: TimeServer ← NIL;
  maxHops: CARDINAL ← defaultMaxHops;
  where: PupAddress ← [[0], [0], PupTypes.miscSrvSoc];
  now, target: STRING ← NIL;

  TimeServer: TYPE = POINTER TO TimeServerObject;
  TimeServerObject: TYPE = RECORD [
    next: TimeServer,
    where: PupTypes.PupAddress,
    on: BOOLEAN,
    offset: LONG INTEGER];

  ScanCircle: FormSW.ProcType =
    BEGIN
    IF running THEN
      BEGIN MsgSW.Post[msg, "Somebody is already running..."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    running ← TRUE;
    Process.Detach[FORK ScanSeveral[]];
    END;

  ScanTarget: FormSW.ProcType =
    BEGIN
    IF running THEN
      BEGIN MsgSW.Post[msg, "Somebody is already running..."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    WriteString["  Finding time on "L];
    IF ~FindPath[] THEN RETURN;
    running ← TRUE;
    Process.Detach[FORK ScanOne[]];
    END;

  ResetLocalTimeFromTarget: FormSW.ProcType =
    BEGIN
    IF running THEN
      BEGIN MsgSW.Post[msg, "Somebody is already running..."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    WriteString["  Resetting time from "L];
    IF ~FindPath[] THEN RETURN;
    running ← TRUE;
    Process.Detach[FORK ResetLocalFromTarget[]];
    END;

  FixupCircle: FormSW.ProcType =
    BEGIN
    IF running THEN
      BEGIN MsgSW.Post[msg, "Somebody is already running..."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    running ← TRUE;
    Process.Detach[FORK FixupSeveral[]];
    END;

  FixupTarget: FormSW.ProcType =
    BEGIN
    IF running THEN
      BEGIN MsgSW.Post[msg, "Somebody is already running..."L]; RETURN; END;
    WriteCR[];
    WriteCurrentDateAndTime[];
    WriteString["  Finding time servers on "L];
    IF ~FindPath[] THEN RETURN;
    running ← TRUE;
    Process.Detach[FORK FixupOne[]];
    END;

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

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

  FindPath: PROCEDURE RETURNS [BOOLEAN] =
    BEGIN
    WriteString[target];
    WriteChar['=];
    GetPupAddress[
      @where, target !
      PupNameTrouble =>
	BEGIN MsgSW.Post[msg, e]; WriteLine[e]; GOTO Trouble; END];
    PrintPupAddress[where];
    WriteLine["."L];
    RETURN[TRUE];
    EXITS Trouble => RETURN[FALSE];
    END;

  ResetLocalFromTarget: PROCEDURE =
    BEGIN
    sequenceNumber: CARDINAL ← GetNextSequenceNumber[];
    soc: PupSocket ← PupSocketMake[fillInSocketID, where, SecondsToTocks[5]];
    hit: BOOLEAN ← FALSE;
    FOR i: CARDINAL IN [0..5) UNTIL pleaseStop OR hit DO
      b: PupBuffer ← GetFreePupBuffer[];
      b.pupID.a ← b.pupID.b ← sequenceNumber;
      b.pupType ← dateAltoRequest;
      SetPupContentsWords[b, 0];
      soc.put[b];
      UNTIL hit OR (b ← soc.get[]) = NIL DO
	SELECT TRUE FROM
	  ((b.pupType # dateAltoIs) OR (b.pupID.a # sequenceNumber) OR
	    (b.pupID.b # sequenceNumber)) =>
	    BEGIN
	    temp: STRING = [100];
	    PupDefs.AppendErrorPup[temp, b];
	    MsgSW.Post[msg, temp];
	    END;
	  ENDCASE =>
	    BEGIN
	    old: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
	    new: System.GreenwichMeanTime;
	    timeInfo: LONG POINTER TO TimeServerDefs.PupTimeFormat;
	    timeInfo ← LOOPHOLE[@b.pupWords[0]];
	    new ← LOOPHOLE[InlineDefs.BcplToMesaLongNumber[timeInfo.time]];
	    Clock.SetTime[new];
	    WriteCurrentDateAndTime[];
	    Put.Text[log, "  Time reset from "L];
	    PrintPupAddress[where];
	    Put.Text[log, ", correction was "L];
	    Put.LongDecimal[log, new - old];
	    Put.Line[log, "."L];
	    hit ← TRUE;
	    UpdateNow[];
	    EXIT;
	    END;
	ReturnFreePupBuffer[b];
	b ← NIL;
	ENDLOOP;
      IF b # NIL THEN ReturnFreePupBuffer[b];
      ENDLOOP;
    PupSocketDestroy[soc];
    running ← FALSE;
    END;

  ScanSeveral: PROCEDURE =
    BEGIN
    first: BOOLEAN ← TRUE;
    SetupBoxes[];
    FOR net: CARDINAL IN [1..PupDefs.defaultNumberOfNetworks) UNTIL pleaseStop DO
      IF GetHopsToNetwork[[net]] > maxHops THEN LOOP;
      where ← [[net], [0], miscSrvSoc];
      IF first THEN Put.Text[log, "  Searching network "]
      ELSE Put.Text[log, ", "];
      Put.Number[log, net, [8, FALSE, TRUE, 0]];
      Put.Char[log, '(];
      Put.Decimal[log, GetHopsToNetwork[[net]]];
      Put.Char[log, ')];
      ScanSingle[];
      first ← FALSE;
      ENDLOOP;
    Put.CR[log];
    SetDownBoxes[];
    PrintTimeServerList[];
    DeleteList[];
    running ← FALSE;
    END;

  ScanOne: PROCEDURE =
    BEGIN
    SetupBoxes[];
    ScanSingle[];
    SetDownBoxes[];
    PrintTimeServerList[];
    DeleteList[];
    running ← FALSE;
    END;

  ScanSingle: PROCEDURE =
    BEGIN
    sequenceNumber: CARDINAL ← GetNextSequenceNumber[];
    soc: PupSocket ← PupSocketMake[fillInSocketID, where, SecondsToTocks[5]];
    launch: System.GreenwichMeanTime;
    FOR i: CARDINAL IN [0..5) DO
      b: PupBuffer ← GetFreePupBuffer[];
      b.pupID.a ← b.pupID.b ← sequenceNumber;
      b.pupType ← TimeServerDefs.timeStatsRequest;
      SetPupContentsWords[b, 0];
      soc.put[b];
      b ← GetFreePupBuffer[];
      b.pupID.a ← b.pupID.b ← sequenceNumber;
      b.pupType ← dateAltoRequest;
      SetPupContentsWords[b, 0];
      launch ← System.GetGreenwichMeanTime[];
      soc.put[b];
      UNTIL (b ← soc.get[]) = NIL DO
	SELECT TRUE FROM
	  (b.pupType = TimeServerDefs.timeStatsReply
          AND (b.pupID.a = sequenceNumber) AND (b.pupID.b = sequenceNumber)) =>
	    BEGIN FlipBoxes[]; AddToList[b.source, FALSE, 0]; END;
	  ((b.pupType = dateAltoIs) AND (b.pupID.a = sequenceNumber)
          AND (b.pupID.b = sequenceNumber)) =>
	    BEGIN FlipBoxes[]; LookAtTimeResponse[b, launch]; END;
	  ENDCASE =>
	    BEGIN
	    temp: STRING = [100];
	    PupDefs.AppendErrorPup[temp, b];
	    MsgSW.Post[msg, temp];
	    END;
	ReturnFreePupBuffer[b];
	ENDLOOP;
      IF b # NIL THEN ReturnFreePupBuffer[b];
      ENDLOOP;
    PupSocketDestroy[soc];
    END;

  FixupSeveral: PROCEDURE =
    BEGIN
    first: BOOLEAN ← TRUE;
    SetupBoxes[];
    FOR net: CARDINAL IN [1..PupDefs.defaultNumberOfNetworks) UNTIL pleaseStop DO
      IF GetHopsToNetwork[[net]] > maxHops THEN LOOP;
      where ← [[net], [0], miscSrvSoc];
      IF first THEN Put.Text[log, "  Finding time servers on network "]
      ELSE Put.Text[log, ", "];
      Put.Number[log, net, [8, FALSE, TRUE, 0]];
      Put.Char[log, '(];
      Put.Decimal[log, GetHopsToNetwork[[net]]];
      Put.Char[log, ')];
      ScanSingle[];
      first ← FALSE;
      ENDLOOP;
    Put.CR[log];
    FixupList[];
    DeleteList[];
    SetDownBoxes[];
    running ← FALSE;
    END;

  FixupOne: PROCEDURE =
    BEGIN
    SetupBoxes[];
    ScanSingle[];
    FixupList[];
    DeleteList[];
    SetDownBoxes[];
    running ← FALSE;
    END;

  FixupList: PROCEDURE =
    BEGIN
    FOR ts: TimeServer ← first, ts.next UNTIL ts = NIL DO
      IF ~ts.on THEN LOOP;  -- don't muck with IFSs
      WriteCurrentDateAndTime[];
      Put.Text[log, "  Resetting time on "L];
      PrintPupAddress[ts.where];
      Put.Text[log, "..."L];
      IF ResetOne[ts.where] THEN Put.Line[log, "ok."L]
      ELSE Put.Line[log, "no response."L];
      ENDLOOP;
    END;

  ResetOne: PROCEDURE [where: PupAddress] RETURNS [worked: BOOLEAN] =
    BEGIN
    sequenceNumber: CARDINAL ← GetNextSequenceNumber[];
    soc: PupSocket ← PupSocketMake[fillInSocketID, where, SecondsToTocks[5]];
    magic: WORD = 27182;
    worked ← FALSE;
    FOR i: CARDINAL IN [0..5) UNTIL worked OR pleaseStop DO
      b: PupBuffer ← GetFreePupBuffer[];
      b.pupID.a ← magic;
      b.pupID.b ← sequenceNumber;
      b.pupType ← TimeServerDefs.resetTimeRequest;
      b.address ← GetLocalPupAddress[miscSrvSoc, @where];
      SetPupContentsWords[b, SIZE[PupAddress]];
      soc.put[b];
      UNTIL worked OR (b ← soc.get[]) = NIL DO
	SELECT TRUE FROM
	  ((b.pupType # TimeServerDefs.resetTimeReply) OR (b.pupID.a # magic) OR
	    (b.pupID.b # sequenceNumber)) =>
	    BEGIN
	    temp: STRING = [100];
	    PupDefs.AppendErrorPup[temp, b];
	    MsgSW.Post[msg, temp];
	    END;
	  ENDCASE => BEGIN FlipBoxes[]; worked ← TRUE; END;
	ReturnFreePupBuffer[b];
	b ← NIL;
	ENDLOOP;
      IF b # NIL THEN ReturnFreePupBuffer[b];
      ENDLOOP;
    PupSocketDestroy[soc];
    END;

  LookAtTimeResponse: PUBLIC PROCEDURE [
    b: PupDefs.PupBuffer, t: System.GreenwichMeanTime] =
    BEGIN
    timeStamp: System.GreenwichMeanTime;
    timeInfo: LONG POINTER TO TimeServerDefs.PupTimeFormat;
    offset: LONG INTEGER;
    timeInfo ← LOOPHOLE[@b.pupWords[0]];
    timeStamp ← LOOPHOLE[InlineDefs.BcplToMesaLongNumber[timeInfo.time]];
    offset ← timeStamp - t;
    AddToList[b.source, TRUE, offset];
    END;

  AddToList: PROCEDURE [
    where: PupTypes.PupAddress, on: BOOLEAN, offset: LONG INTEGER] =
    BEGIN
    finger: TimeServer ← NIL;
    new: TimeServer;
    FOR ts: TimeServer ← first, ts.next UNTIL ts = NIL DO
      IF where = ts.where THEN
	BEGIN
	ts.offset ← MIN[ts.offset, offset];
	ts.on ← ts.on OR on;
	RETURN;
	END;
      IF LessPupAddress[ts.where, where] THEN finger ← ts;
      ENDLOOP;
    new ← Storage.Node[SIZE[TimeServerObject]];
    new↑ ← [next: NIL, where: where, on: on, offset: offset];
    SELECT TRUE FROM
      first = NIL => first ← new; -- first
      finger = NIL =>
        BEGIN new.next ← first; first ← new; END;  -- insert at front of list
      ENDCASE =>
        BEGIN new.next ← finger.next; finger.next ← new; END;  -- middle or end
    END;

  LessPupAddress: PROCEDURE [a, b: PupAddress] RETURNS [BOOLEAN] =
    BEGIN
    IF a.net < b.net THEN RETURN[TRUE];
    IF a.net > b.net THEN RETURN[FALSE];
    IF a.host < b.host THEN RETURN[TRUE];
    IF a.host > b.host THEN RETURN[FALSE];
    IF a.socket.a < b.socket.a THEN RETURN[TRUE];
    IF a.socket.a > b.socket.a THEN RETURN[FALSE];
    IF a.socket.b < b.socket.b THEN RETURN[TRUE];
    IF a.socket.b > b.socket.b THEN RETURN[FALSE];
    RETURN[FALSE];
    END;

  DeleteList: PROCEDURE =
    BEGIN
    ts: TimeServer ← first;
    UNTIL ts = NIL DO
      next: TimeServer ← ts.next; Storage.Free[ts]; ts ← next; ENDLOOP;
    first ← NIL;
    END;

  PrintTimeServerList: PROCEDURE =
    BEGIN
    ts: TimeServer ← first;
    Put.Line[log, "Offset    Name"L];
    UNTIL ts = NIL DO
      temp: STRING = [40];
      PupDefs.AppendHostName[temp, ts.where];
      IF ts.on THEN Put.LongNumber[log, ts.offset, [10, FALSE, FALSE, 6]]
      ELSE Put.Text[log, "      "L];
      Put.Text[log, "  "L];
      Put.Text[log, temp];
      Put.Text[log, " = "L];
      PrintPupAddress[ts.where];
      Put.CR[log];
      ts ← ts.next;
      ENDLOOP;
    END;

  nextSequenceNumber: CARDINAL ← 123;
  GetNextSequenceNumber: PROCEDURE RETURNS [CARDINAL] =
    BEGIN RETURN[nextSequenceNumber ← nextSequenceNumber + 1]; END;

  Enable: FormSW.ProcType =
    BEGIN
    status: Password.Status;
    status ← Password.ValidMemberOfGroup[
      Profile.userName, Profile.userPassword, "TimeFixers↑.internet"L];
    SELECT status FROM
      yes =>
        BEGIN
        FormSW.FindItem[form, circleIX].flags.invisible ← FALSE;
        FormSW.FindItem[form, targetIX].flags.invisible ← FALSE;
        FormSW.Display[form];
        END;
      nil => MsgSW.Post[msg, "Name or Password is NIL."L];
      allDown => MsgSW.Post[msg, "All Grapevine servers appear to be down."L];
      notFound => MsgSW.Post[msg, "Grapevine doesn't like your name."L];
      badPwd => MsgSW.Post[msg, "Grapevine doesn't like your password."L];
      group => MsgSW.Post[msg, "Grapevine thinks you are a group."L];
      no => MsgSW.Post[msg, "You are not in TimeFixers↑.internet."L];
      notGroup => MsgSW.Post[msg, "Grapevine doesn't recognize TimeFixers↑.internet."L];
      error => MsgSW.Post[msg, "Error from GrapevineUser package."L];
      ENDCASE => ERROR;
    END;

  ForwardOneMin: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, 60]];
    UpdateNow[];
    END;

  ForwardTenSec: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, 10]];
    UpdateNow[];
    END;

  ForwardOneSec: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, 1]];
    UpdateNow[];
    END;

  BackOneMin: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, -60]];
    UpdateNow[];
    END;

  BackTenSec: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, -10]];
    UpdateNow[];
    END;

  BackOneSec: FormSW.ProcType =
    BEGIN
    now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
    Clock.SetTime[System.AdjustGreenwichMeanTime[now, -1]];
    UpdateNow[];
    END;

  -- IO things

  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;

  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 = [20]; Time.AppendCurrent[time]; WriteString[time]; END;

  PrintPupAddress: PROCEDURE [a: PupAddress] =
    BEGIN temp: STRING = [40]; AppendPupAddress[temp, a]; WriteString[temp]; END;

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

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

  FlipBoxes: PROCEDURE =
    BEGIN
    SELECT indicator FROM
      left => indicator ← right;
      off, right => indicator ← left;
      ENDCASE;
    Window.DisplayInvert[boxes, indicatorBox];
    END;

  SetDownBoxes: PROCEDURE =
    BEGIN indicator ← off; Window.DisplayWhite[boxes, indicatorBox]; END;

  MakeBoxesSW: PROCEDURE [window: Window.Handle] =
    BEGIN
    boxes ← ToolWindow.CreateSubwindow[parent: window, display: DisplayBoxes];
    boxes.box.dims.h ← 36;
    Tool.AddThisSW[window: window, sw: boxes, swType: LOOPHOLE[0]];
    END;

  MakeSWs: Tool.MakeSWsProc =
    BEGIN
    logFileName: STRING = [40];
    msg ← Tool.MakeMsgSW[window: window, lines: 5];
    form ← Tool.MakeFormSW[window: window, formProc: MakeForm];
    MakeBoxesSW[window];
    Tool.UnusedLogName[logFileName, "FixTimeServers.log$"L];
    log ← Tool.MakeFileSW[window: window, name: logFileName];
    Put.Line[
      log,
      "
Offset is in seconds.
  It is positive if the remote clock is faster than ours.
The phone company time server in Palo Alto is 767-8900.
Target can be a net as well as a specific machine.
  That's why it takes longer than it should to do simple things.
"L];
    END;

  UpdateIt: PROCEDURE =
    BEGIN
    then: System.GreenwichMeanTime ← System.GetGreenwichMeanTime[];
    UNTIL stopUpdating DO
      IF then # System.GetGreenwichMeanTime[] THEN
	BEGIN
	then ← System.GetGreenwichMeanTime[];
	UpdateNow[];
	END;
      Process.Pause[Process.MsecToTicks[250]];
      ENDLOOP;
    stopUpdating ← FALSE;
    END;

  UpdateNow: PROCEDURE =
    BEGIN
    left, right: CARDINAL;
    new: STRING = [30];
    item: FormSW.ItemHandle = FormSW.FindItem[form, nowIX];
    Time.Append[new, Time.Unpack[System.GetGreenwichMeanTime[]]];
    right ← MIN[new.length, now.length];
    FOR left ← 0, left + 1 UNTIL left = right DO
      IF new[left] # now[left] THEN EXIT; ENDLOOP;
    FOR i: CARDINAL IN [0..new.length - left) DO new[i] ← new[i + left]; ENDLOOP;
    new.length ← new.length - left;
    item.flags.readOnly ← FALSE;
    FormSW.ModifyEditable[form, nowIX, left, now.length - left, new];
    item.flags.readOnly ← TRUE;
    END;

  nowIX: CARDINAL = 1;
  circleIX: CARDINAL = 3;
  targetIX: CARDINAL = 6;
  MakeForm: FormSW.ClientItemsProcType =
    BEGIN
    nParams: CARDINAL = 16;
    items ← FormSW.AllocateItemDescriptor[nParams];

    items[0] ← FormSW.CommandItem[
      tag: "Stop"L, proc: Stop, place: FormSW.newLine];
    items[nowIX] ← FormSW.StringItem[tag: "Now"L, string: @now, readOnly: TRUE];

    items[2] ← FormSW.CommandItem[
      tag: "ScanCircle"L, proc: ScanCircle, place: FormSW.newLine];
    items[circleIX] ← FormSW.CommandItem[
      tag: "FixupCircle"L, proc: FixupCircle, invisible: TRUE];
    items[4] ← FormSW.NumberItem[
      tag: "MaxHops"L, value: @maxHops, default: defaultMaxHops];

    items[5] ← FormSW.CommandItem[
      tag: "ScanTarget"L, proc: ScanTarget, place: FormSW.newLine];
    items[targetIX] ← FormSW.CommandItem[
      tag: "FixupTarget"L, proc: FixupTarget, invisible: TRUE];
    items[7] ← FormSW.StringItem[tag: "Target"L, string: @target];

    items[8] ← FormSW.CommandItem[
      tag: "AheadOneMin"L, proc: ForwardOneMin, place: FormSW.newLine];
    items[9] ← FormSW.CommandItem[tag: "AheadTenSec"L, proc: ForwardTenSec];
    items[10] ← FormSW.CommandItem[tag: "AheadOneSec"L, proc: ForwardOneSec];

    items[11] ← FormSW.CommandItem[
      tag: "BackOneMin"L, proc: BackOneMin, place: FormSW.newLine];
    items[12] ← FormSW.CommandItem[tag: "BackTenSec"L, proc: BackTenSec];
    items[13] ← FormSW.CommandItem[tag: "BackOneSec"L, proc: BackOneSec];

    items[14] ← FormSW.CommandItem[
      tag: "ResetLocalTimeFromTarget"L, proc: ResetLocalTimeFromTarget,
      place: FormSW.newLine];

    items[15] ← FormSW.CommandItem[tag: "Enable"L, proc: Enable];

    RETURN[items, TRUE];
    END;

  ClientTransition: ToolWindow.TransitionProcType =
    BEGIN
    SELECT TRUE FROM
      old = inactive =>
	BEGIN
	now ← Storage.String[20];
	target ← Storage.String[20];
	String.AppendString[target, "ME"L];
	PupDefs.PupPackageMake[];
	TimeServerDefs.PupTimeServerOn[];
	Process.Detach[FORK UpdateIt[]];
	END;
      new = inactive =>
	BEGIN
	IF running THEN Off[];
	stopUpdating ← TRUE;
	WHILE stopUpdating DO Process.Yield[]; ENDLOOP;
	TimeServerDefs.PupTimeServerOff[];
	PupDefs.PupPackageDestroy[];
	Storage.FreeString[target];
	Storage.FreeString[now];
	END;
      ENDCASE;
    END;

  Broom: PROCEDURE [why: Event.Reason] =
    BEGIN
    SELECT why FROM
      makeImage, makeCheck =>
	BEGIN IF running THEN Off[]; PupDefs.PupPackageDestroy[]; END;
      startImage, restartCheck, continueCheck =>
	BEGIN PupDefs.PupPackageMake[]; END;
      ENDCASE => NULL;
    END;

  -- Main Body

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