-- File: TimeChecker.mesa - last edit:
-- AOF                  3-Feb-88 19:09:05
-- HGM                 25-Jun-85 15:39:23
-- Copyright (C) 1984, 1985, 1988 by Xerox Corporation. All rights reserved. 

DIRECTORY
  Ascii USING [CR],
  CmFile USING [Handle, TableError],
  Format USING [StringProc],
  Heap USING [Create, systemZone],
  MFile USING [AddNotifyProc, Handle],
  MStream USING [ReadWrite, Error, GetLength],
  Process USING [Pause, SecondsToTicks, Ticks],
  ProcessorFace USING [microsecondsPerHundredPulses],
  Put USING [Text],
  Stream USING [Delete, Handle, PutString, SetPosition],
  String USING [
    AppendChar, AppendString, AppendStringAndGrow, AppendDecimal, AppendLongDecimal,
    CopyToNewString, Replace],
  StringLookUp USING [noMatch, TableDesc],
  System USING [
    AdjustGreenwichMeanTime, GetClockPulses, GetGreenwichMeanTime,
    gmtEpoch, GreenwichMeanTime, NetworkAddress, nullSocketNumber,
    Pulses, PulsesToMicroseconds, SocketNumber],
  Time USING [AppendCurrent, Current],
  Token USING [Filtered, FreeTokenString, Item, Line, LongDecimal],
  Unformat USING [Error, NetworkAddress],

  AddressTranslation USING [Error, PrintError, StringToNetworkAddress],
  NSBuffer USING [Body, Buffer],
  Indirect USING [Close, GetParmFileName, NextValue, OpenSection],
  Mailer USING [Level, SendGVMail],
  NSConstants USING [timeServerSocket],
  NSTypes USING [wordsPerExchangeHeader],
  PopCorn USING [Error, GetClockOffset],
  PupWireFormat USING [BcplToMesaLongNumber],
  PupDefs USING [
    Body, GetPupAddress, GetPupContentsBytes,
    PupAddress, PupBuffer, PupNameTrouble, PupSocket, PupSocketDestroy, PupSocketMake,
    PupPackageDestroy, PupPackageMake, SecondsToTocks, SetPupContentsWords,
    AccessHandle, DestroyPool, GetBuffer, MakePool, Buffer, ReturnBuffer],
  PupTypes USING [fillInSocketID, miscSrvSoc],
  PupTimeServerFormat USING [PupTimeFormat],
  Socket USING [
    ChannelHandle, Create,
    Delete, GetPacket, GetPacketBytes, GetSendBuffer, GetSource, PutPacket,
    ReturnBuffer, SetDestination, SetPacketWords,
    SetWaitTime, TimeOut],
  TimeServerClock USING [AdjustClock],
  TimeServerFormat USING [TSPacket, Version, WireToGMT, WireToLong],
  TimeServerOps USING [GetClockError];

