DIRECTORY BasicTime USING [ Update, Now ], CardTable USING [ Create, Delete, EachPairAction, Fetch, Pairs, Ref, Store ], DESFace USING [ GetRandomKey, Key ], IV USING [ KeyTableBody ], MBQueue USING [ QueueClientAction ], Process USING [ Detach, SecondsToTicks, SetTimeout ], Pup USING [ nullSocket, Socket ], PupName USING [ AddressToRope ], RefID USING [ ID, Reseal, Unseal ], Rope USING [ Equal ], RPC USING [ CallFailed, GetConversationID, ShortROPE ], ThNet USING [ pd ], ThParty, ThPartyPrivate USING [ ConversationBody, ConversationData, ConvState, ConvStateBody, DoDescribeParty, GetRnameFromParty, PartyAvailable, PartyBody, PartyData, SmartsBody, SmartsData ], ThPartyMonitorImpl, Thrush USING [ ActionReport, AlertKind, CallUrgency, ConversationID, ConvEvent, ConvEventBody, Credentials, EncryptionKey, epoch, InterfaceSpec, KeyTable, NB, NetAddress, noAddress, nullConvID, nullID, nullKey, PartyID, Reason, ROPE, SHHH, SmartsID, StateID, StateInConv, unencrypted ], ThSmartsRpcControl USING [ InterfaceRecord ], Triples USING [ Any, Erase, Foreach, ForeachProc, Make, Select ], TU USING [ MakeUnique ], VoiceUtils USING [ OwnNetAddress, Problem ] ; ThPartyOpsImpl: CEDAR MONITOR LOCKS root IMPORTS BasicTime, CardTable, DESFace, MBQueue, Process, PupName, RefID, root: ThPartyMonitorImpl, Rope, RPC, ThNet, ThParty, ThPartyPrivate, Thrush, Triples, TU, VoiceUtils EXPORTS ThParty, ThPartyPrivate SHARES ThPartyMonitorImpl = { ConvEvent: TYPE = Thrush.ConvEvent; ConvState: TYPE = ThPartyPrivate.ConvState; ConversationData: TYPE = ThPartyPrivate.ConversationData; ConversationID: TYPE = Thrush.ConversationID; Credentials: TYPE = Thrush.Credentials; Reseal: PROC[r: REF] RETURNS[RefID.ID] = INLINE {RETURN[RefID.Reseal[r]]; }; NB: TYPE = Thrush.NB; nullConvID: Thrush.ConversationID = Thrush.nullConvID; PartyBody: TYPE = ThPartyPrivate.PartyBody; -- Concrete PartyData: TYPE = ThPartyPrivate.PartyData; -- REF Concrete PartyID: TYPE = Thrush.PartyID; -- ID nullID: Thrush.PartyID = Thrush.nullID; Reason: TYPE = Thrush.Reason; ROPE: TYPE = Thrush.ROPE; SHHH: TYPE = Thrush.SHHH; none: SHHH = Thrush.unencrypted; SmartsBody: TYPE = ThPartyPrivate.SmartsBody; -- Concrete SmartsData: TYPE = ThPartyPrivate.SmartsData; -- REF Concrete SmartsID: TYPE = Thrush.SmartsID; -- ID nextConv: ConversationID _ Thrush.epoch; CreateConversation: PUBLIC ENTRY PROC[ shhh: SHHH, credentials: Credentials, state: Thrush.StateInConv, urgency: Thrush.CallUrgency, alertKind: Thrush.AlertKind, reason: Thrush.Reason, comment: ROPE, subject: ROPE ] RETURNS [ nb: NB, convEvent: ConvEvent ] = { ENABLE UNWIND => NULL; conv: ConversationData = NEW[ThPartyPrivate.ConversationBody _ [ timeOfID: BasicTime.Now[], startTime: BasicTime.Now[], subject: subject, urgency: urgency, alertKind: alertKind, conferenceHost: ConferenceHost[], keyTable: NEW[IV.KeyTableBody[20B]] ] ]; TRUSTED { []_EnterKey[conv, DESFace.GetRandomKey[]]; }; conv.convID _ EnhandleConv[conv]; credentials.stateID _ 0; credentials.state _ $neverWas; credentials.convID _ conv.convID; [nb, convEvent] _ DoAdvance[credentials: credentials, state: state, newInConv: TRUE, reason: reason, comment: comment]; IF nb # $success THEN DeleteConversation[ conv ]; }; Alert: PUBLIC ENTRY PROC[ shhh: SHHH, credentials: Credentials, calledPartyID: PartyID, comment: ROPE ] RETURNS [ nb: NB ] = { ENABLE UNWIND => NULL; convState: ConvState; conv: ConversationData; callingParty: PartyData; calledParty: PartyData; IF calledPartyID=credentials.partyID THEN RETURN[nb: $narcissism]; [, callingParty,, nb] _ Verify[credentials]; IF nb # $success THEN RETURN; credentials.partyID _ calledPartyID; [conv, calledParty, convState, nb] _ Verify[credentials]; SELECT nb FROM $notInConv => NULL; $success => IF conv#NIL THEN RETURN[$convStillActive]; -- already in the conversation! $noSuchParty => RETURN[$noSuchParty2]; ENDCASE=> RETURN; credentials.stateID _ 0; IF calledParty=NIL OR (calledParty.type = $trunk AND calledParty.reservedBy # Reseal[callingParty]) THEN RETURN[nb: $noSuchParty2]; nb _ DoAdvance[credentials: credentials, state: $notified, newInConv: TRUE, originatingParty: callingParty].nb; }; Advance: PUBLIC ENTRY PROC[ shhh: SHHH, credentials: Credentials, state: Thrush.StateInConv, reportToAll: BOOL, reason: Thrush.Reason, comment: ROPE ] RETURNS [nb: NB, convEvent: ConvEvent] = { ENABLE UNWIND => NULL; [nb, convEvent] _ DoAdvance[ credentials: credentials, state: state, reportToAll: reportToAll, reason: reason, comment: comment, newInConv: FALSE ]; }; DoAdvance: PUBLIC INTERNAL PROC [ credentials: Credentials, state: Thrush.StateInConv, reportToAll: BOOL_FALSE, reason: Thrush.Reason_NIL, comment: ROPE_NIL, newInConv: BOOL, -- if TRUE, party must not be in the conversation yet . . . and vice versa originatingParty: PartyData_NIL ] RETURNS [nb: NB_$success, convEvent: ConvEvent_NIL] = { convState: ConvState; conv: ConversationData; party: PartyData; [conv, party, convState, nb] _ Verify[credentials, newInConv, originatingParty]; SELECT nb FROM $success => { IF convState=NIL THEN RETURN; IF state = $active AND party.partyActive THEN RETURN[nb: $partyAlreadyActive]; IF conv.numIdle = conv.numParties AND state # $idle THEN RETURN[nb: $convIdle]; credentials.state _ state; }; $stateMismatch => credentials.state _ convState.state; ENDCASE => RETURN; convEvent _ PostConvEvent[ credentials: credentials, convState: convState, reason: reason, comment: comment, reportToAll: reportToAll]; }; GetConversationInfo: PUBLIC ENTRY PROC [ shh: SHHH_none, convID: ConversationID ] RETURNS [ nb: NB_$success, cInfo: ThParty.ConversationInfo_[] ] = { conv: ConversationData = UnsealConv[convID]; IF conv=NIL THEN RETURN[nb: $noSuchConv]; cInfo.subject _ conv.subject; cInfo.urgency _ conv.urgency; cInfo.alertKind _ conv.alertKind; cInfo.startTime _ conv.startTime; cInfo.conferenceHost _ conv.conferenceHost; cInfo.numParties _ conv.numParties; cInfo.numActive _ conv.numActive; cInfo.numIdle _ conv.numIdle; cInfo.originator _ Reseal[Triples.Select[$Originator, conv, -- originator --]]; }; GetPartyInfo: PUBLIC ENTRY PROC [ shh: SHHH_none, credentials: Credentials, nameReq: ThParty.NameReq, allParties: BOOL ] RETURNS [ nb: NB, pInfo: ThParty.PartyInfo_NIL ] = { conv: ConversationData = UnsealConv[credentials.convID]; convState: ConvState; ownParty: PartyData = UnsealParty[credentials.partyID]; GetParties: INTERNAL FinishProcType = { index: NAT; HowToReport _ NIL; IF smarts.properties.role # $voiceTerminal THEN RETURN; IF party=ownParty THEN { index _ 0; pInfo.numParties_MAX[1, pInfo.numParties]; } ELSE { index _ MAX[1, pInfo.numParties]; pInfo.numParties _ index+1; }; pInfo[index] _ [ partyID: Reseal[party], type: party.type, state: convState.state, name: IF nameReq=$none THEN NIL ELSE ThPartyPrivate.DoDescribeParty[party, nameReq], numConvs: party.numConvs, enabled: party.enabled, partyActive: party.partyActive, socket: [smarts.properties.netAddress.net, smarts.properties.netAddress.host, convState.sockID]]; }; nb _IF conv=NIL THEN $noSuchConv ELSE IF ownParty=NIL THEN $noSuchParty ELSE $success; IF nb=$success THEN { convState _ GetConvState[conv, ownParty]; IF convState=NIL THEN nb _ $notInConv }; IF nb#$success THEN RETURN; pInfo _ NEW[ThParty.PartyInfoSeq[IF allParties THEN conv.numParties ELSE 1]]; pInfo.conferenceHost _ conv.conferenceHost; pInfo[0] _ []; Report[ FinishProc: GetParties, reportToAll: allParties, conv: conv, party: ownParty, whatToReport: NIL]; }; GetKeyTable: PUBLIC ENTRY PROC [ shh: SHHH_none, credentials: Credentials ] RETURNS [ nb: NB, keyTable: Thrush.KeyTable_NIL ] = { conv: ConversationData; [conv,,,nb] _ Verify[credentials]; IF nb # $success THEN RETURN; keyTable _ conv.keyTable; }; RegisterKey: PUBLIC ENTRY PROC [ shh: SHHH _ none, credentials: Credentials, key: Thrush.EncryptionKey, reportNewKeys: BOOL] RETURNS [ nb: NB, keyIndex: [0..17B] _ 0 ] = { ENABLE UNWIND => NULL; conv: ConversationData; [conv, , , nb] _ Verify[credentials]; IF nb=$success OR nb=$stateMismatch THEN keyIndex _ EnterKey[conv, key].keyIndex; }; RegisterServiceInterface: PUBLIC ENTRY PROC[ shhh: SHHH _ none, credentials: Thrush.Credentials, interfaceSpecPattern: Thrush.InterfaceSpec ] RETURNS[nb: NB, interfaceSpec: Thrush.InterfaceSpec] = { interface: REF Thrush.InterfaceSpec; instance: RPC.ShortROPE_NIL; hostHint: Thrush.NetAddress; party: PartyData; type: RPC.ShortROPE = interfaceSpecPattern.interfaceName.type; [party: party, nb: nb] _ Verify[credentials]; SELECT nb FROM $success, $stateMismatch => NULL; $noSuchParty => IF party = NIL THEN RETURN ; -- else just not yet enabled ENDCASE => RETURN; nb _ $success; interface _ FindServiceInterface[party, type]; IF interface = NIL THEN { interface _ NEW[Thrush.InterfaceSpec _ [interfaceName: [type: type], serviceID: interfaceSpecPattern.serviceID]]; party.actionInterfaces _ CONS[interface, party.actionInterfaces]; }; IF interfaceSpecPattern.hostHint # Thrush.noAddress THEN hostHint _ interfaceSpecPattern.hostHint ELSE IF shhh=NIL THEN hostHint _ VoiceUtils.OwnNetAddress[] ELSE { na: Thrush.NetAddress = LOOPHOLE[RPC.GetConversationID[shhh]]; hostHint _ [na.net, na.host, Pup.nullSocket]; }; interface.hostHint _ hostHint; instance _ interfaceSpecPattern.interfaceName.instance; IF instance = NIL THEN { hostHint.socket _ NewId[]; instance _ PupName.AddressToRope[hostHint]; }; interface.interfaceName.instance _ instance; interface.interfaceName.version _ interfaceSpecPattern.interfaceName.version; interface.serviceID _ interfaceSpecPattern.serviceID; interfaceSpec _ interface^; }; LookupServiceInterface: PUBLIC ENTRY PROC[ shhh: SHHH _ none, credentials: Thrush.Credentials, serviceParty: PartyID, type: RPC.ShortROPE ] RETURNS[nb: NB _ $success, interfaceSpec: Thrush.InterfaceSpec] = { interface: REF Thrush.InterfaceSpec; party: PartyData; [nb: nb] _ Verify[credentials]; SELECT nb FROM $success, $stateMismatch => NULL; ENDCASE => RETURN; party _ UnsealParty[serviceParty]; IF party=NIL THEN { nb _ $noSuchParty2; RETURN; }; interface _ FindServiceInterface[party, type]; IF interface=NIL THEN { party _ NARROW[Triples.Select[$Poaching, party, NIL]]; IF party#NIL THEN interface _ FindServiceInterface[party, type]; IF interface = NIL THEN { nb _ $noSuchInterface; RETURN; }; }; nb _ $success; interfaceSpec _ interface^; }; Verify: INTERNAL PROC[ credentials: Credentials, newInConv: BOOL_FALSE, originatingParty: PartyData_NIL ] RETURNS [ conv: ConversationData_NIL, party: PartyData_NIL, convState: ConvState_NIL, nb: Thrush.NB ] = { smarts: SmartsData _ UnsealSmarts[credentials.smartsID]; conv_UnsealConv[credentials.convID]; party_UnsealParty[credentials.partyID]; nb _ SELECT NIL[REF ANY] FROM smarts => $noSuchSmarts, party => $noSuchParty, conv => IF credentials.convID#nullConvID THEN $noSuchConv ELSE $success, ENDCASE=> $success; IF nb=$success AND ~party.enabled THEN nb _ $noSuchParty; IF nb = $success AND newInConv THEN nb _ ThPartyPrivate.PartyAvailable[originatingParty, party]; IF nb#$success OR conv=NIL THEN RETURN; convState _ GetConvState[conv, party, newInConv]; IF convState=NIL THEN { nb _ $notInConv; RETURN; }; SELECT credentials.stateID FROM < convState.stateID => nb_$stateMismatch; > convState.stateID => nb_$interfaceError; -- really bogus behaviour! ENDCASE; }; GetConvState: INTERNAL PROC[ conv: ConversationData, party: PartyData, createOK: BOOL_FALSE] RETURNS [convState: ConvState_NIL] = { GCS: Triples.ForeachProc = { WITH trip.att SELECT FROM cS: ConvState => { convState_cS; RETURN[FALSE]; }; ENDCASE; }; Triples.Foreach[Triples.Any, conv, party, GCS]; IF convState#NIL OR ~createOK THEN RETURN; convState _ NEW[ThPartyPrivate.ConvStateBody _ [sockID: NewId[]]]; Triples.Make[convState, conv, party]; Triples.Make[$Party, conv, party]; IF conv.numParties=0 THEN TU.MakeUnique[$Originator, conv, party]; party.numConvs _ party.numConvs+1; conv.numParties _ conv.numParties+1; }; PostConvEvent: INTERNAL PROC[ credentials: Credentials, convState: ConvState, reason: Thrush.Reason_NIL, comment: Thrush.ROPE_NIL, reportToAll: BOOL ] RETURNS [convEvent: ConvEvent ] = { conv: ConversationData _ UnsealConv[credentials.convID]; party: PartyData _ UnsealParty[credentials.partyID]; SELECT convState.state FROM -- states that was, but will no longer be $idle => { conv.numIdle _ conv.numIdle-1; party.numConvs _ party.numConvs+1; }; $active => { conv.numActive _ conv.numActive -1; party.partyActive _ FALSE; }; ENDCASE; SELECT credentials.state FROM -- states that will be $idle => { conv.numIdle _ conv.numIdle+1; party.numConvs _ party.numConvs-1; }; $active => { conv.numActive _ conv.numActive+1; party.partyActive _ TRUE; }; ENDCASE; convState.stateID _ credentials.stateID _ convState.stateID+1; convState.state _ credentials.state; conv.timeOfID _ convState.time _ BasicTime.Now[]; convEvent _ NEW[ Thrush.ConvEventBody _ [ self: [ ], other: credentials, time: convState.time, reason: reason, comment: comment ] ]; Report[FinishEvent, reportToAll, conv, party, convEvent ]; -- will queue up either a report or an idle check for each recipient. }; PackagedReport: TYPE = REF ReportBody; ReportBody: TYPE = RECORD [ smarts: SmartsData, whatToReport: REF ]; FinishProcType: TYPE = PROC[party: PartyData, smarts: SmartsData, convState: ConvState, whatToReport: REF] RETURNS [HowToReport: PROC[r: REF]_NIL, whatToReallyReport: REF_NIL]; Report: PUBLIC INTERNAL PROC[ FinishProc: FinishProcType, -- provides Report proc as one of its return values reportToAll: BOOL, conv: ConversationData, party: PartyData, whatToReport: REF ] = { ReportToOne: INTERNAL Triples.ForeachProc = { party: PartyData = NARROW[trip.val]; ReportToParty[party, party]; }; ReportToParty: INTERNAL PROC[party: PartyData, participant: PartyData] = { pSmarts: SmartsData; packagedReport: PackagedReport; realReport: REF _ whatToReport; HowToReport: PROC[r: REF]_NIL; IF party=NIL OR ~party.enabled THEN RETURN; pSmarts _ NARROW[Triples.Select[$Smarts, party, --pSmarts--]]; IF pSmarts#NIL AND FinishProc#NIL THEN [HowToReport, realReport] _ FinishProc[participant, pSmarts, GetConvState[conv, participant], whatToReport]; IF HowToReport#NIL THEN { packagedReport _ NEW[ReportBody _ [smarts: pSmarts, whatToReport: realReport] ]; conv.numReportsOut _ conv.numReportsOut + 1; pSmarts.notifications.QueueClientAction[HowToReport, packagedReport]; }; party _ NARROW[Triples.Select[$Poaching, party, -- poachee --]]; IF party#NIL THEN ReportToParty[party, participant]; }; IF ~reportToAll THEN ReportToParty[party, party] ELSE Triples.Foreach[$Party, conv, Triples.Any --party--, ReportToOne]; }; FinishEvent: INTERNAL FinishProcType = { convEvent: ConvEvent = NARROW[whatToReport]; reportEvent: ConvEvent = IF (convEvent.other.smartsID = Reseal[smarts]) THEN convEvent ELSE NEW[Thrush.ConvEventBody _ convEvent^]; reportEvent.self _ [ Reseal[party], Reseal[smarts], reportEvent.other.convID, convState.state, convState.stateID]; IF convEvent = reportEvent THEN RETURN[ReportCheckIdle, UnsealConv[convEvent.self.convID]] -- no report to initiator ELSE RETURN[ReportProgress, reportEvent]; }; ReportProgress: PROC[r: REF] = { report: PackagedReport = NARROW[r]; convEvent: ConvEvent = NARROW[report.whatToReport]; { ENABLE RPC.CallFailed => { IF ~report.smarts.failed THEN { VoiceUtils.Problem["Communications to smarts failed", $System]; []_ThParty.Deregister[smartsID: Reseal[report.smarts]]; }; report.smarts.failed_TRUE; CONTINUE; }; IF report.smarts.failed THEN RETURN; report.smarts.interface.clientStubProgress[ interface: report.smarts.interface, shh: report.smarts.shh, convEvent: convEvent]; CheckIdle[UnsealConvE[convEvent.self.convID]]; }; }; ReportCheckIdle: PROC[r: REF] = { report: PackagedReport = NARROW[r]; CheckIdle[NARROW[report.whatToReport]]; }; CheckIdle: ENTRY PROC[conv: ConversationData] = { conv.numReportsOut _ conv.numReportsOut - 1; IF conv.numIdle = conv.numParties AND conv.numReportsOut<=0 THEN TRUSTED { Process.Detach[FORK IdleAfterAWhile[conv]]; }; }; IdleAfterAWhile: ENTRY PROC[conv: ConversationData] = TRUSTED { idler: CONDITION; Process.SetTimeout[@idler, Process.SecondsToTicks[ThNet.pd.postIdleTimeout]]; WAIT idler; DeleteConversation[conv]; }; DeleteConversation: INTERNAL PROC[ conv: ConversationData ] = { Triples.Erase[Triples.Any, conv, Triples.Any]; []_convTable.Delete[LOOPHOLE[conv.convID]]; -- KillConv[conv.convID]; }; NewId: PROC RETURNS [ Pup.Socket ] = TRUSTED { idKey: DESFace.Key _ DESFace.GetRandomKey[]; -- connection specs. keys: LONG POINTER TO ARRAY[0..2) OF Pup.Socket _ LOOPHOLE[LONG[@idKey]]; RETURN[keys[0]]; }; FindServiceInterface: INTERNAL PROC[ party: PartyData, type: RPC.ShortROPE ] RETURNS[interface: REF Thrush.InterfaceSpec_NIL] = { FOR iF: LIST OF REF Thrush.InterfaceSpec _ party.actionInterfaces, iF.rest WHILE iF#NIL DO IF type.Equal[iF.first.interfaceName.type, FALSE] THEN RETURN[iF.first]; ENDLOOP; }; SubstReport: TYPE = REF SubstReportRec; SubstReportRec: TYPE = RECORD [ convEvent: Thrush.ConvEvent, oldPartyID: PartyID, newPartyID: PartyID ]; MakeSubstitution: PUBLIC INTERNAL PROC[oldParty: PartyData, newParty: PartyData] = { SpliceNewPartyIntoConv: INTERNAL Triples.ForeachProc -- [trip: TripleRec] -- RETURNS [continue: BOOLEAN _ TRUE] -- ={ -- copy attributes Triples.Make[trip.att, trip.obj, newParty]; -- newParty # oldParty asserted. Triples.Erase[trip.att, trip.obj, trip.val]; -- Make before break, although it doesn't matter. }; SubInOneConv: INTERNAL Triples.ForeachProc -- [trip: TripleRec] -- RETURNS [continue: BOOLEAN _ TRUE] -- ={ conv: ThPartyPrivate.ConversationData = NARROW[trip.obj]; convEvent: ConvEvent; convState: ThPartyPrivate.ConvState = GetConvState[conv, oldParty, FALSE]; IF convState=NIL THEN ERROR; IF newParty.type#$telephone AND ThPartyPrivate.GetRnameFromParty[oldParty, $owner].rAtom # ThPartyPrivate.GetRnameFromParty[newParty, $owner].rAtom THEN RETURN; newParty.partyActive _ oldParty.partyActive; oldParty.partyActive _ FALSE; IF convState.state#idle THEN { newParty.numConvs _ newParty.numConvs+1; oldParty.numConvs _ oldParty.numConvs-1; }; Triples.Foreach[Triples.Any, conv, oldParty, SpliceNewPartyIntoConv]; convEvent _ NEW[Thrush.ConvEventBody _ [ self: [], other: [ partyID: Reseal[newParty], smartsID: Reseal[Triples.Select[$Smarts, newParty, Triples.Any]], convID: conv.convID, state: convState.state, stateID: convState.stateID _ convState.stateID + 1 -- Clock must tick! ], time: conv.timeOfID _ convState.time _ BasicTime.Now[], reason: $substituting ]]; Report[ FinishProc: FinishSubstitution, reportToAll: TRUE, conv: conv, party: NIL, whatToReport: NEW[SubstReportRec_[ convEvent: convEvent, oldPartyID: Reseal[oldParty], newPartyID: Reseal[newParty]]] ]; }; IF oldParty=NIL OR newParty=NIL OR oldParty=newParty THEN RETURN; Triples.Foreach[$Party, Triples.Any, oldParty, SubInOneConv]; }; FinishSubstitution: INTERNAL FinishProcType = { substReport: SubstReport _ NARROW[whatToReport]; HowToReport _ ReportSubstitution; substReport _ NEW[SubstReportRec _ substReport^]; whatToReallyReport _ substReport; substReport.convEvent _ NEW[Thrush.ConvEventBody _ substReport.convEvent^]; substReport.convEvent.self _ [ Reseal[party], Reseal[smarts], substReport.convEvent.other.convID, convState.state, convState.stateID]; }; ReportSubstitution: PROC[r: REF] = { report: PackagedReport = NARROW[r]; substReport: SubstReport = NARROW[report.whatToReport]; { ENABLE RPC.CallFailed => { IF ~report.smarts.failed THEN { VoiceUtils.Problem["Communications to smarts failed", $System]; []_ThParty.Deregister[smartsID: Reseal[report.smarts]]; }; report.smarts.failed_TRUE; CONTINUE; }; IF report.smarts.failed THEN RETURN; report.smarts.interface.clientStubSubstitution[ interface: report.smarts.interface, shh: report.smarts.shh, convEvent: substReport.convEvent, oldPartyID: substReport.oldPartyID, newPartyID: substReport.newPartyID]; CheckIdle[UnsealConvE[substReport.convEvent.self.convID]]; }; }; AReport: TYPE = REF AReportRec; AReportRec: TYPE = RECORD [ report: Thrush.ActionReport, numReportsOut: INTEGER_0, -- for selfOnCompletion feature finalReport: AReport_NIL, -- for selfOnCompletion feature reportToAll: BOOL, selfOnCompletion: BOOL ]; ReportAction: PUBLIC ENTRY PROC[ shhh: SHHH _ none, report: Thrush.ActionReport, -- includes credentials (in other field) identifying the service party. reportToAll: BOOL _ FALSE, selfOnCompletion: BOOL _ FALSE ] RETURNS [nb: NB, numReportsIssued: NAT_0] = { rReport: AReport; conv: ConversationData; [nb: nb, conv: conv] _ Verify[report.other]; IF nb # $success THEN RETURN; -- Reports are not that important. rReport _ NEW[AReportRec _ [report, 0, NIL, reportToAll, selfOnCompletion]]; rReport.finalReport _ rReport; -- for selfOnCompletion feature. Report[FinishProc: FinishReportAction, reportToAll: TRUE, conv: conv, party: NIL, whatToReport: rReport]; numReportsIssued _ rReport.numReportsOut; }; FinishReportAction: INTERNAL FinishProcType = { aReport: AReport _ NARROW[whatToReport]; aReportCopy: AReport _ aReport; IF aReport.report.other.smartsID = Reseal[smarts] THEN IF aReport.selfOnCompletion THEN NULL ELSE RETURN -- requestingParty ELSE IF ~aReport.reportToAll AND Reseal[party] # aReport.report.requestingParty THEN RETURN -- no report to non-requesting party. ELSE aReportCopy _ NEW[AReportRec _ aReport^]; -- must be a copy, because . . . aReportCopy.report.self _ [ Reseal[party], Reseal[smarts], aReportCopy.report.other.convID, convState.state, convState.stateID]; IF aReportCopy = aReport THEN RETURN; -- Will be used indirectly through finalReport link of other reports if selfOnCompletion aReport.numReportsOut _ aReport.numReportsOut+1; -- aReport = aReportCopy.finalReport RETURN[DoReportAction, aReportCopy]; }; DoReportAction: PROC[r: REF] = { report: PackagedReport = NARROW[r]; aReport: AReport _ NARROW[report.whatToReport]; finalReport: AReport _ aReport.finalReport; { ENABLE RPC.CallFailed => { IF ~report.smarts.failed THEN { VoiceUtils.Problem["Communications to smarts failed", $System]; []_ThParty.Deregister[smartsID: Reseal[report.smarts]]; }; report.smarts.failed_TRUE; CONTINUE; }; conv: ConversationData; IF report.smarts.failed THEN RETURN; -- What about numReportsOut counts? report.smarts.interface.clientStubReportAction[ interface: report.smarts.interface, shh: report.smarts.shh, report: aReport.report]; conv _ UnsealConvE[aReport.report.self.convID]; IF aReport.selfOnCompletion THEN { FinalReport: ENTRY PROC= { pSmarts: SmartsData; finalReport.numReportsOut _ finalReport.numReportsOut-1; IF finalReport.numReportsOut#0 THEN RETURN; pSmarts _ UnsealSmarts[finalReport.report.self.smartsID]; conv.numReportsOut _ conv.numReportsOut + 1; pSmarts.notifications.QueueClientAction[DoReportAction, finalReport]; }; FinalReport[]; }; CheckIdle[conv]; }; }; convTable: CardTable.Ref = CardTable.Create[]; UnsealConv: PUBLIC INTERNAL PROC[ convID: ConversationID] RETURNS [conv: ConversationData_NIL ] = { RETURN[NARROW[convTable.Fetch[LOOPHOLE[convID]].val]]; }; EnhandleConv: INTERNAL PROC[ conv: ConversationData ] RETURNS [ convID: ConversationID_nextConv ] = { nextConv _ BasicTime.Update[nextConv, 1]; IF ~convTable.Store[LOOPHOLE[convID], conv] THEN ERROR; }; ConferenceHost: INTERNAL PROC RETURNS [conferenceHost: Thrush.NetAddress] = { inUse: PACKED ARRAY [0..100B) OF BOOL _ ALL[FALSE]; MarkInUse: CardTable.EachPairAction = { conv: ConversationData = NARROW[val]; inUse[conv.conferenceHost.host] _ TRUE; RETURN[FALSE]; }; []_convTable.Pairs[MarkInUse]; FOR hIndex: [0..100B) IN [2..100B) DO IF ~inUse[hIndex] THEN RETURN [[[173B], [hIndex], Pup.nullSocket]]; ENDLOOP; VoiceUtils.Problem["Out of conference hosts!", $System]; RETURN[[[173B],[1], Pup.nullSocket]]; }; UnsealConvE: ENTRY PROC[convID: ConversationID] RETURNS [conv: ConversationData_NIL ] = { RETURN[UnsealConv[convID]]; }; UnsealParty: PUBLIC PROC[partyID: Thrush.PartyID] RETURNS[party: PartyData] = { RETURN[NARROW[RefID.Unseal[partyID]]]; }; UnsealSmarts: PUBLIC PROC[smartsID: Thrush.SmartsID] RETURNS[smarts: SmartsData] = { RETURN[NARROW[RefID.Unseal[smartsID]]]; }; EKResults: TYPE = { new, duplicate, disagreement, full }; EnterKey: INTERNAL PROC[conv: ConversationData, key: Thrush.EncryptionKey] RETURNS [ keyIndex: [0..17B], results: EKResults] = { IF key=Thrush.nullKey THEN RETURN[0, $duplicate]; FOR keyIndex IN [1..17B] DO IF conv.keyTable[keyIndex]=key THEN RETURN[keyIndex, $duplicate] ELSE IF conv.keyTable[keyIndex][0].b=0 AND conv.keyTable[keyIndex] = Thrush.nullKey THEN EXIT; REPEAT FINISHED => RETURN[0, full]; ENDLOOP; conv.keyTable[keyIndex] _ key; RETURN[keyIndex, new]; }; }. μThPartyOpsImpl.mesa Copyright c 1985, 1986 by Xerox Corporation. All rights reserved. Last modified by D. Swinehart, June 3, 1986 1:41:42 am PDT Copies Conversation ID stuff Implementation of ThParty This conv's key, will be key index 1. credentials.smartsID remains the originator's  this produces a strange value for convEvent.situation.other.smartsID; at present, no one uses that  if they do, they'll have to understand. cInfo.lastTime _ conv.timeOfID; Computes the socket values (and corresponding party/smarts ident) for each voice terminal participant in a conversation, when invoked by Report. stateID: convState.stateID, lastTime: convState.time, Don't care about stateMismatch here. Produce hostHint value Utilities Validate parameters: existence, in conversation, state match; produce dehandled values. Validation for smarts is only partial; verifies that it's a smarts. originatingParty is only for use in PartyAvailable. See Poachnotes, it's too hairy to describe here. Party will not be available if some party related-by-hardware to this one is busy in a way that would make the hardware for this party fail to work or interfere with the other's operation. We are only interested in this situation when we are contemplating beginning another conversation. Not interested in any conversation, or it doesn't exist IF newInConv is TRUE and convState already existed, it's a logical error, but we're not checking it. Prev. line is for protection: you're not supposed to leave idle state, or enter it twice, but this will keep the counts correct if it happens. party.numConvs becomes 0 once a party goes idle. Report Progress Generic reporting routine, synchronous with ThPartyOps procedure, and protected by its monitor. Depending on reportToAll, queues up the report to be distributed to the smarts' for all parties in the conversation (TRUE), or just to the requesting party (FALSE). Reports are made to the indicated parties and their poachees. $visiting will be dealt with later. Having identified a party and smarts to which to report, calls FinishProc to complete the report (to indicate in the report the addressee of the report, for instance); FinishProc may veto the report by returning FALSE. [trip: Triples.TripleRec] RETURNS [continue: BOOLEAN _ TRUE] party is the one we're reporting to; it might be a poachee of the participant. participant is the party that's actually a member of the conversation. Called as report is being prepared to fill in addressee information Two cases: 1) reporting to originating smarts: don't copy convEvent, and use report only to do CheckIdle asynchronously with the originating operation. 2) reporting to some other smarts: copy convEvent before filling in self field, request a ReportProgress. Called to issue state-change progress report Specific reporting routine, dispatched from a smarts notification queue, for reporting party state changes. If all parties are idle in conversation, conversation can go away. If all parties are idle in conversation, conversation can go away. If all parties are idle in conversation, conversation can go away. Substitution of one party for another in a conversation If both parties exist, either a poacher is arriving or leaving. In either case, the oldParty might be involved in conversations that the new party should inherit. Arrange for that here. Here we actually move to newParty all the relations between the conversation and the oldParty. This feels too drastic to work, but we'll try it. When newParty is the poacher, splice only occurs if the owner rName of oldParty and newParty are the same. Otherwise, the poaching relationship is OK, but it doesn't affect ongoing conversations.  Otherwise, conv is dead for this party, but make the splice anyway, since non-dead parties still have to be able to understand what's what. Prepare and issue a report to all parties (report will go to the new, not the old, party.) PROC[party: PartyData, smarts: SmartsData, convState: ConvState, whatToReport: REF] RETURNS [HowToReport: PROC[r: REF]_NIL, whatToReallyReport: REF_NIL]; The original SubstReport and its ConvEvent are not used in any final report; there needs to be a separate report for each party, and it's easier this way; infrequency argument. Specific reporting routine, dispatched from a smarts notification queue, for reporting substitutions of poachers for poachees and the reverse. Necessary, it is believed, only to keep the report count correct Reporting of actions generated by voice services If report.reportToAll is FALSE, the actual reportee will be detected in due time; it isn't the caller of ReportAction. Three cases: 1) reporting to caller of ThParty.ReportAction. Don't report (at least wait, if selfOnCompletion). 2) reporting to the originator of the service request, as defined by the action report, and this party isn't the originator. Don't report. 3 reporting to all or reporting to the originator of the service request, and this is it. Generate a report. Specific reporting routine, dispatched from a smarts notification queue, for reporting party state changes. To original reporter, when all others have been delivered! Conversation ID management At present, called only from Verify Called only from CreateConv. Must be called before new conversation's ID is entered in table. Returns an available multicast host number. KillConv: INTERNAL PROC[ convID: ConversationID ] = INLINE { []_convTable.Delete[LOOPHOLE[convID]]; }; Key Table management Presently returns full if table is full; user should hang up and restart. Move-to-front would work nicely here!! Temporarily out of service at this time Swinehart, May 15, 1985 10:46:39 am PDT changes to: SetProse, DescribeInterval, PostConvEvent Swinehart, May 16, 1985 10:09:48 am PDT Switch to RedBlackTree for conversation handle stuff changes to: DIRECTORY, ThPartyOpsImpl, nextConv, convTable, cID, ConvTableEntry, ConvTableRecord, UnsealConv, EnhandleConv, KillHandleConv, ConvCompare, ConvGet, EnterKey Swinehart, October 25, 1985 6:24:50 pm PDT Handle => ID, eliminate conversation class stuff! changes to: DIRECTORY, ConversationID, Reseal, nullConvID, nullID, PartyID, SmartsID, nextConv, Alert, DoAdvance, OtherParty, FindOtherParty, CreateConversation, CreateConv, DestroyConversation, MergeConversations, RegisterKey, SetSubject, GetSubject, ConversationsForParty, PostConvEvent, GetOneActive, ConvTableRecord, cTE, UnsealConv, EnhandleConv, KillIDConv, UnsealParty, UnsealSmarts, convTable, ThPartyOpsImpl, Activate, GetEvent Swinehart, November 4, 1985 9:59:14 pm PST Tracking changes to Thrush types, Get/Create code changes to: Alert, DoAdvance, r, r, OtherParty, CreateConv, SetIntervals, SetProse, DescribeInterval, RegisterKey, Verify, PostConvEvent, GetOneActive, Activate, AllIdle, EKResults, EnterKey, Validity, Advance Swinehart, November 7, 1985 4:22:59 pm PST Major change to basic switching methods, simplifying ThParty's role in managing them. changes to: DIRECTORY, Alert, CreateConv, SetSubject, GetSubject, ConversationsForParty, Verify, PostConvEvent, GetOneActive, UnsealConv, UnsealParty, UnsealSmarts, ConvEvent, NB, nullConvID, PartyBody, Reason, SmartsBody, SmartsData, SmartsID, CreateConversation, Advance, DoAdvance, FindOtherParty, MergeConversations, NewId, DestroyConversation, EnterKey, VerifyEnt, GCS, PostConvEvent, RegisterKey, DeleteConversation Swinehart, November 8, 1985 8:44:36 am PST More of above, merge the supervisor stuff, eliminating ThPartySupervisorImpl. changes to: PostConvEvent, PackagedReport, ReportBody, Report, ReportToOne, ReportToParty, Progress Swinehart, April 24, 1986 3:15:04 pm PST When new poacher's owner is same as new poachee's owner, allow poacher to inherit ongoing calls involving poachee. changes to: DIRECTORY, MakeSubstitution Swinehart, May 17, 1986 5:29:46 pm PDT Cedar 6.1 changes to: DIRECTORY, NewId Κ¨˜šœ™Icodešœ Οmœ7™BJšœ:™:—J˜šΟk ˜ Jšœ žœ˜ Jšœ žœ>˜MJšœžœ˜$Jšžœžœ˜Jšœžœ˜$Jšœžœ(˜5Jšœžœ˜!Jšœžœ˜ Jšœžœžœ˜#Jšœžœ ˜Jšžœžœ.˜7Jšœžœ˜J˜šœžœ˜Jšœ‘˜‘—J˜šœžœ˜JšœΥžœžœ0˜—Jšœžœ˜-Jšœžœ4˜AJšžœžœ˜Jšœ žœ˜+J˜J˜—šœžœžœžœ˜(šž˜Jšœ ˜ J˜ J˜J˜J˜J˜J˜J˜J˜Jšžœ˜J˜J˜J˜J˜J˜Jšžœ˜J˜ —Jšžœ˜Jšžœ˜J˜—šœ™Jšœ žœ˜#Jšœ žœ˜+Jšœžœ#˜9Jšœžœ˜-Jšœ žœ˜'JšΟnœžœžœžœžœžœžœ˜LJšžœžœ žœ˜J˜6Jšœ žœΟc ˜7Jšœ žœ ˜;šœ žœ ˜%J˜'—Jšœžœ˜Jšžœžœ žœ˜šžœžœ žœ˜Jšœžœ˜ —Jšœ žœ  ˜9Jšœ žœ ˜=Jšœ žœ ˜'J˜—™J˜J˜(J˜—šœ™J˜šŸœžœžœžœ˜&Jšœžœ˜ J˜J˜Jšœ˜Jšœ˜J˜Jšœ žœ˜Jšœ ž˜ Jšœžœ%˜.Jšžœžœžœ˜šœžœ$˜@J˜J˜J˜Jšœ˜Jšœ˜J˜!Jšœ žœ˜#Jšœ˜—šžœ0˜7Jšœ%™%—Jšœ!˜!J˜J˜J˜!JšœOžœ$˜wJšžœžœ˜1Jšœ˜J˜—šŸœžœž œ˜Jšœžœ˜ J˜J˜Jšœ ž˜ Jšœžœžœ˜Jšžœžœžœ˜Jšœ˜Jšœ˜Jšœ˜Jšœ˜Jšžœ#žœžœ˜BJšœ,˜,Jšžœžœžœ˜J˜$Jšœ9˜9šžœž˜Jšœžœ˜Jš œ žœžœžœžœ ˜VJšœžœ˜&Jšžœžœ˜—JšœΌ™ΌJ˜šžœ žœž˜Jšœžœ0ž˜RJšžœ˜—JšœFžœ%˜oJ˜—J˜šŸœžœžœžœ˜Jšœžœ˜ J˜J˜Jšœ žœ˜J˜Jšœ ž˜ Jšœžœžœ˜,Jšžœžœžœ˜šœ˜Jšœ˜Jšœ ˜ Jšœ˜Jšœ˜Jšœ˜Jšœ ž˜Jšœ˜—Jšœ˜—J˜šŸ œžœžœžœ˜!J˜J˜Jšœ žœžœ˜Jšœžœ˜Jšœ žœž˜Jšœ žœ J˜[Jšœž˜Jšœžœžœ žœ˜9Jšœ˜J˜J˜JšœP˜Pšžœž˜˜ Jšžœ žœžœžœ˜Jšžœžœžœžœ˜NJšžœ žœžœžœ˜OJ˜J˜—J˜6Jšžœžœ˜—šœ˜šœ/˜/Jšœ<˜<——Jšœ˜J˜—šŸœžœžœ˜Qšžœ˜ Jšœžœ ˜Jšœ"˜"J˜—J˜,Jšžœžœžœžœ˜)J˜J˜J˜!J˜!J™J˜+Jšœ#˜#Jšœ!˜!Jšœ˜Jšœ< œ˜OJ˜J˜—šŸ œžœ˜!JšœžœGžœ˜Všžœ˜ Jšœžœ˜Jšœž˜J˜—J˜8J˜J˜7šΟb œžœ˜'J™Jšœžœ˜ Jšœžœ˜Jšžœ)žœžœ˜7šžœžœ˜Jšœ ˜ Jšœžœ˜*Jšœ˜—šžœ˜Jšœžœ˜!Jšœ˜Jšœ˜—šœ˜J˜J˜J˜J™J™šœžœžœž˜Jšžœ0˜4—J˜J˜J˜˜J˜Y——J˜—šœžœžœžœ ˜ Jš žœžœ žœžœžœ ˜5—šžœ žœ˜J˜)Jšžœ žœžœ˜%J˜—Jšžœ žœžœ˜Jš œžœžœ žœžœ˜MJ˜+J˜˜Jšœ1˜1Jšœ+žœ˜0—J˜J˜—š Ÿ œžœžœžœžœ!˜KJšžœžœžœ˜6J˜J˜"Jšžœžœžœ˜J˜J˜J˜—šŸ œžœ˜ Jšœžœ˜Jšœ˜J˜Jšœžœ˜Jšœžœžœ˜/Jšžœžœžœ˜J˜Jšœ%˜%J™$Jšžœ žœžœ)˜QJšœ˜J˜—šŸœžœžœžœ˜,Kšœžœ˜K˜ Kšœ*˜*Kšœžœžœ*˜:Kšœ žœ˜$Kšœ žœ žœ˜K˜K˜Kšœžœ5˜>Kšœ-˜-šžœž˜Kšœžœ˜!Kš œžœ žœžœžœ ˜IKšžœžœ˜—K˜Kšœ.˜.šžœ žœž˜Kšœ žœb˜qKšœžœ$˜AK˜—K™šžœ2ž˜8Kšœ(˜(—Kšžœ7˜;šžœ˜Kšœžœžœ˜>K˜-K˜—K˜K˜7šžœ žœžœ˜Kšœ˜K˜+K˜—K˜,K˜MK˜5K˜K˜—K˜šŸœžœ˜*Kšœžœ˜K˜ K˜Kšœžœ ˜Kšœžœžœ5˜EKšœ žœ˜$K˜Kšœ˜Kš žœžœžœžœžœ˜CK˜"Kšžœžœžœžœ˜2Kšœ.˜.šžœ žœžœ˜Kšœžœ"žœ˜6Kšžœžœžœ0˜AKšžœ žœžœžœ˜;K˜—K˜K˜K˜J˜——™ J˜š Ÿœžœžœ'žœžœžœ˜hšœžœ˜ Jšœžœ˜Jšœžœ˜Jšœžœ˜Jšœ žœ˜—J™WJ™CJ™eJšœ8˜8Jšœ$˜$Jšœ'˜'šœžœž œž˜J˜J˜Jšœžœžœ žœ ˜HJšžœ ˜—Jšžœ žœžœ˜9šžœžœ ž˜#Jšœ<˜J˜$J˜1šœ žœ˜)J˜ J˜J˜J˜J˜J˜—J™Jšœ; E˜€Jšœ˜J˜—Jšœžœžœ ˜&šœ žœžœ˜J˜Jšœž˜J˜J˜—šŸœžœ˜šžœLžœ˜TJš žœžœžœžœžœžœ˜E——J˜Jš œoΟo œ]žœ$žœΎžœ™ΘJ™šŸœžœ˜Jšœ 3˜OJšœ žœ˜J˜J˜Jšœž˜J˜–@ -- [trip: Triples.TripleRec] RETURNS [continue: BOOLEAN _ TRUE]šœ žœ˜-JšΠck<™šžœ žœžœ žœ˜BJšœP˜P—šžœ žœžœ˜Jšœžœ<˜PJ˜,JšœE˜EJ˜—Jšœžœ"  œ˜@Jšžœžœžœ#˜4J˜—Jšžœžœ˜0Jšžœ+  œ˜GJ˜—J˜J™CšŸ œžœ˜(™ J™ŒJ™i—Jšœžœ˜,šœžœ-žœ ˜VJšžœžœ$˜,—šœ˜Jšœ]˜]—šžœžœ˜ Jšžœ5 ˜T—Jšžœžœ˜)J˜J˜—J™,šŸœžœžœ˜ J™kJšœžœ˜#Jšœžœ˜3˜šžœžœ˜šžœžœ˜J˜?Jšœ7˜7J˜—Jšœžœ˜Jšžœ˜ Jšœ˜—Jšžœžœžœ˜$šœ+˜+JšœR˜R—J˜.J˜—Jšœ˜—J˜šŸœžœžœ˜!J™BJšœžœ˜#Jšœ žœ˜'J˜J˜—šŸ œžœžœ˜1J™BKšœ,˜,šžœ žœžœžœ˜JKšœžœ˜.—J˜J˜—šŸœžœžœžœ˜?J™BJšœž œ˜K˜MKšžœ˜ Kšœ˜J˜J˜—šŸœžœžœ˜?J˜.Jšœžœ ˜EJšœ˜J˜—šŸœžœžœžœ˜.Jšœ- ˜Aš œžœžœžœžœžœ ˜1Jšžœžœ ˜—Jšžœ ˜Jšœ˜J˜—šŸœžœžœ˜$K˜Kšœžœ ˜Kšœžœ žœžœ˜6š žœžœžœžœ8žœžœž˜ZKšžœ)žœžœžœ ˜HKšžœ˜—K˜K˜——™7J™Jšœ žœžœ˜'šœžœžœ˜J˜J˜J˜J˜J˜—šŸœžœžœ.˜TJ™»šŸœžœ ˜IJš (œ ˜?Jšœ,  ˜LJšœ- 1˜^J˜—šŸ œžœ£˜?Jš£(œ˜,Jšœ(žœ ˜9Jšœ˜JšœCžœ˜JJšžœ žœžœžœ˜JšΟt’œΗ€™Ϊšžœžœ;˜ZJšœ9žœžœ˜E—J˜,Jšœžœ˜šžœžœ˜Jšœ‹™‹J˜(J˜(J˜—JšœE˜EJ™Zšœ žœ˜(J˜ ˜J˜J˜AJ˜J˜Jšœ3 ˜FJ˜—J˜7J˜J˜—šœ˜Jšœ˜Jšœ žœ˜Jšœ ˜ Jšœžœ˜ šœžœ˜"Jšœ˜Jšœ<˜<—J˜—J˜—Jšžœ žœžœ žœžœžœžœ˜AJ˜=J˜J˜—šŸœžœ˜/šžœLžœ™TJš žœžœžœžœžœžœ™E—J™°Jšœžœ˜0J˜!Jšœžœ ˜1J˜!Jšœžœ0˜Kšœ˜Jšœg˜g—J˜J˜—šŸœžœžœ˜$J™ŽJšœžœ˜#Jšœžœ˜7˜šžœžœ˜šžœžœ˜J˜?Jšœ7˜7J˜—Jšœžœ˜Jšžœ˜ Jšœ˜—Jšžœžœžœ˜$šœ/˜/Jšœ;˜;Jšœ!˜!JšœH˜H—šœ:˜:J™@—J˜—J˜J˜J˜——™0J˜Jšœ žœžœ ˜šœ žœžœ˜J˜Jšœžœ ˜9Jšœžœ ˜9Jšœ ž˜Jšœž˜J˜J˜—šŸ œžœžœžœ˜ Jšœžœ˜Jšœ G˜dJšœ žœž˜Jšœžœž˜Kšœžœžœžœ˜/Jšœ˜J˜J˜,Jšžœžœžœ "˜@Jšœ žœžœ"˜LJšœ  ˜?šœ4žœ˜9Jšœžœ˜/JšœU‘œ™v—J˜)J˜J˜—šŸœžœ˜/™ J™bJ™‹J™l—Jšœžœ˜(Jšœ˜šžœ0ž˜6Jš žœžœžœžœžœ ˜D—šžœžœžœ/˜OJšžœžœ %˜1—Jšžœžœ  ˜Ošœ˜Jšœd˜d—Jšžœžœžœ X˜~Jšœ1 $˜UJšžœ˜$J˜J˜—šŸœžœžœ˜ J™kJšœžœ˜#Jšœžœ˜/J˜+˜šžœžœ˜šžœžœ˜J˜?Jšœ7˜7J˜—Jšœžœ˜Jšžœ˜ Jšœ˜—J˜Jš žœžœžœ€œ €˜Jšœ/˜/JšœT˜T—Jšœ/˜/šžœžœ˜"šŸ œžœžœ˜Jšœ:™:J˜Jšœ8˜8Jšžœžœžœ˜+J˜9J˜,JšœE˜EJ˜—J˜J˜—Jšœ˜J˜—Jšœ˜J˜——™J˜J˜.J˜šŸ œžœžœžœ˜9J™#Jšžœžœ˜)Jšžœžœžœ˜9—J˜šŸ œžœžœ˜5J™Jšžœ(˜/Jšœ)˜)Jšžœžœžœžœ˜7J˜—J˜šŸœžœžœžœ(˜M™VJ™—Jš œžœžœ žœžœžœžœ˜3šŸ œ˜'Jšœžœ˜%Jšœ"žœ˜'Jšžœžœ˜J˜—J˜šžœžœ ž˜%Jšžœžœžœ&˜CJšžœ˜—J˜8Jšžœ˜%J˜J˜—šŸœžœžœžœ™