DIRECTORY Ascii, Atom, BasicTime USING [GMT, Now, nullGMT, Period, Update], Booting, Convert, DB, DBCommon, DBDefs, FingerLog, FingerOps, FingerSimpleOps, FS USING[StreamOpen, Error], IO, LupineRuntime, Process USING [Detach, Ticks, SecondsToTicks, Pause], ThisMachine USING[Name], RefText USING[New, AppendChar, TrustTextAsRope], RefTab, RemoteFingerOps, Rope, RemoteFingerOpsRpcControl, TextFind USING [CreateFromRope, Finder, SearchRope], UserProfile USING [Token]; FingerOpsImpl: CEDAR MONITOR IMPORTS Ascii, Atom, BasicTime, Booting, Convert, DB, FingerSimpleOps, FS, IO, LupineRuntime, Process, ThisMachine, RefTab, RefText, Rope, RemoteFingerOpsRpcControl, TextFind, UserProfile EXPORTS FingerOps, RemoteFingerOps = BEGIN OPEN FingerOps, FingerLog; machineName: Rope.ROPE = ThisMachine.Name[$Pup]; fingerSegmentRope: Rope.ROPE = UserProfile.Token[key: "Finger.segment", default: "[Luther.Alpine]7.0>Finger.segment"]; fingerSegment: DBDefs.Segment = $Finger; fingerSegmentNumber: NAT = 260B; fingerTransaction: DBCommon.TransactionHandle _ NIL; FingerError: PUBLIC ERROR[reason: FingerOps.Reason] ~ CODE; activity: BOOL _ TRUE; ticksToWait: Process.Ticks _ Process.SecondsToTicks[10]; SomethingHappened: CONDITION; person, machine: DBDefs.Domain; machineType: DB.TypeSpec = [ indirect[ machineTypeProc ] ]; machineTypeProc: PROC[] RETURNS[type: DB.TypeCode] = { type _ DB.TypeForDomain[machine] }; personType: DB.TypeSpec = [ indirect[ personTypeProc ] ]; personTypeProc: PROC[] RETURNS[type: DB.TypeCode] = { type _ DB.TypeForDomain[person] }; event: DB.Domain; eventType: DB.TypeSpec = [ indirect[ eventTypeProc ] ]; eventTypeProc: PROC[] RETURNS[type: DB.TypeCode] = { type _ DB.TypeForDomain[event] }; login: DB.Entity; logout: DB.Entity; unknown: DB.Entity; userRelation: DBDefs.Relation; machineIs: CARDINAL = 0; -- a key of the relation userIs: CARDINAL = 1; -- the person performing the last event lastEventIs: CARDINAL = 2; -- whether the last event was login or logout lastEventTimeIs: CARDINAL = 3; -- when the last event occurred userRelationType: DB.FieldSpec = DB.L2FS[LIST[[name: "machine", type: machineType], [name: "user", type: personType], [name: "lastEvent", type: eventType], [name: "lastEventTime", type: DB.Time]]]; timeIndex: DB.Index; -- an index is maintained on time of last event lastChangedProp: DB.Relation; lastChangedTime: CARDINAL = 1; machinePropType: DB.FieldSpec = DB.L2FS[LIST[[name: "of", type: machineType], [name: "is", type: DB.String]]]; userPropType: DB.FieldSpec = DB.L2FS[LIST[[name: "of", type: personType], [name: "is", type: DB.String]]]; versionType: DB.FieldSpec = DB.L2FS[LIST[[name: "stamp", type: DB.Integer]]]; lastChangedType: DB.FieldSpec = DB.L2FS[LIST[[name: "of", type: personType], [name: "is", type: DB.Time]]]; machinePropVersion, userPropVersion: INT _ -1; machinePropRelation: DBDefs.Relation; machinePropAttr: CARDINAL = 0; machinePropVersionRelship: DBDefs.Relship; -- the single relship of this relation userPropRelation: DBDefs.Relation; userPropAttr: CARDINAL = 0; userPropVersionRelship: DBDefs.Relship; -- the single relship of this relation userPropNames: DB.Domain; machinePropNames: DB.Domain; PropertyTable: TYPE = RECORD[names: DB.Domain _ NIL, table: RefTab.Ref]; machineProps: PropertyTable _ [table: RefTab.Create[]]; userProps: PropertyTable _ [table: RefTab.Create[]]; DevoPattern: PROC [value: Rope.ROPE] RETURNS [ItsADevoPattern:BOOL] ~ { lastPos: INT _ (Rope.Length[base: value] - 1); RETURN [Rope.Find[s1: value, s2: "*", pos1: 0, case: FALSE] = lastPos] }; -- DevoPattern RopeBracket: PROC [base: Rope.ROPE] RETURNS [low, high: Rope.ROPE] ~ { lastPos: INT _ (Rope.Length[base: base] - 1); starPos: INT _ Rope.Find[s1: base, s2: "*", pos1: 0, case: FALSE]; IF Rope.Equal[s1: base, s2: "*"] THEN { low _ high _ NIL; RETURN }; IF starPos = lastPos THEN { lastLetter: CHAR _ Rope.Fetch[base: base, index: lastPos - 1]; low _ Rope.Substr[base: base, start: 0, len: lastPos]; high _ Rope.Concat[base: Rope.Substr[base: low, start: 0, len: lastPos - 1], rest: Rope.FromChar[c: lastLetter.SUCC]] } ELSE IF starPos = -1 THEN low _ high _ base ELSE low _ high _ NIL; }; -- RopeBracket WatchDBActivity: PROC[] = { WHILE TRUE DO Process.Pause[ticksToWait]; CheckConnection[] ENDLOOP }; CheckConnection: ENTRY PROC[] = { ENABLE UNWIND => NULL; IF NOT activity THEN { CloseTransaction[]; WAIT SomethingHappened }; activity _ FALSE }; CloseTransaction: INTERNAL PROC [] = { caughtAborted: BOOL _ FALSE; IF fingerTransaction = NIL THEN RETURN; DB.CloseTransaction[fingerTransaction ! DB.Aborted => { caughtAborted _ TRUE; CONTINUE }; DB.Error, DB.Failure => CONTINUE ]; IF caughtAborted THEN DB.AbortTransaction[fingerTransaction]; fingerTransaction _ NIL }; OpenTransaction: INTERNAL PROC [] = { schemaInvalid: BOOL; IF fingerTransaction = NIL THEN { [fingerTransaction, schemaInvalid] _ DB.OpenTransaction[$Finger]; IF schemaInvalid THEN ResetSchema[] } }; CarefullyOpenTransaction: INTERNAL PROC [] ~ { ENABLE UNWIND => NULL; BEGIN ENABLE BEGIN DB.Error => GOTO Error; DB.Failure => GOTO Failure; END; aborted: BOOL _ FALSE; IF NOT activity THEN {activity _ TRUE; NOTIFY SomethingHappened}; BEGIN ENABLE DB.Aborted => { aborted _ TRUE; CONTINUE }; OpenTransaction[]; END; IF NOT aborted THEN RETURN; -- no aborted occurred BEGIN ENABLE DB.Aborted => GOTO Aborted; AbortTransaction[]; -- now try again OpenTransaction[]; END EXITS Error => { CloseTransaction[]; ERROR FingerError[Error] }; Failure => ERROR FingerError[Failure]; Aborted => { CloseTransaction[]; ERROR FingerError[Aborted] } END; }; AbortTransaction: INTERNAL PROC [] = { IF fingerTransaction = NIL THEN RETURN; DB.AbortTransaction[fingerTransaction ! DB.Error, DB.Failure, DB.Aborted => CONTINUE ]; fingerTransaction _ NIL }; InitFingerDB: ENTRY PROC = BEGIN ENABLE UNWIND => NULL; DB.Initialize[nCachePages: 256]; DB.DeclareSegment[filePath: fingerSegmentRope, segment: fingerSegment, number: fingerSegmentNumber, nPagesInitial: 256, nPagesPerExtent: 256]; CarefullyOpenTransaction[]; END; ResetSchema: INTERNAL PROC[] ~ { person _ DB.DeclareDomain["person", fingerSegment]; machine _ DB.DeclareDomain["machine", fingerSegment]; event _ DB.DeclareDomain["event", fingerSegment]; login _ DB.CopyEntity[DB.DeclareEntity[event, "login"]]; logout _ DB.CopyEntity[DB.DeclareEntity[event, "logout"]]; unknown _ DB.CopyEntity[DB.DeclareEntity[event, "unknown"]]; machinePropRelation _ DB.DeclareRelation[name: "MachineVersion", segment: fingerSegment, fields: versionType]; machinePropVersionRelship _ DB.FirstRelship[machinePropRelation]; IF machinePropVersionRelship = NIL THEN machinePropVersionRelship _ DB.CopyRelship[DB.CreateRelship[r: machinePropRelation, init: DB.L2VS[LIST[[integer[0]]]]]] ELSE machinePropVersionRelship _ DB.CopyRelship[machinePropVersionRelship]; userPropRelation _ DB.DeclareRelation[name: "UserVersion", segment: fingerSegment, fields: versionType]; userPropVersionRelship _ DB.FirstRelship[userPropRelation]; IF userPropVersionRelship = NIL THEN userPropVersionRelship _ DB.CopyRelship[DB.CreateRelship[r: userPropRelation, init: DB.L2VS[LIST[[integer[0]]]]]] ELSE userPropVersionRelship _ DB.CopyRelship[userPropVersionRelship]; lastChangedProp _ DB.DeclareProperty[name: "lastChanged", segment: fingerSegment, fields: lastChangedType]; machinePropNames _ DB.DeclareDomain[name: "MachineProps", segment: fingerSegment]; machineProps.names _ machinePropNames; userPropNames _ DB.DeclareDomain[name: "UserProps", segment: fingerSegment]; userProps.names _ userPropNames; userRelation _ DB.DeclareProperty[name: "UserInfo", segment: fingerSegment, fields: userRelationType]; timeIndex _ DB.DeclareIndex[userRelation, DB.L2F[LIST[lastEventIs, lastEventTimeIs]]]; BEGIN DBMachineStamp: INT = DB.V2I[DB.GetF[machinePropVersionRelship, machinePropAttr]]; DBUserStamp: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; IF DBMachineStamp # machinePropVersion THEN { machineProps.table _ RefTab.Create[]; EnumerateProperties[machineProps]; machinePropVersion _ DBMachineStamp }; IF DBUserStamp # userPropVersion THEN { userProps.table _ RefTab.Create[]; EnumerateProperties[userProps]; userPropVersion _ DBUserStamp }; END; SetAttributes[machineProps, machine]; SetAttributes[userProps, person] }; EnumerateProperties: INTERNAL PROC[propTable: PropertyTable] = { attrSet: DB.EntitySet = DB.DomainSubset[d: propTable.names]; FOR prop: DB.Entity _ DB.NextEntity[attrSet], DB.NextEntity[attrSet] UNTIL prop = NIL DO propName: Rope.ROPE = DB.EntityInfo[prop].name; [] _ RefTab.Store[x: propTable.table, key: Atom.MakeAtom[propName], val: NIL] ENDLOOP}; SetAttributes: INTERNAL PROC[propTable: PropertyTable, domain: DB.Domain] = { type: DB.TypeCode = DB.TypeForDomain[domain]; EachProp: RefTab.EachPairAction = { name: Rope.ROPE = Atom.GetPName[NARROW[key]]; fullName: Rope.ROPE = Rope.Concat[name, DB.DomainInfo[domain].name]; propRelation: DB.Relation = DB.DeclareProperty[name: fullName, segment: fingerSegment, fields: DB.L2FS[LIST[[name: "of", type: [direct[type]]], [name: "is", type: DB.String]]]]; [] _ RefTab.Store[ propTable.table, key, propRelation ]; quit _ FALSE }; [] _ RefTab.Pairs[propTable.table, EachProp] }; ListUserProps: PUBLIC PROC[] RETURNS[propList: LIST OF ATOM] = { Do: ENTRY PROC[] ~ { ENABLE UNWIND => NULL; EachProp: RefTab.EachPairAction = { propList _ CONS[NARROW[key], propList]; quit _ FALSE }; [] _ RefTab.Pairs[userProps.table, EachProp]; }; FingerSimpleOps.AttemptToPlayLog[]; Do[]; }; ListMachineProps: PUBLIC PROC[] RETURNS[propList: LIST OF ATOM] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; EachProp: RefTab.EachPairAction = { propList _ CONS[NARROW[key], propList]; quit _ FALSE }; [] _ RefTab.Pairs[machineProps.table, EachProp]; }; FingerSimpleOps.AttemptToPlayLog[]; Do[]; }; AddUserProp: PUBLIC PROC[name: Rope.ROPE] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; FingerSimpleOps.Log[ logEntry: NEW[LogEntryObject _ [AddUserProp [name: name, version: userPropVersion]]] ]; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; AddMachineProp: PUBLIC PROC[name: Rope.ROPE] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; FingerSimpleOps.Log[ NEW[LogEntryObject _ [AddMachineProp[name: name, version: machinePropVersion]]] ]; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; DeleteUserProp: PUBLIC PROC[name: Rope.ROPE] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; FingerSimpleOps.Log[ NEW[LogEntryObject _ [ DeleteUserProp[name: name, version: userPropVersion]]] ]; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; DeleteMachineProp: PUBLIC PROC[name: Rope.ROPE] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; FingerSimpleOps.Log[ NEW[LogEntryObject _ [DeleteMachineProp[name: name, version: machinePropVersion]]] ]; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; SetUserProps: PUBLIC PROC [user: Rope.ROPE, props: LIST OF FingerOps.PropPair] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; now: BasicTime.GMT = BasicTime.Now[]; FOR p: LIST OF PropPair _ props, p.rest UNTIL p = NIL DO FingerSimpleOps.Log[NEW[LogEntryObject _ [UserPropChange [user: user, name: Atom.GetPName[p.first.prop], val: Canonicalize[p.first.val], time: now]]] ] ENDLOOP; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; SetMachineProps: PUBLIC PROC[machine: Rope.ROPE, props: LIST OF PropPair] = { Do: ENTRY PROC [] ~ { ENABLE UNWIND => NULL; FOR p: LIST OF PropPair _ props, p.rest UNTIL p = NIL DO FingerSimpleOps.Log[NEW[LogEntryObject _ [ MachinePropChange[machine: machine, name: Atom.GetPName[p.first.prop], val: Canonicalize[p.first.val]]]] ] ENDLOOP; }; Do[]; FingerSimpleOps.AttemptToPlayLog[] }; GetUserProps: PUBLIC PROC[user: Rope.ROPE] RETURNS[props: LIST OF PropPair] = { DoGetProps: INTERNAL PROC[] = { entity: DB.Entity = DB.LookupEntity[person, Canonicalize[user]]; EachProp: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; property: DB.Relship = DB.LookupProperty[relation, entity]; propertyValue: Rope.ROPE = IF property # NIL THEN DB.V2S[DB.GetF[property, 1]] ELSE NIL; props _ CONS[NEW[PropPairObject _ [prop: NARROW[key], val: propertyValue]], props]; quit _ FALSE }; IF entity = NIL THEN RETURN; [] _ RefTab.Pairs[userProps.table, EachProp] }; FingerSimpleOps.AttemptToPlayLog[]; -- make sure no pending updates exist CarefullyApply[DoGetProps] }; GetMachineProps: PUBLIC PROC[machineName: Rope.ROPE] RETURNS[props: LIST OF PropPair] = { DoGetProps: INTERNAL PROC[] = { entity: DB.Entity = DB.LookupEntity[machine, Canonicalize[machineName]]; EachProp: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; property: DB.Relship = DB.LookupProperty[relation, entity]; propertyValue: Rope.ROPE = IF property # NIL THEN DB.V2S[DB.GetF[property, 1]] ELSE NIL; props _ CONS[NEW[PropPairObject _ [prop: NARROW[key], val: propertyValue]], props]; quit _ FALSE }; IF entity = NIL THEN RETURN; [] _ RefTab.Pairs[machineProps.table, EachProp] }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGetProps] }; CarefullyApply: ENTRY PROC [proc: PROC[]] ~ { ENABLE UNWIND => NULL; BEGIN ENABLE BEGIN DB.Error => GOTO Error; DB.Failure => GOTO Failure; END; aborted: BOOL _ FALSE; IF NOT activity THEN {activity _ TRUE; NOTIFY SomethingHappened}; BEGIN ENABLE DB.Aborted => { aborted _ TRUE; CONTINUE }; OpenTransaction[]; proc[] END; IF NOT aborted THEN RETURN; -- no aborted occurred BEGIN ENABLE DB.Aborted => GOTO Aborted; AbortTransaction[]; -- now try again OpenTransaction[]; proc[] END EXITS Error => { CloseTransaction[]; Unexport[]; ERROR FingerError[Error] }; Failure => { Unexport[]; ERROR FingerError[Failure] }; Aborted => { CloseTransaction[]; Unexport[]; ERROR FingerError[Aborted] } END; }; Unexport: PROC [] ~ { RemoteFingerOpsRpcControl.UnexportInterface[ ! LupineRuntime.BindingError => CONTINUE ]; }; PersonExists: PUBLIC PROC [name: Rope.ROPE] RETURNS[result: BOOLEAN] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { result _ DB.LookupEntity[d: person, name: Canonicalize[name]] # NIL }; CarefullyApply[DoGet] }; MachineExists: PUBLIC PROC [name: Rope.ROPE] RETURNS[result: BOOLEAN] = { DoGet: INTERNAL PROC[] = { result _ DB.LookupEntity[machine, Canonicalize[name]] # NIL }; CarefullyApply[DoGet] }; GetMachineData: PUBLIC PROC [name: Rope.ROPE] RETURNS[lastChange: StateChange, time: BasicTime.GMT, user: Rope.ROPE _ NIL] = { DoGet: INTERNAL PROC[] = { theMachine: DB.Entity = DB.DeclareEntity[machine, Canonicalize[name]]; machineRel: DB.Relship _ DB.LookupProperty[userRelation, theMachine]; IF DB.NullRelship[machineRel] THEN machineRel _ DB.CreateRelship[userRelation, DB.L2VS[LIST[DB.E2V[theMachine], DBDefs.NullValue, DB.E2V[unknown], DB.T2V[BasicTime.Now[]]]]]; lastChange _ IF DB.EntityEq[DB.V2E[DB.GetF[machineRel, lastEventIs]], login] THEN FingerLog.StateChange[login] ELSE FingerLog.StateChange[logout]; time _ DB.V2T[DB.GetF[machineRel, lastEventTimeIs]]; IF time # BasicTime.nullGMT THEN user _ DB.EntityInfo[DB.V2E[DB.GetF[machineRel, userIs]]].name }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; GetUserData: PUBLIC PROC [name: Rope.ROPE] RETURNS[machineList: LIST OF Rope.ROPE] = { DoGet: INTERNAL PROC[] = { user: DB.Entity = DB.DeclareEntity[person, Canonicalize[name]]; usedMachines: DB.RelshipSet = DB.RelshipsWithEntityField[userRelation, userIs, user]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[usedMachines]; FOR machines: DB.Relship _ DB.NextRelship[usedMachines], DB.NextRelship[usedMachines] UNTIL machines = NIL DO machineList _ CONS[DB.EntityInfo[DB.V2E[DB.GetF[machines, machineIs]]].name, machineList] ENDLOOP; END; DB.ReleaseRelshipSet[usedMachines] }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; MatchUserProperty: PUBLIC PROC[propVal: PropPair] RETURNS[result: LIST OF Rope.ROPE] = { DoGet: INTERNAL PROC[] = { relation: DB.Relation = NARROW[RefTab.Fetch[userProps.table, propVal.prop].val]; indexList: LIST OF DB.Index _ DB.OtherIndices[relation]; relships: DB.RelshipSet; IF (indexList # NIL) AND DevoPattern[propVal.val] THEN { low, high: Rope.ROPE _ NIL; [low, high] _ RopeBracket[propVal.val]; relships _ DB.RelationSubset[r: relation, index: indexList.first, constraint: DB.L2C[LIST[DBDefs.ValueConstraint[rope[low: low, high: high]]]]]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO result _ CONS[DB.EntityInfo[e: DB.V2E[v: DB.GetF[t: next, field: 0]]].name, result] ENDLOOP; RETURN } ELSE relships _ DB.RelationSubset[r: relation, index: NIL, constraint: NIL]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[relships]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO IF Rope.Match[DB.V2S[DB.GetF[next, 1]], Canonicalize[propVal.val]] THEN result _ CONS[DB.EntityInfo[DB.V2E[DB.GetF[next, 0]]].name, result] ENDLOOP; DB.ReleaseRelshipSet[relships] END }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; -- MatchUserProperty MatchMachineProperty: PUBLIC PROC[propVal: PropPair] RETURNS[result: LIST OF Rope.ROPE] ~ { DoGet: INTERNAL PROC[] = { relation: DB.Relation = NARROW[RefTab.Fetch[machineProps.table, propVal.prop].val]; indexList: LIST OF DB.Index _ DB.OtherIndices[relation]; relships: DB.RelshipSet; IF (indexList # NIL) AND DevoPattern[propVal.val] THEN { low, high: Rope.ROPE _ NIL; [low, high] _ RopeBracket[propVal.val]; relships _ DB.RelationSubset[r: relation, index: indexList.first, constraint: DB.L2C[LIST[DBDefs.ValueConstraint[rope[low: Canonicalize[low], high: Canonicalize[high]]]]]]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO result _ CONS[DB.EntityInfo[e: DB.V2E[DB.GetF[t: next, field: 0 ]]].name, result] ENDLOOP; RETURN } ELSE relships _ DB.RelationSubset[r: relation, index: NIL, constraint: NIL]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[relships]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO IF Rope.Match[pattern: Canonicalize[propVal.val], object: DB.V2S[DB.GetF[next, 1]], case: FALSE] THEN result _ CONS[DB.EntityInfo[DB.V2E[DB.GetF[next, 0]]].name, result]; ENDLOOP END; DB.ReleaseRelshipSet[relships] }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; -- MatchMachineProperty GetMatchingPersons: PUBLIC PROC [pattern: Rope.ROPE] RETURNS [result: LIST OF Rope.ROPE] ~ { DoGet: INTERNAL PROC[] = { patn: Rope.ROPE _ Canonicalize[pattern]; setOfEntities: DB.EntitySet = DB.DomainSubset[d: person, lowName: RopeBracket[patn].low, highName: RopeBracket[patn].high]; finder: TextFind.Finder = TextFind.CreateFromRope[pattern: patn, ignoreCase: TRUE, addBounds: TRUE]; BEGIN ENABLE UNWIND => DB.ReleaseEntitySet[setOfEntities]; IF DevoPattern[value: patn] THEN { FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; result _ CONS[name, result] ENDLOOP; } ELSE { FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; IF TextFind.SearchRope[finder, name].found THEN result _ CONS[name, result] ENDLOOP }; DB.ReleaseEntitySet[setOfEntities] END }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; GetMatchingMachines: PUBLIC PROC [pattern: Rope.ROPE] RETURNS [result: LIST OF Rope.ROPE] = { DoGet: INTERNAL PROC[] = { patn: Rope.ROPE _ Canonicalize[pattern]; setOfEntities: DB.EntitySet = DB.DomainSubset[d: machine, lowName: RopeBracket[patn].low, highName: RopeBracket[patn].high]; finder: TextFind.Finder = TextFind.CreateFromRope[pattern: patn, ignoreCase: TRUE, addBounds: TRUE]; BEGIN ENABLE UNWIND => {DB.ReleaseEntitySet[setOfEntities]; CONTINUE}; IF DevoPattern[value: patn] THEN { FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; result _ CONS[name, result] ENDLOOP; } ELSE { FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; IF TextFind.SearchRope[finder, name].found THEN result _ CONS[name, result] ENDLOOP }; DB.ReleaseEntitySet[setOfEntities] END }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[DoGet] }; CurrentUsers: PUBLIC PROC[] RETURNS[userList: LIST OF Rope.ROPE] = { twoDaysInSeconds: INT = LONG[48] * 60 * 60; now: BasicTime.GMT = BasicTime.Now[]; twoDaysAgo: BasicTime.GMT = BasicTime.Update[now, -twoDaysInSeconds]; GetUsers: INTERNAL PROC[] = { activeSet: DB.RelshipSet = DB.RelationSubset[userRelation, timeIndex, DB.L2C[LIST[DB.ValueConstraint[entity[login]], DB.ValueConstraint[time[low: twoDaysAgo, high: now]]]]]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[activeSet]; FOR active: DB.Relship _ DB.NextRelship[activeSet], DB.NextRelship[activeSet] UNTIL active = NIL DO user: Rope.ROPE = DB.EntityInfo[DB.V2E[DB.GetF[active, userIs]]].name; userList _ CONS[user, userList] ENDLOOP END; DB.ReleaseRelshipSet[activeSet] }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[GetUsers] }; FreeMachines: PUBLIC PROC[] RETURNS[machineList: LIST OF Rope.ROPE] = { oneWeekInSeconds: INT = LONG[168] * 60 * 60; now: BasicTime.GMT = BasicTime.Now[]; oneWeekAgo: BasicTime.GMT = BasicTime.Update[now, -oneWeekInSeconds]; ListFree: INTERNAL PROC[] = { freeSet: DB.RelshipSet = DB.RelationSubset[userRelation, timeIndex, DB.L2C[LIST[DB.ValueConstraint[entity[logout]], DB.ValueConstraint[time[low: oneWeekAgo, high: now]]]]]; BEGIN ENABLE UNWIND => {DB.ReleaseRelshipSet[freeSet]; CONTINUE}; FOR free: DB.Relship _ DB.NextRelship[freeSet], DB.NextRelship[freeSet] UNTIL free = NIL DO machineList _ CONS[DB.EntityInfo[DB.V2E[DB.GetF[free, machineIs]]].name, machineList] ENDLOOP END; DB.ReleaseRelshipSet[freeSet] }; FingerSimpleOps.AttemptToPlayLog[]; CarefullyApply[ListFree] }; Canonicalize: PROC[name: Rope.ROPE] RETURNS[ lowerCase: Rope.ROPE ] = { newText: REF TEXT _ RefText.New[name.Size[]]; FOR i: INT IN [0..name.Size[]) DO newText _ RefText.AppendChar[newText, Ascii.Lower[name.Fetch[i]]] ENDLOOP; lowerCase _ RefText.TrustTextAsRope[newText] }; ReadMachineMap: PUBLIC PROC [file: Rope.ROPE, resetProperties: BOOLEAN _ FALSE] = { fileStream, tokenStream: IO.STREAM; DoReadMap: INTERNAL PROC = { line: REF TEXT _ RefText.New[500]; line _ fileStream.GetLine[line]; IF line[0] = Ascii.NUL THEN line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; IF resetProperties THEN { DB.SetF[machinePropVersionRelship, machinePropAttr, DB.I2V[0]]; BEGIN machinePropsSet: DB.EntitySet = DB.DomainSubset[machinePropNames]; FOR next: DB.Entity _ DB.NextEntity[machinePropsSet], DB.NextEntity[machinePropsSet] UNTIL next = NIL DO relationName: Rope.ROPE = Rope.Concat[DB.EntityInfo[next].name, DB.DomainInfo[machine].name]; relation: DB.Relation = DB.DeclareProperty[relationName, fingerSegment, machinePropType]; DB.DestroyRelation[relation]; [] _ RefTab.Delete[x: machineProps.table, key: Atom.MakeAtom[DB.EntityInfo[next].name]]; DB.MarkTransaction[fingerTransaction]; ENDLOOP; DB.ReleaseEntitySet[machinePropsSet]; END; }; BEGIN ENABLE IO.EndOfStream => CONTINUE; DO property: Rope.ROPE = tokenStream.GetRopeLiteral[]; index: BOOLEAN _ tokenStream.GetAtom[] = $Index; IF RefTab.Fetch[machineProps.table, Atom.MakeAtom[property]].found THEN LOOP; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[ property, DB.DomainInfo[machine].name]; relation: DB.Relation = DB.DeclareProperty[fullName, fingerSegment, machinePropType]; [] _ DB.DeclareEntity[machinePropNames, property]; [] _ RefTab.Store[ machineProps.table, Atom.MakeAtom[property], relation ]; IF index THEN [] _ DB.DeclareIndex[r: relation, fields: DB.L2F[LIST[1]]]; END ENDLOOP END; BEGIN ENABLE IO.EndOfStream => CONTINUE; machineName: Rope.ROPE; machineEntity: DB.Entity; machineRelship: DB.Relship; property: Rope.ROPE; propValue: Rope.ROPE; operationCount: INT _ 0; DO line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; machineName _ tokenStream.GetRopeLiteral[]; machineEntity _ DB.DeclareEntity[machine, Canonicalize[machineName]]; IF DB.NullRelship[DB.LookupProperty[userRelation, machineEntity]] THEN machineRelship _ DB.CreateRelship[userRelation, DB.L2VS[LIST[DB.E2V[machineEntity], DBDefs.NullValue, DB.E2V[unknown], DB.T2V[BasicTime.Now[]]]]]; DO line _ fileStream.GetLine[line]; TRUSTED{ IF Rope.Equal[LOOPHOLE[line], ""] THEN EXIT }; tokenStream _ IO.TIS[line, tokenStream]; property _ tokenStream.GetRopeLiteral[]; propValue _ Canonicalize[tokenStream.GetRopeLiteral[]]; BEGIN relation: DB.Relation = NARROW[RefTab.Fetch[machineProps.table, Atom.MakeAtom[property]].val]; IF relation = NIL THEN LOOP; BEGIN prop: DB.Relship = DB.LookupProperty[relation, machineEntity]; IF prop # NIL THEN DB.SetF[prop, 1, DB.S2V[propValue]] ELSE [] _ DB.CreateRelship[relation, DB.L2VS[LIST[DB.E2V[machineEntity], DB.S2V[propValue]]]] END; operationCount _ operationCount+1; IF operationCount = 20 THEN { DB.MarkTransaction[fingerTransaction]; operationCount _ 0 } END ENDLOOP; ENDLOOP END }; IF file = NIL THEN RETURN; fileStream _ FS.StreamOpen[fileName: file, accessOptions: $read ! FS.Error => {fileStream _ NIL; CONTINUE} ]; IF fileStream = NIL THEN RETURN; CarefullyApply[DoReadMap] }; WriteMachineMap: PUBLIC PROC [file: Rope.ROPE] ~ { stream: IO.STREAM; DoWriteMap: INTERNAL PROC = { setOfEntities: DB.EntitySet = DB.DomainSubset[machine]; entity: DB.Entity; WritePropName: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; stream.Put[IO.rope[" "]]; stream.Put[IO.rope["\""]]; stream.Put[IO.rope[Atom.GetPName[NARROW[key]]]]; stream.Put[IO.rope["\""]]; IF DB.OtherIndices[r: relation] # NIL THEN stream.Put[IO.rope[" $Index"]] ELSE stream.Put[IO.rope[" $NoIndex"]]; quit _ FALSE }; -- WritePropName WritePropValue: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; prop: DB.Relship = DB.LookupProperty[relation, entity]; propertyValue: Rope.ROPE = IF prop # NIL THEN DB.V2S[DB.GetF[prop, 1]] ELSE NIL; IF propertyValue # NIL THEN { stream.Put[IO.rope["\""]]; stream.Put[IO.rope[Atom.GetPName[NARROW[key]]]]; stream.Put[IO.rope["\""]]; stream.Put[IO.rope[" "], IO.rope[Convert.RopeFromRope[propertyValue]]]; stream.Put[IO.rope["\n"]] }; quit _ FALSE }; -- WritePropValue operationCount: INT _ 0; [] _ RefTab.Pairs[machineProps.table, WritePropName]; stream.Put[IO.rope["\n"]]; FOR entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; stream.Put[IO.rope[Convert.RopeFromRope[name]]]; stream.Put[IO.rope["\n"]]; [] _ RefTab.Pairs[machineProps.table, WritePropValue]; stream.Put[IO.rope["\n"]]; operationCount _ operationCount+1; IF operationCount = 20 THEN { DB.MarkTransaction[fingerTransaction]; operationCount _ 0 } ENDLOOP; DB.ReleaseEntitySet[setOfEntities]; stream.Close[] }; -- WritePropName IF file = NIL THEN RETURN; stream _ FS.StreamOpen[fileName: file, accessOptions: $create ! FS.Error => {stream _ NIL; CONTINUE }]; IF stream = NIL THEN RETURN; CarefullyApply[DoWriteMap] }; -- WriteMachineMap ReadUserMap: PUBLIC PROC [file: Rope.ROPE, resetProperties: BOOLEAN _ FALSE] ~ { fileStream, tokenStream: IO.STREAM; DoReadMap: INTERNAL PROC ~ { line: REF TEXT _ RefText.New[500]; line _ fileStream.GetLine[line]; IF line[0] = Ascii.NUL THEN line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; IF resetProperties THEN { DB.SetF[userPropVersionRelship, userPropAttr, DB.I2V[0]]; BEGIN userPropsSet: DB.EntitySet = DB.DomainSubset[userPropNames]; FOR next: DB.Entity _ DB.NextEntity[userPropsSet], DB.NextEntity[userPropsSet] UNTIL next = NIL DO relationName: Rope.ROPE = Rope.Concat[DB.EntityInfo[next].name, DB.DomainInfo[person].name]; relation: DB.Relation = DB.DeclareProperty[relationName, fingerSegment, userPropType]; DB.DestroyRelation[relation]; [] _ RefTab.Delete[x: userProps.table, key: Atom.MakeAtom[DB.EntityInfo[next].name]]; DB.MarkTransaction[fingerTransaction]; ENDLOOP; DB.ReleaseEntitySet[userPropsSet]; END; }; BEGIN ENABLE IO.EndOfStream => CONTINUE; DO property: Rope.ROPE = tokenStream.GetRopeLiteral[]; index: BOOLEAN _ tokenStream.GetAtom[] = $Index; IF RefTab.Fetch[userProps.table, Atom.MakeAtom[property]].found THEN LOOP; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[ property, DB.DomainInfo[person].name]; relation: DB.Relation = DB.DeclareProperty[fullName, fingerSegment, userPropType]; [] _ DB.DeclareEntity[userPropNames, property]; [] _ RefTab.Store[userProps.table, Atom.MakeAtom[property], relation ]; IF index THEN [] _ DB.DeclareIndex[r: relation, fields: DB.L2F[LIST[1]]]; END ENDLOOP END; BEGIN ENABLE IO.EndOfStream => CONTINUE; userName: Rope.ROPE; userEntity: DB.Entity; property: Rope.ROPE; propValue: Rope.ROPE; operationCount: INT _ 0; DO line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; userName _ tokenStream.GetRopeLiteral[]; userEntity _ DB.DeclareEntity[person, Canonicalize[userName]]; DO line _ fileStream.GetLine[line]; TRUSTED{ IF Rope.Equal[LOOPHOLE[line], ""] THEN EXIT }; tokenStream _ IO.TIS[line, tokenStream]; property _ tokenStream.GetRopeLiteral[]; propValue _ Canonicalize[tokenStream.GetRopeLiteral[]]; BEGIN relation: DB.Relation = NARROW[RefTab.Fetch[userProps.table, Atom.MakeAtom[property]].val]; IF relation = NIL THEN LOOP; BEGIN prop: DB.Relship = DB.LookupProperty[relation, userEntity]; IF prop # NIL THEN DB.SetF[prop, 1, DB.S2V[propValue]] ELSE [] _ DB.CreateRelship[relation, DB.L2VS[LIST[DB.E2V[userEntity], DB.S2V[propValue]]]] END; operationCount _ operationCount+1; IF operationCount = 20 THEN { DB.MarkTransaction[fingerTransaction]; operationCount _ 0 } END ENDLOOP; ENDLOOP END }; -- DoReadMap IF file = NIL THEN RETURN; fileStream _ FS.StreamOpen[fileName: file, accessOptions: $read ! FS.Error => {fileStream _ NIL; CONTINUE} ]; IF fileStream = NIL THEN RETURN; CarefullyApply[DoReadMap] }; -- ReadUserMap WriteUserMap: PUBLIC PROC [file: Rope.ROPE] ~ { stream: IO.STREAM; DoWriteMap: INTERNAL PROC = { setOfEntities: DB.EntitySet = DB.DomainSubset[person]; entity: DB.Entity; WritePropName: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; stream.Put[IO.rope[" "]]; stream.Put[IO.rope["\""]]; stream.Put[IO.rope[Atom.GetPName[NARROW[key]]]]; stream.Put[IO.rope["\""]]; stream.Put[IO.rope[" "]]; IF DB.OtherIndices[r: relation] # NIL THEN stream.Put[IO.rope["$Index"]] ELSE stream.Put[IO.rope["$NoIndex"]]; quit _ FALSE }; -- WritePropName WritePropValue: RefTab.EachPairAction = { relation: DB.Relation = NARROW[val]; prop: DB.Relship = DB.LookupProperty[relation, entity]; propertyValue: Rope.ROPE = IF prop # NIL THEN DB.V2S[DB.GetF[prop, 1]] ELSE NIL; IF propertyValue # NIL THEN { stream.Put[IO.rope["\""]]; stream.Put[IO.rope[Atom.GetPName[NARROW[key]]]]; stream.Put[IO.rope["\""]]; stream.Put[IO.rope[" "], IO.rope[Convert.RopeFromRope[propertyValue]]]; stream.Put[IO.rope["\n"]] }; quit _ FALSE }; -- WritePropValue operationCount: INT _ 0; [] _ RefTab.Pairs[userProps.table, WritePropName]; stream.Put[IO.rope["\n"]]; FOR entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.EntityInfo[entity].name; stream.Put[IO.rope[Convert.RopeFromRope[name]]]; stream.Put[IO.rope["\n"]]; [] _ RefTab.Pairs[userProps.table, WritePropValue]; stream.Put[IO.rope["\n"]]; operationCount _ operationCount+1; IF operationCount = 20 THEN { DB.MarkTransaction[fingerTransaction]; operationCount _ 0 } ENDLOOP; DB.ReleaseEntitySet[setOfEntities]; stream.Close[] }; -- DoWriteMap IF file = NIL THEN RETURN; stream _ FS.StreamOpen[fileName: file, accessOptions: $create ! FS.Error => {stream _ NIL; CONTINUE }]; IF stream = NIL THEN RETURN; CarefullyApply[DoWriteMap] }; -- WriteUserMap CloseDown: ENTRY Booting.CheckpointProc = { CloseTransaction[] }; PlayLog: PUBLIC PROC [items: LIST OF LogEntry] RETURNS [success: BOOLEAN _ FALSE] ~ { DoPlay: INTERNAL PROC[] ~ TRUSTED { logEntry: LogEntry; WHILE items # NIL DO bogus: BOOLEAN _ FALSE; logEntry _ items.first; WITH logEntry: logEntry SELECT FROM StateChange => IF Rope.Equal[logEntry.user, NIL] OR Rope.Equal[logEntry.machine, NIL] THEN LOOP; UserPropChange => IF Rope.Equal[logEntry.user, NIL] THEN LOOP; MachinePropChange => IF Rope.Equal[logEntry.machine, NIL] THEN LOOP; ENDCASE; WITH logEntry: logEntry SELECT FROM StateChange => { user: DB.Entity = DB.DeclareEntity[person, Canonicalize[logEntry.user]]; thisMachine: DB.Entity _ DB.DeclareEntity[machine, Canonicalize[logEntry.machine]]; machineData: DB.Relship _ DB.LookupProperty[userRelation, thisMachine]; IF machineData = NIL THEN machineData _ DB.CreateRelship[userRelation, DB.L2VS[LIST[DB.E2V[thisMachine], DB.E2V[user], DB.E2V[login], DB.T2V[BasicTime.Now[]]]]]; IF logEntry.event = login THEN { DB.SetF[machineData, lastEventTimeIs, DB.T2V[logEntry.time]]; IF NOT DB.EntityEq[DB.V2E[DB.GetF[machineData, lastEventIs]], login] THEN DB.SetF[machineData, lastEventIs, DB.E2V[login]]; DB.SetF[machineData, userIs, DB.E2V[user]] } ELSE { DB.SetF[machineData, lastEventTimeIs, DB.T2V[logEntry.time]]; DB.SetF[machineData, lastEventIs, DB.E2V[logout]]; IF NOT DB.EntityEq[DB.V2E[DB.GetF[machineData, userIs]], user] THEN DB.SetF[machineData, userIs, DB.E2V[user]] } }; MachinePropChange => { m: DB.Entity = DB.DeclareEntity[machine, Canonicalize[logEntry.machine]]; relation: DB.Relation = NARROW[RefTab.Fetch[machineProps.table, Atom.MakeAtom[logEntry.name]].val]; IF relation = NIL THEN {items _ items.rest; LOOP}; BEGIN prop: DB.Relship = DB.LookupProperty[relation, m]; IF prop # NIL THEN DB.SetF[prop, 1, DB.S2V[logEntry.val]] ELSE [] _ DB.CreateRelship[relation, DB.L2VS[LIST[[entity[m]], [rope[logEntry.val]]]]] END }; UserPropChange => { user: DB.Entity = DB.DeclareEntity[person, Canonicalize[logEntry.user]]; DBVersion: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; relation: DB.Relation = NARROW[RefTab.Fetch[userProps.table, Atom.MakeAtom[logEntry.name]].val]; hisLastChangeProp: DB.Relship _ DB.LookupProperty[lastChangedProp, user]; lastChanged: BasicTime.GMT; IF DBVersion # userPropVersion THEN ERROR DB.Aborted; IF relation = NIL THEN {items _ items.rest; LOOP}; IF hisLastChangeProp = NIL THEN hisLastChangeProp _ DB.CreateRelship[lastChangedProp, DB.L2VS[LIST[DB.E2V[user], DB.T2V[BasicTime.Now[]]]]]; lastChanged _ DB.V2T[DB.GetF[hisLastChangeProp, lastChangedTime]]; IF lastChanged # BasicTime.nullGMT AND BasicTime.Period[from: lastChanged, to: logEntry.time] < 0 THEN { items _ items.rest; LOOP }; BEGIN prop: DB.Relship = DB.LookupProperty[relation, user]; IF prop # NIL THEN DB.SetF[prop, 1, DB.S2V[logEntry.val]] ELSE [] _ DB.CreateRelship[relation, DB.L2VS[LIST[[entity[user]], [rope[logEntry.val]]]]] END; DB.SetP[user, lastChangedProp, lastChangedTime, DB.T2V[logEntry.time]] }; AddMachineProp => { DBVersion: INT = DB.V2I[DB.GetF[machinePropVersionRelship, machinePropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; IF DBVersion # logEntry.version THEN {items _ items.rest; LOOP}; IF RefTab.Fetch[machineProps.table, propAtom].found THEN {items _ items.rest; LOOP}; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.DomainInfo[machine].name]; relation: DB.Relation = DB.DeclareProperty[fullName, fingerSegment, machinePropType]; [] _ DB.DeclareEntity[machinePropNames, logEntry.name]; DB.SetF[machinePropVersionRelship, machinePropAttr, DB.I2V[DBVersion+1]]; [] _ RefTab.Store[ machineProps.table, propAtom, relation ]; machinePropVersion _ DBVersion+1 END }; AddUserProp => { DBVersion: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; IF DBVersion # logEntry.version THEN {items _ items.rest; LOOP}; IF RefTab.Fetch[userProps.table, propAtom].found THEN {items _ items.rest; LOOP}; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.DomainInfo[person].name]; relation: DB.Relation = DB.DeclareProperty[fullName, fingerSegment, userPropType]; [] _ DB.DeclareEntity[userPropNames, logEntry.name]; DB.SetF[userPropVersionRelship, userPropAttr, DB.I2V[DBVersion+1]]; [] _ RefTab.Store[ userProps.table, propAtom, relation ]; userPropVersion _ DBVersion+1 END }; DeleteUserProp => { DBVersion: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; relation: DB.Relation = NARROW[RefTab.Fetch[userProps.table, propAtom].val]; IF DBVersion # logEntry.version THEN {items _ items.rest; LOOP}; IF relation = NIL THEN {items _ items.rest; LOOP}; -- not there! BEGIN propEntity: DB.Entity = DB.DeclareEntity[userPropNames, logEntry.name]; DB.DestroyRelation[relation]; DB.DestroyEntity[propEntity]; DB.SetF[userPropVersionRelship, userPropAttr, DB.I2V[DBVersion+1]]; [] _ RefTab.Delete[userProps.table, propAtom]; userPropVersion _ DBVersion + 1 END}; DeleteMachineProp => { DBVersion: INT = DB.V2I[DB.GetF[machinePropVersionRelship, machinePropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; relation: DB.Relation = NARROW[RefTab.Fetch[machineProps.table, propAtom].val]; IF DBVersion # logEntry.version THEN {items _ items.rest; LOOP}; IF relation = NIL THEN {items _ items.rest; LOOP}; -- not there! BEGIN propEntity: DB.Entity = DB.DeclareEntity[machinePropNames, logEntry.name]; DB.DestroyRelation[relation]; DB.DestroyEntity[propEntity]; DB.SetF[machinePropVersionRelship, machinePropAttr, DB.I2V[DBVersion+1]]; [] _ RefTab.Delete[machineProps.table, Atom.MakeAtom[logEntry.name]]; machinePropVersion _ DBVersion + 1 END}; ENDCASE; DB.MarkTransaction[fingerTransaction]; items _ items.rest ENDLOOP }; CarefullyApply[DoPlay]; success _ TRUE }; Booting.RegisterProcs[c: CloseDown]; InitFingerDB[]; TRUSTED{ Process.Detach[FORK WatchDBActivity[]] }; END. "FingerOpsImpl.mesa Last Edited by: Khall, August 31, 1984 1:26:35 pm PDT Last Edited by: Donahue, July 2, 1986 9:55:15 am PDT Last Edited by: Gifford, July 26, 1985 3:52:34 pm PDT Carl Hauser, March 16, 1987 4:09:26 pm PST Ewan Tempero September 10, 1986 6:24:55 pm PDT The name of the machine currently running Finger All of the data for the Finger database: its name and segment type and (if non-NIL), the current transaction open on the database If something goes wrong, this is what gets raised. If the reason is Failure, then can't contact the server; if it's Aborted, then the transaction was aborted after attempting to recover; if it's Error, then it should be a program error (but this may not always be true) The database stores information about people and machines login, logout, and unknown are the three events in the event domain The most important relation is which user is currently using (or being used by) a machine We also record the time at which updates to the properties of any user were last changed; if entries exist on the log of some machine for updates that precede the last change recorded in the database, then we do not perform the changes (they're out of date!) The database records version numbers for the properties of machines and users (so that when recovering from aborts, we don't need to completely rebuild the property tables). These property numbers are updated each time new properties are added to the database. On recovering from an aborted transaction, if the version numbers in the database and program match, then the RefTabs for the machine properties and user properties are enumerated to reset the attribute entries; if the version numbers disagree, then the list of properties must be completely recovered from the new contents of the database the database information to compute the stored machine property version stamp the database information to compute the stored user property version stamp The names of all of the attributes of people and machines are stored in the following domains: the property lists for machines and users a property table consists of a DB.Domain that can be used enumerate all of the properties to be stored in the table and a RefTab containing the DB.Relation for each property ... returns true iff value is of the form *. ... if base is of the form * the replace with the first possible match () and the first mismatch (replace the last character of with the next character). The Domains The property version stamp relations Make sure that only one relationship exists in the relation The property name relations The basic relation between users and machines The index maintained for the database sorts the userRelation by the last event (so that all the currently free machines can be found) and user (so all the machines currently in use by a single user will be given in order) check the version stamps for the properties of machines and users. If they agree, then just enumerate the entries already in the property tables to set up the attributes; otherwise, enumerate the property name relations in the database to determine what the proper set of property entries should be throw away the old property list here fill in the attributes from the contents of the propTable the list in the table is good enough (it may be out of date, but you'll find out when attempting to use one of the properties ... will return a list of user names that match the pattern propVal.val in the property propVal.prop. ... we know that the relation we are dealing with is a binary relation so if there are any Indices on this relation then this attribute is a index attribute. Of course if it's a complicated pattern we have then we just use the normal method. ... will return a list of machine names that match the pattern propVal.val in the property propVal.prop. ... we know that the relation we are dealing with is a binary relation so if there are any Indices on this relation then this attribute is a index attribute. Of course if it's a complicated pattern we have then we just use the normal method. ... returns the list of people that match the pattern. Tries to optimize for the degenerate pattern *. If the time is more than a week, then the machine record is probably bogus the first line contains the list of properties for machines the successive line contain the name and net address of each machine followed by a lines of property value pairs; a blank line separates the data for each machine This nasty little hack is designed to get around the problem that the input file may be a tioga file and so has a root node containing nothing at the beginning of the file.--edt Set the machine property version stamp back to zero, since the entire database is about to be loaded from the contents of the dump file. Throw away all of the current machine property relations Each machine entry begins with the name of the machine, followed by a sequence of line with property value pairs, followed by a blank line This is the equivalent of ReadMachineMap. --edt the first line contains the list of properties for machines the successive line contain the name and net address of each machine followed by a lines of property value pairs; a blank line separates the data for each machine This nasty little hack is designed to get around the problem that the input file may be a tioga file and so has a root node containing nothing at the beginning of the file.--edt Set the user property version stamp back to zero, since the entire database is about to be loaded from the contents of the dump file. Throw away all of the current user property relations Each user entry begins with the name of the machine, followed by a sequence of line with property value pairs, followed by a blank line This is the equivalent of WriteMachineMap that I hacked together. --edt Procedures exported to RemoteFingerOps First a little sanity check. If the entry is bogus, just ignore it and count it as being successfully processed. If the version numbers don't match, then it's not wise to perform the update if this succeeds, the client can be told all is well: the entire request has been processed. Ewan Tempero August 8, 1986 5:09:10 pm PDT Added WriteUserMap and ReadUsermap. Also change the way WriteMachineMap and ReadMachineMap worked. Now they write/read atom PNames rather than the atoms themselves. Ewan Tempero August 27, 1986 2:46:57 pm PDT Implemented indices, specified from ReadUserMap and ReadMachineMap (with resetProperties set). MatchMachineProperty and MatchUserProperty will now use any existing indices to do the lookup. This require a change to the format of the maps ( see FingerSchemaDoc.Tioga ) Carl Hauser, September 12, 1986 10:52:43 am PDT Pulling non-DB operations out into separate implementation for ultimate inclusion in BasicLoadees., DIRECTORY, ListUserProps, ListMachineProps, AddUserProp, AddMachineProp, DeleteUserProp, DeleteMachineProp, SetUserProps, SetMachineProps, GetUserProps, GetMachineProps, GetMachineData, GetUserData, MatchUserProperty, MatchMachineProperty, GetMatchingPersons, GetMatchingMachines, CurrentUsers, FreeMachines Κ4r– "cedar" style˜codešœ™Kšœ5™5K™4K™5K™*K™.—K˜šΟk ˜ K˜K˜Kšœ œœ ˜4K˜K˜Kšœ˜K˜ K˜K˜ Kšœ ˜ K˜Kšœœ˜Kšœ˜Kšœ˜Kšœœ(˜5Kšœ œ˜Kšœœ#˜0K˜K˜Kšœ˜Kšœ˜Kšœ œ&˜4Kšœ œ ˜—K˜šΟn œœ˜K˜š˜Kšœ*œœœn˜³—K˜š˜Kšœ˜—K˜š˜Kšœ˜K˜šœ0™0Kšœœ˜0K˜—šœ™Kšœœb˜~Kšœ(˜(Kšœœ˜ Jšœ0œ˜4—K˜šœŽ™ŽJšž œœœœ˜;—J˜Jšœ œœ˜J˜J˜8J˜Jšžœ œ˜J˜™9K˜Kšœ˜K˜Kšœ œ,˜;K˜šœœœœ ˜4Kšœ œ˜%—K˜Kšœ œ+˜9K˜šœœœœ ˜3Kšœ œ˜$—K˜Kšœœ˜K˜Kšœ œ*˜7K˜šœœœœ ˜2Kšœ œ˜#—K˜Kšœœ˜K˜Kšœœ˜K˜Kšœ œ˜KšœC™CK˜—™Yšœ˜Kšœ œΟc˜2KšœœŸ'˜>Kšœ œŸ-˜IKšœœŸ˜?Kš œœ œœœ ˜ΕK˜Kšœ œŸ/˜D——K˜šœ‚™‚Kšœœ ˜Kšœœ˜—K˜KšœΪ™Ϊ˜Kš œœ œœ5œ ˜nK˜Kš œœ œœ4œ ˜jK˜Kš œ œ œœœ ˜MK˜Kš œœ œœ4œ ˜kK˜Kšœ%œ˜.K˜šœM™MKšœ%˜%Kšœœ˜Kšœ,Ÿ&˜R—K˜šœJ™JKšœ"˜"Kšœœ˜Kšœ)Ÿ&˜O——K™™^Kšœœ˜K˜Kšœœ˜—K˜šœ)™)Kš œœœœ œ˜IKšœœŒ™­Kšœ7˜7Kšœ4˜4—K˜š ž œœœœžœœ˜GKšœΟbœ™5Kšœ œ#˜/K™Kšœ/œ ˜FKšœŸ˜K˜—š ž œœ œœœ˜FK™²K•StartOfExpansion[base: ROPE]˜Kšœ œ!˜-Kšœ œ/œ˜CK˜–-[s1: ROPE, s2: ROPE, case: BOOL _ TRUE]šœœ˜'Kšœ œ˜Kš˜K˜K˜—šœœ˜K– [base: ROPE, index: INT _ 0]šœ œ.˜>Kšœ6˜6K– [c: CHAR]šœoœ˜uK˜šœœœ˜+Kšœœ˜——KšœŸ˜K˜—šžœœ˜šœœ˜ Jšœ˜Jšœ˜Jš˜—Jšœ˜J˜—šžœœœ˜!Jšœœœ˜Jšœœ œœ˜DJšœ ˜J˜—J˜šžœœœ˜&Jšœœœ˜Jšœœœœ˜'šœ%˜'Jšœœœ˜1Jšœœ œ˜#—Jšœœœ%˜=Jšœœ˜J˜—šžœœœ˜%Jšœœ˜šœœœ˜!Jšœ%œ˜AJšœœ˜(J˜——šžœ œ˜.Kšœœœ˜š˜šœ˜ Jšœ œ˜Jšœ œ ˜Jšœ˜—Jšœ œœ˜Jš œœ œ œœ˜Aš˜Jšœœœœ˜2J˜Jšœ˜—Jš œœ œœŸ˜2š˜Jšœœ œ ˜"JšœŸ˜$J˜Jš˜—š˜Jšœœ˜:Jšœ œ˜&Jšœ!œ˜=—Jšœ˜—K˜—J˜šžœœœ˜&Jšœœœœ˜'Jš œ&œœ œ œ˜WJšœœ˜—J˜šž œœœ˜š˜Kšœœœ˜Jšœ˜ JšœŒ˜ŽJšœ˜—Kšœ˜—K˜šž œœœ˜ šœ ™ Kšœ œ(˜3Kšœ œ)˜5Kšœœ'˜1K˜Kšœœ œ ˜8Kšœ œ œ!˜:Kšœ œ œ"˜<—K™šœ$™$KšœœV˜nKšœ;™;Kšœœ#˜Ašœœ˜'Kš œœ œ-œœ˜w—Kšœœ(˜KK˜KšœœS˜hKšœœ ˜;šœœ˜$Kš œœ œ*œœ˜q—Kšœœ%˜EK˜KšœœW˜k—K™šœ™Kšœœ=˜RKšœ&˜&K˜Kšœœ:˜LKšœ ˜ —K™šœ-™-KšœœU˜f—K˜šœέ™έKšœ œœœ!˜V—K™Kšœ«™«š˜Kšžœœœœ3˜RKšž œœœœ-˜Išœ%œ˜-K˜%Kšœ ™ Kšœ"˜"Kšœ&˜&—šœœ˜'Kšœ"˜"Kšœ˜Kšœ ˜ —Kšœ˜—Kšœ%˜%Kšœ ˜ K˜—K˜šžœœœ˜@Kšœ œ œ"˜<š œœ œœœœ˜XKšœœœ˜/KšœIœ˜MKšœ˜ ——K˜šž œœœ#œ ˜MKšœ>™>Kšœœ œ˜-šžœ˜#Kšœ œœ˜-Kšœœœ˜DKš œœ œAœœ8œ ˜±Kšœ8˜8Kšœœ˜—Kšœ/˜/—K˜šž œœœœ œœœ˜@Kšœ}™}šžœœœ˜Kšœœœ˜šžœ˜!Kšœ œœœ˜9—Kšœ-˜-K˜—Kšœ#˜#K˜Kšœ˜—K˜šžœœœœ œœœ˜Cšžœ œ˜Kšœœœ˜šžœ˜!Kšœ œœœ˜9—Kšœ0˜0K˜—Kšœ#˜#K˜Kšœ˜—K˜šž œœœ œ˜-šžœ œ˜Kšœœœ˜KšœœJ˜lK˜—J˜Kšœ%˜%—K˜šžœœœ œ˜0šžœœœ˜Kšœœœ˜KšœœO˜gK˜—K˜Kšœ%˜%—K˜šžœœœ œ˜0šžœœœ˜Kšœœœ˜KšœœM˜eK˜—K˜Kšœ%˜%—K˜šžœœœ œ˜3šžœœœ˜Kšœœœ˜KšœœR˜jK˜—K˜Kšœ%˜%K˜—š ž œœœ œ œœ˜Ršžœœœ˜Kšœœœ˜Kšœœ˜%š œœœœœ˜8Kšœœ€˜—Kšœ˜—K˜—K˜Kšœ%˜%—K˜š žœœœœ œœ˜Mšžœœœ˜Kšœœœ˜š œœœœœ˜8Kšœœ˜–Kšœ˜—K˜—K˜Kšœ%˜%—K˜šž œœœ œœœœ˜Ošž œœœ˜Kšœœ œ*˜@šžœ˜#Kšœ œ œ˜$Kšœ œ œ"˜;Kšœœœ œœœœœœ˜XKšœœœœ$˜SKšœœ˜—Kšœ œœœ˜Kšœ/˜/—Kšœ$Ÿ%˜IKšœ˜K˜—šžœœœœœœœ˜Yšž œœœ˜Kšœœ œ2˜Hšžœ˜#Kšœ œ œ˜$Kšœ œ œ"˜;Kšœœœ œœœœœœ˜XKšœœœœ$˜SKšœœ˜—Kšœ œœœ˜Kšœ2˜2—Kšœ#˜#Kšœ˜—K˜šžœœœœ˜-Jšœœœ˜š˜šœ˜ Jšœ œ˜Jšœ œ ˜Jšœ˜—Jšœ œœ˜Jš œœ œ œœ˜Aš˜Jšœœœœ˜2J˜Jšœ˜Jšœ˜—Jš œœ œœŸ˜2š˜Jšœœ œ ˜"JšœŸ˜$J˜Jšœ˜Jš˜—š˜Jšœ+œ˜FJšœœ˜6Jšœ-œ˜J—Jšœ˜—Jšœ˜—J˜šžœœ˜KšœMœ˜XK˜—K˜š ž œœœ œœ œ˜HKšœœœ˜šžœœœ˜Kšœ œ5œ˜F—Kšœ˜—K˜š ž œœœ œœ œ˜Išžœœœ˜Kšœ œ-œ˜>—Kšœ˜—K˜šžœœœ œœ*œ œœ˜~šžœœœ˜Kšœ œ œ,˜FKšœ œ œ*˜Ešœœœ˜#Kš œ œœœœ$œœ˜‹—Kš œ œœ œœ(œœ˜’Kšœœœ$˜4šœœ˜!Kšœœ œœ#˜A——Kšœ#˜#Kšœ˜—K˜šž œœœ œœœœœ˜Všžœœœ˜Kšœœ œ+˜?Kšœœœ5˜Uš˜Kšœœœ!˜4š œ œ œœœ œ˜mKš œœœ œœ/˜YKšœ˜—Kšœ˜—Kšœ#˜%—Kšœ#˜#Kšœ˜—K˜šžœœœœ œœœ˜XKšœ<  œ  œ™fšžœœœ˜Kšœ œ œ2˜PKš œ œœœ œ˜8Kšœ œ ˜K™K™ρšœœœœ˜8Kšœœœ˜K™Kšœ'˜'Kšœ œAœœ7˜š œœ œœœœ˜]K–([t: DBDefs.Relship, field: CARDINAL]š œ œœœœ(˜SKšœ˜—Kš˜K˜—šœ˜Kšœ œ$œœ˜G—K˜š˜Kšœœœ˜0š œœ œœœœ˜]šœ œœ,˜GKš œ œœ œœ˜C—Kšœ˜—Kšœ˜Kšœ˜——Kšœ#˜#KšœŸ˜-K˜—šžœœœœ œœœ˜[Kšœ?  œ  œ™iK˜šžœœœ˜Kšœ œ œ5˜SKš œ œœœ œ˜8Kšœ œ ˜K™K™ρšœœœœ˜8Kšœœœ˜K™K˜'Kšœ œAœœS˜¬š œœ œœœœ˜]K–[e: DBDefs.Entity]š œ œœœœ)˜QKšœ˜—Kš˜K˜—šœ˜Kšœ œ$œœ˜G—K˜š˜Kšœœœ˜0š œœ œœœœ˜]–6[pattern: ROPE, object: ROPE, case: BOOL _ TRUE]š œ8œœœ˜eKš œ œœ œœ˜D—Kš˜—Kšœ˜—Kšœ˜!—Kšœ#˜#KšœŸ˜0—K˜šžœœœœœ œœœ˜\K™oK™šžœœœ˜Kšœ œ˜(K–h[d: DBDefs.Domain, lowName: ROPE _ NIL, highName: ROPE _ NIL, start: DBCommon.FirstLast _ First]šœœ œ[˜{K–°[pattern: ROPE, literal: BOOL _ FALSE, word: BOOL _ FALSE, ignoreCase: BOOL _ FALSE, addBounds: BOOL _ FALSE, patternStart: INT _ 0, patternLen: INT _ 2147483647]šœMœ œ˜dK˜š˜Kšœœœ!˜4šœœ˜"š œ œ œœœ œ˜hKšœ œœ˜-Kšœ œ˜Kšœ˜—K˜Kšœ˜š œ œ œœœ œ˜hKšœ œœ˜-Kšœ)œ œ˜KKš˜—K˜—Kšœ ˜"Kšœ˜——Kšœ#˜#K˜Kšœ˜—K˜šžœœœœœ œœœ˜]šžœœœ˜Kšœ œ˜(Kšœœ œ\˜|KšœMœ œ˜dš˜Kšœœœ"œ˜@šœœ˜"š œ œ œœœ œ˜hKšœ œœ˜-Kšœ œ˜Kšœ˜—K˜Kšœ˜š œ œ œœœ œ˜hKšœ œœ˜-Kšœ)œ œ˜KKš˜—K˜K˜—Kšœ ˜"Kšœ˜——Kšœ#˜#Kšœ˜—K˜šž œœœœ œœœ˜DKšœœœ˜+Kšœœ˜%Kšœœ,˜Ešžœœœ˜Kš œ œœ)œœœ!œ6˜­š˜Kšœœœ˜1š œ œ œœœ œ˜cKš œ œœ œœ˜FKšœ œ˜Kš˜—Kšœ˜—Kšœ ˜"—Kšœ#˜#Kšœ˜—K˜šž œœœœœœœ˜GKšœœœ˜,Kšœœ˜%Kšœœ,˜EKšœJ™Jšžœœœ˜Kš œ œœ)œœœ"œ6˜¬š˜Kšœœœœ˜;š œœ œœœœ˜[Kš œœœ œœ+˜UKš˜—Kšœ˜—Kšœ˜ —Kšœ#˜#Kšœ˜—K˜K˜K˜š ž œœ œœœ˜GKšœ œœ˜-šœœœ˜!KšœA˜AKšœ˜—Kšœ/˜/—K˜J˜š žœœœ œœœ˜SJšœœœ˜#šž œœœ˜Kšœ;™;Kšœ’™’Kšœœœ˜"Jšœ ˜ J˜J™±Kšœœœ!˜Kš œœœœœ˜6Kš œœœœœœ˜]Kšœ˜—K˜"šœœ˜Kšœ$˜&K˜—Kš˜—Kšœ˜—Kš˜—Kšœ˜——Jšœœœœ˜šœ œ0˜?Jšœœœœ˜-—Kšœœœœ˜ K˜Kšœ˜—J˜K˜šžœœœ œ˜2Jšœœœ˜šž œœœ˜Kšœœ œ˜7Kšœœ˜K˜šž œ˜(Kšœ œ œ˜$K˜Kšœ œ ˜Kšœ œ ˜Kšœ œœ ˜0Kšœ œ ˜K–[r: DBDefs.Relation]šœœœœ œœ œ˜pKšœœŸ˜ K˜—šžœ˜)Kšœ œ œ˜$Kšœœ œ"˜7Kšœœœœœœœœœ˜PK˜šœœœ˜Kšœ œ ˜Kšœ œœ ˜0Kšœ œ ˜Kšœ œ œ,˜GKšœ œ˜—KšœœŸ˜!K˜—Kšœœ˜Jšœ5˜5Jšœ œ ˜š œ œœœ œ˜]Kšœ œœ˜-Kšœ œ#˜0Kšœ œ ˜Jšœ6˜6Kšœ œ ˜K˜"šœœ˜Kšœ$˜&K˜—Kšœ˜—Kšœ!˜#KšœŸ˜"K˜—Jšœœœœ˜šœ œ2˜=Jšœœœœ˜)—Jšœ œœœ˜K˜KšœŸ˜—J˜Jš ž œœœ œœœ˜Lšœ˜J™/Jšœœœ˜#šž œœœ˜Kšœ;™;Kšœ’™’Kšœœœ˜"Jšœ ˜ J˜J™±Kšœœœ!˜œœŸ˜[š˜Kšœœœ˜IKšœ œ œ8˜RK˜Kšœœ)˜0KšœG˜GK–6[r: DBDefs.Relation, fields: DBDefs.FieldSequence]š œœœ#œœ˜IKš˜——Kš˜Kšœ˜—Kšœ‡™‡š˜Kšœœœ˜"Kšœœ˜Kšœ œ˜Kšœœ˜Kšœœ˜Kšœœ˜š˜Kšœ ˜ Kšœœœ˜(K˜(Kšœ œ/˜>š˜Kšœ ˜ Kš œœ œ œœ˜7Kšœœœ˜(Kšœ(˜(Kšœ7˜7š˜Kšœ œ œ=˜[Kšœ œœœ˜š˜Kšœœ œ&˜;Kš œœœœœ˜6Kš œœœœœœ˜ZKšœ˜—K˜"šœœ˜Kšœ$˜&K˜—Kš˜—Kšœ˜—Kš˜—KšœŸ ˜——Jšœœœœ˜šœ œ0˜?Jšœœœœ˜-—Kšœœœœ˜ K˜KšœŸ˜K˜—šž œœœ œ˜/Jšœ œ™G—šœ˜Jšœœœ˜K˜šž œœœ˜Kšœœ œ˜6Kšœœ˜K˜šž œ˜(Kšœ œ œ˜$K˜Kšœ œ ˜Kšœ œ ˜Kšœ œœ ˜0Kšœ œ ˜Kšœ œ ˜K–[r: DBDefs.Relation]šœœœœ œœ œ˜nKšœœŸ˜ K˜—šžœ˜)Kšœ œ œ˜$Kšœœ œ"˜7Kšœœœœœœœœœ˜PK˜šœœœ˜Kšœ œ ˜Kšœ œœ ˜0Kšœ œ ˜Kšœ œ œ,˜GKšœ œ˜—KšœœŸ˜!K˜—Kšœœ˜Jšœ2˜2Jšœ œ ˜š œ œœœ œ˜]Kšœ œœ˜-Kšœ œ#˜0Kšœ œ ˜Jšœ3˜3Kšœ œ ˜K˜"šœœ˜Kšœ$˜&K˜—Kšœ˜—Kšœ!˜#KšœŸ ˜—J˜Jšœœœœ˜šœ œ2˜=Jšœœœœ˜)—Jšœ œœœ˜K˜KšœŸ˜—J˜Jšž œœ1˜AJ˜J™&šžœœœ œœ œ œœ˜UK˜šžœœœœ˜#K˜šœ œ˜Kšœœœ˜Kšœ˜K™qšœœ˜#Kš œœœœœœœ˜`Kš œœœœœ˜>Kš œœœœœ˜DKš˜—šœœ˜#šœ˜Kšœœ œ4˜HKšœ œ œ8˜SKšœ œ œ+˜Gšœœœ˜Kšœœœœœœ œ œ˜‡—šœœ˜ Kšœ$œ˜=š œœœ œœ(˜DKšœœ œ ˜6—Kšœœ ˜,—šœ˜Kšœ$œ˜=Kšœ œ˜2š œœœ œœ"˜>Kšœœœ˜4———˜Kšœœ œ8˜IKšœ œ œE˜cKšœ œœœ˜2š˜Kšœœ œ˜2Kš œœœœœ˜9Kšœœœœ%˜VKšœ˜——˜Kšœœ œ4˜HKšž œœœœ-˜GKšœ œ œB˜`Kšœœ œ'˜IKšœœ˜Kšœœœœ ˜5Kšœ œœœ˜3šœœ˜Kš œœ œœœ œ˜l—Kšœœœ+˜Bšœ ˜"Kšœ<˜CKšœœ˜—š˜Kšœœ œ ˜5Kš œœœœœ˜9Kšœœœœ(˜YKšœ˜—Kšœ.œ˜I—˜Kšž œœœœ3˜MKšœ œ ˜.Kšœœœ˜@KšœL™Lšœ2˜8KšœœŸ˜,—š˜Kšœœœ˜NKšœ œ œ;˜UKšœœ0˜7Kšœ2œ˜IKšœ<˜