-- File: TimeServerClockImpl.mesa - last edit:
-- HGM                 25-Jun-85  2:47:16 - version to LogClockChange
-- KAM                 15-Apr-85 18:02:42

-- Copyright (C) 1984, 1985 by Xerox Corporation. All rights reserved.


DIRECTORY
  Buffer USING [NSBuffer],
  Inline USING [DIVMOD, LowHalf],
  NSConstants USING [timeServerSocket],
  PacketExchange USING [ExchangeClientType],
  Process USING [
    Abort, Pause, priorityBackground, SecondsToTicks, SetPriority, Ticks],
  Router USING [
    endEnumeration, EnumerateRoutingTable, FillRoutingTable, FindMyHostID,
    GetDelayToNet, infinity, NoTableEntryForNet, startEnumeration],
  Socket USING [
    BroadcastPacketToAllConnectedNets, ChannelHandle, Create, Delete, GetPacket,
    GetSendBuffer, PutPacket, ReturnBuffer, SetPacketWords, SetWaitTime,
    TimeOut],
  SpecialSystem USING [AdjustClock, clockSlipping, SetBackTooFar, TimeLastSet],
  System USING [
    AdjustGreenwichMeanTime, broadcastHostNumber, GetClockPulses,
    GetGreenwichMeanTime, GreenwichMeanTime, HostNumber, Microseconds,
    NetworkAddress, NetworkNumber, nullNetworkAddress, nullNetworkNumber,
    nullSocketNumber, Pulses, PulsesToMicroseconds, SecondsSinceEpoch],
  TimeServerClock USING [Error, Rate, undefined],
  TimeServerFormat USING [LongToWire, TSPacket, Version, WireToGMT, WireToLong],
  TimeServerLog USING [LogClockChange, LogInconsistancy];
  
