<> <> <> <> <> <<>> <> <> DIRECTORY Arpa USING [Address, MyAddress, nullAddress], ArpaUDP USING [nullPort, Port], Ascii USING [CR, FF, LF, SP, TAB], Basics USING [HFromCard16, IsBound, LowHalf], BasicTime USING [earliestGMT, GMT, Now, Period, TimeNotKnown, Update], Convert USING [RopeFromArpaAddress], Process USING [Detach, PauseMsec], RefText USING [Append, New, ObtainScratch, ReleaseScratch], Rope USING [FromRefText, Length, ROPE, Substr, ToRefText], SunPMap USING [udpPort], SunRPC USING [Destroy, Error, Handle], SunRPCOnUDP USING [Create, SetRemote], SunRPCAuth USING [Conversation, Error, Initiate, Terminate], SunYP USING [EachMapNameProc, ResponseKeyVal, ResponseVal, Status], SunYPAgent USING [TextSeq, TextSeqObject], SunYPAgentPrivate USING [Object, ServerObject], SunYPClient USING [First, Maplist, Match, Next, Null], SunYPFind USING [Error, GetDefaultDomain, GetServerAddressAndPortForDomain], SymTab USING [Create, Fetch, Ref, Store, Update, UpdateAction, Val], ThisMachineRegistry USING [ThisMachineProcsRec, ThisMachineRef, RegisterThisMachineProcs] ; SunYPAgentImpl: CEDAR MONITOR LOCKS lock USING lock: Lock IMPORTS Arpa, Basics, BasicTime, Convert, Process, RefText, Rope, SunRPC, SunRPCOnUDP, SunRPCAuth, SunYPClient, SunYPFind, SymTab, ThisMachineRegistry EXPORTS SunYPAgent ~ { <> msecBetweenSweeps: CARD ¬ 11111; initialServerTTL: CARDINAL ¬ 300; initialCachedValueTTL: CARDINAL ¬ 300; maxCachedValues: CARDINAL ¬ 31; timeoutBase: CARD ¬ 500; timeoutMsecPerHop: CARD ¬ 250; pauseMsecPerHop: CARD ¬ 100; maxTriesForSpecifiedServer: CARDINAL ¬ 8; pauseMsecBetweenRetriesForSpecifiedServer: CARD ¬ 1000; pMapPort: ArpaUDP.Port ¬ Basics.HFromCard16[Basics.LowHalf[SunPMap.udpPort]]; maxArgLength: CARDINAL ¬ 100; hostsMapName: ROPE ¬ "hosts.byaddr"; hostNamePos: CARDINAL = 1; <> ROPE: TYPE ~ Rope.ROPE; Lock: TYPE ~ REF LockObject; LockObject: TYPE ~ MONITORED RECORD []; Error: PUBLIC ERROR [code: ATOM] ~ CODE; <> Server: TYPE ~ REF ServerObject; ServerObject: TYPE ~ SunYPAgentPrivate.ServerObject; serverTab: SymTab.Ref ¬ SymTab.Create[]; serverLock: Lock ~ NEW[LockObject]; serverList: Server ¬ NIL; nullServerLock: Lock ~ NEW[LockObject]; nullServerList: Server ¬ NIL; GetServerByAddressAndPort: ENTRY PROC [address: Arpa.Address, port: ArpaUDP.Port, lock: Lock ¬ serverLock] RETURNS [server: Server] ~ { FOR server ¬ serverList, server.next WHILE (server # NIL) AND ((server.address # address) OR (server.port # port)) DO NULL ENDLOOP; IF server = NIL THEN { server ¬ NEW[ServerObject ¬ [serverList, address, port, initialServerTTL, FALSE]]; serverList ¬ server; }; }; SweepServers: ENTRY PROC [seconds: CARD, lock: Lock ¬ serverLock] ~ { FOR server: Server ¬ serverList, server.next WHILE server # NIL DO server.ttl ¬ (IF server.ttl > seconds THEN server.ttl - seconds ELSE 0); ENDLOOP; }; IsNullServer: PROC [server: Server] RETURNS [BOOL] ~ INLINE { RETURN [server.address = Arpa.nullAddress] }; GetNullServer: ENTRY PROC [lock: Lock ¬ nullServerLock] RETURNS [server: Server] ~ { server ¬ NEW[ServerObject ¬ [nullServerList, Arpa.nullAddress, ArpaUDP.nullPort, initialServerTTL, FALSE]]; nullServerList ¬ server; }; SweepNullServers: ENTRY PROC [seconds: CARD, lock: Lock ¬ nullServerLock] ~ { prev, server: Server; prev ¬ NIL; server ¬ nullServerList; WHILE server # NIL DO IF server.ttl > seconds THEN { server.ttl ¬ server.ttl - seconds; prev ¬ server; } ELSE { server.ttl ¬ 0; IF prev = NIL THEN nullServerList ¬ server.next ELSE prev.next ¬ server.next; }; server ¬ server.next; ENDLOOP; }; GetServerForDomain: PROC [domain: ROPE] RETURNS [server: Server] ~ { <> server ¬ NARROW[SymTab.Fetch[serverTab, domain].val]; SELECT TRUE FROM (server = NIL) => NULL; (IsNullServer[server]) => IF server.ttl = 0 THEN server ¬ NIL; (server.down) => server ¬ NIL; (server.ttl = 0) AND (NOT ServerResponding[server]) => server ¬ NIL; <> ENDCASE; IF server # NIL THEN RETURN; -- IF server = NIL THEN -- { address: Arpa.Address ¬ Arpa.nullAddress; port: ArpaUDP.Port ¬ ArpaUDP.nullPort; [address, port] ¬ SunYPFind.GetServerAddressAndPortForDomain[domain ! SunYPFind.Error => CONTINUE]; IF address # Arpa.nullAddress THEN server ¬ GetServerByAddressAndPort[address, port]; }; IF server = NIL THEN { server ¬ GetNullServer[]; }; [] ¬ SymTab.Store[serverTab, domain, server]; }; GetServerByAddress: PROC [address: Arpa.Address, domain: ROPE] RETURNS [server: Server] ~ { <> errorCode: ATOM ¬ NIL; port: ArpaUDP.Port; [, port] ¬ SunYPFind.GetServerAddressAndPortForDomain[domain, address ! SunYPFind.Error => { errorCode ¬ code; CONTINUE } ]; IF errorCode = NIL THEN { server ¬ GetServerByAddressAndPort[address, port]; server.down ¬ FALSE; server.ttl ¬ initialServerTTL; [] ¬ SymTab.Store[serverTab, domain, server]; } ELSE { ERROR Error[errorCode]; }; }; ServerResponding: PROC [server: Server] RETURNS [responding: BOOL ¬ FALSE] ~ { h: SunRPC.Handle; c: SunRPCAuth.Conversation; IF NOT IsNullServer[server] THEN { ENABLE SunRPC.Error, SunRPCAuth.Error => CONTINUE; h ¬ SunRPCOnUDP.Create[remoteAddress~server.address, remotePort~server.port]; c ¬ SunRPCAuth.Initiate[]; server.ttl ¬ initialServerTTL; SunYPClient.Null[h, c]; responding ¬ TRUE; }; server.down ¬ NOT responding; IF h # NIL THEN SunRPC.Destroy[h]; h ¬ NIL; IF c # NIL THEN SunRPCAuth.Terminate[c]; c ¬ NIL; }; <> Handle: TYPE ~ REF Object; Object: PUBLIC TYPE ~ SunYPAgentPrivate.Object; ObtainHandle: PUBLIC PROC [domainName: ROPE, conversation: SunRPCAuth.Conversation, serverAddress: Arpa.Address] RETURNS [h: Handle] ~ { server: Server ¬ NIL; rpcHandle: SunRPC.Handle; serverSpecified: BOOL ¬ (serverAddress # Arpa.nullAddress); IF domainName = NIL THEN domainName ¬ SunYPFind.GetDefaultDomain[]; IF serverSpecified THEN { FOR try: CARDINAL ¬ 0, try.SUCC DO server ¬ GetServerByAddress[serverAddress, domainName ! Error => IF try >= maxTriesForSpecifiedServer THEN REJECT ELSE CONTINUE]; IF server # NIL THEN EXIT; Process.PauseMsec[pauseMsecBetweenRetriesForSpecifiedServer]; ENDLOOP; } ELSE { server ¬ GetServerForDomain[domainName]; IF IsNullServer[server] THEN ERROR Error[$domainNotFound]; }; IF conversation = NIL THEN conversation ¬ SunRPCAuth.Initiate[]; rpcHandle ¬ SunRPCOnUDP.Create[remoteAddress~server.address, remotePort~server.port]; h ¬ NEW[Object ¬ [domain~domainName, server~server, rpcHandle~rpcHandle, conversation~conversation, serverSpecified~serverSpecified]]; }; RefreshHandle: PUBLIC PROC [h: Handle, conversation: SunRPCAuth.Conversation, serverAddress: Arpa.Address] ~ { server: Server; IF serverAddress = Arpa.nullAddress THEN { server ¬ GetServerForDomain[h.domain]; IF IsNullServer[server] THEN ERROR Error[$domainNotFound]; } ELSE { server ¬ GetServerByAddress[serverAddress, h.domain]; }; SunRPCAuth.Terminate[h.conversation]; h.conversation ¬ IF conversation # NIL THEN conversation ELSE SunRPCAuth.Initiate[]; IF server # h.server THEN { h.server ¬ server; h.rpcHandle ¬ SunRPCOnUDP.SetRemote[h.rpcHandle, server.address, server.port]; }; }; ReleaseHandle: PUBLIC PROC [h: Handle] ~ { IF h.rpcHandle # NIL THEN { SunRPC.Destroy[h.rpcHandle]; h.rpcHandle ¬ NIL }; h.server ¬ NIL; IF h.conversation # NIL THEN { SunRPCAuth.Terminate[h.conversation]; h.conversation ¬ NIL }; }; FixupForDownServer: PROC [h: Handle] RETURNS [fixed: BOOL ¬ FALSE] ~ { ENABLE Error => CONTINUE; oldServer: Server; IF h.serverSpecified THEN RETURN; oldServer ¬ h.server; h.server ¬ GetServerForDomain[h.domain]; IF IsNullServer[h.server] THEN RETURN; IF h.server.address = oldServer.address THEN RETURN; IF h.server.down THEN RETURN; fixed ¬ TRUE; }; <> CachedValue: TYPE ~ REF CachedValueObject; CachedValueObject: TYPE ~ RECORD [ next, prev: CachedValue, ttl: CARDINAL ¬ initialCachedValueTTL, busy: CARDINAL ¬ 0, value: REF TEXT, key: ROPE, table: SymTab.Ref ]; cachedValueLock: Lock ~ NEW[LockObject]; cachedValueHead, cachedValueTail: CachedValue; numCachedValues: CARD ¬ 0; cachedValueAvailable: CONDITION; tabByDomain: SymTab.Ref ~ SymTab.Create[]; RemoveCachedValueFromTable: PROC [cV: CachedValue] ~ { value: SymTab.Val ~ cV; DeleteValue: SymTab.UpdateAction -- [found, val] RETURNS [op, new] -- ~ { IF found AND val = value THEN RETURN [delete, NIL] ELSE RETURN [none, NIL]; }; IF (cV.table = NIL) OR (cV.key = NIL) THEN ERROR; SymTab.Update[cV.table, cV.key, DeleteValue]; }; RemoveCachedValueFromList: INTERNAL PROC [cV: CachedValue] ~ { IF cV = cachedValueTail THEN cachedValueTail ¬ cV.prev ELSE cV.next.prev ¬ cV.prev; IF cV = cachedValueHead THEN cachedValueHead ¬ cV.next ELSE cV.prev.next ¬ cV.next; }; LookupCachedValue: ENTRY PROC [domainName, mapName, key: ROPE, lock: Lock ¬ cachedValueLock] RETURNS [cV: CachedValue] ~ { tabByMap, tabByKey: SymTab.Ref; GetTabByMap: SymTab.UpdateAction -- [found, val] RETURNS [op, new] -- ~ { IF found THEN { tabByMap ¬ NARROW[val]; RETURN [none, NIL] } ELSE { tabByMap ¬ SymTab.Create[]; RETURN [store, tabByMap] }; }; GetTabByKey: SymTab.UpdateAction -- [found, val] RETURNS [op, new] -- ~ { IF found THEN { tabByKey ¬ NARROW[val]; RETURN [none, NIL] } ELSE { tabByKey ¬ SymTab.Create[]; RETURN [store, tabByKey] }; }; SymTab.Update[tabByDomain, domainName, GetTabByMap]; SymTab.Update[tabByMap, mapName, GetTabByKey]; cV ¬ NARROW[SymTab.Fetch[tabByKey, key].val]; SELECT TRUE FROM (cV = NIL) => { DO SELECT TRUE FROM (numCachedValues < maxCachedValues) => { cV ¬ NEW[CachedValueObject]; numCachedValues ¬ numCachedValues + 1; EXIT; }; (cachedValueHead = NIL) => { WAIT cachedValueAvailable; LOOP; }; ENDCASE => { cV ¬ cachedValueTail; RemoveCachedValueFromTable[cV]; RemoveCachedValueFromList[cV]; EXIT; }; ENDLOOP; cV.ttl ¬ initialCachedValueTTL; cV.key ¬ key; cV.value ¬ NIL; cV.table ¬ tabByKey; }; (cV.busy = 0) => { RemoveCachedValueFromList[cV]; }; ENDCASE; cV.busy ¬ cV.busy + 1; }; CacheValue: ENTRY PROC [cV: CachedValue, newValue: REF TEXT ¬ NIL, lock: Lock ¬ cachedValueLock] ~ { IF newValue # NIL THEN { IF cV.busy # 1 THEN ERROR; cV.value ¬ newValue; [] ¬ SymTab.Store[cV.table, cV.key, cV]; }; IF (cV.busy ¬ cV.busy - 1) = 0 THEN { IF cachedValueHead = NIL THEN { cV.next ¬ cV.prev ¬ NIL; cachedValueHead ¬ cachedValueTail ¬ cV; NOTIFY cachedValueAvailable; } ELSE { cV.next ¬ cachedValueHead; cV.prev ¬ NIL; cachedValueHead.prev ¬ cV; cachedValueHead ¬ cV; }; }; }; FreeCachedValue: INTERNAL PROC [cV: CachedValue, lock: Lock] ~ { cV.key ¬ NIL; cV.value ¬ NIL; cV.table ¬ NIL; cV.next ¬ cV.prev ¬ NIL; numCachedValues ¬ numCachedValues - 1; NOTIFY cachedValueAvailable; }; ReleaseCacheEntry: ENTRY PROC [cV: CachedValue, lock: Lock ¬ cachedValueLock] ~ { IF cV.busy # 1 THEN ERROR; FreeCachedValue[cV, lock]; }; SweepCachedValues: ENTRY PROC [seconds: CARD, lock: Lock ¬ cachedValueLock] ~ { p, next: CachedValue; FOR p ¬ cachedValueHead, next WHILE p # NIL DO next ¬ p.next; IF p.ttl > seconds THEN { p.ttl ¬ p.ttl - seconds; } ELSE { IF p.busy # 0 THEN ERROR; RemoveCachedValueFromTable[p]; RemoveCachedValueFromList[p]; FreeCachedValue[p, lock]; }; ENDLOOP; }; <> MapErrorCode: PROC [code: ATOM, h: Handle] RETURNS [mappedCode: ATOM] ~ { h.server.down ¬ TRUE; h.server.ttl ¬ initialServerTTL; mappedCode ¬ code; }; MapErrorStatus: PROC [status: SunYP.Status, h: Handle] RETURNS [mappedCode: ATOM] ~ { h.server.down ¬ FALSE; h.server.ttl ¬ initialServerTTL; mappedCode ¬ (SELECT status FROM true => NIL, nomore => $noMoreEntries, nomap => $mapNotFound, nodomain => $domainNotFound, nokey => $keyNotFound, ENDCASE => $protocol); }; Match: PUBLIC PROC [h: Handle, map: ROPE, key: ROPE] RETURNS [val: REF TEXT] ~ { cV: CachedValue; errorCode: ATOM; response: SunYP.ResponseVal; cV ¬ LookupCachedValue[h.domain, map, key]; IF cV.value # NIL THEN { val ¬ cV.value; CacheValue[cV]; RETURN; }; { ENABLE SunRPC.Error => { errorCode ¬ MapErrorCode[code, h]; IF FixupForDownServer[h] THEN RETRY ELSE CONTINUE; }; response ¬ SunYPClient.Match[h.rpcHandle, h.conversation, [domain~h.domain, map~map, key~Rope.ToRefText[key]]]; errorCode ¬ MapErrorStatus[response.status, h]; }; IF errorCode = NIL THEN { val ¬ response.val; CacheValue[cV, val]; RETURN; } ELSE { ReleaseCacheEntry[cV]; ERROR Error[errorCode]; }; }; First: PUBLIC PROC [h: Handle, map: ROPE] RETURNS [key: ROPE, val: REF TEXT] ~ { response: SunYP.ResponseKeyVal; errorCode: ATOM ¬ NIL; { ENABLE SunRPC.Error => { errorCode ¬ MapErrorCode[code, h]; IF FixupForDownServer[h] THEN RETRY ELSE CONTINUE; }; response ¬ SunYPClient.First[h.rpcHandle, h.conversation, [domain~h.domain, map~map]]; errorCode ¬ MapErrorStatus[response.status, h]; }; IF errorCode # NIL THEN ERROR Error[errorCode]; RETURN [Rope.FromRefText[response.key], response.val]; }; Next: PUBLIC PROC [h: Handle, map: ROPE, keyBefore: ROPE] RETURNS [key: ROPE, val: REF TEXT] ~ { response: SunYP.ResponseKeyVal; errorCode: ATOM ¬ NIL; { ENABLE SunRPC.Error => { errorCode ¬ MapErrorCode[code, h]; IF FixupForDownServer[h] THEN RETRY ELSE CONTINUE; }; response ¬ SunYPClient.Next[h.rpcHandle, h.conversation, [domain~h.domain, map~map, key~Rope.ToRefText[keyBefore]]]; errorCode ¬ MapErrorStatus[response.status, h]; }; IF errorCode # NIL THEN ERROR Error[errorCode]; RETURN [Rope.FromRefText[response.key], response.val]; }; MapList: PUBLIC PROC [h: Handle, eachMap: SunYP.EachMapNameProc] ~ { status: SunYP.Status; errorCode: ATOM ¬ NIL; gotOne: BOOL ¬ FALSE; eachMapOuter: SunYP.EachMapNameProc -- [map] -- ~ { gotOne ¬ TRUE; RETURN [eachMap[map]]; }; { ENABLE SunRPC.Error => { errorCode ¬ MapErrorCode[code, h]; IF (NOT gotOne) AND FixupForDownServer[h] THEN RETRY ELSE CONTINUE; }; status ¬ SunYPClient.Maplist[h.rpcHandle, h.conversation, h.domain, eachMapOuter]; errorCode ¬ MapErrorStatus[status, h]; }; IF errorCode # NIL THEN ERROR Error[errorCode]; }; <> ExpandTextSeq: PROC [s: SunYPAgent.TextSeq] RETURNS [new: SunYPAgent.TextSeq] ~ { new ¬ NEW[SunYPAgent.TextSeqObject[2*s.maxLength]]; new.length ¬ s.length; FOR i: CARDINAL IN [0 .. s.length) DO new.refText[i] ¬ s.refText[i]; ENDLOOP; }; IsWhiteSpace: PROC [c: CHAR] RETURNS [isWhite: BOOL] ~ { OPEN Ascii; isWhite ¬ (SELECT c FROM TAB, LF, FF, CR, SP => TRUE, ENDCASE => FALSE); }; TokenizeUsingSeparator: PUBLIC PROC [in: REF READONLY TEXT, sepChar: CHAR, trimLeadingSpace, trimTrailingSpace: BOOL] RETURNS [out: SunYPAgent.TextSeq] ~ { i, iTo, len: CARDINAL; scratch: REF TEXT ¬ RefText.ObtainScratch[in.length]; CountSeparators: PROC [txt: REF READONLY TEXT, sepChar: CHAR] RETURNS [ct: CARDINAL ¬ 0] = { FOR k: CARDINAL IN [0..txt.length) DO IF txt[k] = sepChar THEN ct ¬ ct + 1; ENDLOOP; }; len ¬ CountSeparators[in, sepChar] + 1; out ¬ NEW[SunYPAgent.TextSeqObject[len]]; out.length ¬ 0; i ¬ 0; DO IF i >= in.length THEN EXIT; IF trimLeadingSpace AND IsWhiteSpace[in[i]] THEN { i ¬ i + 1; LOOP }; iTo ¬ 0; DO IF (i >= in.length) OR (in[i] = sepChar) THEN EXIT; scratch[iTo] ¬ in[i]; i ¬ i + 1; iTo ¬ iTo + 1; ENDLOOP; IF trimTrailingSpace THEN WHILE (iTo > 0) AND IsWhiteSpace[scratch[iTo-1]] DO iTo ¬ iTo - 1 ENDLOOP; scratch.length ¬ iTo; IF out.length >= out.maxLength THEN out ¬ ExpandTextSeq[out]; out.refText[out.length] ¬ RefText.Append[to~RefText.New[scratch.length], from~scratch]; out.length ¬ out.length + 1; i ¬ i + 1; ENDLOOP; <> scratch.length ¬ 0; FOR k: CARDINAL DECREASING IN [0..len) DO IF out.refText[k] = NIL THEN { out.refText[k] ¬ RefText.Append[to~RefText.New[scratch.length], from~scratch]; } ELSE EXIT; ENDLOOP; RefText.ReleaseScratch[scratch]; out.length ¬ len; }; Tokenize: PUBLIC PROC [in: REF READONLY TEXT] RETURNS [out: SunYPAgent.TextSeq] ~ { i, iTo: CARDINAL; scratch: REF TEXT ¬ RefText.ObtainScratch[in.length]; out ¬ NEW[SunYPAgent.TextSeqObject[8]]; out.length ¬ 0; i ¬ 0; DO IF i >= in.length THEN EXIT; iTo ¬ 0; DO IF i >= in.length THEN EXIT; IF IsWhiteSpace[in[i]] THEN EXIT; scratch[iTo] ¬ in[i]; i ¬ i + 1; iTo ¬ iTo + 1; ENDLOOP; scratch.length ¬ iTo; IF out.length >= out.maxLength THEN out ¬ ExpandTextSeq[out]; out.refText[out.length] ¬ RefText.Append[to~RefText.New[scratch.length], from~scratch]; out.length ¬ out.length + 1; i ¬ i + 1; WHILE (i < in.length) AND (IsWhiteSpace[in[i]]) DO i ¬ i + 1 ENDLOOP; ENDLOOP; RefText.ReleaseScratch[scratch]; }; <> lastSweepTime: BasicTime.GMT ¬ BasicTime.earliestGMT; Daemon: PROC ~ { DO thisSweepTime: BasicTime.GMT; secondsSinceLastSweep: INT; secs: CARD; -- signed/unsigned ambiguity ... bletch! thisSweepTime ¬ BasicTime.Now[ ! BasicTime.TimeNotKnown => { thisSweepTime ¬ BasicTime.Update[lastSweepTime, (msecBetweenSweeps+999)/1000]; CONTINUE; } ]; secondsSinceLastSweep ¬ BasicTime.Period[from~lastSweepTime, to~thisSweepTime]; IF secondsSinceLastSweep < 0 THEN ERROR; secs ¬ secondsSinceLastSweep; SweepCachedValues[secs]; SweepServers[secs]; SweepNullServers[secs]; Process.PauseMsec[msecBetweenSweeps]; ENDLOOP; }; <> <> <> <> <<};>> <<>> <> IPNameProc: PROC RETURNS [name: ROPE] = { ENABLE Error => GOTO NoYP; h: Handle ¬ NIL; addressRope, unbracketedAddressRope: ROPE; hostsEntry: REF TEXT; seq: SunYPAgent.TextSeq; h ¬ ObtainHandle[NIL, NIL, Arpa.nullAddress]; addressRope ¬ Convert.RopeFromArpaAddress[Arpa.MyAddress[]]; unbracketedAddressRope ¬ Rope.Substr[addressRope, 1, Rope.Length[addressRope]-2]; hostsEntry ¬ Match[h, hostsMapName, unbracketedAddressRope]; seq ¬ Tokenize[hostsEntry]; IF seq.length <= hostNamePos THEN GOTO NoYP; name ¬ Rope.FromRefText[seq.refText[hostNamePos]]; IF h # NIL THEN ReleaseHandle[h]; EXITS NoYP => NULL; }; << Proc will be passed to ThisMachineRegistry to register with the ThisMachine interface. >> IPAddressProc: PROC RETURNS [ROPE] = { RETURN[Convert.RopeFromArpaAddress[Arpa.MyAddress[]]]; }; Init: PROC = { thisMachineProcs: ThisMachineRegistry.ThisMachineRef ¬ NEW[ThisMachineRegistry.ThisMachineProcsRec ¬ [ which: $ip, Name: IPNameProc, Address: IPAddressProc, ProcessorID: NIL ]]; IF Basics.IsBound[LOOPHOLE[ThisMachineRegistry.RegisterThisMachineProcs]] THEN ThisMachineRegistry.RegisterThisMachineProcs[thisMachineProcs]; }; <> TRUSTED { Process.Detach[ FORK Daemon[] ] }; Init[]; <> }...