DIRECTORY Atom, BasicTime USING [GMT, Now, nullGMT, Period], Booting, Convert, DB, FingerLog, FingerOps, FS USING[StreamOpen, Error], Idle USING [IdleReason, IdleHandler, RegisterIdleHandler], IO, PupDefs USING [GetMyName, PupAddressToRope, GetPupAddress], Process USING [Detach, Ticks, SecondsToTicks, Pause], RefText USING[New], RefTab, Rope, TextFind USING [CreateFromRope, Finder, SearchRope], UserCredentials USING [Get]; FingerOpsImpl: CEDAR MONITOR IMPORTS Atom, BasicTime, Booting, Convert, DB, FingerLog, FS, Idle, IO, PupDefs, Process, RefTab, RefText, Rope, TextFind, UserCredentials EXPORTS FingerOps = BEGIN OPEN FingerOps, FingerLog, DB; machineName: Rope.ROPE = PupDefs.GetMyName[]; machineAddress: Rope.ROPE = PupDefs.PupAddressToRope[PupDefs.GetPupAddress[[0,0], machineName]]; fingerSegmentRope: Rope.ROPE = "[Luther.Alpine]Finger.segment"; fingerSegment: DB.Segment = $Finger; fingerSegmentNumber: NAT = 260B; fingerTransaction: DB.Transaction _ NIL; FingerError: PUBLIC ERROR[reason: FingerOps.Reason] ~ CODE; idle: BOOL _ FALSE; activity: BOOL _ TRUE; ticksToWait: Process.Ticks _ Process.SecondsToTicks[5*60]; stateLog: LIST OF LogEntry _ NIL; person, machine: DB.Domain; userRelation: DB.Relation; machineIs: DB.Attribute; -- a key of the relation actualNameIs: DB.Attribute; -- the Pup address of the machine userIs: DB.Attribute; -- the person performing the last event lastEventIs: DB.Attribute; -- whether the last event was login or logout lastEventTimeIs: DB.Attribute; -- when the last event occurred lastChangedProp: DB.Attribute; machinePropVersion, userPropVersion: INT _ 0; machinePropRelation: DB.Relation; machinePropAttr: DB.Attribute; machinePropVersionRelship: DB.Relship; -- the single relship of this relation userPropRelation: DB.Relation; userPropAttr: DB.Attribute; userPropVersionRelship: DB.Relship; -- the single relship of this relation userPropNames: DB.Relation; userPropNameIs: DB.Attribute; machinePropNames: DB.Relation; machinePropNameIs: DB.Attribute; login: INT = 1; logout: INT = 0; PropertyTable: TYPE = RECORD[names: DB.Relation _ NIL, nameAttr: DB.Attribute _ NIL, table: RefTab.Ref]; machineProps: PropertyTable _ [table: RefTab.Create[]]; userProps: PropertyTable _ [table: RefTab.Create[]]; AttributeRecord: TYPE = REF AttrRecordObject; AttrRecordObject: TYPE = RECORD[attr: DB.Attribute, of: DB.Relation]; WatchDBActivity: PROC[] = { WHILE TRUE DO Process.Pause[ticksToWait]; CheckConnection[] ENDLOOP }; CheckConnection: ENTRY PROC[] = { ENABLE UNWIND => NULL; IF NOT activity THEN CloseTransaction[]; activity _ FALSE; }; CloseTransaction: INTERNAL PROC [] = { caughtAborted: BOOL _ FALSE; BEGIN ENABLE DB.Error, DB.Failure => CONTINUE; IF fingerTransaction # NIL THEN DB.CloseTransaction[fingerTransaction ! DB.Aborted => { caughtAborted _ TRUE; CONTINUE }]; IF caughtAborted THEN DB.AbortTransaction[fingerTransaction] END; fingerTransaction _ NIL }; OpenTransaction: INTERNAL PROC [] = { IF fingerTransaction = NIL THEN DB.OpenTransaction[fingerSegment]; fingerTransaction _ DB.GetSegmentInfo[fingerSegment].trans }; AbortTransaction: INTERNAL PROC [] = { IF fingerTransaction # NIL THEN DB.AbortTransaction[fingerTransaction! DB.Failure, DB.Error => CONTINUE]; fingerTransaction _ NIL }; InitFingerDB: ENTRY PROC = BEGIN ENABLE UNWIND => NULL; DB.Initialize[nCachePages: 256]; DB.DeclareSegment[fingerSegmentRope, fingerSegment, fingerSegmentNumber, FALSE] END; ResetSchema: INTERNAL PROC[] ~ { OpenTransaction[]; IF NOT DB.Null[person] THEN RETURN; person _ DB.DeclareDomain["person", fingerSegment]; machine _ DB.DeclareDomain["machine", fingerSegment]; machinePropRelation _ DB.DeclareRelation[name: "MachineVersion", segment: fingerSegment]; machinePropAttr _ DB.DeclareAttribute[r: machinePropRelation, name: "stamp", type: IntType]; BEGIN machineProps: DB.RelshipSet = DB.RelationSubset[machinePropRelation]; machinePropVersionRelship _ DB.NextRelship[machineProps]; IF machinePropVersionRelship = NIL THEN machinePropVersionRelship _ DB.DeclareRelship[r: machinePropRelation]; DB.ReleaseRelshipSet[machineProps] END; userPropRelation _ DB.DeclareRelation[name: "UserVersion", segment: fingerSegment]; userPropAttr _ DB.DeclareAttribute[r: userPropRelation, name: "stamp", type: IntType]; BEGIN userProps: DB.RelshipSet = DB.RelationSubset[userPropRelation]; userPropVersionRelship _ DB.NextRelship[userProps]; IF userPropVersionRelship = NIL THEN userPropVersionRelship _ DB.DeclareRelship[r: userPropRelation] END; lastChangedProp _ DB.DeclareProperty[relationName: "lastChanged", of: person, is: TimeType, segment: fingerSegment]; machinePropNames _ DB.DeclareRelation[name: "MachineProps", segment: fingerSegment]; machinePropNameIs _ DB.DeclareAttribute[r: machinePropNames, name: "name", type: RopeType]; machineProps.names _ machinePropNames; machineProps.nameAttr _ machinePropNameIs; userPropNames _ DB.DeclareRelation[name: "UserProps", segment: fingerSegment]; userPropNameIs _ DB.DeclareAttribute[r: userPropNames, name: "name", type: RopeType]; userProps.names _ userPropNames; userProps.nameAttr _ userPropNameIs; userRelation _ DB.DeclareRelation[name: "UserInfo", segment: fingerSegment]; machineIs _ DB.DeclareAttribute[r: userRelation, name: "machine", type: machine, uniqueness: Key]; actualNameIs _ DB.DeclareAttribute[r: userRelation, name: "actualName", type: RopeType]; userIs _ DB.DeclareAttribute[r: userRelation, name: "user", type: person]; lastEventIs _ DB.DeclareAttribute[r: userRelation, name: "lastEvent", type: IntType]; lastEventTimeIs _ DB.DeclareAttribute[r: userRelation, name: "lastEventTime", type: TimeType]; [] _ DB.DeclareIndex[userRelation, LIST[lastEventIs, userIs]]; 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.RelshipSet = DB.RelationSubset[propTable.names]; FOR attrRel: DB.Relship _ DB.NextRelship[attrSet], DB.NextRelship[attrSet] UNTIL attrRel = NIL DO propName: Rope.ROPE = DB.V2S[DB.GetF[attrRel, propTable.nameAttr]]; [] _ RefTab.Store[x: propTable.table, key: Atom.MakeAtom[propName], val: NIL] ENDLOOP}; SetAttributes: INTERNAL PROC[propTable: PropertyTable, domain: DB.Domain] = { EachProp: RefTab.EachPairAction = { name: Rope.ROPE = Atom.GetPName[NARROW[key]]; fullName: Rope.ROPE = Rope.Concat[name, DB.NameOf[domain]]; propRelation: DB.Relation = DB.DeclareRelation[fullName, fingerSegment]; propOfAttr: DB.Attribute = DB.DeclareAttribute[propRelation, "of", domain, Key]; propIsAttr: DB.Attribute = DB.DeclareAttribute[propRelation, "is", RopeType]; [] _ RefTab.Store[ propTable.table, key, NEW[AttrRecordObject _ [attr: propIsAttr, of: propRelation]] ]; quit _ FALSE }; [] _ RefTab.Pairs[propTable.table, EachProp] }; ListUserProps: PUBLIC ENTRY PROC[] RETURNS[propList: LIST OF ATOM] = { ENABLE UNWIND => NULL; EachProp: RefTab.EachPairAction = { propList _ CONS[NARROW[key], propList]; quit _ FALSE }; IF stateLog # NIL THEN PlayLog[]; [] _ RefTab.Pairs[userProps.table, EachProp]; }; ListMachineProps: PUBLIC ENTRY PROC[] RETURNS[propList: LIST OF ATOM] = { ENABLE UNWIND => NULL; EachProp: RefTab.EachPairAction = { propList _ CONS[NARROW[key], propList]; quit _ FALSE }; IF stateLog # NIL THEN PlayLog[]; [] _ RefTab.Pairs[machineProps.table, EachProp]; }; AddUserProp: PUBLIC ENTRY PROC[name: Rope.ROPE] = { ENABLE UNWIND => NULL; Log[ logEntry: NEW[AddUserProp LogEntryObject _ [AddUserProp [name: name, version: userPropVersion]]] ]; PlayLog[] }; AddMachineProp: PUBLIC ENTRY PROC[name: Rope.ROPE] = { ENABLE UNWIND => NULL; Log[ NEW[AddMachineProp LogEntryObject _ [AddMachineProp[name: name, version: machinePropVersion]]] ]; PlayLog[] }; DeleteUserProp: PUBLIC ENTRY PROC[name: Rope.ROPE] = { ENABLE UNWIND => NULL; Log[ NEW[DeleteUserProp LogEntryObject _ [ DeleteUserProp[name: name, version: userPropVersion]]] ]; PlayLog[] }; DeleteMachineProp: PUBLIC ENTRY PROC[name: Rope.ROPE] = { ENABLE UNWIND => NULL; Log[ NEW[DeleteMachineProp LogEntryObject _ [DeleteMachineProp[name: name, version: machinePropVersion]]] ]; PlayLog[] }; SetUserProps: PUBLIC ENTRY PROC [props: LIST OF FingerOps.PropPair] = { ENABLE UNWIND => NULL; now: BasicTime.GMT = BasicTime.Now[]; currentUser: Rope.ROPE = UserCredentials.Get[].name; FOR p: LIST OF PropPair _ props, p.rest UNTIL p = NIL DO Log[NEW[UserPropChange LogEntryObject _ [UserPropChange [user: currentUser, name: Atom.GetPName[p.first.prop], val: p.first.val, time: now]]] ] ENDLOOP; PlayLog[] }; SetMachineProps: PUBLIC ENTRY PROC[props: LIST OF PropPair] = { ENABLE UNWIND => NULL; FOR p: LIST OF PropPair _ props, p.rest UNTIL p = NIL DO Log[NEW[MachinePropChange LogEntryObject _ [ MachinePropChange[name: Atom.GetPName[p.first.prop], val: p.first.val]]] ] ENDLOOP; PlayLog[] }; GetUserProps: PUBLIC ENTRY PROC[user: Rope.ROPE] RETURNS[props: LIST OF PropPair] = { ENABLE UNWIND => NULL; DoGetProps: INTERNAL PROC[] = { entity: DB.Entity = DB.DeclareEntity[person, user, OldOnly]; EachProp: RefTab.EachPairAction = { attr: DB.Attribute = DB.V2E[NARROW[val, AttributeRecord].attr]; propertyValue: Rope.ROPE = DB.V2S[DB.GetP[entity, DB.V2E[attr]]]; props _ CONS[NEW[PropPairObject _ [prop: NARROW[key], val: propertyValue]], props]; quit _ FALSE }; IF entity = NIL THEN RETURN; [] _ RefTab.Pairs[userProps.table, EachProp] }; IF stateLog # NIL THEN PlayLog[]; -- make sure no pending updates exist CarefullyApply[DoGetProps] }; GetMachineProps: PUBLIC ENTRY PROC[machineName: Rope.ROPE] RETURNS[props: LIST OF PropPair] = { ENABLE UNWIND => NULL; DoGetProps: INTERNAL PROC[] = { entity: DB.Entity = DB.DeclareEntity[machine, machineName, OldOnly]; EachProp: RefTab.EachPairAction = { attr: DB.Attribute = DB.V2E[NARROW[val, AttributeRecord].attr]; propertyValue: Rope.ROPE = DB.V2S[DB.GetP[entity, attr]]; props _ CONS[NEW[PropPairObject _ [prop: NARROW[key], val: propertyValue]], props]; quit _ FALSE }; IF entity = NIL THEN RETURN; [] _ RefTab.Pairs[machineProps.table, EachProp] }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGetProps] }; CarefullyApply: INTERNAL PROC [proc: PROC[]] ~ { ENABLE BEGIN DB.Error => GOTO Error; DB.Failure => GOTO Failure; END; aborted: BOOL _ FALSE; BEGIN ENABLE DB.Aborted => { aborted _ TRUE; CONTINUE }; ResetSchema[]; activity _ TRUE; proc[] END; IF NOT aborted THEN RETURN; -- no aborted occurred AbortTransaction[]; -- now try again BEGIN ENABLE DB.Aborted => GOTO Aborted; ResetSchema[]; proc[] END EXITS Error => { CloseTransaction[]; ERROR FingerError[Error] }; Failure => { CloseTransaction[]; ERROR FingerError[Failure] }; Aborted => { AbortTransaction[]; ERROR FingerError[Aborted] } }; GetMachineData: PUBLIC ENTRY PROC [name: Rope.ROPE] RETURNS[actualName: Rope.ROPE, lastChange: StateChange, time: BasicTime.GMT, user: Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { theMachine: DB.Entity = DB.DeclareEntity[machine, name]; machineRel: DB.Relship = DB.DeclareRelship[userRelation, LIST[AttributeValue[machineIs, theMachine]]]; actualName _ DB.V2S[DB.GetF[machineRel, actualNameIs]]; lastChange _ IF DB.V2I[DB.GetF[machineRel, lastEventIs]] = login THEN FingerOps.StateChange[login] ELSE FingerOps.StateChange[logout]; time _ DB.V2T[DB.GetF[machineRel, lastEventTimeIs]]; user _ DB.NameOf[DB.V2E[DB.GetF[machineRel, userIs]]] }; CarefullyApply[DoGet] }; GetUserData: PUBLIC ENTRY PROC [name: Rope.ROPE] RETURNS[machineList: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { user: DB.Entity = DB.DeclareEntity[person, name]; attrList: LIST OF DB.AttributeValue = LIST[[userIs, user]]; usedMachines: DB.RelshipSet = DB.RelationSubset[userRelation, attrList]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[usedMachines]; FOR machines: DB.Relship _ DB.NextRelship[usedMachines], DB.NextRelship[usedMachines] UNTIL machines = NIL DO machineList _ CONS[DB.NameOf[DB.V2E[DB.GetF[machines, machineIs]]], machineList] ENDLOOP; END; DB.ReleaseRelshipSet[usedMachines] }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGet] }; MatchMachineProperty: PUBLIC ENTRY PROC[propVal: PropPair] RETURNS[result: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { attr: AttributeRecord = NARROW[RefTab.Fetch[machineProps.table, propVal.prop].val]; relation: DB.Relation = attr.of; machineAttribute: DB.Attribute = DB.DeclareAttribute[relation, "of"]; relships: DB.RelshipSet = DB.RelationSubset[relation, LIST[AttributeValue[attr.attr, propVal.val]]]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[relships]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO machine: DB.Entity = DB.V2E[DB.GetF[next, machineAttribute]]; result _ CONS[DB.NameOf[machine], result]; ENDLOOP END }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGet] }; GetMatchingPersons: PUBLIC ENTRY PROC [pattern: Rope.ROPE] RETURNS [result: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { setOfEntities: DB.EntitySet = DB.DomainSubset[person]; finder: TextFind.Finder = TextFind.CreateFromRope[pattern: pattern, ignoreCase: TRUE]; BEGIN ENABLE UNWIND => {DB.ReleaseEntitySet[setOfEntities]; CONTINUE}; FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.NameOf[entity]; IF TextFind.SearchRope[finder, name].found THEN result _ CONS[name, result] ENDLOOP; DB.ReleaseEntitySet[setOfEntities] END }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGet] }; GetMatchingMachines: PUBLIC ENTRY PROC [pattern: Rope.ROPE] RETURNS [result: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { setOfEntities: DB.EntitySet = DB.DomainSubset[machine]; finder: TextFind.Finder = TextFind.CreateFromRope[pattern: pattern, ignoreCase: TRUE]; BEGIN ENABLE UNWIND => {DB.ReleaseEntitySet[setOfEntities]; CONTINUE}; FOR entity: DB.Entity _ DB.NextEntity[setOfEntities], DB.NextEntity[setOfEntities] UNTIL entity = NIL DO name: Rope.ROPE = DB.NameOf[entity]; IF TextFind.SearchRope[finder, name].found THEN result _ CONS[name, result] ENDLOOP; DB.ReleaseEntitySet[setOfEntities] END }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGet] }; MatchUserProperty: PUBLIC ENTRY PROC[propVal: PropPair] RETURNS[result: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; DoGet: INTERNAL PROC[] = { attr: AttributeRecord = NARROW[RefTab.Fetch[userProps.table, propVal.prop].val]; relships: DB.RelshipSet = DB.RelationSubset[attr.of, LIST[AttributeValue[attr.attr, propVal.val]]]; userAttribute: DB.Attribute = DB.DeclareAttribute[attr.of, "of"]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[relships]; FOR next: DB.Relship _ DB.NextRelship[relships], DB.NextRelship[relships] UNTIL next = NIL DO user: DB.Entity = DB.V2E[DB.GetF[next, userAttribute]]; result _ CONS[DB.NameOf[user], result]; ENDLOOP; DB.ReleaseRelshipSet[relships] END }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[DoGet] }; CurrentUsers: PUBLIC ENTRY PROC[] RETURNS[userList: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; GetUsers: INTERNAL PROC[] = { activeSet: DB.RelshipSet = DB.RelationSubset[userRelation, LIST[AttributeValue[lastEventIs, DB.I2V[login]]]]; BEGIN ENABLE UNWIND => DB.ReleaseRelshipSet[activeSet]; thisUser: Rope.ROPE; FOR active: DB.Relship _ DB.NextRelship[activeSet], DB.NextRelship[activeSet] UNTIL active = NIL DO user: Rope.ROPE = DB.NameOf[DB.V2E[DB.GetF[active, userIs]]]; IF NOT Rope.Equal[thisUser, user] THEN { thisUser _ user; userList _ CONS[user, userList] } ENDLOOP END; DB.ReleaseRelshipSet[activeSet] }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[GetUsers] }; FreeMachines: PUBLIC ENTRY PROC[] RETURNS[machineList: LIST OF Rope.ROPE] = { ENABLE UNWIND => NULL; ListFree: INTERNAL PROC[] = { freeSet: DB.RelshipSet = DB.RelationSubset[userRelation, LIST[AttributeValue[lastEventIs, DB.I2V[logout]]]]; BEGIN ENABLE UNWIND => {DB.ReleaseRelshipSet[freeSet]; CONTINUE}; FOR free: DB.Relship _ DB.NextRelship[freeSet], DB.NextRelship[freeSet] UNTIL free = NIL DO machineList _ CONS[DB.NameOf[DB.V2E[DB.GetF[free, machineIs]]], machineList] ENDLOOP END; DB.ReleaseRelshipSet[freeSet] }; IF stateLog # NIL THEN PlayLog[]; CarefullyApply[ListFree] }; PlayLog: INTERNAL PROC[] ~ { DoPlay: INTERNAL PROC[] ~ TRUSTED { logEntry: LogEntry; thisMachine: DB.Entity = DB.DeclareEntity[machine, machineName]; machineData: DB.Relship = DB.DeclareRelship[userRelation, LIST[AttributeValue[machineIs, thisMachine]]]; WHILE stateLog # NIL DO logEntry _ stateLog.first; WITH logEntry: logEntry SELECT FROM StateChange => { user: DB.Entity = DB.DeclareEntity[person, logEntry.user]; IF logEntry.event = login THEN { [] _ DB.SetF[machineData, lastEventTimeIs, DB.T2V[[logEntry.time]]]; [] _ DB.SetF[machineData, lastEventIs, DB.I2V[login]]; [] _ DB.SetF[machineData, userIs, user] } ELSE { [] _ DB.SetF[machineData, lastEventTimeIs, DB.T2V[[logEntry.time]]]; [] _ DB.SetF[machineData, lastEventIs, DB.I2V[logout]]; [] _ DB.SetF[machineData, userIs, user] } }; MachinePropChange => { attrRecord: AttributeRecord = NARROW[RefTab.Fetch[machineProps.table, Atom.MakeAtom[logEntry.name]].val]; IF attrRecord = NIL THEN {stateLog _ stateLog.rest; LOOP}; -- not a property anymore [] _ DB.SetP[thisMachine, attrRecord.attr, DB.S2V[logEntry.val]] }; UserPropChange => { user: DB.Entity = DB.DeclareEntity[person, logEntry.user]; lastChanged: BasicTime.GMT = DB.V2T[DB.GetP[user, lastChangedProp]]; DBVersion: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; prop: AttributeRecord = NARROW[RefTab.Fetch[userProps.table, Atom.MakeAtom[logEntry.name]].val]; IF DBVersion # userPropVersion THEN ERROR DB.Aborted[fingerTransaction]; IF prop = NIL THEN {stateLog _ stateLog.rest; LOOP}; -- not a property anymore IF lastChanged # BasicTime.nullGMT AND BasicTime.Period[from: lastChanged, to: logEntry.time] < 0 THEN { stateLog _ stateLog.rest; LOOP }; [] _ DB.SetP[user, prop.attr, DB.S2V[logEntry.val]]; [] _ DB.SetP[user, lastChangedProp, DB.T2V[[logEntry.time]]] }; AddMachineProp => { DBVersion: INT = DB.V2I[DB.GetF[machinePropVersionRelship, machinePropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; IF DBVersion # logEntry.version THEN {stateLog _ stateLog.rest; LOOP}; IF RefTab.Fetch[machineProps.table, propAtom].found THEN {stateLog _ stateLog.rest; LOOP}; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.NameOf[machine]]; relation: DB.Relation = DB.DeclareRelation[fullName, fingerSegment]; propOfAttr: DB.Attribute = DB.DeclareAttribute[relation, "of", machine, Key]; propIsAttr: DB.Attribute = DB.DeclareAttribute[relation, "is", RopeType]; [] _ DB.DeclareRelship[machinePropNames, LIST[DB.AttributeValue[machinePropNameIs, DB.S2V[logEntry.name]]]]; DB.SetF[machinePropVersionRelship, machinePropAttr, NEW[INT _ DBVersion+1]]; [] _ RefTab.Store[ machineProps.table, propAtom, NEW[AttrRecordObject _ [attr: propIsAttr, of: 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 {stateLog _ stateLog.rest; LOOP}; IF RefTab.Fetch[userProps.table, propAtom].found THEN {stateLog _ stateLog.rest; LOOP}; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.NameOf[person]]; relation: DB.Relation = DB.DeclareRelation[fullName, fingerSegment]; propOfAttr: DB.Attribute = DB.DeclareAttribute[relation, "of", person, Key]; propIsAttr: DB.Attribute = DB.DeclareAttribute[relation, "is", RopeType]; [] _ DB.DeclareRelship[userPropNames, LIST[DB.AttributeValue[userPropNameIs, DB.S2V[logEntry.name]]]]; DB.SetF[userPropVersionRelship, userPropAttr, NEW[INT _ DBVersion+1]]; [] _ RefTab.Store[ userProps.table, propAtom, NEW[AttrRecordObject _ [attr: propIsAttr, of: relation]] ]; userPropVersion _ DBVersion+1 END }; DeleteUserProp => { DBVersion: INT = DB.V2I[DB.GetF[userPropVersionRelship, userPropAttr]]; propAtom: ATOM = Atom.MakeAtom[logEntry.name]; IF DBVersion # logEntry.version THEN {stateLog _ stateLog.rest; LOOP}; IF NOT RefTab.Fetch[userProps.table, propAtom].found THEN {stateLog _ stateLog.rest; LOOP}; -- not there! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.NameOf[person]]; nameRelship: DB.Relship = DB.DeclareRelship[userPropNames, LIST[DB.AttributeValue[userPropNameIs, DB.S2V[logEntry.name]]]]; DB.DestroyRelation[DB.DeclareRelation[fullName, fingerSegment]]; DB.DestroyRelship[nameRelship]; DB.SetF[userPropVersionRelship, userPropAttr, NEW[INT _ 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]; IF DBVersion # logEntry.version THEN {stateLog _ stateLog.rest; LOOP}; IF NOT RefTab.Fetch[userProps.table, propAtom].found THEN {stateLog _ stateLog.rest; LOOP}; -- not there! BEGIN fullName: Rope.ROPE = Rope.Concat[logEntry.name, DB.NameOf[machine]]; nameRelship: DB.Relship = DB.DeclareRelship[machinePropNames, LIST[DB.AttributeValue[machinePropNameIs, DB.S2V[logEntry.name]]]]; DB.DestroyRelation[DB.DeclareRelation[fullName, fingerSegment]]; DB.DestroyRelship[nameRelship]; DB.SetF[machinePropVersionRelship, machinePropAttr, NEW[INT _ DBVersion+1]]; [] _ RefTab.Delete[machineProps.table, Atom.MakeAtom[logEntry.name]]; machinePropVersion _ DBVersion + 1 END}; ENDCASE; DB.MarkTransaction[fingerTransaction]; stateLog _ stateLog.rest ENDLOOP }; CarefullyApply[DoPlay]; FingerLog.FlushLog[] }; AttemptToPlayLog: ENTRY PROC[] = { ENABLE UNWIND => NULL; PlayLog[! FingerError => CONTINUE] }; RegisterLoginOrLogout: ENTRY Idle.IdleHandler = TRUSTED BEGIN ENABLE UNWIND => NULL; idle _ reason = becomingIdle; InternalPutStateChange[IF idle THEN $logout ELSE $login, BasicTime.Now[]]; Process.Detach[FORK AttemptToPlayLog[]] END; PutStateChange: PUBLIC ENTRY PROC [change: FingerOps.StateChange, time: BasicTime.GMT] ~ { ENABLE UNWIND => NULL; InternalPutStateChange[change, time]; IF NOT idle THEN PlayLog[! FingerError => CONTINUE] }; InternalPutStateChange: INTERNAL PROC [change: FingerOps.StateChange, time: BasicTime.GMT] ~ { name: Rope.ROPE = UserCredentials.Get[].name; Log[logEntry: NEW[StateChange LogEntryObject _ [StateChange[event: change, user: name, time: time]]]]; }; RegisterThisMachine: ENTRY PROC = BEGIN ENABLE UNWIND => NULL; DoRegistration: PROC[] = { thisMachine: DB.Entity = DB.DeclareEntity[machine, machineName]; itsRelation: DB.Relship = DB.DeclareRelship[userRelation, LIST[AttributeValue[machineIs, thisMachine], AttributeValue[actualNameIs, DB.S2V[machineAddress]]]]; DB.MarkTransaction[fingerTransaction] }; CarefullyApply[DoRegistration] END; Log: INTERNAL PROC[ logEntry: LogEntry ] = TRUSTED { FingerLog.Log[ logEntry ]; IF stateLog = NIL THEN stateLog _ LIST[logEntry] ELSE { log: LIST OF LogEntry _ stateLog; WHILE log.rest # NIL DO log _ log.rest ENDLOOP; log.rest _ LIST[logEntry] } }; ParseLog: INTERNAL PROC[] ~ { stateLog _ FingerLog.ParseLog[] }; ReadMachineMap: PUBLIC ENTRY PROC [file: Rope.ROPE, startPos: INT _ 0, msgStream: IO.STREAM _ NIL] = { fileStream, tokenStream: IO.STREAM; DoReadMap: INTERNAL PROC = { line: REF TEXT _ RefText.New[500]; IF startPos = 0 THEN { line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; DB.SetF[machinePropVersionRelship, machinePropAttr, NEW[INT _ 0]]; BEGIN machineProps: DB.RelshipSet = DB.RelationSubset[machinePropNames]; FOR next: DB.Relship _ DB.NextRelship[machineProps], DB.NextRelship[machineProps] UNTIL next = NIL DO relationName: Rope.ROPE = DB.V2S[DB.GetF[next, machinePropNameIs]]; relation: DB.Relation = DB.DeclareRelation[relationName, fingerSegment]; DB.DestroyRelation[relation]; DB.MarkTransaction[fingerTransaction]; ENDLOOP; DB.ReleaseRelshipSet[machineProps] END } ELSE fileStream.SetIndex[startPos]; BEGIN ENABLE IO.EndOfStream => CONTINUE; DO atom: ATOM = tokenStream.GetAtom[]; IF RefTab.Fetch[machineProps.table, atom].found THEN LOOP; -- already done! BEGIN fullName: Rope.ROPE = Rope.Concat[Atom.GetPName[atom], DB.NameOf[machine]]; relation: DB.Relation = DB.DeclareRelation[fullName, fingerSegment]; propOfAttr: DB.Attribute = DB.DeclareAttribute[relation, "of", machine, Key]; propIsAttr: DB.Attribute = DB.DeclareAttribute[relation, "is", RopeType]; [] _ DB.DeclareRelship[machinePropNames, LIST[DB.AttributeValue[machinePropNameIs, DB.S2V[Atom.GetPName[atom]]]]]; [] _ RefTab.Store[ machineProps.table, atom, NEW[AttrRecordObject _ [attr: propIsAttr, of: relation]] ]; END ENDLOOP END; BEGIN ENABLE IO.EndOfStream => CONTINUE; machineName, netAddress: Rope.ROPE; machineEntity: DB.Entity; machineRelship: DB.Relship; propAtom: ATOM; propValue: Rope.ROPE; operationCount: INT _ 0; DO line _ fileStream.GetLine[line]; tokenStream _ IO.TIS[line, tokenStream]; machineName _ tokenStream.GetRopeLiteral[]; netAddress _ tokenStream.GetRopeLiteral[]; machineEntity _ DB.DeclareEntity[machine, machineName]; machineRelship _ DB.DeclareRelship[userRelation, LIST[AttributeValue[machineIs, machineEntity], AttributeValue[actualNameIs, DB.S2V[netAddress]]]]; DO line _ fileStream.GetLine[line]; TRUSTED{ IF Rope.Equal[LOOPHOLE[line], ""] THEN EXIT }; tokenStream _ IO.TIS[line, tokenStream]; propAtom _ tokenStream.GetAtom[]; propValue _ tokenStream.GetRopeLiteral[]; BEGIN attrRecord: AttributeRecord = NARROW[RefTab.Fetch[machineProps.table, propAtom].val]; IF attrRecord = NIL THEN LOOP; [] _ DB.SetP[machineEntity, attrRecord.attr, DB.S2V[propValue]]; 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 ENTRY PROC [file: Rope.ROPE] = { ENABLE UNWIND => NULL; stream: IO.STREAM; DoWriteMap: INTERNAL PROC = { setOfEntities: DB.EntitySet = DB.DomainSubset[machine]; entity: DB.Entity; WritePropName: RefTab.EachPairAction = { stream.Put[IO.rope[" "], IO.rope[Convert.RopeFromAtom[NARROW[key]]]]; quit _ FALSE }; WritePropValue: RefTab.EachPairAction = { attr: DB.Attribute = DB.V2E[NARROW[val, AttributeRecord].attr]; propertyValue: Rope.ROPE = DB.V2S[DB.GetP[entity, attr]]; stream.Put[IO.rope[Convert.RopeFromAtom[NARROW[key]]]]; stream.Put[IO.rope[" "], IO.rope[Convert.RopeFromRope[propertyValue]]]; stream.Put[IO.rope["\n"]]; quit _ FALSE }; 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.NameOf[entity]; rel: DB.Relship = DB.DeclareRelship[userRelation, LIST[AttributeValue[machineIs, entity]]]; stream.Put[IO.rope[Convert.RopeFromRope[name]]]; stream.Put[IO.rope[" "], IO.rope[Convert.RopeFromRope[DB.V2S[DB.GetF[rel, actualNameIs]]]]]; 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[] }; IF file = NIL THEN RETURN; stream _ FS.StreamOpen[fileName: file, accessOptions: $create ! FS.Error => {stream _ NIL; CONTINUE }]; IF stream = NIL THEN RETURN; CarefullyApply[DoWriteMap] }; GetLogAndLoginUser: ENTRY PROC[] ~ { ENABLE UNWIND => NULL; ParseLog[]; InternalPutStateChange[$login, BasicTime.Now[]]; PlayLog[! FingerError => CONTINUE] }; OpenUp: Booting.RollbackProc = TRUSTED{ Process.Detach[FORK GetLogAndLoginUser[]] }; CloseDown: ENTRY Booting.CheckpointProc = { CloseTransaction[] }; Booting.RegisterProcs[r: OpenUp, c: CloseDown]; InitFingerDB[]; RegisterThisMachine[]; GetLogAndLoginUser[]; TRUSTED{ Process.Detach[FORK WatchDBActivity[]] }; [] _ Idle.RegisterIdleHandler[handler: RegisterLoginOrLogout] END. ²FingerOpsImpl.mesa Last Edited by: Khall, August 31, 1984 1:26:35 pm PDT Last Edited by: Donahue, May 15, 1985 9:31:02 am PDT The name and Pup address 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) is the machine in the idle state; if so, don't bother connecting to the database to save any log entries made (just collect them and replay them when you come out of idle) The database stores information about people and machines 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 relation: The mapping between login/logout and the boolean actually stored in the database the property lists for machines and users a property table consists of a DB.Relation that can be used enumerate all of the properties to be stored in the table and a RefTab containing all of the property attributes (ie., the DB.Attribute for each name); the values of the names and nameAttr fields is set when performing a ResetSchema What is stored as the values of the entries in a property table is the pair of attributes for the relation The Domains The property version stamp relations Make sure that only one relationship exists in the relation 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 Be sure to check for duplicates If the version numbers don't match, then it's not wise to perform the update if this succeeds, then the contents of the log file can be thrown away, since they have all been played to the database record that the current user has logged off of this machine (entries are changed for both the user and the machine) Write the entry on the log file and also on the internal log list 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 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 and net address of the machine, followed by a sequence of line with property value pairs, followed by a blank line Ê$ ˜codešœ™Kšœ5™5K™4—K˜šÏk ˜ K˜Kšœ œœ˜,K˜K˜Kšœ˜K˜ Kšœ ˜ Kšœœ˜Kšœœ0˜:Kšœ˜Kšœœ.˜;Kšœœ(˜5K˜K˜Kšœ˜Kšœ œ&˜4Kšœœ˜—K˜šœœ˜K˜š˜Kšœ#œ]˜‚—K˜š˜Kšœ ˜ —K˜š˜Kšœ˜K˜šœ@™@Kšœœ˜-KšœœG˜`—K™šœ™Kšœœ/˜KKšœœ˜$Kšœœ˜ Jšœœœ˜(—K˜šœŽ™ŽJšœ œœœ˜;—J˜Jšœœœ˜Jšœ«™«J˜Jšœ œœ˜J˜J˜:J˜Jšœ œœ œ˜!J˜™9Kšœœ˜—K˜™Yšœœ ˜Kšœ œ Ïc˜2Kšœœ ž!˜=Kšœœ ž'˜>Kšœ œ ž-˜IKšœœ ž˜?——K˜šœ‚™‚Kšœœ ˜—K˜KšœÚ™Ú˜Kšœ%œ˜-K˜šœM™MKšœœ ˜!Kšœœ ˜Kšœœ ž&˜N—K˜šœJ™JKšœœ ˜Kšœœ ˜Kšœœ ž&˜K——K™™_Kšœœ ˜Kšœœ ˜K˜Kšœœ ˜Kšœœ ˜ —K™šœP™PKšœœ˜Kšœœ˜—K˜šœ)™)Kš œœœœ œ œ œ˜iKšœ¤™¤Kšœ7˜7Kšœ4˜4Kšœj™jKšœœœ˜-Kš œœœœœ ˜E—K˜šÏnœœ˜šœœ˜ Jšœ˜Jšœ˜Jš˜—Jšœ˜J˜—šŸœœœ˜!Jšœœœ˜Jšœœ œ˜(Jšœ œ˜J˜—J˜šŸœœœ˜&Jšœœœ˜š˜Jšœœœ œ˜(šœœ˜šœ#˜%Jšœœœœ˜4——Jšœœœ$˜<—Jšœ˜Jšœœ˜J˜—šŸœœœ˜%Jšœœœœ ˜BJšœœ'˜=—J˜šŸœœœ˜&šœœ˜Jšœ%œ œ œ˜I—Jšœœ˜—J˜šŸ œœœ˜š˜Kšœœœ˜Jšœ˜ JšœGœ˜O—Kšœ˜—K˜šŸ œœœ˜ Kšœ˜Kš œœœœœ˜#šœ ™ Kšœ œ(˜3Kšœ œ)˜5—K™šœ$™$KšœœA˜YKšœœH˜\Kšœ;™;š˜Kšœœœ%˜EKšœœ˜9šœœ˜'Kšœœ(˜F—Kšœ ˜"Kšœ˜—K˜Kšœœ>˜SKšœœG˜XKšœ;™;š˜Kšœ œœ"˜?Kšœœ˜3šœœ˜$Kšœœ$˜?—Kšœ˜—K˜Kšœœ`˜t—K™šœ™Kšœœ?˜TKšœœE˜[Kšœ&˜&Kšœ*˜*K˜Kšœœ<˜NKšœœB˜UKšœ ˜ Kšœ$˜$—K™šœ-™-Kšœœ;˜LKšœ œT˜bKšœœG˜XKšœ œ?˜JKšœœE˜UKšœœJ˜^—K˜šœÝ™ÝKšœœœ˜>—K™Kšœ«™«š˜Kšœœœœ3˜RKšœ œœœ-˜Išœ%œ˜-K˜%Kšœ ™ Kšœ"˜"Kšœ&˜&—šœœ˜'Kšœ"˜"Kšœ˜Kšœ ˜ —Kšœ˜—Kšœ%˜%Kšœ ˜ K˜—K˜šŸœœœ˜@Kšœ œœ!˜<š œ œ œœœ œ˜aKšœœœœ$˜CKšœIœ˜MKšœ˜ ——K˜šŸ œœœ#œ ˜MKšœ>™>šœ#˜#Kšœ œœ˜-Kšœœœ˜;Kšœœ œ*˜HKšœ œ œ3˜PKšœ œ œ0˜MKšœ)œ<˜hKšœœ˜—Kšœ/˜/—K˜šŸ œœœœœ œœœ˜FKšœ}™}Kšœœœ˜šœ!˜!Kšœ œœœ˜9—Kšœ œœ ˜!Kšœ-˜-Kšœ˜—K˜šŸœœœœœ œœœ˜IKšœœœ˜šœ!˜!Kšœ œœœ˜9—Kšœ œœ ˜!Kšœ0˜0Kšœ˜—K˜š Ÿ œœœœ œ˜3Kšœœœ˜KšœœV˜hKšœ ˜ —K˜š Ÿœœœœ œ˜6Kšœœœ˜Kšœœ^˜fKšœ ˜ —K˜š Ÿœœœœ œ˜6Kšœœœ˜Kšœœ\˜dKšœ ˜ —K˜š Ÿœœœœ œ˜9Kšœœœ˜Kšœœd˜lKšœ ˜ K˜—š Ÿ œœœœ œœ˜GKšœœœ˜Kšœœ˜%Kšœœ˜4š œœœœœ˜8Kšœœˆ˜Kšœ˜—Kšœ ˜ —K˜š Ÿœœœœœœ˜?Kšœœœ˜š œœœœœ˜8Kšœœp˜wKšœ˜—Kšœ ˜ —K˜šŸ œœœœ œœœœ˜UKšœœœ˜šŸ œœœ˜Kšœœ œ&˜<šœ#˜#Kšœœ œœ˜?Kš œœœœœ ˜AKšœœœœ$˜SKšœœ˜—Kšœ œœœ˜Kšœ/˜/—Kšœ œœ ž%˜GKšœ˜—K˜šŸœœœœœœœœ˜_Kšœœœ˜šŸ œœœ˜Kšœœ œ.˜Dšœ#˜#Kšœœ œœ˜?Kšœœœœ˜9Kšœœœœ$˜SKšœœ˜—Kšœ œœœ˜Kšœ2˜2—Kšœ œœ ˜!Kšœ˜—K˜šŸœœœœ˜0šœ˜ Jšœ œ˜Jšœ œ ˜Jšœ˜—Jšœ œœ˜š˜Jšœœœœ˜2Jšœ˜Jšœ œ˜Jšœ˜Jšœ˜—Jš œœ œœž˜2Jšœž˜$š˜Jšœœ œ ˜"Jšœ˜Jšœ˜Jš˜—š˜Jšœœ˜:Jšœ!œ˜>Jšœ!œ˜@——K˜šŸœœœœ œœœ+œ œ˜•Kšœœœ˜šŸœœœ˜Kšœ œ œ˜8Kšœ œ œœ)˜fKšœ œœ!˜7Kš œ œœœ(œœ˜†Kšœœœ$˜4Kšœœœœ˜8—Kšœ˜—K˜šŸ œœœœ œœœœœ˜\Kšœœœ˜šŸœœœ˜Kšœœ œ˜1Kš œ œœœœ˜;Kšœœœ(˜Hš˜Kšœœœ!˜4š œ œ œœœ œ˜mKš œœœœœ*˜PKšœ˜—Kšœ˜—Kšœ#˜%—Kšœ œœ ˜!Kšœ˜—K˜šŸœœœœœ œœœ˜aKšœœœ˜šŸœœœ˜Kšœœ5˜SKšœ ˜ KšœE˜EKšœ œœœ*˜dš˜Kšœœœ˜0š œœ œœœœ˜]Kšœ œ œœ˜=Kšœ œœ˜*Kš˜—Kšœ˜——Kšœ œœ ˜!Kšœ˜—K˜šŸœœœœœœ œœœ˜bKšœœœ˜šŸœœœ˜Kšœœ œ˜6KšœPœ˜Vš˜Kšœœœ"œ˜@š œ œ œœœ œ˜hKšœ œœ˜$šœ)˜/Kšœ œ˜—Kšœ˜—Kšœ ˜"Kšœ˜——Kšœ œœ ˜!K˜Kšœ˜—K˜šŸœœœœœœ œœœ˜cKšœœœ˜šŸœœœ˜Kšœœ œ˜7KšœPœ˜Vš˜Kšœœœ"œ˜@š œ œ œœœ œ˜hKšœ œœ˜$šœ)˜/Kšœ œ˜—Kšœ˜—Kšœ ˜"Kšœ˜——Kšœ œœ ˜!Kšœ˜—K˜šŸœœœœœ œœœ˜^Kšœœœ˜šŸœœœ˜Kšœœ2˜PKšœ œœœ*˜cKšœA˜Aš˜Kšœœœ˜0š œœ œœœœ˜]Kšœœ œœ˜7Kšœ œœ˜'Kšœ˜—Kšœ˜Kšœ˜——Kšœ œœ ˜!Kšœ˜—K˜šŸ œœœœœ œœœ˜JKšœœœ˜šŸœœœ˜Kš œ œœœœ˜mš˜Kšœœœ˜1Kšœœ˜š œ œ œœœ œ˜cKšœ œ œœ˜=Kšœ™šœœœ˜(K˜Kšœ œ˜!—Kš˜—Kšœ˜—Kšœ ˜"—Kšœ œœ ˜!Kšœ˜—K˜šŸ œœœœœœœœ˜MKšœœœ˜šŸœœœ˜Kš œ œœœœ˜lš˜Kšœœœœ˜;š œœ œœœœ˜[Kš œœœœœ&˜LKš˜—Kšœ˜—Kšœ˜ —Kšœ œœ ˜!Kšœ˜—K˜šŸœœœ˜šŸœœœœ˜#K˜Kšœ œ œ%˜@Kšœ œ œœ*˜hšœ œ˜Kšœ˜šœœ˜#šœ˜Kšœœ œ&˜:šœœ˜ Kšœœ$œ˜DKšœœ œ ˜6Kšœœ"˜)—šœ˜Kšœœ$œ˜DKšœœ œ˜7Kšœœ%˜,——˜KšœœE˜iKš œœœœž˜UKšœœ$œ˜C—˜Kšœœ œ&˜:Kšœœœœ˜DKšœ œœœ-˜GKšœœB˜`Kšœœœœ˜HKš œœœœž˜Ošœ ˜"Kšœ<˜CKšœœ˜#—Kšœœœ˜4Kšœœœ˜?—˜Kšœ œœœ3˜MKšœ œ ˜.Kšœœœ˜FKšœL™Lšœ2˜8Kšœœž˜2—š˜Kšœœœ˜EKšœ œ œ*˜DKšœ œ œ0˜MKšœ œ œ,˜IKš œœ"œœ#œ˜lKšœ2œœ˜LKšœ1œ8˜lKšœ ˜ Kšœ˜——˜Kšœ œœœ-˜GKšœ œ ˜.Kšœœœ˜Fšœ/˜5Kšœœž˜2—š˜Kšœœœ˜DKšœD˜DKšœ œ œ/˜LKšœ œ œ,˜IKš œœœœ œ˜fKšœ,œœ˜FKšœ.œ8˜iKšœ˜Kšœ˜——˜Kšœ œœœ-˜GKšœ œ ˜.Kšœœœ˜Fšœœ/˜9Kšœœž ˜/—š˜Kšœœœ˜DKš œ œ œœœ œ˜{Kšœœ+˜@Kšœ˜Kšœ,œœ˜FKšœ.˜.Kšœ˜Kšœ˜——˜Kšœ œœœ3˜MKšœ œ ˜.Kšœœœ˜Fšœœ/˜9Kšœœž ˜/—š˜Kšœœœ˜EKš œ œ œ"œœ#œ˜Kšœœ+˜@Kšœ˜Kšœ2œœ˜LKšœE˜EKšœ"˜"Kšœ˜——Kšœ˜—Kšœ$˜&K˜Kšœ˜ ——K˜Kšœw™wKšœ˜—K˜šŸœœœ˜"Kšœœœ˜Kšœœ˜%—K˜šœœ˜/šœ˜ Kšœœœ˜Kšœ˜Kšœœœ œ˜JKšœœ˜'—Kšœ˜—K˜š Ÿœœœœ1œ˜ZKšœs™sKšœœœ˜K˜%Kšœœœœ˜6—K˜šŸœœœ1œ˜^Kšœ œ˜-KšœœU˜fK˜—K˜šŸœœœ˜!š˜Kšœœœ˜šŸœœ˜Kšœ œ œ%˜@Kš œ œ œœFœ˜žKšœ&˜(—K˜—Kšœ˜—K˜šŸœœœœ˜4JšœA™AK˜Kšœ œœ œ ˜0šœ˜Kšœœœ˜!Kšœ œœœ˜/Kšœ œ ˜K˜—Jšœ˜—J˜JšŸœœœ)˜@J˜šŸœœœœ œ œœœœ˜fJšœœœ˜#šŸ œœœ˜Kšœ;™;Kšœ¢™¢Kšœœœ˜"šœœ˜Kšœ ˜ Kšœœœ˜(Kšœˆ™ˆKšœ2œœ˜BKšœ8™8š˜Kšœœœ"˜Bš œœ œœœœ˜eKšœœœœ ˜CKšœ œ œ.˜HKšœ˜Kšœ$˜&Kšœ˜—Kšœ ˜"Kšœ˜——Kšœ˜#š˜Kšœœœ˜"š˜Kšœœ˜#Kšœ.œœž˜Kš˜Kšœœ$œ˜KKšœ œ œ*˜DKšœ œ œ0˜MKšœ œ œ,˜IKš œœ"œœ#œ˜rKšœ-œ8˜hKš˜——Kš˜Kšœ˜—Kšœš™šš˜Kšœœœ˜"Kšœœ˜#Kšœœ˜Kšœœ ˜Kšœ œ˜Kšœœ˜Kšœœ˜š˜Kšœ ˜ Kšœœœ˜(K˜+K˜*Kšœœ%˜7KšœœœHœ˜“š˜Kšœ ˜ Kš œœ œ œœ˜7Kšœœœ˜(Kšœ!˜!Kšœ)˜)š˜Kšœœ1˜UKšœœœœ˜Kšœœ&œ˜@K˜"šœœ˜Kšœ$˜&K˜—Kš˜—Kšœ˜—Kš˜—Kšœ˜——Jšœœœœ˜šœ œ0˜?Jšœœœœ˜-—Kšœœœœ˜ K˜Kšœ˜—J˜š Ÿœœœœ œ˜8Kšœœœ˜Jšœœœ˜šŸ œœœ˜Kšœœ œ˜7Kšœœ˜šœ(˜(Kšœ œ œœ ˜EKšœœ˜—šœ)˜)Kšœœ œœ˜?Kšœœœœ˜9Kšœ œœ ˜7Kšœ œ œ,˜GKšœ œ ˜Kšœœ˜—Kšœœ˜Jšœ5˜5Jšœ œ ˜š œ œœœ œ˜]Kšœ œœ˜$Kšœœ œœ%˜[Kšœ œ#˜0Kš œ œ œœœ˜\Kšœ œ ˜Jšœ6˜6Kšœ œ ˜K˜"šœœ˜Kšœ$˜&K˜—Kšœ˜—Kšœ!˜#K˜—Jšœœœœ˜šœ œ2˜=Jšœœœœ˜)—Jšœ œœœ˜K˜—Kšœ˜J˜šŸœœœ˜$Jšœœœ˜Jšœ ˜ Jšœ0˜0Jšœœ˜"Jšœ˜—J˜Jšœœœ˜TJ˜šœ œ1˜AJ˜—Jšœ/˜/Kšœ˜K˜Kšœ˜Kšœœ˜2Kšœ=˜=—Kšœ˜——…—oÒ¥