<<>> <> <> <> <> DIRECTORY Basics USING [FWORD, HWORD, Int16FromH, Int32FromF], BasicTime USING [GMT, Now, nullGMT], CHNameP2V0 USING [], Commander USING [CommandProc, Register], CrRPC USING [BulkDataSink, CreateClientHandle, DestroyClientHandle, Error, ErrorReason, Handle], FilingP10V5, IO, PFS, PFSBackdoor USING [ErrorCode, ProduceError], PFSClass, PFSNames, RefText, Rope, TrickleChargeP9813V411 USING [AnyBodyHome, EnumerateForInfo, Error, FileInfo, FileNotFound, Retrieve], XNS USING [Address, unknownAddress], XNSCH USING [LookupAddressFromRope], XNSCHName USING [Name, RopeFromName], XNSRemoteFileTypes, XNSStream USING [ConnectionClosed, Timeout], XNSWKS USING [courier]; TrickleChargeRemoteFileImpl: CEDAR PROGRAM IMPORTS Basics, BasicTime, CrRPC, Commander, IO, PFS, PFSBackdoor, PFSClass, PFSNames, RefText, Rope, TrickleChargeP9813V411, XNSCH, XNSCHName, XNSStream ~ BEGIN OPEN XNSRemoteFileTypes; <> GMT: TYPE = BasicTime.GMT; ROPE: TYPE = Rope.ROPE; STREAM: TYPE = IO.STREAM; PATH: TYPE = PFSNames.PATH; Version: TYPE = PFSNames.Version; FSHandle: TYPE = PFSClass.FSHandle; <> myFlavor: ATOM ¬ $TC; initialConnectionTTL: CARD ¬ 8; credentialsErrorTTL: CARD ¬ 30; <> xnsFlavor: ATOM ~ $TC; <> mo: PFSClass.MaintenanceProcs ~ NEW [PFSClass.MaintenanceProcsObject ¬ [ sweep: TCSweep, validate: TCValidate ]]; <> fmo: PFSClass.FileManipulationProcs ~ NEW[PFSClass.FileManipulationProcsObject ¬ [ delete: TCDelete, enumerateForInfo: TCEnumerateForInfo, enumerateForNames: TCEnumerateForNames, fileInfo: TCFileInfo, lookupName: TCLookup, rename: TCRename, copy: TCCopy, setAttributes: TCSetAttributes, setByteCountAndUniqueID: TCSetByteCountAndUniqueID, setClientProperty: TCSetClientProperty, getClientProperty: TCGetClientProperty, enumerateClientProperties: TCEnumerateClientProperties, read: TCRead, write: TCWrite, open: TCOpen, close: TCClose, store: TCStore, retrieve: TCRetrieve, -- attach: TCAttach, getInfo: TCGetInfo ]]; <> vfsClass: PFSClass.FSHandle ~ NEW [PFSClass.FSObject ¬ [ flavor: xnsFlavor, name: NIL, -- filled in upon instantiation maintenanceProcs: mo, procs: fmo, data: NIL -- filled in upon instantiation ]]; <> TransportProc: TYPE ~ PROC; -- [tr: Transport]; Transport: TYPE ~ REF TransportBody; TransportBody: TYPE ~ MONITORED RECORD [ crH: CrRPC.Handle, vfs: VFS ]; <> VFS: TYPE ~ REF VFSInstance; VFSInstance: TYPE ~ RECORD [ fs: ROPE, flavorSpecified: BOOL, sD: ServerData ]; <> ServerData: TYPE ~ REF ServerDataObject; ServerDataObject: TYPE ~ RECORD [ qualified: XNSCHName.Name, serverName: ROPE, address: REF XNS.Address, opened: BOOL, version: INT16 ]; dcedarVersion: INT16 ~ 311; -- magic <> nullGMT: BasicTime.GMT ~ BasicTime.nullGMT; <> startTime: BasicTime.GMT ¬ BasicTime.Now[]; startEnum: BasicTime.GMT ¬ BasicTime.nullGMT; endEnum: BasicTime.GMT ¬ BasicTime.nullGMT; startFileInfo: BasicTime.GMT ¬ BasicTime.nullGMT; endFileInfo: BasicTime.GMT ¬ BasicTime.nullGMT; startRetrieve: BasicTime.GMT ¬ BasicTime.nullGMT; endRetrieve: BasicTime.GMT ¬ BasicTime.nullGMT; <> TCDelete: PFSClass.DeleteProc = { PFSBackdoor.ProduceError[notImplemented, "Delete not allowed for TC servers"]; }; TCCopy: PFSClass.CopyProc = { PFSBackdoor.ProduceError[notImplemented, "Copy not allowed for TC servers"]; }; TCEnumerateForInfo: PFSClass.EnumerateForInfoProc ~ { <<[h: FSHandle, pattern: PATH, proc: PFS.InfoProc, lbound: PATH, hbound: PATH]>> <> P: PROC [file: PATH, bytes: INT, created: GMT, version: Version] RETURNS [continue: BOOL] = { continue ¬ proc[fullFName: file, attachedTo: NIL, uniqueID: MakeUID[created], bytes: bytes, mutability: immutable, fileType: PFS.tUnspecified] }; InnerEnumerate[h, pattern, FALSE, P]; }; TCEnumerateForNames: PFSClass.EnumerateForNamesProc ~ { <<[h: FSHandle, pattern: PATH, proc: PFS.NameProc, lbound: PATH, hbound: PATH]>> <> P: PROC [file: PATH, bytes: INT, created: GMT, version: Version] RETURNS [continue: BOOL] = { continue ¬ proc[file] }; InnerEnumerate[h, pattern, TRUE, P]; }; EnumerateProc: TYPE ~ PROC [--file:-- PATH, --bytes:-- INT, --created:-- GMT, --version: -- Version] RETURNS [--continue:-- BOOL]; InnerEnumerate: PROC [h: FSHandle, pattern: PATH, namesOnly: BOOL, proc: EnumerateProc] = { Listing: CrRPC.BulkDataSink = { <<[h: CrRPC.Handle, s: STREAM, checkAbort: CrRPC.BulkDataCheckAbortProc] RETURNS [abort: BOOL]>> name: ROPE; nameLength: INT16; -- if the name of the file is bigger than this we have a real problem. created: INT; -- really BasicTime.GMT LOOPHOLEd size: INT; -- number of bytes in the file version: Version; UNTIL IO.EndOf[self: s] DO continue: BOOL ¬ FALSE; i: INT ¬ 0; Proc: PROC RETURNS [c: CHAR] = { c ¬ IO.GetChar[self: s ! IO.EndOfStream => GOTO Error]; EXITS Error => ERROR PFS.Error[[$environment, $brainDamage, "TC protocol error"]]; }; nameLength ¬ Basics.Int16FromH[IO.GetHWord[self: s ! IO.EndOfStream => EXIT]]; created ¬ Basics.Int32FromF[IO.GetFWord[self: s ! IO.EndOfStream => GOTO Error]]; size ¬ Basics.Int32FromF[IO.GetFWord[self: s ! IO.EndOfStream => GOTO Error]]; name ¬ Rope.FromProc[len: nameLength, p: Proc]; version ¬ VersionFromName[name: name]; continue ¬ proc[--file:-- PFS.PathFromRope[name], --bytes:-- size, --created:-- LOOPHOLE[created], -- version: -- version]; IF NOT continue THEN RETURN[TRUE]; IF checkAbort[h] THEN RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; EXITS Error => ERROR PFS.Error[[$environment, $brainDamage, "TC protocol error"]]; }; class: PFSClass.FSHandle ~ h; vfs: VFS ~ NARROW[class.data]; data: ServerData ~ vfs.sD; <> crH: CrRPC.Handle ¬ GetConnection[data]; patRope: ROPE ¬ PFS.RopeFromPath[pattern]; toUse: ROPE ¬ IF data.version # dcedarVersion THEN patRope ELSE Rope.Cat["/", vfs.fs, patRope]; { -- for ENABLE -- ENABLE UNWIND => { ReturnConnection[crH] }; P: PROC = { TrickleChargeP9813V411.EnumerateForInfo[h: crH, pattern: toUse, info: Listing]; ReturnConnection[crH]; }; startEnum ¬ BasicTime.Now[]; CallProtected[proc: P, h: h, filePath: NIL, uid: PFS.nullUniqueID]; endEnum ¬ BasicTime.Now[]; }; }; TCGetInfo: PFSClass.GetInfoProc ~ { <> vfs: VFS ~ NARROW[h.data]; <> fullFName ¬ file.fullFName; attachedTo ¬ file.attachedTo; uniqueID ¬ file.uniqueID; bytes ¬ file.bytes; mutability ¬ file.mutability; fileType ¬ file.fileType; }; <> <> <> <> <> <<};>> <<>> TCFileInfo: PFSClass.FileInfoProc = { <<[h: FSHandle, file: PATH, wantedUniqueID: UniqueID] RETURNS [version: Version, attachedTo: PATH, bytes: INT, uniqueID: UniqueID, mutability: PFS.Mutability, fileType: PFS.FileType]>> class: PFSClass.FSHandle ~ h; vfs: VFS ~ NARROW[class.data]; data: ServerData ~ vfs.sD; crH: CrRPC.Handle ¬ GetConnection[data]; { -- for ENABLE -- ENABLE UNWIND => ReturnConnection[crH]; P: PROC = { created: GMT; fullFName: ROPE; createInt: INT; fileRope: ROPE ¬ PFS.RopeFromPath[file]; toUse: ROPE ¬ IF data.version # dcedarVersion THEN fileRope ELSE Rope.Cat["/", vfs.fs, fileRope]; [fullFName: fullFName, bytes: bytes, created: createInt] ¬ TrickleChargeP9813V411.FileInfo[h: crH, name: toUse, wantedCreatedTime: LOOPHOLE[wantedUniqueID.egmt.gmt]]; ReturnConnection[crH]; version ¬ VersionFromName[name: fullFName]; created ¬ LOOPHOLE[createInt]; uniqueID ¬ MakeUID[created]; mutability ¬ immutable; fileType ¬ PFS.tUnspecified; attachedTo ¬ NIL; }; startFileInfo ¬ BasicTime.Now[]; CallProtected[proc: P, h: h, filePath: file, uid: wantedUniqueID]; endFileInfo ¬ BasicTime.Now[]; }; }; TCRename: PFSClass.RenameProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "Rename not allowed for TC servers"]; }; TCRetrieve: PFSClass.RetrieveProc ~ { <<[h: FSHandle, file: PATH, wantedUniqueID: UniqueID, proc: PFS.RetrieveConfirmProc, checkFileType: BOOL _ FALSE, fileType: PFS.FileType]>> <> class: PFSClass.FSHandle ~ h; vfs: VFS ~ NARROW[class.data]; data: ServerData ~ vfs.sD; crH: CrRPC.Handle ¬ GetConnection[data]; localStream: STREAM ¬ NIL; Content: CrRPC.BulkDataSink = { <<[h: CrRPC.Handle, s: STREAM, checkAbort: CrRPC.BulkDataCheckAbortProc] RETURNS [abort: BOOL]>> buffer: REF TEXT = RefText.ObtainScratch[512]; nBytes: NAT; abort ¬ FALSE; DO IF s.EndOf[] THEN EXIT; -- seems to end this way sometimes nBytes ¬ s.GetBlock[buffer, 0]; IF nBytes = 0 THEN EXIT; localStream.PutBlock[buffer, 0, nBytes]; ENDLOOP; RefText.ReleaseScratch[buffer]; }; { -- for ENABLE -- ENABLE UNWIND => ReturnConnection[crH]; P: PROC = { version: Version; bytes: INT; created: GMT; fullFName: ROPE; createInt: INT; fileRope: ROPE ¬ PFS.RopeFromPath[file]; toUse: ROPE ¬ IF data.version # dcedarVersion THEN fileRope ELSE Rope.Cat["/", vfs.fs, fileRope]; [fullFName: fullFName, bytes: bytes, created: createInt] ¬ TrickleChargeP9813V411.FileInfo[h: crH, name: toUse, wantedCreatedTime: LOOPHOLE[wantedUniqueID.egmt.gmt]]; version ¬ VersionFromName[name: fullFName]; created ¬ LOOPHOLE[createInt]; localStream ¬ proc[file, bytes, MakeUID[created]]; IF localStream # NIL THEN TrickleChargeP9813V411.Retrieve[h: crH, name: toUse, wantedCreatedTime: LOOPHOLE[wantedUniqueID.egmt.gmt], data: Content]; ReturnConnection[crH]; }; startRetrieve ¬ BasicTime.Now[]; CallProtected[proc: P, h: h, filePath: file, uid: wantedUniqueID]; endRetrieve ¬ BasicTime.Now[]; }; }; TCStore: PFSClass.StoreProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "Store not allowed for TC servers"]; }; TCSetAttributes: PFSClass.SetAttributesProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "SetAttributes not allowed for TC servers"]; }; TCSetByteCountAndUniqueID: PFSClass.SetByteCountAndUniqueIDProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "SetByteCountAndUniqueID not allowed for TC servers"]; }; TCSetClientProperty: PFSClass.SetClientPropertyProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "SetClientProperty not allowed for TC servers"]; }; TCGetClientProperty: PFSClass.GetClientPropertyProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "GetClientProperty not allowed for TC servers"]; }; TCEnumerateClientProperties: PFSClass.EnumerateClientPropertiesProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "EnumerateClientProperties not allowed for TC servers"]; }; TCRead: PFSClass.ReadProc = { bytesRead ¬ 0; PFSBackdoor.ProduceError[code: notImplemented, explanation: "Read not allowed for TC servers"]; }; TCWrite: PFSClass.WriteProc = { bytesWritten ¬ 0; PFSBackdoor.ProduceError[code: notImplemented, explanation: "Write not allowed for TC servers"]; }; TCOpen: PFSClass.OpenProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "Open not allowed for TC servers"]; RETURN[NIL]; }; TCClose: PFSClass.CloseProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "Close not allowed for TC servers"]; }; TCLookup: PFSClass.LookupNameProc = { PFSBackdoor.ProduceError[code: notImplemented, explanation: "LookupName not allowed for TC servers"]; RETURN[file]; }; MakeUID: PROC[gmt: GMT] RETURNS[uid: PFS.UniqueID] = { uid.egmt.gmt ¬ gmt }; <> EnsureOneLeadingSlash: PROC [maybeWithSlash: ROPE] RETURNS [withSlash: ROPE] ~ { <> IF ( maybeWithSlash.Fetch[0] # '/ ) THEN RETURN[Rope.Concat["/", maybeWithSlash] ]; IF ( maybeWithSlash.Fetch[1] # '/ ) THEN RETURN[maybeWithSlash]; RETURN[maybeWithSlash.Substr[1]]; }; RemoveLeadingSlash: PROC [maybeWithSlash: ROPE] RETURNS [noSlash: ROPE] ~ { noSlash ¬ IF ( maybeWithSlash.Fetch[0] = '/ ) THEN maybeWithSlash.Substr[1] ELSE maybeWithSlash; }; ConvertFSNameToXNS: PROC [file: ROPE, op: Op] RETURNS [ROPE] = { AddChar: PROC [c: CHAR] = {text[text.length] ¬ c; text.length ¬ text.length + 1}; name, versionR: ROPE ¬ NIL; text: REF TEXT ¬ RefText.ObtainScratch[nChars: 200]; text.length ¬ 0; FOR i: INT IN [0 .. Rope.Length[file]) DO c: CHAR ~ Rope.Fetch[base: file, index: i]; SELECT c FROM '< => LOOP; -- throw the < away. '> => AddChar['/]; -- the XNS server uses / as a file separator '* => {AddChar[c]; AddChar[c]}; -- kludge to handle * '! => { -- the version part is next versionR ¬ Rope.Substr[base: file, start: i]; -- the whole version including the ! EXIT; }; ENDCASE => AddChar[c]; ENDLOOP; IF versionR # NIL THEN { SELECT Rope.Length[versionR] FROM 0 => versionR ¬ NIL; -- no version specified 1 => -- the version rope is just a ! so do the 'default' -- { SELECT op FROM delete => versionR ¬ "!-"; -- lowest enumerate, enumerateNames => versionR ¬ NIL; rename, retrieve => versionR ¬ "!+"; -- highest store => versionR ¬ "!+"; -- is this right? ENDCASE => ERROR; -- can't happen }; 2 => { -- is it H, L or *? SELECT Rope.Fetch[versionR, 1] FROM 'h, 'H => versionR ¬ "!+"; 'l, 'L => versionR ¬ "!-"; '* => versionR ¬ NIL; ENDCASE => NULL; }; ENDCASE => NULL; }; name ¬ Rope.Concat[base: Rope.FromRefText[s: text], rest: versionR]; RefText.ReleaseScratch[t: text]; RETURN[name]; }; <> GetConnection: PUBLIC PROC [data: ServerData] RETURNS [crH: CrRPC.Handle] = { crH ¬ CrRPC.CreateClientHandle[$CMUX, data.address ! CrRPC.Error => MakeCrRPCError[errorReason, text]]; }; ReturnConnection: PUBLIC PROC [crH: CrRPC.Handle] = { IF crH # NIL THEN CrRPC.DestroyClientHandle[crH ! CrRPC.Error => CONTINUE]; }; <> TCGetServer: PFSClass.GetHandleProc ~ { <<[fs: ROPE, flavorSpecified: BOOL] RETURNS [h: FSHandle, downMsg: ROPE]>> <> <> <> vfs: VFS ~ NEW[VFSInstance ¬ [fs: fs, flavorSpecified: flavorSpecified, sD: NIL] ]; data: ServerData; session: REF Session; qualified: XNSCHName.Name; address: REF XNS.Address; name: ROPE; crH: CrRPC.Handle; opened: BOOL ¬ FALSE; version: INT16 ¬ 0; downMsg ¬ "can\'t connect"; { ENABLE { CrRPC.Error => {downMsg ¬ IO.PutFR["CrRPC Error: %g, %g", [rope[crRPCErrorRopes[errorReason]]], [rope[text]]]; CONTINUE}; TrickleChargeP9813V411.Error => {downMsg ¬ IO.PutFR1["Server reports %g", [rope[description]]]; CONTINUE}; }; add: XNS.Address; [qualified, add] ¬ XNSCH.LookupAddressFromRope[fs]; IF add = XNS.unknownAddress THEN -- host not known -- RETURN[NIL, NIL]; name ¬ XNSCHName.RopeFromName[qualified]; add.socket ¬ XNSWKS.courier; address ¬ NEW[XNS.Address ¬ add]; crH ¬ CrRPC.CreateClientHandle[$CMUX, address]; version ¬ TrickleChargeP9813V411.AnyBodyHome[h: crH]; CrRPC.DestroyClientHandle[crH]; opened ¬ TRUE; }; IF NOT opened THEN RETURN [NIL, downMsg]; data ¬ NEW[ServerDataObject ¬ [qualified: qualified, serverName: name, address: address, opened: opened, version: version]]; vfs.sD ¬ data; h ¬ NEW[PFSClass.FSObject ¬ vfsClass­]; h.name ¬ fs; h.data ¬ vfs; RETURN [h, NIL]; }; TCSweep: PFSClass.SweepProc -- [h: FSHandle, seconds: CARD] -- ~ { <> <> <> <> }; TCValidate: PFSClass.ValidateProc ~ { <<[h: FSHandle] RETURNS [obsolete: BOOL, downMsg: ROPE]>> class: PFSClass.FSHandle ~ h; vfs: VFS ~ NARROW[class.data]; data: ServerData ~ vfs.sD; RETURN [FALSE, NIL]; }; <> CallProtected: PUBLIC PROC[proc: PROC, h: FSHandle, filePath: PATH, uid: PFS.UniqueID] ~ { time: GMT ~ uid.egmt.gmt; file: ROPE ~ PFS.RopeFromPath[filePath]; gName: ROPE ~ Rope.Cat["[", h.name, "]", file]; quotedGName: ROPE ~ Rope.Cat["\"", gName, "\""]; { -- for ENABLE ENABLE { XNSStream.ConnectionClosed => PFSBackdoor.ProduceError[resourceLimitExceeded, IO.PutFR1["Server for %g closed the connection.", [rope[quotedGName]]]]; --??? CrRPC.Error => MakeCrRPCError[errorReason, text]; IO.EndOfStream => ERROR PFS.Error[[environment, $unexpectedClose, IO.PutFR1["Server for %g closed the connection.", [rope[quotedGName]]]]]; IO.Error => ERROR PFS.Error[[bug, $unexpectedError, IO.PutFR1["File: %g unexpected IO.Error.", [rope[quotedGName]]]]]; TrickleChargeP9813V411.Error => ERROR PFS.Error[[environment, $notSure, IO.PutFR["Server for %g reported %g.", [rope[quotedGName]], [rope[description]]]]]; TrickleChargeP9813V411.FileNotFound => UnknownFile[gName, time]; PFSClass.Error => ERROR PFS.Error[[environment, code, msg]]; }; proc[ ! XNSStream.Timeout => RESUME]; -- since the timeout is retryable }; }; VersionFromName: PROC[name: ROPE] RETURNS[Version] = { path: PATH ~ PFS.PathFromRope[name]; RETURN[PFSNames.ShortName[path].version]; }; UnknownFile: PUBLIC PROC[name: ROPE, createdTime: GMT] = { IF createdTime = nullGMT THEN PFSBackdoor.ProduceError[unknownFile, IO.PutFR1["Could not find %g", [rope[name]]] ] ELSE PFSBackdoor.ProduceError[unknownUniqueID, IO.PutFR["Could not find \"%g\" created on %t", [rope[name]], [time[createdTime]]]]; }; REQUIREDROPE: TYPE ~ ROPE ¬; crRPCErrorRopes: REF ARRAY CrRPC.ErrorReason OF REQUIREDROPE ~ NEW[ARRAY CrRPC.ErrorReason OF REQUIREDROPE ¬ [ unknown: "Unknown", unknownClass: "unknownClass", courierVersionMismatch: "courierVersionMismatch", rejectedNoSuchProgram: "rejectedNoSuchProgram", rejectedNoSuchVersion: "rejectedNoSuchVersion", rejectedNoSuchProcedure: "rejectedNoSuchProcedure", rejectedInvalidArgument: "rejectedInvalidArgument", rejectedUnspecified: "rejectedUnspecified", remoteError: "remoteError", cantConnectToRemote: "Can't Connect To Remote", argsError: "Args Error", resultsError: "Results Error", bulkDataError: "Bulk DataError", protocolError: "Protocol Error", remoteClose: "Remote Close", communicationFailure: "Communication Failure", notImplemented: "notImplemented", unknownOperation: "unknownOperation", notServerHandle: "notServerHandle", notClientHandle: "notClientHandle", addressInappropriateForClass: "addressInappropriateForClass", other: "Other" ]]; MakeCrRPCError: PROC [errorReason: CrRPC.ErrorReason, text: ROPE] = { r: ROPE ~ IO.PutFR["CrRPC Error: %g, %g", [rope[crRPCErrorRopes[errorReason]]], [rope[text]]]; ERROR PFS.Error[[$environment, $CrRPCError, r]]; }; FromPath: PROC[p: PFS.PATH] RETURNS[ROPE] ~ { RETURN[PFS.RopeFromPath[p]] }; nullGMTRope: ROPE ~ "nullGMT"; TCStats: Commander.CommandProc ~ { cmd.out.PutF1["***** Client started at %g\n", [time[startTime]] ]; cmd.out.PutF["startEnum: %g\n\t enumEnd: %g\n", IF startEnum = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[startEnum]], IF endEnum = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[endEnum]] ]; cmd.out.PutF["startFileInfo: %g\n\t enumEnd: %g\n", IF startFileInfo = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[startFileInfo]], IF endFileInfo = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[endFileInfo]] ]; cmd.out.PutF["startRetrieve: %g\n\t endRetrieve: %g\n", IF startRetrieve = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[startRetrieve]], IF endRetrieve = BasicTime.nullGMT THEN [rope[nullGMTRope]] ELSE [time[endRetrieve]] ]; }; <> Init: PROC = { PFSClass.Register[myFlavor, TCGetServer]; }; Init[]; Commander.Register["TCStats", TCStats, "Statistics about the TrickleChargeClient"]; END.