TimeChecker: MONITOR
  IMPORTS
    CmFile, Heap, MFile, MStream, Process, ProcessorFace, Put, Stream,
    String, System, Time, Token, Unformat,
    AddressTranslation, Indirect, Mailer, PopCorn, PupWireFormat,
    PupDefs, Socket, TimeServerClock, TimeServerFormat, TimeServerOps =
  BEGIN OPEN PupDefs;

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

  Mode: TYPE = {pup, ns, popCorn};
  Handle: TYPE = LONG POINTER TO Object;
  Object: TYPE = RECORD [
    next: Handle,
    mode: Mode,
    target: LONG STRING];

  first: Handle ← NIL;
  timesAround: CARDINAL ← 0;
  parmFileName: LONG STRING ← Indirect.GetParmFileName[];
  pleaseStop: BOOLEAN ← FALSE;
  watcher: PROCESS ← NIL;
  threshold: LONG CARDINAL ← LAST[LONG CARDINAL];
  
  troubles, to, cc: LONG STRING ← NIL;
  mail: LONG STRING ← NIL;

  Init: ENTRY PROCEDURE =
    BEGIN
    MFile.AddNotifyProc[Inspect, [parmFileName, null, readOnly], NIL];
    Starter[];
    END;

  Starter: INTERNAL PROCEDURE =
    BEGIN
    IF ~FindTargets[] THEN RETURN;
    timesAround ← 0;
    pleaseStop ← FALSE;
    [] ← PupPackageMake[];
    watcher ← FORK Watcher[];
    END;

  Stopper: INTERNAL PROCEDURE =
    BEGIN
    pleaseStop ← TRUE;
    JOIN watcher[];
    watcher ← NIL;
    ForgetTargets[];
    PupPackageDestroy[];
    END;

  FindTargets: PROCEDURE RETURNS [BOOLEAN] =
    BEGIN
    cmFile: CmFile.Handle;
    Option: TYPE = MACHINE DEPENDENT{
      pup(0), ns, popCorn, threshold, troubles, to, cc, noMatch(StringLookUp.noMatch)};
    DefinedOption: TYPE = Option [pup..cc];
    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 ← [
      pup: "Pup"L, ns: "NS"L, popCorn: "PopCorn"L,
      threshold: "Threshold"L,
      troubles: "Troubles"L, to: "to"L, cc: "cc"L];
    modeText: ARRAY Mode OF STRING ← [pup: "Pup"L, ns: "NS"L, popCorn: "PopCorn"L];

    cmFile ← Indirect.OpenSection["TimeChecker"L];
    IF cmFile = NIL THEN
      BEGIN
      Message["Can't find [TimeChecker] section in parameter file"L];
      RETURN[FALSE];
      END;
    DO
      option: Option;
      mode: Mode;
      temp: Handle;
      target: LONG STRING;
      option ← MyNextValue[cmFile, DESCRIPTOR[optionTable] !
        CmFile.TableError =>
          BEGIN
	  IF name[0] # '; THEN Message["Unrecognized parameter: ", name];
	  RETRY;
	  END];
      SELECT option FROM
        noMatch => EXIT;
        pup => mode ← pup;
	ns => mode ← ns;
	popCorn => mode ← popCorn;
	threshold =>
	  BEGIN
	  text: STRING = [20];
	  threshold ← Token.LongDecimal[cmFile];
	  String.AppendLongDecimal[text, threshold];
	  Message["The clock fixup threshold is "L, text, " ms"L];
	  LOOP;
	  END;
        troubles =>
          BEGIN
	  temp: LONG STRING ← Token.Item[cmFile, FALSE];
	  String.Replace[@troubles, temp, z];
          CheckForRegistry[troubles];
	  [] ← Token.FreeTokenString[temp];
	  Message["Grapevine will send trouble reports to "L, troubles];
	  LOOP;
          END;
        to =>
          BEGIN
	  temp: LONG STRING ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
	  String.Replace[@to, temp, z];
          CheckForRegistry[to];
	  [] ← Token.FreeTokenString[temp];
	  Message["Mail will be sent to "L, to];
	  LOOP;
          END;
        cc =>
          BEGIN
	  temp: LONG STRING ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
	  String.Replace[@cc, temp, z];
          CheckForRegistry[cc];
	  [] ← Token.FreeTokenString[temp];
	  Message["Copies will be sent to "L, cc];
	  LOOP;
          END;
        ENDCASE => ERROR;
      temp ← z.NEW[Object];
      temp.mode ← mode;
      target ← Token.Filtered[cmFile, NIL, Token.Line, whiteSpace, FALSE];
      temp.target ← z.NEW[StringBody[target.length]];
      String.AppendString[temp.target, target];
      [] ← Token.FreeTokenString[target];
      temp.next ← NIL;
      IF first = NIL THEN first ← temp
      ELSE
        BEGIN
        last: Handle ← first;
        UNTIL last.next = NIL DO last ← last.next; ENDLOOP;
        last.next ← temp;
        END;
      Message[modeText[mode], " target is "L, temp.target];
      ENDLOOP;
    Indirect.Close[cmFile];
    RETURN[first # NIL];
    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;

  ForgetTargets: INTERNAL PROCEDURE =
    BEGIN
    finger: Handle ← first;
    UNTIL first = NIL DO
      finger ← first;
      first ← first.next;
      z.FREE[@finger.target];
      z.FREE[@finger];
      ENDLOOP;
    END;

  Watcher: PROCEDURE =
    BEGIN
    oneMinute: Process.Ticks = Process.SecondsToTicks[60];
    when: LONG CARDINAL;
    THROUGH [0..5) UNTIL pleaseStop DO  -- let time get set
      Process.Pause[oneMinute];
      ENDLOOP;
    when ← Time.Current[];
    UNTIL pleaseStop DO
      FOR finger: Handle ← first, finger.next UNTIL finger = NIL DO
        IF pleaseStop THEN EXIT;
        SELECT finger.mode FROM
          pup => ProbePup[finger.target];
	  ns => ProbeNS[finger.target];
	  popCorn => ProbePopCorn[finger.target];
	  ENDCASE => ERROR;
        ENDLOOP;
      IF mail # NIL THEN SendMail[];
      when ← when + 3600;
      IF Time.Current[] > when THEN
        BEGIN
        missed: CARDINAL ← 0;
        text: STRING = [100];
        WHILE Time.Current[] > when DO
          missed ← missed + 1;
          when ← when + 3600;
          timesAround ← timesAround + 1;
          ENDLOOP;
        Time.AppendCurrent[text];
        String.AppendString[text, "  Oops, it looks like we got stuck for "L];
        String.AppendDecimal[text, missed];
        String.AppendString[text, " hours"L];
        LogString[text];
        END;
      THROUGH [0..120) UNTIL pleaseStop OR Time.Current[] > when DO
        [] ← GetMyGreenwichMeanTime[];  -- Keep clock up to date
        Process.Pause[oneMinute];
        ENDLOOP;
      timesAround ← timesAround + 1;
      ENDLOOP;
    END;

  ProbePup: PROCEDURE [target: LONG STRING] =
    BEGIN
    soc: PupSocket;
    pool: PupDefs.AccessHandle;
    diff: LONG INTEGER ← LAST[LONG INTEGER];
    hits: CARDINAL ← 0;
    worked: BOOLEAN;
    who: PupAddress;
    
    [worked, who] ← FindPupAddress[target];
    IF ~worked THEN RETURN;
    pool ← PupDefs.MakePool[send: 1, receive: 10];
    soc ← PupSocketMake[
      PupTypes.fillInSocketID, who, SecondsToTocks[5]];
      
    FOR i: CARDINAL IN [0..10) DO
      body: PupDefs.Body;
      b: PupBuffer ← PupDefs.GetBuffer[pool, send];
      body ← b.pup;
      body.pupType ← dateAltoRequest;
      body.pupID ← [i, i];
      SetPupContentsWords[b, 0];
      soc.put[b];
      DO
        b ← soc.get[];
	IF b = NIL THEN EXIT;
	body ← b.pup;
        SELECT TRUE FROM
	  body.pupType # dateAltoIs
	    OR PupDefs.GetPupContentsBytes[b] # 2*SIZE[PupTimeServerFormat.PupTimeFormat]
	    OR body.pupID # [i, i]
	    OR body.source # who => NULL;
	  ENDCASE =>
	    BEGIN
            info: LONG POINTER TO PupTimeServerFormat.PupTimeFormat;
            me, him: LONG INTEGER;  -- NB: not LONG CARDINAL
            info ← LOOPHOLE[@body.pupWords];
            me ← Time.Current[];
            him ← PupWireFormat.BcplToMesaLongNumber[info.time];
            diff ← MIN[diff, (him - me)];
            hits ← hits + 1;
	    END;
        PupDefs.ReturnBuffer[b];
        ENDLOOP;
      ENDLOOP;
    PupSocketDestroy[soc];
    PupDefs.DestroyPool[pool];
    PrintResponse[hits # 0, target, diff*1000];
    END;
    
  FindPupAddress: PROCEDURE [target: LONG STRING] RETURNS [worked: BOOLEAN, who: PupAddress] =
    BEGIN
    worked ← TRUE;
    who.socket ← PupTypes.miscSrvSoc;
    GetPupAddress[@who, target !
      PupDefs.PupNameTrouble =>
        BEGIN
        text: STRING = [150];
	worked ← FALSE;
        Time.AppendCurrent[text];
        String.AppendString[
          text, "  TimeChecker: Troubles finding Pup address for "L];
        String.AppendString[text, target];
        String.AppendString[text, ", "L];
        String.AppendString[text, e];
        LogString[text];
        CONTINUE;
        END ];
    END;

  ProbeNS: PROCEDURE [target: LONG STRING] =
    BEGIN
    soc: Socket.ChannelHandle;
    diff: LONG INTEGER ← LAST[LONG INTEGER];
    error: LONG CARDINAL ← LAST[LONG CARDINAL];
    hits: CARDINAL ← 0;
    worked: BOOLEAN;
    who: System.NetworkAddress;
    version: WORD = TimeServerFormat.Version;
    flight: LONG CARDINAL;
  
    [worked, who] ← FindNSAddress[target];
    IF ~worked THEN RETURN;
    soc ← Socket.Create[socket: System.nullSocketNumber, receive: 1];
    Socket.SetWaitTime[soc, 5000];

    FOR i: CARDINAL IN [0..10) DO
      start: System.Pulses;
      request: LONG POINTER TO  timeRequest TimeServerFormat.TSPacket;
      wordsInRequest: CARDINAL = SIZE[timeRequest TimeServerFormat.TSPacket];
      b: NSBuffer.Buffer ← Socket.GetSendBuffer[soc];
      body: NSBuffer.Body ← b.ns;
      Socket.SetDestination[b, who];
      body.packetType ← packetExchange;
      body.exchangeID ← [i, i];
      body.exchangeType ← timeService;
      request ← LOOPHOLE[@body.exchangeBody];
      request↑ ← [version, timeRequest[]];
      Socket.SetPacketWords[b, NSTypes.wordsPerExchangeHeader + wordsInRequest];
      start ← System.GetClockPulses[];
      Socket.PutPacket[soc, b];
      DO
        response: LONG POINTER TO timeResponse TimeServerFormat.TSPacket;
        wordsInResponse: CARDINAL = SIZE[timeResponse TimeServerFormat.TSPacket];
        b ← Socket.GetPacket[soc ! Socket.TimeOut =>  EXIT];
	body ← b.ns;
	response ← LOOPHOLE[@body.exchangeBody];
        SELECT TRUE FROM
	  body.packetType # packetExchange
	    OR body.exchangeID # [i, i]
            OR Socket.GetPacketBytes[b] # 2*(NSTypes.wordsPerExchangeHeader + wordsInResponse)
            OR (body.exchangeType # timeService)
	    OR response.version # version
	    OR response.type # timeResponse
 	    OR Socket.GetSource[b].host # who.host =>
	    NULL;
          ENDCASE =>
            BEGIN  -- the response we were looking for
	    stop: System.Pulses = System.GetClockPulses[];
	    hisError: LONG CARDINAL = TimeServerFormat.WireToLong[response.absoluteError];
            totalError: LONG CARDINAL = hisError + flight + 1000;
            me, him: LONG INTEGER;  -- NB: not LONG CARDINAL
            me ← Time.Current[];
	    him ← TimeServerFormat.WireToGMT[response.time];
	    IF response.errorAccurate THEN
	      BEGIN
	      IF totalError < error THEN
	        BEGIN
	        error ← totalError;
	        diff ← (him - me);
	        flight ← System.PulsesToMicroseconds[[stop - start]]/1000;
	        END;
	      END
	    ELSE
	      BEGIN
              diff ← MIN[diff, (him - me)];
	      error ← LAST[LONG CARDINAL];
	      flight ← System.PulsesToMicroseconds[[stop - start]]/1000;
	      END; 
            hits ← hits + 1;
            END;
        Socket.ReturnBuffer[b];
        ENDLOOP;
      ENDLOOP;
    Socket.Delete[soc];
    PrintResponse[hits # 0, target, diff*1000, error, flight];
    END;

  FindNSAddress: PROCEDURE [target: LONG STRING] RETURNS [worked: BOOLEAN, who: System.NetworkAddress] =
    BEGIN
    Problem: PROCEDURE [e: LONG STRING] =
      BEGIN
      text: STRING = [200];
      Time.AppendCurrent[text];
      String.AppendString[
        text, "  TimeChecker: Troubles finding NS address for "L];
      String.AppendString[text, target];
      String.AppendString[text, ", "L];
      String.AppendString[text, e];
      LogString[text];
      END;
    worked ← TRUE;
    who ← GetAddress[target, NSConstants.timeServerSocket !
      Trouble =>
        BEGIN
	Problem[reason];
        worked ← FALSE;
        CONTINUE;
        END ];
    END;
	
  ProbePopCorn: PROCEDURE [target: LONG STRING] =
    BEGIN
    diff, delta: LONG INTEGER;
    hisError, ourError: LONG CARDINAL;
    flight: LONG CARDINAL;
    worked, known, mixup: BOOLEAN ← FALSE;
    who: PupAddress;
    interesting: BOOLEAN = ((timesAround MOD 24) = 0);
  
    [worked, who] ← FindPupAddress[target];
    IF ~worked THEN RETURN;

    [diff, flight] ← PopCorn.GetClockOffset[who, IF interesting THEN 5 ELSE 2 !
      PopCorn.Error =>
        BEGIN
	temp: STRING = [200];
        Time.AppendCurrent[temp];
	String.AppendString[temp, "  TimeChecker: Troubles from "L];
        String.AppendString[temp, target];
	String.AppendString[temp, ", "L];
	String.AppendString[temp, text];
        LogString[temp];
        IF interesting THEN AppendToLogFile[temp];
	worked ← FALSE;
	CONTINUE;
	END;];
    IF ~worked THEN RETURN;
    IF diff > 0 THEN  -- convert to seconds
      delta ← (diff + 500) / 1000
    ELSE delta ← (diff - 500) / 1000;
    hisError ← 1000 + flight + 1000;  -- quantum and fudge
    hisError ← hisError + 80;  -- DLion clock jitters  ****************
    
    PrintResponse[TRUE, target, diff, hisError, flight];
    
    IF timeLastSet = System.gmtEpoch THEN
      BEGIN
      now: System.GreenwichMeanTime ← System.GetGreenwichMeanTime[];
      SetMyGreenwichMeanTime[System.AdjustGreenwichMeanTime[now, delta]];
      END
    ELSE
      BEGIN
      now: System.GreenwichMeanTime ← System.GetGreenwichMeanTime[];
      myNow: System.GreenwichMeanTime ← GetMyGreenwichMeanTime[];
      myDelta: LONG INTEGER ← myNow - now;
      PrintDelta[myDelta - delta];
      END;

    [known, ourError] ← TimeServerOps.GetClockError[];
    mixup ← known AND (ABS[diff] > (hisError + ourError));
    IF mixup OR threshold # LAST[LONG CARDINAL] THEN
      BEGIN
      IF mixup OR ~known OR (hisError + ourError > threshold) THEN
        BEGIN
        TimeServerClock.AdjustClock[delta, hisError, TRUE];
	IF mixup THEN Message["Clock Mixup.  Clock synced to "L, target]
	ELSE Message["Threshold tripped.  Clock synced to "L, target];
	END;
      END;
      
    END;
	
  PrintResponse: PROCEDURE [
    hit: BOOLEAN,
    target: LONG STRING,
    diff: LONG INTEGER,
    hisError: LONG CARDINAL ← LAST[LONG CARDINAL],
    flight: LONG CARDINAL ← 0] =
    BEGIN
    text: STRING = [300];
    mixup: BOOLEAN ← FALSE;
    known: BOOLEAN;
    ourError: LONG CARDINAL;
    [known, ourError] ← TimeServerOps.GetClockError[];
    IF known AND hisError # LAST[LONG CARDINAL] THEN
      BEGIN
      mixup ← ABS[diff] > (hisError + ourError + flight);
      END;
    Time.AppendCurrent[text];
    IF hit THEN
      BEGIN
      SELECT TRUE FROM
        (timesAround = 0) =>
          BEGIN String.AppendString[text, "  At the start of this run, "L]; END;
        (timesAround = 1) =>
          BEGIN String.AppendString[text, "  After one hour, "L]; END;
        (timesAround = 24) =>
          BEGIN String.AppendString[text, "  After one day, "L]; END;
        (timesAround = 168) =>
          BEGIN String.AppendString[text, "  After one week, "L]; END;
        ((timesAround MOD 24) = 0) =>
          BEGIN
          String.AppendString[text, "  After "L];
          String.AppendDecimal[text, timesAround/24];
          String.AppendString[text, " days, "L];
          END;
        ENDCASE =>
          BEGIN
          String.AppendString[text, "  After "L];
          String.AppendDecimal[text, timesAround];
          String.AppendString[text, " hours, "L];
          END;
      String.AppendString[text, target];
      String.AppendString[text, "'s clock is "L];
      String.AppendLongDecimal[text, diff];
      String.AppendString[text, " ms"L];
      String.AppendString[text, " faster than ours"L];
      IF hisError # LAST[LONG CARDINAL] THEN
        BEGIN
        String.AppendChar[text, '.];
	IF mixup THEN String.AppendString[text, "  ***************"L];
        String.AppendChar[text, Ascii.CR];
        String.AppendString[text, "His error: "L];
        String.AppendLongDecimal[text, hisError];
        String.AppendString[text, " ms"L];
        IF known THEN
	  BEGIN
          String.AppendString[text, ",  Our error: "L];
          String.AppendLongDecimal[text, ourError];
          String.AppendString[text, " ms"L];
	  END
	END;
      IF flight # 0 THEN
        BEGIN
	IF hisError = LAST[LONG CARDINAL] THEN
	  BEGIN
	  String.AppendChar[text, '.];
          String.AppendChar[text, Ascii.CR];
	  END
	ELSE String.AppendString[text, ",  "L];
        String.AppendString[text, "Flight time: "L];
        String.AppendLongDecimal[text, flight];
        String.AppendString[text, " ms"L];
	END;
      END
    ELSE
      BEGIN
      String.AppendString[text, "  TimeChecker: No response from "L];
      String.AppendString[text, target];
      END;
    LogString[text];
    IF ((timesAround MOD 24) = 0) OR mixup THEN AppendToLogFile[text];
    END;
    
  PrintDelta: PROCEDURE [delta: LONG INTEGER] =
    BEGIN
    text: STRING = [200];
    Time.AppendCurrent[text];
    SELECT TRUE FROM
      (timesAround = 0) =>
        BEGIN String.AppendString[text, "  At the start of this run, "L]; END;
      (timesAround = 1) =>
        BEGIN String.AppendString[text, "  After one hour, "L]; END;
      (timesAround = 24) =>
        BEGIN String.AppendString[text, "  After one day, "L]; END;
      (timesAround = 168) =>
        BEGIN String.AppendString[text, "  After one week, "L]; END;
      ((timesAround MOD 24) = 0) =>
        BEGIN
        String.AppendString[text, "  After "L];
        String.AppendDecimal[text, timesAround/24];
        String.AppendString[text, " days, "L];
        END;
      ENDCASE =>
        BEGIN
        String.AppendString[text, "  After "L];
        String.AppendDecimal[text, timesAround];
        String.AppendString[text, " hours, "L];
        END;
    String.AppendString[text, "my simulated clock is "L];
    String.AppendLongDecimal[text, delta];
    String.AppendString[text, " seconds"L];
    String.AppendString[text, " faster than it should be"L];
    LogString[text];
    IF ((timesAround MOD 24) = 0) THEN AppendToLogFile[text];
    END;
    
  Message: PROCEDURE [one, two, three: LONG STRING ← NIL] =
    BEGIN
    text: STRING = [250];
    Time.AppendCurrent[text];
    String.AppendString[text, "  TimeChecker: "L];
    String.AppendString[text, one];
    IF two # NIL THEN String.AppendString[text, two];
    IF three # NIL THEN String.AppendString[text, three];
    LogString[text];
    END;

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

  AppendToLogFile: PROCEDURE [s: LONG STRING] =
    BEGIN
    sh: Stream.Handle ← NIL;
    sh ← MStream.ReadWrite["TimeServer.log"L, [], text ! MStream.Error => CONTINUE ];
    IF sh = NIL THEN RETURN;
    Stream.SetPosition[sh, MStream.GetLength[sh]];
    Stream.PutString[sh, s];
    Stream.Delete[sh];
    AppendToMail[s];
    END;

  AppendToMail: PROCEDURE [s: LONG STRING] =
    BEGIN
    String.AppendStringAndGrow[@mail, s, z];
    END;

  SendMail: PROCEDURE =
    BEGIN
    subject: STRING = "Report from TimeChecker"L;
    Info: PROCEDURE [s: LONG STRING, level: Mailer.Level] =
      BEGIN
      copy: LONG STRING ← String.CopyToNewString[s, Heap.systemZone, 2];
      LogString[copy];
      Heap.systemZone.FREE[@copy];
      END;
    IF to # NIL THEN
      [] ← Mailer.SendGVMail[subject, to, cc, mail, troubles, Info];
    z.FREE[@mail];
    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 watcher # NIL THEN Stopper[];
    Starter[];
    RETURN[FALSE]
    END;
  
    
  -- Copied (more or less) from GMTUsingIntervalTimer
  
  timeLastSet: System.GreenwichMeanTime ← System.gmtEpoch;
  gmtSimulated: System.GreenwichMeanTime ← System.gmtEpoch;
  pulsesGmtSimulated: LONG CARDINAL;  -- interval timer value corresponding to gmtSimulated
  pulsesPer100Seconds: LONG CARDINAL ← 10*(1D9/ProcessorFace.microsecondsPerHundredPulses);
  
  SetMyGreenwichMeanTime: PROCEDURE [gmt: System.GreenwichMeanTime] =
    BEGIN
    pulsesGmtSimulated ← System.GetClockPulses[];
    gmtSimulated ← gmt;
    timeLastSet ← gmt;
    END;
  
  GetMyGreenwichMeanTime: PROCEDURE RETURNS [System.GreenwichMeanTime] =
    BEGIN
    newPulses, pulsesJumped, new100Seconds, leftoverSeconds: LONG CARDINAL;
    IF gmtSimulated = System.gmtEpoch THEN RETURN[gmtSimulated]; -- clock not set
    newPulses ← System.GetClockPulses[] - pulsesGmtSimulated;
    new100Seconds ← newPulses / pulsesPer100Seconds;
    pulsesJumped ← new100Seconds * pulsesPer100Seconds;
    pulsesGmtSimulated ← pulsesGmtSimulated + pulsesJumped;
    gmtSimulated ← System.AdjustGreenwichMeanTime[gmtSimulated, new100Seconds * 100];
    leftoverSeconds ← ((newPulses - pulsesJumped) * 100) / pulsesPer100Seconds;
    RETURN[System.AdjustGreenwichMeanTime[gmtSimulated, leftoverSeconds]];
    END;
      
      

  Trouble: ERROR [reason: LONG STRING] = CODE;
  GetAddress: PROCEDURE [host: LONG STRING, socket: System.SocketNumber]
    RETURNS [addr: System.NetworkAddress] =
    BEGIN
    localFailed: BOOLEAN ← FALSE;
    IF host = NIL THEN ERROR Trouble["NIL => Address Fault"L];
    addr ← Unformat.NetworkAddress[host, octal !
      Unformat.Error => BEGIN localFailed ← TRUE; CONTINUE; END ];
    IF localFailed THEN
      BEGIN
      addr ← AddressTranslation.StringToNetworkAddress[host !
        AddressTranslation.Error =>
	  BEGIN
	  temp: STRING = [200];
	  proc: Format.StringProc = {String.AppendString[temp, s]};
	  AddressTranslation.PrintError[errorRecord, proc];
	  ERROR Trouble[temp];
	  END].addr;
        addr.socket ← socket;  -- CH returns trash in socket
      END;
    IF addr.socket = System.nullSocketNumber THEN addr.socket ← socket;
    END;

  Init[];
  END.