<> <> <> DIRECTORY Atom USING [ GetPName, GetProp, PutProp ], Commander USING [ CommandProc, Register ], Convert USING [ RopeFromInt, IntFromRope ], BasicTime USING [ Update, Now, GMT ], GVNames USING [ AddForward, AuthenticateKey, CreateIndividual, Expand, ExpandInfo, GetConnect, Outcome, RemoveForward, RListHandle, SetConnect ], IO, Log USING [ CLog, CloseCLog, MakeCLog, RedoCLog, Report, WriteCLog ], MBQueue USING [ Action, Create, DequeueAction, Flush, Queue, QueueClientAction ], MBQueueImpl USING [ QueueObj ], -- I HATE OPAQUE TYPES! Names, NamesGV, Process USING [ Detach --, Pause, SecondsToTicks-- ], RefTab USING [ Create, EachPairAction, Fetch, GetSize, Pairs, Ref, Store ], SymTab USING [ Create, EachPairAction, Fetch, Pairs, Ref, Store ], Rope USING [ Cat, Concat, Find, Index, Length, ROPE, Substr ], RPC USING [ EncryptionKey ] ; NamesGVImpl: CEDAR MONITOR IMPORTS Atom, BasicTime, Commander, Convert, GVNames, IO, Log, MBQueue, MBQueueImpl, Names, Process, RefTab, SymTab, Rope EXPORTS NamesGV, MBQueue SHARES MBQueueImpl = { OPEN IO; <> <<>> Queue: TYPE = REF QueueObj; QueueObj: PUBLIC TYPE = MBQueueImpl.QueueObj; ROPE: TYPE= Names.ROPE; larkRegistry: ROPE=".Lark"; Results: TYPE = {ok, notFound, error}; ModeNotFound: TYPE = { ok, create, error }; -- create not available CacheBehavior: TYPE = { lookupAfter, lookupFirst, lookInCacheOnly }; Authenticity: TYPE = NamesGV.Authenticity; -- { unknown, authentic, bogus }; AttributeSeq: TYPE = NamesGV.AttributeSeq; AttributeSeqRec: TYPE = NamesGV.AttributeSeqRec; GVDetails: TYPE=REF GVDetailsR; GVDetailsR: TYPE=RECORD [ rName: ROPE, attributes: DetailsAttributes_NIL, authenticity: Authenticity_unknown, key: RPC.EncryptionKey_NULL, valid: BOOL_FALSE, canCreate: BOOL_FALSE, -- GetDetails can make one if it isn't there. mustAuthenticate: BOOL_FALSE, lastTimeValid: BasicTime.GMT, recording: BOOL_FALSE, -- Attribute update is illegal while this is TRUE dirty: BOOL_FALSE, -- Some attribute has been changed by the client done: CONDITION ]; DetailsAttribute: TYPE = REF DetailsAttributeR; DetailsAttributeR: TYPE = RECORD [ attributeValue: ROPE, -- NIL only when this attribute is being deleted. formerValue: ROPE_NIL -- Non-NIL only when this attribute has changed. ]; DetailsCache: TYPE = SymTab.Ref; DetailsAttributes: TYPE = RefTab.Ref; gvCacheLog: Log.CLog _ NIL; cacheMod: CARDINAL _ 37; squawking: BOOL _ FALSE; queuesEmpty: BOOL_TRUE; queuesEmptyCond: CONDITION; gvCacheVersion: ATOM _ $V1; cache: DetailsCache_NIL; cacheQueue: Queue; refreshCacheQueue: Queue; assumedValidInterval: INT _ 60; -- GV updates done elsewhere may take a minute, and two calls, to be noticed. assumedValidIntervalAfterChange: INT _ 600; -- GV updates done elsewhere may take a minute, and two calls, to be noticed. GV updates done elsewhere after a change here may take up to ten minutes to get noticed unless you explicitly flush the local cache. realOldTime: BasicTime.GMT _ ValidUntil[-1]; <> GVGetAttribute: PUBLIC ENTRY PROC[rName: ROPE, attribute: ATOM, default: ROPE_NIL] RETURNS [value: ROPE] = { details: GVDetails _ GetGVDetails[rName].details; detailsAttribute: DetailsAttribute; IF ~details.valid OR details.attributes=NIL THEN RETURN[default]; detailsAttribute _ NARROW[details.attributes.Fetch[key: attribute].val]; IF detailsAttribute=NIL THEN RETURN[default]; RETURN[detailsAttribute.attributeValue]; }; GVSetAttribute: PUBLIC ENTRY PROC[rName: ROPE, attribute: ATOM, value: ROPE] = { details: GVDetails _ GetGVDetails[rName: rName, mode: create].details; detailsAttribute: DetailsAttribute; IF ~details.valid THEN RETURN; IF (~details.dirty) OR details.recording THEN UNTIL queuesEmpty DO WAIT queuesEmptyCond; ENDLOOP; -- GVWait IF details.recording THEN ERROR; -- Sanity check details.dirty _ TRUE; IF details.attributes # NIL THEN detailsAttribute _ NARROW[details.attributes.Fetch[key: attribute].val]; IF detailsAttribute = NIL THEN StoreAttribute[details, attribute, value, NIL] ELSE detailsAttribute.attributeValue _ value; }; GVIsAuthenticated: PUBLIC ENTRY PROC[rName: ROPE] RETURNS [authenticity: Authenticity] = { details: GVDetails _ GetGVDetails[rName].details; RETURN[details.authenticity]; }; GVAuthenticate: PUBLIC ENTRY PROC[rName: ROPE, key: RPC.EncryptionKey] RETURNS [authenticity: Authenticity] = { details: GVDetails _ GetGVDetails[rName: rName, mode: error, key: key, authenticate: TRUE].details; RETURN[details.authenticity]; }; GVGetAttributeSeq: PUBLIC PROC [rName: ROPE, attribute: ATOM] RETURNS [value: AttributeSeq] = { attributeValue: ROPE _ GVGetAttribute[rName, attribute]; index: INT_0; IF attributeValue = NIL THEN RETURN[NIL]; value _ NEW[AttributeSeqRec[10]]; WHILE index < attributeValue.Length[] DO newIndex: INT _ attributeValue.Index[pos1: index, s2: ", "]; value[value.length].attributeValue _ attributeValue.Substr[index, newIndex-index]; value.length _ value.length+1; index _ newIndex+2; ENDLOOP; }; <<>> GVSetAttributeSeq: PUBLIC PROC[rName: ROPE, attribute: ATOM, value: AttributeSeq] = { attVal: ROPE_NIL; FOR i: INT IN [0..value.length) DO attVal _ Rope.Cat[attVal, value[i].attributeValue, ", "]; ENDLOOP; IF attVal#NIL THEN attVal _ attVal.Substr[len: attVal.Length[]-2]; GVSetAttribute[rName, attribute, attVal]; }; GVGetAttributes: PUBLIC ENTRY PROC[rName: ROPE] RETURNS [value: AttributeSeq_NIL] = { details: GVDetails _ GetGVDetails[rName: rName, mode: create].details; len: CARDINAL; i: CARDINAL_0; GetEachAttribute: RefTab.EachPairAction = { quit_FALSE; value[i] _ [NARROW[key], NARROW[val, DetailsAttribute].attributeValue]; i_i+1; }; IF ~details.valid OR details.attributes=NIL THEN RETURN; len _ details.attributes.GetSize[]; value _ NEW[AttributeSeqRec[len]]; value.length _ len; []_details.attributes.Pairs[GetEachAttribute]; }; <<>> GVWait: PUBLIC ENTRY PROC = { UNTIL queuesEmpty DO WAIT queuesEmptyCond; ENDLOOP; }; GVUpdate: PUBLIC ENTRY PROC[rName: ROPE] = { SetGVDetails[GetGVDetails[rName, error].details]; }; GVUpdateAll: PUBLIC ENTRY PROC = { UpdateEachRName: INTERNAL SymTab.EachPairAction = { details: GVDetails _ NARROW[val]; IF details.dirty AND ~details.recording THEN QueueAction[refreshCacheQueue, SetNewDetails, details]; quit_FALSE; }; []_SymTab.Pairs[cache, UpdateEachRName]; }; GVFlushCache: PUBLIC ENTRY PROC = { GetGVCache[TRUE]; -- Old one fades into oblivion }; GVSaveCache: PUBLIC ENTRY PROC = { <> ros: IO.STREAM; SaveEachRName: SymTab.EachPairAction = { details: GVDetails _ NARROW[val]; SaveEachAttribute: RefTab.EachPairAction = { attributeName: ATOM _ NARROW[key]; detailsAttribute: DetailsAttribute _ NARROW[val]; quit_FALSE; ros.PutF["%s: %s\n", rope[Atom.GetPName[attributeName]], rope[detailsAttribute.attributeValue]]; }; quit_FALSE; ros _ IO.ROS[ros]; IF ~details.valid OR details.authenticity=bogus THEN RETURN; ros.PutF["rname: %s\n", rope[details.rName]]; IF details.authenticity = authentic THEN TRUSTED { key: RPC.EncryptionKey _ details.key; cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL = LOOPHOLE[LONG[@key]]; ros.PutF["key: %bB %bB\n", card[cardKey[0]], card[cardKey[1]]]; }; IF details.attributes#NIL THEN []_details.attributes.Pairs[SaveEachAttribute]; ros.PutChar['\n]; Log.WriteCLog[gvCacheLog, IO.RopeFromROS[ros, FALSE]]; }; GetGVCache[FALSE]; gvCacheLog _ Log.MakeCLog["GVCacheLog.txt", RestoreEntry, TRUE, 3]; IF gvCacheLog=NIL THEN { Log.Report["GV:** Couldn't create GVCacheLog.txt", $System]; RETURN; }; ros _ IO.ROS[]; []_cache.Pairs[SaveEachRName]; gvCacheLog_Log.CloseCLog[gvCacheLog]; ros.Close[]; }; GVRestoreCache: PUBLIC ENTRY PROC = { GetGVCache[FALSE]; gvCacheLog _ Log.MakeCLog["GVCacheLog.txt", RestoreEntry, FALSE, 3]; IF gvCacheLog=NIL THEN { Log.Report["GV:** Couldn't find GVCacheLog.txt", $System]; RETURN; }; GetGVCache[TRUE]; -- Old one fades into oblivion Log.RedoCLog[gvCacheLog]; gvCacheLog_Log.CloseCLog[gvCacheLog]; }; <<>> <> GetGVDetails: INTERNAL PROC[rName: ROPE, mode: ModeNotFound_ok, behavior: CacheBehavior_lookupAfter, authenticate: BOOL_FALSE, key: RPC.EncryptionKey_NULL] RETURNS [results: Results_ok, details: GVDetails_NIL] = TRUSTED { GetGVCache[FALSE]; details _ NARROW[cache.Fetch[rName].val]; IF details=NIL AND behavior#lookInCacheOnly THEN []_cache.Store[rName, details _ NEW[GVDetailsR_[rName: rName, lastTimeValid: realOldTime]]]; details.mustAuthenticate _ details.mustAuthenticate OR authenticate; IF authenticate THEN details.key _ key; details.canCreate _ ~details.valid AND mode=create; SELECT behavior FROM lookupFirst => { QueueAction[cacheQueue, LookupDetails, details]; WAIT details.done; }; lookupAfter => IF details.mustAuthenticate AND details.authenticity=unknown OR Names.GMTComp[BasicTime.Now[], details.lastTimeValid]=greater THEN { dontKnow: BOOL = ~details.valid OR (details.authenticity=unknown AND details.mustAuthenticate); <> details.lastTimeValid _ ValidUntil[assumedValidInterval]; <> QueueAction[ (IF dontKnow THEN cacheQueue ELSE refreshCacheQueue), LookupDetails, details]; IF dontKnow THEN WAIT details.done; }; ENDCASE; details.canCreate _ FALSE; IF ~details.valid THEN RETURN[ IF mode=ok THEN notFound ELSE Report[notFound, notFound, details], details]; IF squawking THEN Log.Report[IO.PutFR["GV: Found %g", rope[rName]], $System]; }; SetGVDetails: INTERNAL PROC[details: GVDetails] = { GetGVCache[FALSE]; IF details=NIL OR details.dirty = FALSE THEN RETURN; details.lastTimeValid _ ValidUntil[assumedValidIntervalAfterChange]; details.recording _ TRUE; QueueAction[refreshCacheQueue, SetNewDetails, details]; IF squawking THEN Log.Report[IO.PutFR["GV: Modified %g", rope[details.rName]], $System]; }; RestoreEntry: SAFE PROC[cLog: Log.CLog] = { -- DoCLog command procedure logStream: IO.STREAM = cLog.logStream; details: GVDetails; DO line: ROPE_ReadLine[logStream]; key: ATOM; val: ROPE; IF line=NIL OR line.Length[]=0 THEN RETURN; [key, val] _ ParseAttribute[line]; SELECT key FROM $rname => { IF details#NIL THEN ERROR; details _ NEW[GVDetailsR_[ rName: val, lastTimeValid: realOldTime, valid: TRUE, authenticity: perhaps]]; []_cache.Store[key: val, val: details]; }; $key => TRUSTED { keyStream: IO.STREAM _ IO.RIS[val]; key: RPC.EncryptionKey; cardKey:LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL = LOOPHOLE[LONG[@key]]; cardKey[0] _ IO.GetCard[keyStream]; cardKey[1] _ IO.GetCard[keyStream]; IF details=NIL THEN ERROR; details.key _ key; IO.Close[keyStream]; details.mustAuthenticate _ TRUE; details.authenticity _ authentic; }; NIL => ERROR; -- Didn't find a key! ENDCASE => { IF details=NIL THEN ERROR; StoreAttribute[details, key, val, val]; }; ENDLOOP; }; <> <<>> LookupDetails: SAFE PROC[reallyDetails: REF ANY] = TRUSTED { details: GVDetails _ NARROW[reallyDetails]; authenticity: Authenticity_details.authenticity; rName: ROPE = details.rName; connect: ROPE; expandInfo: GVNames.ExpandInfo _ [notFound []]; info, aInfo: GVNames.Outcome_notFound; entries: GVNames.RListHandle_NIL; valid: BOOL_FALSE; RecordDetails: ENTRY PROC = CHECKED INLINE { details.rName _ rName; details.valid _ valid; details.lastTimeValid _ ValidUntil[ IF details.canCreate AND details.valid THEN assumedValidIntervalAfterChange ELSE assumedValidInterval]; details.authenticity _ authenticity; details.canCreate _ FALSE; ParseDetails[details, entries]; NOTIFY details.done; }; IF details.dirty THEN { IF squawking THEN Log.Report[IO.PutFR["GV:---Not seeking %g (dirty)", rope[rName]], $System]; RETURN; }; IF squawking THEN Log.Report[IO.PutFR["GV:---Seeking %g", rope[rName]], $System]; [info, connect] _ GVNames.GetConnect[rName]; valid _ info=individual; IF ~valid THEN IF info=notFound THEN info _ DoCreate[details, "MFLFLX"] ELSE []_Report[info, error, details] ELSE { expandInfo _ GVNames.Expand[rName]; WITH expandInfo SELECT FROM group => entries_members; noChange, individual => NULL; ENDCASE => []_Report[type, error, details]; }; IF connect#NIL THEN { connect _ Rope.Concat["connect: ", connect]; IF entries=NIL THEN entries _ LIST[ connect ] ELSE entries _ CONS[connect, entries]; }; IF valid AND (SELECT details.authenticity FROM bogus, authentic, nonexistent => FALSE, unknown, perhaps =>TRUE, ENDCASE=>ERROR) THEN IF details.mustAuthenticate THEN { IF squawking THEN Log.Report[IO.PutFR["GV:---Authenticating %g", rope[rName]], $System]; SELECT (aInfo_GVNames.AuthenticateKey[rName, details.key]) FROM badPwd => authenticity_bogus; individual => authenticity_authentic; ENDCASE => []_Report[aInfo, error, details]; } ELSE authenticity _ perhaps; IF info=notFound THEN authenticity _ nonexistent; RecordDetails[]; IF squawking THEN Log.Report[IO.PutFR["GV:---End %g", rope[rName]], $System]; }; SetNewDetails: SAFE PROC[reallyDetails: REF ANY] = TRUSTED { details: GVDetails _ NARROW[reallyDetails]; outcome: GVNames.Outcome; somethingStillDirty: BOOL_FALSE; EachAttribute: RefTab.EachPairAction = TRUSTED { attribute: DetailsAttribute _ NARROW[val]; attributeName: ATOM _ NARROW[key]; quit_FALSE; IF attribute.attributeValue=attribute.formerValue THEN RETURN; <> IF attributeName=$connect THEN { outcome _ GVNames.SetConnect[ user: Names.CurrentRName[], password: Names.CurrentPasskey[], individual: details.rName, connect: attribute.attributeValue]; SELECT outcome FROM noChange, individual => attribute.formerValue _ attribute.attributeValue; ENDCASE=> { []_Report[outcome, error, details, TRUE]; somethingStillDirty _ TRUE; RETURN; }; }; IF attribute.formerValue # NIL THEN { oldFwd: ROPE_ Atom.GetPName[attributeName].Cat[": ", attribute.formerValue]; outcome _ GVNames.RemoveForward[ user: Names.CurrentRName[], password: Names.CurrentPasskey[], individual: details.rName, dest: oldFwd]; SELECT outcome FROM noChange, individual => attribute.formerValue _ NIL; ENDCASE=> { []_Report[outcome, error, details, TRUE]; somethingStillDirty _ TRUE; RETURN; }; }; IF attribute.attributeValue # NIL THEN { newFwd: ROPE_ Atom.GetPName[NARROW[key]].Cat[": ", attribute.attributeValue]; outcome _ GVNames.AddForward[ user: Names.CurrentRName[], password: Names.CurrentPasskey[], individual: details.rName, dest: newFwd]; SELECT outcome FROM noChange, individual => attribute.formerValue _ attribute.attributeValue; ENDCASE=> { []_Report[outcome, error, details, TRUE]; somethingStillDirty _ TRUE; RETURN; }; }; }; { ENABLE UNWIND => details.recording _ FALSE; IF squawking THEN Log.Report[IO.PutFR["GV:---Change attributes for %g", rope[details.rName]], $System]; IF details.attributes # NIL THEN []_RefTab.Pairs[details.attributes, EachAttribute]; details.recording _ FALSE; details.dirty _ somethingStillDirty; IF squawking THEN Log.Report["GV:---End update", $System]; }; }; DoCreate: PROC[details: GVDetails, password: ROPE] RETURNS [info: GVNames.Outcome] = TRUSTED { IF ~details.canCreate THEN RETURN[notFound]; IF squawking THEN Log.Report[IO.PutFR["GV:---Creating %g", rope[details.rName]], $System]; info _ GVNames.CreateIndividual[ user: Names.CurrentRName[], password: Names.CurrentPasskey[], individual: details.rName, newPwd: Names.CurrentPasskey[password]]; SELECT info FROM individual => NULL; ENDCASE => []_Report[info, error]; }; ParseDetails: PROC[details: GVDetails, entries: GVNames.RListHandle] = { attributeTable: RefTab.Ref_NIL; details.attributes_NIL; IF ~details.valid THEN RETURN; FOR e: GVNames.RListHandle _ entries, e.rest WHILE e#NIL DO key: ATOM; val: ROPE; [key, val] _ ParseAttribute[e.first]; IF key=NIL THEN LOOP; -- Not the right syntax for an entry StoreAttribute[details, key, val, val]; ENDLOOP; }; ParseAttribute: PROC[attributeSpec: ROPE] RETURNS [key: ATOM_NIL, val: ROPE_NIL] = { index: INT _ attributeSpec.Find[": "]; IF index<0 THEN RETURN; -- Not the right syntax for an entry key _ Names.MakeAtom[attributeSpec.Substr[start: 0, len: index], FALSE]; val _ attributeSpec.Substr[start: index+2]; }; StoreAttribute: PROC[details: GVDetails, key: ATOM, val: ROPE, oldVal: ROPE] = { detailsAttribute: DetailsAttribute _ NEW[DetailsAttributeR _ [val, oldVal]]; IF details.attributes = NIL THEN details.attributes _ RefTab.Create[]; [] _ details.attributes.Store[key: key, val: detailsAttribute]; }; <> QueueAction: INTERNAL PROC [q: MBQueue.Queue, proc: PROC [REF ANY], data: REF ANY] = { queuesEmpty_FALSE; MBQueue.QueueClientAction[q, proc, data]; IF q=cacheQueue THEN MBQueue.QueueClientAction[refreshCacheQueue, Arise, NIL]; }; Arise: SAFE PROC [whatever: REF] = { NULL }; MBQueueEmpty: ENTRY PROC[q: Queue, notifyIfEmpty: BOOL_FALSE] RETURNS [empty: BOOL] = { empty _ q.firstEvent=NIL; IF ~empty OR ~notifyIfEmpty THEN RETURN; queuesEmpty_TRUE; NOTIFY queuesEmptyCond; }; Action: TYPE = MBQueue.Action; Notifier: PROC [] = { <> <> <> DO ENABLE ABORTED => { MBQueue.Flush[cacheQueue]; MBQueue.Flush[refreshCacheQueue]; LOOP; }; UNTIL MBQueueEmpty[cacheQueue] DO WITH MBQueue.DequeueAction[cacheQueue] SELECT FROM e2: Action.client => { queuesEmpty_FALSE; e2.proc[e2.data]; }; ENDCASE => ERROR; ENDLOOP; [] _ MBQueueEmpty[refreshCacheQueue, TRUE]; -- if TRUE, about to wait WITH MBQueue.DequeueAction[refreshCacheQueue] SELECT FROM e2: Action.client => { queuesEmpty_FALSE; e2.proc[e2.data]; }; ENDCASE => ERROR; ENDLOOP; }; <<>> SemiTok: IO.BreakProc = TRUSTED {RETURN[IF char='; THEN break ELSE other]; }; CommaTok: IO.BreakProc = TRUSTED {RETURN[IF char=', THEN break ELSE other]; }; Report: PROC[outcome: GVNames.Outcome, r: Results, details: GVDetails_NIL, timeout: BOOL_FALSE] <> RETURNS[rr: Results] = { rName: ROPE_NIL; rr_r; IF details#NIL THEN { details.valid_FALSE; rName_details.rName; IF timeout THEN details.lastTimeValid _ realOldTime; -- next query will go to GV fer sherr }; Log.Report[IO.PutFR["GV: **%s %s\n", rope[rName], rope[SELECT outcome FROM noChange => "no change", group => "group", individual => "individual", notFound => "not found", protocolError => "protocol error", wrongServer => "wrong server", allDown => "all servers down", badPwd => "bad password", outOfDate => "out of date", notAllowed => "not allowed", ENDCASE => "??"]], $System]; }; <<>> ValidUntil: PROC[interval: INT] RETURNS [BasicTime.GMT] = { RETURN[BasicTime.Update[BasicTime.Now[], interval]]; }; <<>> GetGVCache: INTERNAL PROC[new: BOOL_FALSE] = { <> <> <> cRef: REF DetailsCache _ NIL; cache_NIL; DO ref: REF _ Atom.GetProp[$GVCache, gvCacheVersion]; IF ref=NIL THEN EXIT; WITH ref SELECT FROM cRef1: REF DetailsCache => { cRef_cRef1; EXIT; }; ENDCASE; gvCacheVersion _ Names.MakeAtom[ Rope.Cat["V", Convert.RopeFromInt[ Convert.IntFromRope[ Rope.Substr[ Atom.GetPName[gvCacheVersion], 1] ]+1 ] ] ]; ENDLOOP; IF ~new AND cRef#NIL THEN { cache _ cRef^; RETURN}; cache _ SymTab.Create[cacheMod, FALSE]; Atom.PutProp[$GVCache, gvCacheVersion, NEW[DetailsCache _ cache]]; }; <> CmdSaveGVCache: Commander.CommandProc = { GVSaveCache[]; }; CmdRestoreGVCache: Commander.CommandProc = { GVRestoreCache[]; }; CmdRefreshGVCache: ENTRY Commander.CommandProc = { RefreshEachRName: INTERNAL SymTab.EachPairAction = { details: GVDetails _ NARROW[val]; quit_FALSE; IF ~details.dirty THEN QueueAction[refreshCacheQueue, LookupDetails, details]; }; []_SymTab.Pairs[cache, RefreshEachRName]; }; CmdUpdateGVCache: Commander.CommandProc = { GVUpdateAll[]; }; CmdFlushGVCache: Commander.CommandProc = { GVFlushCache[]; }; CmdWaitForGV: Commander.CommandProc = { GVWait[]; }; CmdGVSquawk: Commander.CommandProc = { squawking _ ~squawking; Log.Report[IO.PutFR["Squawking[%g]", bool[squawking]], $System]; }; ReadLine: PROC[s: IO.STREAM] RETURNS [ line: ROPE] = { RETURN[s.GetLineRope[]]; }; <> <<>> cacheQueue _ MBQueue.Create[pushModel: FALSE]; refreshCacheQueue _ MBQueue.Create[pushModel: FALSE]; TRUSTED { Process.Detach[FORK Notifier[]]; }; Commander.Register["GVFlushCache", CmdFlushGVCache, "Flush GV Cache"]; Commander.Register["GVSaveCache", CmdSaveGVCache, "Save GV Cache"]; Commander.Register["GVRestoreCache", CmdRestoreGVCache, "Restore GV Cache"]; Commander.Register["GVRefreshCache", CmdRefreshGVCache, "Refresh all GV Cache entries from GV"]; Commander.Register["GVUpdateCache", CmdUpdateGVCache, "Write all dirty cache entries to GV"]; Commander.Register["GVWait", CmdWaitForGV, "Wait for GV communications to complete"]; Commander.Register["GVSquawk", CmdGVSquawk, "Inform user of GV activity"]; }.