BluejaySmartsImpl.mesa
Copyright © 1985, 1986 by Xerox Corporation. All rights reserved.
Last modified by D. Swinehart, October 19, 1986 4:17:20 pm PDT
Doug Terry, November 18, 1986 3:55:55 pm PST
DIRECTORY
BluejaySmarts,
GList   USING [ DRemove, Nconc ],
IO,
Jukebox  USING [ Handle, IntervalSpec, VoiceDirection ],
MBQueue USING [ QueueClientAction ],
Process USING [ Detach, SecondsToTicks, SetTimeout ],
PupSocket  USING [ SetGetTimeout, SetRemoteAddress, Socket ],
RecordingServiceRegister USING [ jayShh, jukebox, numParties ],
RefID  USING [ ID ],
ThParty  USING [ Advance, ConversationInfo, GetConversationInfo, GetKeyTable, GetPartyInfo, PartyInfo, RegisterKey, ReportAction ],
ThPartyPrivate USING [ GetPupSocket ],
Needing this is a travesty. See comments about sockets in ThPartyPrivate.
Thrush  USING [ ActionID, ActionReport, ConversationID, ConvEvent, Credentials, EncryptionKey, NB, NetAddress, notReallyInConv, nullConvID, PartyID, ROPE, SHHH, SmartsID, StateInConv ],
ThSmarts,
VoiceStream USING [ Close, VoiceStreamEvent, Handle, NotifyProc, Open, SetSocket, WaitEmpty, wholeTune ],
VoiceUtils USING [ Problem, ReportFR ]
;
BluejaySmartsImpl: CEDAR MONITOR
IMPORTS GList, IO, MBQueue, Process, PupSocket, RecordingServiceRegister, ThParty, ThPartyPrivate, VoiceStream, VoiceUtils
EXPORTS BluejaySmarts, ThSmarts = {
OPEN IO;
Types, Definitions
ConversationID: TYPE = Thrush.ConversationID;
nullConvID: ConversationID = Thrush.nullConvID;
PartyID: TYPE = Thrush.PartyID;
SHHH: TYPE = Thrush.SHHH;
SmartsID: TYPE = Thrush.SmartsID;
StateInConv: TYPE = Thrush.StateInConv;
JayInfo: TYPE = BluejaySmarts.JayInfo;
NB: TYPE = Thrush.NB;
IntID: TYPE = BluejaySmarts.IntID;
nullIntID: IntID = BluejaySmarts.nullIntID;
IntervalSpec: TYPE = Jukebox.IntervalSpec;
IntervalReq: TYPE = BluejaySmarts.IntervalReq;
infos: PUBLIC ARRAY[0..100) OF BluejaySmarts.JayInfo←ALL[NIL];
keyDistributionTimeoutInSeconds: INT ← 300;
ConvDesc: TYPE = BluejaySmarts.ConvDesc;
ConvDescBody: TYPE = BluejaySmarts.ConvDescBody;
NconcIntervals: PROC[l1, l2: LIST OF IntervalReq] RETURNS[LIST OF IntervalReq] =
INLINE { RETURN[NARROW[GList.Nconc[l1, l2]]]; };
Call Supervision
Progress: PUBLIC ENTRY PROC[
shh: Thrush.SHHH,
convEvent: Thrush.ConvEvent
] = {
ENABLE UNWIND => NULL; -- RestoreInvariant;
Some party has changed state in a conversation presumably relevant.
info: JayInfo = InfoForSmarts[smartsID: convEvent.self.smartsID];
convID: Thrush.ConversationID = convEvent.self.convID;
cDesc: ConvDesc ← GetConv[info, convID];
whatNeedsDoing: WhatNeedsDoing;
IF cDesc=NIL THEN { VoiceUtils.Problem["No info at Progress", $Bluejay]; RETURN; };
IF convEvent.self.partyID = convEvent.other.partyID THEN { -- own state changed
NoteNewState[cDesc, convEvent];
SELECT info.currentConvID FROM
convID, nullConvID => NULL;
(new conversation and we're idle) or (this is the one we like)
ENDCASE => []𡤌hangeState[cDesc, $idle, $busy, "One conversation at a time, please"];
We have to reject this, since we're already dealing with another conv.
We are (still) willing to seriously consider only one conversation at a time.
RETURN;
};
IF convID#info.currentConvID THEN {
A change in a conversation we haven't previously heard about??
VoiceUtils.Problem["Strange progress report", $Bluejay]; ForgetConv[cDesc]; RETURN; };
Someone else's state changed in a conv. we're interested in; see if it means anything to us!
whatNeedsDoing ← whatNeedsDoingIf[cDesc.situation.self.state][convEvent.other.state];
SELECT whatNeedsDoing FROM
$noop, $ntiy, $reac, $deac => NULL;
-- No action is needed, or we don't yet know what one to take.
$imp => ERROR; -- This is supposed to be impossible!
$xrep => VoiceUtils.Problem["Didn't expect state change report", $Bluejay];
$frgt => ForgetConv[cDesc];
$invl => {
VoiceUtils.Problem["Invalid state transition", $Bluejay];
[]𡤌hangeState[cDesc: cDesc, state: $idle,
reason: $error, comment: "System Error: Invalid state transition"];
};
$idle => {
nb2: Thrush.NB; cInfo: ThParty.ConversationInfo;
[nb2, cInfo] ← ThParty.GetConversationInfo[shh: RecordingServiceRegister.jayShh, convID: convID];
IF nb2 # $success OR (cInfo.numParties-cInfo.numIdle) <= 1 THEN
[]𡤌hangeState[cDesc, $idle,
IF nb2#$success THEN $error ELSE $terminating];
};
ENDCASE => ERROR;
};
Substitution: PUBLIC ENTRY PROC[
shh: Thrush.SHHH,
convEvent: Thrush.ConvEvent,
oldPartyID: Thrush.PartyID,
newPartyID: Thrush.PartyID
] = {
ENABLE UNWIND => NULL; -- RestoreInvariant;
A poacher has substituted for a poachee, or the other way around. Update state so that if it's us, we know who we are.
info: JayInfo = InfoForSmarts[smartsID: convEvent.self.smartsID];
cDesc: ConvDesc ← GetConv[info, convEvent.self.convID];
IF cDesc=NIL THEN { VoiceUtils.Problem["No info at Substitution", $Bluejay]; RETURN; };
NoteNewState[cDesc, convEvent];
};
ReportAction: PUBLIC ENTRY PROC[
shh: SHHH,
report: Thrush.ActionReport
] = {
ENABLE UNWIND => NULL;
info: JayInfo = InfoForSmarts[smartsID: report.self.smartsID];
convID: Thrush.ConversationID = report.self.convID;
cDesc: ConvDesc ← GetConv[info, convID];
IF cDesc=NIL THEN { VoiceUtils.Problem["can't find conversation for report", $Bluejay]; RETURN; };
SELECT report.actionClass FROM
$keyDistribution => {
BROADCAST cDesc.keysMightBeDistributed;
cDesc.keysDistributed ← TRUE;
RETURN;
};
ENDCASE=> shh ← shh; -- a place to stand during debugging
};
NoteNewState: INTERNAL PROC[cDesc: ConvDesc, convEvent: Thrush.ConvEvent] = {
OPEN now: cDesc.situation.self;
nb: Thrush.NB;
state: StateInConv ← convEvent.self.state;
previousState: StateInConv = cDesc.situation.self.state;
info: JayInfo = cDesc.info;
cDesc.situation ← convEvent^;
IF state = previousState THEN RETURN; -- No conceivable value in acting.
IF info.currentConvID=nullConvID THEN info.currentConvID ← cDesc.situation.self.convID;
SELECT state FROM
$notified => {
nb ← OpenConnection[cDesc]; -- Set up Bluejay streams, socket stuff
nb ← ChangeState[cDesc, IF nb=$success THEN $active ELSE $idle];
IF nb#$success THEN ERROR; -- should work regardless of everything.
};
$idle, $neverWas => ForgetConv[cDesc];
$active => NULL; -- Need take no action until recording or playback requested.
ENDCASE;
};
Tunes: control of recording and playback
GetConversation: PUBLIC ENTRY PROC [ -- exported to BluejaySmarts
smartsID: Thrush.SmartsID,
conv: Thrush.ConversationID
]
RETURNS [nb: Thrush.NB←$success, cDesc: ConvDesc] = {
ENABLE UNWIND => NULL;
info: JayInfo ← InfoForSmarts[smartsID];
cDesc ← GetConv[info, conv];
nb ← SELECT TRUE FROM
info=NIL => $noSuchSmarts2,
conv=nullConvID OR cDesc=NIL => $noSuchConv,
conv#info.currentConvID => $notInConv,
cDesc.situation.self.state#$active => $convNotActive,
ENDCASE => $success;
IF nb=$success AND cDesc.keyTable=NIL THEN
[nb, cDesc.keyTable] ← ThParty.GetKeyTable[info.shh, cDesc.situation.self];
};
DistributeKey: PUBLIC ENTRY PROC [ -- exported to BluejaySmarts
cDesc: ConvDesc,
key: Thrush.EncryptionKey,
wait: BOOLFALSE
]
RETURNS [nb: Thrush.NB, keyIndex: [0..17B]] = TRUSTED {
ENABLE UNWIND => NULL;
keyID: LONG POINTER TO Thrush.ActionID = LOOPHOLE[LONG[@key]];
num: NAT;
[nb, keyIndex] ← ThParty.RegisterKey[credentials: cDesc.situation.self, key: key];
SELECT nb FROM $success, $newKeys, $stateMismatch => NULL; ENDCASE => GOTO Fail;
IF wait THEN {
We don't want to schedule new voice until the encryption keys for it have been distributed. An ActionReport is issued to tell everybody about the keys and then tell us that it's told everybody. We wait one time around a timeout for a special condition variable (ugh) and then give up.
[nb, num] ← ThParty.ReportAction[report: [other: cDesc.situation.self, requestingParty: cDesc.situation.self.partyID, actionID: keyID^, actionClass: $keyDistribution, actionType: $newKeys], reportToAll: TRUE, selfOnCompletion: TRUE];
SELECT nb FROM $success, $stateMismatch => nb ← $success; ENDCASE => GOTO Fail;
IF num=0 THEN RETURN;
nb ← $newKeys;
cDesc.keysDistributed ← FALSE;
Process.SetTimeout[@cDesc.keysMightBeDistributed, Process.SecondsToTicks[keyDistributionTimeoutInSeconds]];
WAIT cDesc.keysMightBeDistributed;
IF ~cDesc.keysDistributed THEN GOTO Fail;
};
EXITS
Fail => nb ← ChangeState[cDesc, $idle, $error, "Could not distribute encryption key"];
};
SetInterval: PUBLIC ENTRY SAFE PROC [ir: IntervalReq] -- exported to BluejaySmarts
RETURNS [nb: Thrush.NB←$success] = CHECKED {
ENABLE UNWIND => NULL;
IF ir.iSpec.tuneID <0 THEN RETURN[nb: $noTuneSpecified];
IF ir.iSpec.length = -1 THEN ir.iSpec.length ← VoiceStream.wholeTune;
IF ir.intID # nullIntID THEN {
ir.reportRequested ← TRUE;
IssueReport[ir, $scheduled];
};
ir.cDesc.info.intervalReqs ← NconcIntervals[ir.cDesc.info.intervalReqs, LIST[ir]];
};
IssueReport: INTERNAL PROC[req: IntervalReq, event: VoiceStream.VoiceStreamEvent] = {
cDesc: ConvDesc ← req.cDesc;
info: JayInfo ← cDesc.info;
nb: NB;
If the event is a completion event ($finished or $flushed), the activity might have been a write. If it was a write, the tune will still be locked, and actions the client might take referring to the tune may fail. To avoid this, we want to wait until Bluejay has finished everything up (if there's no further activity scheduled), or — when there is activity — until Bluejay has moved on to the next request (indicated by the next $started report). ReportFromBluejay queues its reports, but it also nudges a condition variable and associated count in real time, so that one can wait in one report for the next one to come in. Since they're serialized, there will be at most one of these waiters-for-report pending per Bluejay Smarts.
SELECT event FROM
$finished, $flushed => {
IF event=$finished AND ~req.lastInterval THEN RETURN;
IF info.intervalReqs=NIL THEN TRUSTED {VoiceStream.WaitEmpty[info.stream]}
ELSE WHILE info.numNotifications<=0 DO WAIT info.reportsExist; ENDLOOP;
};
$started, $scheduled => IF ~req.firstInterval THEN RETURN;
ENDCASE;
nb ← ThParty.ReportAction[
shhh: RecordingServiceRegister.jayShh,
report: [
self: cDesc.situation.self, -- placeholder, will be filled in by ThParty
other: cDesc.situation.self,
requestingParty: req.requestingParty,
actionID: req.intID,
actionClass: IF req.direction = $record THEN $recording ELSE $playback,
actionType: event
],
reportToAll: TRUE
].nb;
IF nb#$success THEN VoiceUtils.Problem["Bluejay report failed", $Bluejay];
};
Reports are queued because they can be generated directly by procedures called from entry procedures in this module. Perhaps the queueing should be done by Bluejay?
IREvent: TYPE = REF IREventRec;
IREventRec: TYPE = RECORD [
ir: IntervalReq,
event: VoiceStream.VoiceStreamEvent
];
EP: ENTRY PROC[info: JayInfo] = TRUSTED {
info.numNotifications ← info.numNotifications + 1;
BROADCAST info.reportsExist;
};
ReportFromBluejay: VoiceStream.NotifyProc = TRUSTED {
ir: IntervalReq ← NARROW[clientData];
irE: IREvent;
IF ir=NIL OR ir.cDesc=NIL THEN RETURN; -- We don't report actions that do not affect our requests
irE ← NEW[IREventRec ← [ir, event]];
ir.cDesc.info.notifications.QueueClientAction[QdReportFromBluejay, irE];
Process.Detach[FORK EP[ir.cDesc.info]]; -- could be a separate MBQueue
};
QdReportFromBluejay: ENTRY PROC[r: REF] = {
Any interval prior to the one being reported was not finished normally. We treat it as if it were flushed. We remember the last one that requested a report. We extend that to include the interval being reported if the event is $flushed and it wasn't a record request (which we convert to $finished.)
Once we reach the reported interval in our saved list of requests, we report on any intervals that were flushed, then on the reported intervals, if a report was requested for it and the event is $started or $finished.
irE: IREvent ← NARROW[r];
ir: IntervalReq ← irE.ir;
event: VoiceStream.VoiceStreamEvent ← irE.event;
cDesc: ConvDesc;
info: JayInfo;
rqst: BOOLFALSE;
irFlush: IntervalReq ← NIL;
cDesc ← ir.cDesc;
IF cDesc.situation.self.state <= Thrush.notReallyInConv THEN RETURN;
info ← cDesc.info;
info.numNotifications ← info.numNotifications-1;
FOR irS: LIST OF IntervalReq ← info.intervalReqs, irS.rest WHILE irS#NIL DO
ir1: IntervalReq ← irS.first;
match: BOOL;
rqst ← ir1.reportRequested;
match ← ir=ir1;
IF match AND rqst AND event=$flushed AND ir.direction=record THEN event←$finished;
Recording is usually stopped by a flush event; we treat it as finished.
IF rqst AND (~match OR event=$flushed) THEN irFlush ← ir1;
IF ~(match AND event=$started) THEN info.intervalReqs ← irS.rest; -- Shorten list
IF match THEN EXIT;
REPEAT FINISHED => rqst ← FALSE;
ENDLOOP;
IF irFlush#NIL THEN IssueReport[irFlush, $flushed];
IF rqst AND event#$flushed THEN IssueReport[ir, event];
};
Utilities
GetConv: --PUBLIC-- INTERNAL PROC[info: JayInfo, convID: ConversationID ← nullConvID]
RETURNS [ cDesc: ConvDesc ] = {
IF info = NIL THEN RETURN[NIL];
IF convID = nullConvID THEN convID ← info.currentConvID;
IF convID = nullConvID THEN RETURN[NIL];
FOR convs: LIST OF BluejaySmarts.ConvDesc ← info.conversations, convs.rest WHILE convs#NIL DO
IF convs.first.situation.self.convID = convID THEN RETURN[convs.first];
ENDLOOP;
cDesc ← NEW[ConvDescBody←[]];
cDesc.situation.self.convID ← convID;
cDesc.info ← info;
info.conversations ← CONS[cDesc, info.conversations];
};
InfoForSmarts: INTERNAL PROC [ smartsID: SmartsID ] RETURNS [ info: JayInfo←NIL ] = {
FOR i: NAT IN [0..RecordingServiceRegister.numParties) DO
IF smartsID=infos[i].credentials.smartsID THEN RETURN [ infos[i] ];
ENDLOOP;
};
ChangeState: INTERNAL PROC[
cDesc: ConvDesc, state: StateInConv, reason: ATOMNIL, comment: ROPENIL]
RETURNS [nb: NB] = {
convEvent: Thrush.ConvEvent;
[nb, convEvent] ← ThParty.Advance[
shhh: RecordingServiceRegister.jayShh,
credentials: cDesc.situation.self,
state: state,
reason: reason,
comment: comment,
reportToAll: TRUE -- parameterize if other states than $idle and $active become possible
];
SELECT nb FROM
$success => NoteNewState[cDesc, convEvent];
$stateMismatch => {
We and some other smarts acting on our behalf requested something at the same time. The other one won. This should not have happened. The important thing is not to get hung up in weird loops.
VoiceUtils.Problem["Statemismatch for recording party", $Bluejay];
NoteNewState[cDesc, convEvent];
nb ← ChangeState[cDesc, $idle, $error, "State Mismatch bug"];
};
$noSuchConv => VoiceUtils.Problem["Conv doesn't exist at Advance", $Bluejay];
Unusual, but possible if idle report took a real long time to get here.
$partyAlreadyActive => NULL; -- Deal with $canActivate here some time
ENDCASE =>ERROR; --=> AssessDamage[nb, cDesc, convEvent] Error management needed
};
ForgetConv: INTERNAL PROC[cDesc: ConvDesc ] = {
info: JayInfo ← cDesc.info;
CloseConnection[cDesc];
IF cDesc.situation.self.convID = info.currentConvID THEN info.currentConvID ← nullConvID;
info.conversations ← NARROW[GList.DRemove[cDesc, info.conversations]];
};
OpenConnection: INTERNAL PROC[cDesc: ConvDesc] RETURNS[nb: NB] = TRUSTED {
info: JayInfo = cDesc.info;
pInfo: ThParty.PartyInfo;
socket: PupSocket.Socket;
remoteAddress: Thrush.NetAddress;
IF info.stream=NIL THEN info.stream ← VoiceStream.Open[jukebox: RecordingServiceRegister.jukebox, proc: ReportFromBluejay];
[nb, pInfo] ←
ThParty.GetPartyInfo[credentials: cDesc.situation.self, nameReq: $none, allParties: TRUE];
IF nb # $success OR pInfo[0].partyID=0 THEN {
VoiceUtils.Problem["No conversation info, or incomplete", $Bluejay]; RETURN; }; -- This is really bad!
Neither this nor Bluejay deals at all properly with multicasting. Worse yet on recording! See also ThPartyPrivate discussion about sockets, and discussion of socket assignments in LarkSmartsSupImpl.ComputeConnection.
socket ← ThPartyPrivate.GetPupSocket[pInfo[1].partyID];
This is a value that includes our own net/host, but the remote party's assigned socket ID.
IF socket=NIL THEN ERROR;
remoteAddress ← pInfo[1].socket; -- Transmit to remote net and host
remoteAddress.socket ← pInfo[0].socket.socket; -- Transmit to own assigned socket ID
PupSocket.SetRemoteAddress[socket, remoteAddress];
PupSocket.SetGetTimeout[socket, 100];
VoiceStream.SetSocket[socket: socket, handle: info.stream];
VoiceUtils.ReportFR["C %d ", $Bluejay, NIL, card[info.credentials.smartsID]];
};
CloseConnection: INTERNAL PROC[cDesc: ConvDesc] = TRUSTED {
info: JayInfo = cDesc.info;
IF info.stream#NIL THEN VoiceStream.Close[info.stream];
info.stream ← NIL;
VoiceUtils.ReportFR["D %d ", $Bluejay, NIL, card[info.credentials.smartsID]];
};
State Transition Tables
WhatNeedsDoing: TYPE = ATOM; -- {
Just codes to dispatch on in Supervisor; explained there
$noop, $idle, $frgt, $reac, $deac, -- cases explained in code
$invl, -- considered an invalid request
$xrep, -- we got a report we feel we shouldn't have got
$ntiy, -- not implemented yet
$imp -- this situation should not arise even in the face of invalid requests
};
whatNeedsDoingIf: ARRAY StateInConv OF ARRAY StateInConv OF WhatNeedsDoing ← [
If we're in the state identified by the row, and someone else in the conversation reports a transition onto the state identified by the column, what should we do?
never idle failed resrv pars init notif rback ring canAc activ inact -- ← (other)
[ $imp, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt], --neverWas
(Clip this table to view without these comments.)
This situation arises when we've forgotten about the conversation that somebody else is still reporting on.
[ $imp, $noop, $frgt, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $ntiy ], -- idle
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- failed
[ $imp, $imp, $imp , $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- reserved
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- parsing
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp , $imp, $imp, $imp ], -- initiating
[ $imp, $idle,$idle, $invl, $invl, $invl, $xrep, $noop, $noop, $ntiy, $noop, $ntiy ], -- notified
We don't expect to hear from others while we're deciding whether to play
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- ringback
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- ringing
[ $imp, $idle,$idle,$invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $noop, $ntiy ], -- canActivate
[ $imp, $idle,$idle,$invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $reac, $deac ], -- active
[ $imp, $idle,$idle,$invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ntiy, $ntiy ] -- inactive (current ^)
failed column and possibly idle column is bogus!
];
}.
Swinehart, May 15, 1985 10:30:42 am PDT
Cedar 6.0
changes to: InitJay
Swinehart, May 22, 1985 12:07:31 pm PDT
recording => service, Jay => Recording
changes to: InitJay
Swinehart, May 17, 1986 3:53:47 pm PDT
Cedar6.1
changes to: DIRECTORY, BluejaySmartsImpl, Report, Supervise, OpenConnection, CloseConnection, ReportDoneEntry, InitJay, ReportBluejay
Doug Terry, July 29, 1986 11:41:35 am PDT
Removed operations for recording and playing voice ropes; their implementations are included in VoiceRopeImpl.
changes to: DIRECTORY, BluejaySmartsImpl, IntID, nullIntID, IntervalSpec, NoteNewState, SetInterval, IssueReport, GetConv, InitJay
Doug Terry, July 30, 1986 12:07:36 pm PDT
Moved JayInit to RecordingServiceRegister.
changes to: DIRECTORY, BluejaySmartsImpl, CloseConnection, ConvDescBody
Doug Terry, August 18, 1986 2:16:07 pm PDT
Tracked change to BluejaySmarts.IntervalReqBody.
changes to: DIRECTORY, IntID, nullIntID, SetInterval, IssueReport