TimeServerClockImpl: MONITOR
  IMPORTS
    Inline, Process, Router, Socket, SpecialSystem, System, TimeServerFormat,
    TimeServerLog
  EXPORTS TimeServerClock =
  BEGIN
  
  -- Initial values to these variables are assigned by the procedure Init
  tsError: TimeServerClock.Error;
  tsDrift, tsWobble: TimeServerClock.Rate;
  tsResetPeriod: CARDINAL;
  
  clockInvalid: BOOLEAN ← FALSE;  -- set if SpecialSystem.SetBackTooFar is raised
  ignoringServer: BOOLEAN ← FALSE;
  ignoringServerAddress: System.HostNumber;
  
  tsSourceLastReset: System.NetworkAddress ← System.nullNetworkAddress;
  tsLastChange: LONG INTEGER ← 0;
  tsVersion: LONG CARDINAL ← 0;
  
  started: BOOLEAN ← FALSE;
  surveyorProcess: PROCESS;
  lastID: LONG CARDINAL ← 1;
  
  oneSecond: Process.Ticks = Process.SecondsToTicks[1];
  triesPerNetwork: CARDINAL = 3;
  resetTries: CARDINAL = 10;
  
  Inconsistent: ERROR = CODE;
  Invalid: PUBLIC SIGNAL = CODE;
  
  -- public procedures
  
  AdjustClock: PUBLIC PROCEDURE [
    amount: LONG INTEGER, error: TimeServerClock.Error, newVersion: BOOLEAN] =
    BEGIN
    UpVersion: ENTRY PROCEDURE [update: BOOLEAN] RETURNS [LONG CARDINAL] =  {
      RETURN[IF update THEN SUCC[tsVersion] ELSE tsVersion]};
    AdjustClockEntry[
      amount, error, UpVersion[newVersion], 0, System.nullNetworkAddress, TRUE];
    END;
  
  GetParameters: PUBLIC ENTRY PROCEDURE RETURNS [
    drift, wobble: TimeServerClock.Rate,
    resetPeriod: CARDINAL, timeLastReset: System.GreenwichMeanTime,
    sourceLastReset: System.NetworkAddress, changeLastReset: LONG INTEGER] = {
    RETURN[
      tsDrift, tsWobble, tsResetPeriod, SpecialSystem.TimeLastSet[],
      tsSourceLastReset, tsLastChange]};
  
  Read: PUBLIC ENTRY PROCEDURE RETURNS [
    time: System.GreenwichMeanTime, error: TimeServerClock.Error,
    accurate: BOOLEAN, version: LONG CARDINAL] =
    BEGIN
    IF clockInvalid THEN SIGNAL Invalid;
    error ← ErrorInternal[];
    RETURN[
      System.GetGreenwichMeanTime[], error,
      (error # TimeServerClock.undefined) AND NOT SpecialSystem.clockSlipping, 
      tsVersion];
    END;

  ResetClock: PUBLIC PROCEDURE [from: System.NetworkAddress]
    RETURNS [reset: BOOLEAN ← FALSE] =
    BEGIN
    cH: Socket.ChannelHandle ← Socket.Create[System.nullSocketNumber];
    b: Buffer.NSBuffer ← NIL;
    Socket.SetWaitTime[cH, WaitForHops[Router.GetDelayToNet[
      from.net ! Router.NoTableEntryForNet => {
        Socket.Delete[cH]; GOTO NoReset}]]];
    IF from.host # System.broadcastHostNumber THEN
      BEGIN ENABLE UNWIND => IF b # NIL THEN Socket.ReturnBuffer[b];
      time: LONG INTEGER;
      error, delay, version: LONG CARDINAL;
      sendMsec: System.Microseconds;
      
      MaxVersion: ENTRY PROCEDURE RETURNS [LONG CARDINAL] = {
        RETURN[MAX[tsVersion, version]]};
	
      FOR tries: CARDINAL IN [0.. 2*resetTries) UNTIL reset DO
	sendMsec ← System.PulsesToMicroseconds[System.GetClockPulses[]];
	SendTimeRequest[cH, from, TRUE, (tries < resetTries)];
	b ← Socket.GetPacket[cH ! Socket.TimeOut => LOOP];
	IF b.ns.packetType = packetExchange AND b.ns.exchangeType =
	  PacketExchange.ExchangeClientType[timeService]
	  AND LOOPHOLE[b.ns.exchangeID, LONG CARDINAL] = lastID THEN
	  BEGIN
	  p: LONG POINTER TO TimeServerFormat.TSPacket =
	    LOOPHOLE[@b.ns.exchangeBody];
	  delay ← (
	    System.PulsesToMicroseconds[System.GetClockPulses[]] - sendMsec)
	    /1000;
	  WITH r: p↑ SELECT FROM
	    internalTimeResponse =>
	      BEGIN
	      reset ← TRUE;
	      time ← System.SecondsSinceEpoch[TimeServerFormat.WireToGMT[r.time]]
		- System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]];
	      error ← TimeServerFormat.WireToLong[r.absoluteError];
	      IF error # TimeServerClock.undefined
		THEN error ← error + delay + 1000;
	      version ← TimeServerFormat.WireToLong[r.timeVersion];
	      END;
	    timeResponse =>
	      BEGIN
	      reset ← TRUE;
	      time ← System.SecondsSinceEpoch[TimeServerFormat.WireToGMT[r.time]]
		- System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]];
	      error ← IF NOT r.errorAccurate THEN TimeServerClock.undefined
	      ELSE TimeServerFormat.WireToLong[r.absoluteError] + delay + 1000;
	      version ← 0;
	      END;
	    ENDCASE;
	  IF reset THEN AdjustClockEntry[
	    time, error, MaxVersion[], delay, from, TRUE];
	  END;
	Socket.ReturnBuffer[b];
	ENDLOOP;
      END
    ELSE
      FOR tries: CARDINAL IN [0.. resetTries) UNTIL reset DO
        from.socket ← NSConstants.timeServerSocket;
	BEGIN
	  ENABLE Inconsistent => RETRY;
	  pulsesSent: System.Pulses = System.GetClockPulses[]; 
	  SendTimeRequest[cH, from, (tries = 0), TRUE];
	  reset ← CollectTimeReplies[cH, pulsesSent];
	  END;
        ENDLOOP;
    Socket.Delete[cH];
    EXITS NoReset => NULL;
    END;

  ResyncNow: PUBLIC ENTRY PROCEDURE = {
    IF started THEN Process.Abort[surveyorProcess]};

  SetParameters: PUBLIC ENTRY PROCEDURE [
    drift, wobble: TimeServerClock.Rate, resetPeriod: CARDINAL] =
    BEGIN
    tsDrift ← drift; tsWobble  ← wobble; tsResetPeriod ← resetPeriod + Random[];
    IF started THEN Process.Abort[surveyorProcess]; -- signal new wait time 
    END;
  
  Start: PUBLIC ENTRY PROCEDURE =
    BEGIN ENABLE UNWIND => NULL;
    IF started THEN RETURN;
    clockInvalid ← FALSE;
    tsVersion ← 0;
    surveyorProcess ← FORK SurveyorProcess[];
    started ← TRUE;
    END;
  
  Stop: PUBLIC ENTRY PROCEDURE =
    BEGIN ENABLE UNWIND => NULL;
    IF NOT started THEN RETURN;
    started ← FALSE;
    Process.Abort[surveyorProcess]; -- do after setting started to FALSE
    JOIN surveyorProcess;
    END;
  
  -- private procedures
  
  AdjustClockEntry: ENTRY PROCEDURE [
    amount: LONG INTEGER, error: TimeServerClock.Error, version: LONG CARDINAL,
    delay: LONG CARDINAL, who: System.NetworkAddress, useAlways: BOOLEAN] =
    BEGIN ENABLE UNWIND => NULL;
    me: TimeServerClock.Error = ErrorInternal[];
    IF useAlways OR version > tsVersion
      OR (version = tsVersion AND error < me) THEN
      BEGIN
      TimeServerLog.LogClockChange[
        System.GetGreenwichMeanTime[], amount, who,
	(me = TimeServerClock.undefined), me, error, delay, version];
      SpecialSystem.AdjustClock[amount];
      tsError ← error;
      tsSourceLastReset ← who;
      tsLastChange ← amount;
      IF tsVersion < version THEN SendNewVersionNote[tsVersion ← version];
      END;
    END;
  
  BroadcastAddress: PROCEDURE [net: System.NetworkNumber]
    RETURNS [System.NetworkAddress] = INLINE {
    RETURN[
      [net: net, host: System.broadcastHostNumber,
      socket: NSConstants.timeServerSocket]]};
    
  CheckForConsistency:  ENTRY PROCEDURE [
    amount: LONG INTEGER, error: TimeServerClock.Error, version: LONG CARDINAL,
    who: System.NetworkAddress] =
    BEGIN ENABLE UNWIND => NULL;
    MarkInconsistent: INTERNAL PROCEDURE =
      BEGIN
      now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
      TimeServerLog.LogInconsistancy[
        who, now, me, System.AdjustGreenwichMeanTime[now, amount], error];
      ignoringServer ← TRUE;
      ignoringServerAddress ← who.host;
      tsError ← TimeServerClock.undefined;
      END;
    me: TimeServerClock.Error = ErrorInternal[];
    IF tsVersion = version
      AND me # TimeServerClock.undefined AND ABS[amount*1000] > error + me THEN {
      MarkInconsistent[]; ERROR Inconsistent};
    END;
  
  CollectTimeReplies: PROCEDURE [
    cH: Socket.ChannelHandle, sent: System.Pulses] RETURNS [success: BOOLEAN] =
    BEGIN 
    sendMsec: System.Microseconds = System.PulsesToMicroseconds[sent];
    bestTime, time: LONG INTEGER;
    bestError: LONG CARDINAL ← TimeServerClock.undefined;
    bestVersion: LONG CARDINAL ← 0;
    error, delay, version, bestDelay: LONG CARDINAL;
    bestServer: System.NetworkAddress;
    myHostNumber: System.HostNumber = Router.FindMyHostID[];
    b: Buffer.NSBuffer;
    success ← FALSE;
    DO
      b ← Socket.GetPacket[cH !
        Socket.TimeOut => EXIT; ABORTED => IF started THEN RETRY];
      IF b.ns.packetType = packetExchange
        AND b.ns.exchangeType = PacketExchange.ExchangeClientType[timeService]
	AND LOOPHOLE[b.ns.exchangeID, LONG CARDINAL] = lastID
	AND b.ns.source.host # myHostNumber
	AND (NOT ignoringServer OR b.ns.source.host # ignoringServerAddress) THEN
	BEGIN
	p: LONG POINTER TO TimeServerFormat.TSPacket =
	  LOOPHOLE[@b.ns.exchangeBody];
	delay ← (System.PulsesToMicroseconds[System.GetClockPulses[]]-sendMsec)
	  /1000;
	WITH r: p↑ SELECT FROM
	  internalTimeResponse =>
	    BEGIN
	    error ← TimeServerFormat.WireToLong[r.absoluteError];
	    IF error # TimeServerClock.undefined THEN
	      BEGIN
	      success ← TRUE;
	      error ← error + delay + 1000;
	      time ← System.SecondsSinceEpoch[TimeServerFormat.WireToGMT[r.time]]
		- System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]];
	      version ← TimeServerFormat.WireToLong[r.timeVersion];
	      IF version > bestVersion
		OR (version = bestVersion AND error <= bestError) THEN {
		bestTime ← time; bestError ← error; bestVersion ← version;
		bestServer ← b.ns.source; bestDelay ← delay};
	      END;
	    END;
	  ENDCASE;
	END;
      Socket.ReturnBuffer[b];
      ENDLOOP;
    IF success THEN
      BEGIN
      CheckForConsistency[bestTime, bestError, bestVersion, bestServer];
      AdjustClockEntry[bestTime, bestError, bestVersion, bestDelay,
        bestServer, FALSE ! 
          SpecialSystem.SetBackTooFar => {clockInvalid ← TRUE; CONTINUE}];
      END;
    END;
  
  ErrorInternal: INTERNAL PROCEDURE RETURNS [error: LONG CARDINAL] =
    BEGIN
    wobbleBase, driftBase: LONG CARDINAL;
    IF tsError = TimeServerClock.undefined THEN RETURN [tsError];
    driftBase ← System.GetGreenwichMeanTime[] - SpecialSystem.TimeLastSet[];
    wobbleBase ← MIN[tsWobble.base, driftBase];
    error ← tsWobble.amount*wobbleBase/tsWobble.base
      + tsDrift.amount*driftBase/tsDrift.base;
    RETURN[error + tsError];
    END;
  
  Init: PROCEDURE =
    BEGIN
    tsError ← TimeServerClock.undefined;
    tsDrift ← [base: 86400, amount: 1000];
    tsWobble ← [base: 3600, amount: 1000];
    tsResetPeriod ← 15 + Random[];
    END;

  Random: PROCEDURE RETURNS [CARDINAL] =
    -- This generates a "random" (across servers) number between 0 and 10
    -- which is added to the reset rate in order to keep the server polling
    -- unsynchronized 
    BEGIN
    words: ARRAY [0.. SIZE[System.HostNumber]) OF CARDINAL
      = LOOPHOLE[Router.FindMyHostID[]];
    RETURN[Inline.DIVMOD[words[SIZE[System.HostNumber]-1], 10].remainder];
    END;

  RequestTime: PROCEDURE =
    BEGIN 
    haveReply: BOOLEAN ← FALSE;
    incID: BOOLEAN;
    pulsesSent: System.Pulses;
    cH: Socket.ChannelHandle ← Socket.Create[System.nullSocketNumber, 1, 5];
    BEGIN ENABLE UNWIND => {Socket.Delete[cH]; Router.FillRoutingTable[0]};
    Router.FillRoutingTable[];
    Process.Pause[2*oneSecond];
    FOR hops: CARDINAL IN [0.. Router.infinity) UNTIL haveReply DO
      Socket.SetWaitTime[cH, WaitForHops[hops]];
      incID ← TRUE;
      THROUGH [0.. triesPerNetwork) UNTIL haveReply DO
        pulsesSent ← System.GetClockPulses[];
        IF hops = 0 THEN
	  BEGIN
	  SendTimeRequest[
	    cH, BroadcastAddress[System.nullNetworkNumber], TRUE, TRUE];
	  incID ← FALSE;
	  END
	ELSE FOR net: System.NetworkNumber ←
	  Router.EnumerateRoutingTable[Router.startEnumeration, hops],
	  Router.EnumerateRoutingTable[net, hops]
	  UNTIL net = Router.endEnumeration DO
	  SendTimeRequest[cH, BroadcastAddress[net], incID, TRUE];
	  incID ← FALSE;
	  ENDLOOP;
	-- if incID is FALSE here, then a time request was sent
	IF NOT incID THEN haveReply ← CollectTimeReplies[cH, pulsesSent];
        ENDLOOP;
      ENDLOOP;
    Socket.Delete[cH];
    Router.FillRoutingTable[0];
    END;
    END;
    
  SendNewVersionNote: INTERNAL PROCEDURE [new: LONG CARDINAL] =
    BEGIN
    cH: Socket.ChannelHandle ← Socket.Create[System.nullSocketNumber];
    b: Buffer.NSBuffer = Socket.GetSendBuffer[cH];
    note: LONG POINTER TO noteNewVersion TimeServerFormat.TSPacket 
       = LOOPHOLE[@b.ns.exchangeBody];
    b.ns.packetType ← packetExchange;
    b.ns.exchangeID ← LOOPHOLE[lastID];
    b.ns.exchangeType ← PacketExchange.ExchangeClientType[timeService];
    b.ns.destination ← BroadcastAddress[System.nullNetworkNumber];
    Socket.SetPacketWords[b, 3 + 
      SIZE[noteNewVersion TimeServerFormat.TSPacket]];
    note↑ ← [
      version: TimeServerFormat.Version,
      tsBody: noteNewVersion[TimeServerFormat.LongToWire[new]]];
    Socket.BroadcastPacketToAllConnectedNets[cH, b];
    Socket.Delete[cH];
    END;

  SendTimeRequest: PROCEDURE [
    cH: Socket.ChannelHandle, who: System.NetworkAddress, incrementID: BOOLEAN,
    internalFormat: BOOLEAN] =
    BEGIN
    b: Buffer.NSBuffer = Socket.GetSendBuffer[cH];
    iReq: LONG POINTER TO internalTimeRequest TimeServerFormat.TSPacket 
       = LOOPHOLE[@b.ns.exchangeBody];
    eReq: LONG POINTER TO timeRequest TimeServerFormat.TSPacket 
       = LOOPHOLE[@b.ns.exchangeBody];
    IF incrementID THEN lastID ← lastID + 1;
    b.ns.packetType ← packetExchange;
    b.ns.exchangeID ← LOOPHOLE[lastID];
    b.ns.exchangeType ← PacketExchange.ExchangeClientType[timeService];
    b.ns.destination ← who;
    Socket.SetPacketWords[b, 3 + (
      IF internalFormat THEN SIZE[internalTimeRequest TimeServerFormat.TSPacket]
      ELSE SIZE[timeRequest TimeServerFormat.TSPacket])];
    IF internalFormat THEN
      iReq↑ ← [version: TimeServerFormat.Version, tsBody: internalTimeRequest[]]
      ELSE eReq↑ ← [version: TimeServerFormat.Version, tsBody: timeRequest[]];
    (IF who.net = System.nullNetworkNumber THEN
      Socket.BroadcastPacketToAllConnectedNets ELSE Socket.PutPacket)[cH, b]
    END;

  SurveyorProcess: PROCEDURE =
    BEGIN
    -- broadcasts a time request every tsResetPeriod minutes
    Process.SetPriority[Process.priorityBackground];
    UNTIL NOT started DO
      ENABLE ABORTED => LOOP;
      IF NOT SpecialSystem.clockSlipping
        THEN RequestTime[! Inconsistent => RETRY];
      ignoringServer ← FALSE;
      Wait[tsResetPeriod];
      ENDLOOP;
    END;

  Wait: PROCEDURE [time: CARDINAL  -- minutes --] =
    BEGIN ENABLE ABORTED => GOTO StopWaiting;
    oneMinute: LONG CARDINAL = LONG[Process.SecondsToTicks[60]];
    ticksToWait: LONG CARDINAL = oneMinute*LONG[time];
    maxTicks: Process.Ticks = LAST[Process.Ticks];
    ticksLeft: LONG CARDINAL;
    FOR ticksLeft ← ticksToWait, ticksLeft - MIN[ticksLeft, maxTicks] UNTIL
      ticksLeft = 0 DO Process.Pause[Inline.LowHalf[MIN[ticksLeft, maxTicks]]];
      ENDLOOP;
    EXITS StopWaiting => NULL;
    END;
  
  WaitForHops: PROCEDURE [hops: CARDINAL] RETURNS [delay: CARDINAL] = INLINE {
    RETURN[MAX[2+3*(hops/2), 6]*1000]};
  
  Init[];
  
  END...