-- N.Wirth June 1, 1977 -- S.Andler August 24, 1977 10:49 PM -- C.Geschke August 22, 1977 5:11 PM DIRECTORY InlineDefs: FROM "InlineDefs", IODefs: FROM "IODefs", PupDefs: FROM "PupDefs", StringDefs: FROM "StringDefs", SystemDefs: FROM "SystemDefs", TeleSilDefs: FROM "TeleSilDefs"; DEFINITIONS FROM TeleSilDefs; TeleSilPup: PROGRAM= BEGIN -- External Procedures -- -- From KeyStreams -- SetIdleProc: EXTERNAL PROCEDURE[p: PROCEDURE]; ResetIdleProc: EXTERNAL PROCEDURE; -- From TeleSilResident -- BitMapDisplay: EXTERNAL PROCEDURE; Mark: EXTERNAL PROCEDURE[point: Coord, icon: CursorIcon]; SetCursorIcon: EXTERNAL PROCEDURE[icon: CursorIcon] RETURNS[oldIcon: CursorIcon]; SetMousePosition: EXTERNAL PROCEDURE[point: Coord]; TypeScriptWindow: EXTERNAL PROCEDURE; -- From TeleSilDisplay -- Confirm: EXTERNAL PROCEDURE RETURNS[BOOLEAN]; Error: EXTERNAL PROCEDURE[s: STRING]; -- From TeleSilIO -- InputFile: EXTERNAL PROCEDURE[ EndOfFile: PROCEDURE RETURNS[BOOLEAN], GetWord: PROCEDURE RETURNS[WORD], GetBlock: PROCEDURE[address: POINTER, words: INTEGER]]; OutputFile: EXTERNAL PROCEDURE[ PutWord: PROCEDURE[word: WORD], PutBlock: PROCEDURE[address: POINTER, words: INTEGER]]; -- From TeleSilMain -- InputStatus: EXTERNAL PROCEDURE[GetWord: PROCEDURE RETURNS[WORD]]; NewPicture: EXTERNAL PROCEDURE; OutputStatus: EXTERNAL PROCEDURE[ PutWord: PROCEDURE[word: WORD]]; SetEtherOn: EXTERNAL PROCEDURE[newValue: BOOLEAN]; -- Constants -- BOF: PupDefs.Byte= 0; -- Mark byte to indicate beginning of file EOF: PupDefs.Byte= 1; -- Mark byte to indicate end of file -- Types -- Connection: TYPE= RECORD[ next: ConnPtr, id: ConnId, output: {active, closing, closed, finished}, input: {active, holding, closed, finished}, closeReason: PupDefs.CloseReason, outputWaiting: BOOLEAN, bs: PupDefs.ByteStream, name: STRING]; ConnPtr: TYPE= POINTER TO Connection; ConnSet: TYPE= WORD; -- Simulation of SET InQueueEl: TYPE= RECORD[ next: InQueuePtr, originator: ConnId, pCmd: CmdPtr]; OutQueueEl: TYPE= RECORD[ next: OutQueuePtr, recipients: ConnSet, pCmd: CmdPtr]; InQueuePtr: TYPE= POINTER TO InQueueEl; OutQueuePtr: TYPE= POINTER TO OutQueueEl; -- Variables -- pupVector: PupDefs.PupInterface _ NIL; --pointer to giant dispatch vector listenBs: PupDefs.ByteStream; -- for listening to anybody listenSocket: PupDefs.Pair _ PupDefs.Pair[0,6]; sendHim: PupDefs.Address _ [PupDefs.ThisNet, PupDefs.ThisHost, listenSocket]; -- listenSocket is the agreed upon listening socket sendMe: PupDefs.Address _ [PupDefs.ThisNet, PupDefs.ThisHost, PupDefs.Pair[0,0]]; -- Socket Pair[0,0] gives unique socket sendConn: PupDefs.ConnObject _ [@sendMe, @sendHim]; firstConn: ConnPtr _ NIL; setConn: ConnSet _ emptySet; firstIn, lastIn: InQueuePtr _ NIL; -- List of CmdObj:s coming in firstOut, lastOut: OutQueuePtr _ NIL; -- List of CmdObj:s to be sent out buffer, errorText: STRING _ [80]; -- sharing the same space! hostName, myName: STRING _ NIL; controlState: {passive, requested, granted} _ granted; -------------------------------------------------------------------- -- SETS -------------------------------------------------------------------- emptySet: WORD= 0; -- Simulation of the empty set fullSet: WORD= 177777B; -- Simulation of the full set Set: PROCEDURE[id: ConnId] RETURNS[ConnSet]= BEGIN OPEN InlineDefs; -- BITSHIFT RETURN[BITSHIFT[1, id]] END; InSet: PROCEDURE[set: ConnSet, id: ConnId] RETURNS[BOOLEAN]= BEGIN OPEN InlineDefs; -- BITAND RETURN[BITAND[set, Set[id]]#emptySet] END; SetUnion: PROCEDURE[s1, s2: ConnSet] RETURNS[ConnSet]= BEGIN OPEN InlineDefs; -- BITOR RETURN[BITOR[s1, s2]] END; SetDiff: PROCEDURE[s1, s2: ConnSet] RETURNS[ConnSet]= BEGIN OPEN InlineDefs; -- BITAND, BITNOT RETURN[BITAND[s1, BITNOT[s2]]] END; -------------------------------------------------------------------- -- STRINGS -------------------------------------------------------------------- GetString: PROCEDURE[bs: PupDefs.ByteStream, to: STRING]= BEGIN i: [0..80); to.length _ pupVector.ByteStreamGet[bs]; FOR i IN [0..to.length) DO to[i] _ pupVector.ByteStreamGetChar[bs] ENDLOOP; IF InlineDefs.BITAND[to.length,1]=0 THEN [] _ pupVector.ByteStreamGet[bs] END; PutString: PROCEDURE[bs: PupDefs.ByteStream, from: STRING]= BEGIN i: [0..80); pupVector.ByteStreamPut[bs, from.length]; FOR i IN [0..from.length) DO pupVector.ByteStreamPutChar[bs, from[i]] ENDLOOP; IF InlineDefs.BITAND[from.length,1]=0 THEN pupVector.ByteStreamPut[bs, 0] END; -------------------------------------------------------------------- Yield: PUBLIC PROCEDURE= BEGIN pupVector.ScheduleeYields END; SendCmd: PUBLIC PROCEDURE[pCmd: CmdPtr]= BEGIN SelectiveSendCmd[pCmd, setConn] END; SelectiveSendCmd: PROCEDURE[pCmd: CmdPtr, recipients: ConnSet]= BEGIN -- This procedure copies the command if it has recipients p: OutQueuePtr; pConn: ConnPtr; CursorCmdPtr: TYPE = POINTER TO cursor CmdObj; FOR pConn _ firstConn, pConn.next UNTIL pConn=NIL DO IF InSet[recipients, pConn.id] THEN IF pConn.output=active THEN pConn.outputWaiting _ TRUE ELSE recipients _ SetDiff[recipients, Set[pConn.id]] ENDLOOP; IF recipients#emptySet THEN IF firstOut#NIL AND pCmd.cmdType=cursor AND lastOut.pCmd.cmdType=cursor THEN BEGIN lastOut.recipients _ recipients; LOOPHOLE[lastOut.pCmd, CursorCmdPtr].point _ LOOPHOLE[pCmd, CursorCmdPtr].point END ELSE BEGIN p _ SystemDefs.AllocateHeapNode[SIZE[OutQueueEl]]; p^ _ OutQueueEl[NIL, recipients, CopyCmd[pCmd]]; IF firstOut=NIL THEN firstOut _ p ELSE lastOut.next _ p; lastOut _ p END END; Refresh: PUBLIC PROCEDURE= BEGIN SelectiveRefresh[setConn] END; SelectiveRefresh: PROCEDURE[recipients: ConnSet]= BEGIN OPEN IODefs; -- Write... pConn: ConnPtr; FOR pConn _ firstConn, pConn.next UNTIL pConn=NIL DO IF InSet[recipients, pConn.id] AND pConn.output=active THEN BEGIN WriteString["Refreshing "]; WriteLine[pConn.name]; WriteLine["Waiting for output process to finish"]; WHILE pConn.outputWaiting DO Yield ENDLOOP; pupVector.ByteStreamPutMark[pConn.bs, BOF]; -- indicate beginning of file EtherOutput[pConn.bs] -- send refresh END; ENDLOOP END; RequestControl: PUBLIC PROCEDURE RETURNS[BOOLEAN]= BEGIN cmd: CmdObj; IF controlState=granted THEN RETURN[TRUE]; IF controlState=requested THEN RETURN[FALSE]; controlState _ requested; cmd _ [control[request, [pupVector.myNetwork, pupVector.myHost]]]; SendCmd[@cmd]; RETURN[FALSE] END; CmdWaiting: PUBLIC PROCEDURE RETURNS[BOOLEAN]= BEGIN RETURN[firstIn#NIL] END; NextCmd: PUBLIC PROCEDURE RETURNS[CmdPtr]= BEGIN tmp: InQueuePtr; pCmd: CmdPtr; originator: ConnId; thisGuy, allOthers: ConnSet; DO IF firstIn=NIL THEN RETURN[NIL]; pCmd _ firstIn.pCmd; tmp _ firstIn.next; originator _ firstIn.originator; thisGuy _ Set[originator]; allOthers _ SetDiff[setConn, thisGuy]; SystemDefs.FreeHeapNode[firstIn]; firstIn _ tmp; WITH pCmd SELECT FROM ether => EtherCmd[etherCmd, originator]; cursor => BEGIN SetMousePosition[point]; SelectiveSendCmd[pCmd, allOthers] END; mark => BEGIN Mark[point, icon]; SelectiveSendCmd[pCmd, allOthers] END; setCursorIcon => BEGIN [] _ SetCursorIcon[icon]; SelectiveSendCmd[pCmd, allOthers] END; control => BEGIN SELECT action FROM request => SELECT controlState FROM passive => SelectiveSendCmd[pCmd, allOthers]; requested => BEGIN action _ reject; SelectiveSendCmd[pCmd, thisGuy] END; granted => BEGIN controlState _ passive; action _ grant; SelectiveSendCmd[pCmd, thisGuy] END; ENDCASE; grant => IF address=[pupVector.myNetwork, pupVector.myHost] OR address=[-1,0] THEN controlState _ granted ELSE SelectiveSendCmd[pCmd, allOthers]; reject => IF address=[pupVector.myNetwork, pupVector.myHost] THEN BEGIN IF controlState=requested THEN controlState _ passive END ELSE SelectiveSendCmd[pCmd, allOthers]; ENDCASE; END; ENDCASE => BEGIN SelectiveSendCmd[pCmd, allOthers]; RETURN[pCmd] END; FreeCmd[pCmd]; Yield -- let Pup Package do its thing ENDLOOP END; AllocateCmd: PUBLIC PROCEDURE RETURNS[CmdPtr]= BEGIN RETURN[SystemDefs.AllocateHeapNode[SIZE[CmdObj]]] END; FreeCmd: PUBLIC PROCEDURE[pCmd: CmdPtr]= BEGIN SystemDefs.FreeHeapNode[pCmd]; WITH pCmd SELECT FROM text => SystemDefs.FreeHeapString[s]; ENDCASE; END; CopyCmd: PROCEDURE[oldCmd: CmdPtr] RETURNS[newCmd: CmdPtr]= BEGIN newCmd _ AllocateCmd[]; newCmd^ _ oldCmd^ END; Connect: PUBLIC PROCEDURE= BEGIN ReadAddress; ConnectAndListen[connect]; IF setConn#emptySet THEN controlState _ passive END; Listen: PUBLIC PROCEDURE= BEGIN ConnectAndListen[listen] END; ConnectAndListen: PROCEDURE[action: {connect, listen}]= BEGIN IF myName=NIL THEN BEGIN IODefs.WriteString["Your name: "]; buffer.length _ 0; IODefs.ReadID[buffer]; IODefs.WriteChar[IODefs.CR]; myName _ SystemDefs.AllocateHeapString[buffer.length]; StringDefs.AppendString[myName,buffer]; END; IODefs.WriteLine["Making Pup Package"]; PupDefs.SetSandBarOK[sandBarOK]; pupVector _ PupDefs.PupPackageMake[]; SetIdleProc[pupVector.ScheduleeYields]; -- Idle process for keyboard input IF action=connect THEN WHILE CheckAddress[] DO OpenConnection; IF setConn#emptySet THEN EXIT ELSE ReadAddress ENDLOOP; IODefs.WriteLine["Making Listener"]; listenBs _ pupVector.ByteStreamMakeListener[listenSocket, Notify, NIL]; SetEtherOn[TRUE] END; Disconnect: PUBLIC PROCEDURE= BEGIN cmd: CmdObj; IF setConn#emptySet THEN BEGIN IF controlState = granted THEN BEGIN cmd _ [control[grant, [-1,0]]]; SendCmd[@cmd] END; CloseConnections END; IODefs.WriteLine["Destroying Listener"]; pupVector.ByteStreamDestroy[listenBs]; IODefs.WriteLine["Destroying Pup Package"]; ResetIdleProc; -- Reset idle process for keyboard input pupVector.PupPackageDestroy; controlState _ granted; SetEtherOn[FALSE] END; ReadAddress: PROCEDURE= BEGIN OPEN IODefs; -- Write..., Read... WriteString["Connect to host: "]; buffer.length _ 0; IF hostName#NIL THEN BEGIN StringDefs.AppendString[buffer,hostName]; SystemDefs.FreeHeapString[hostName] END; IODefs.ReadID[buffer]; WriteChar[IODefs.CR]; hostName _ SystemDefs.AllocateHeapString[buffer.length]; StringDefs.AppendString[hostName,buffer] END; CheckAddress: PROCEDURE RETURNS[BOOLEAN]= BEGIN OPEN pupVector, -- GetAddress, PrintAddress IODefs; -- Write..., Read... DO -- until a host hostName has been parsed or no name given IF hostName.length=0 THEN RETURN[FALSE]; IF GetAddress[@sendHim, hostName, errorText] THEN BEGIN -- hostName parsed correctly WriteString["Address parsed: "]; PrintAddress[@sendHim]; WriteChar[IODefs.CR]; RETURN[TRUE] END; Error[errorText]; ReadAddress ENDLOOP END; OpenConnection: PROCEDURE= BEGIN OPEN pupVector, -- ByteStream... IODefs; -- Write..., Read... bs: PupDefs.ByteStream; WriteLine["Opening connection"]; bs _ ByteStreamMake[@sendConn]; BEGIN ENABLE StreamClosing => GOTO streamClosing; ByteStreamOpen[bs]; WriteString["Connected to "]; PrintAddress[@bs.myPacketStream.remote]; WriteChar[IODefs.CR]; -- Exchange names PutString[bs,myName]; ByteStreamSendNow[bs]; GetString[bs,buffer]; WriteString["Connection accepted by "]; WriteString[buffer]; WriteChar[IODefs.CR]; EtherInput[bs]; CreateIOProcesses[IdToConn[MakeConn[bs]]] EXITS streamClosing => BEGIN ByteStreamDestroy[bs]; Error["Remote end not responding"] END END END; CloseConnections: PROCEDURE= BEGIN -- This routine finishes output. The connections will be closed -- by OutputServer and destroyed by InputServer pConn: ConnPtr; IODefs.WriteLine["Closing connections"]; FOR pConn _ firstConn, pConn.next UNTIL pConn=NIL DO IF pConn.output=active THEN pConn.output _ closing ENDLOOP; UNTIL firstConn=NIL DO DestroyConn[firstConn] ENDLOOP END; Notify: PROCEDURE[UNSPECIFIED, bs: PupDefs.ByteStream, remAddr: PupDefs.PAddress]= BEGIN OPEN pupVector; -- ByteStream... IF setConn=fullSet THEN ERROR ELSE BEGIN ENABLE StreamClosing => GOTO streamClosing; -- Get name GetString[bs,buffer]; EtherReport[connecting, MakeConn[bs]] EXITS streamClosing => ByteStreamDestroy[bs] END END; MakeConn: PROCEDURE[bs: PupDefs.ByteStream] RETURNS[ConnId]= BEGIN pConn: ConnPtr _ SystemDefs.AllocateHeapNode[SIZE[Connection]]; id: ConnId; FOR id IN ConnId UNTIL NOT InSet[setConn, id] DO ENDLOOP; setConn _ SetUnion[setConn, Set[id]]; pConn^ _ Connection[next: firstConn, id: id, output: finished, input: finished, closeReason:, outputWaiting: FALSE, bs: bs, name:]; pConn.name _ SystemDefs.AllocateHeapString[buffer.length]; StringDefs.AppendString[pConn.name,buffer]; firstConn _ pConn; RETURN[id] END; CreateIOProcesses: PROCEDURE[pConn: ConnPtr]= BEGIN pConn.input _ active; pConn.output _ active; pupVector.ScheduleeCreate[InputServer, pConn]; pupVector.ScheduleeCreate[OutputServer, pConn]; END; DestroyConn: PROCEDURE[pConn: ConnPtr]= BEGIN previousConn: ConnPtr; WHILE pConn.output#finished OR pConn.input#finished DO Yield ENDLOOP; IF pConn=firstConn THEN firstConn _ pConn.next ELSE FOR previousConn _ firstConn, previousConn.next DO IF previousConn.next=pConn THEN BEGIN previousConn.next _ pConn.next; EXIT END ENDLOOP; pupVector.ByteStreamDestroy[pConn.bs]; SystemDefs.FreeHeapString[pConn.name]; setConn _ SetDiff[setConn, Set[pConn.id]]; SystemDefs.FreeHeapNode[pConn] END; EtherCmd: PROCEDURE[etherCmd: EtherCommand, sender: ConnId]= BEGIN OPEN IODefs; -- Write... pConn: ConnPtr _ IdToConn[sender]; TypeScriptWindow; SELECT etherCmd FROM input => BEGIN EtherInput[pConn.bs]; -- sender is sending picture pConn.input _ active; SelectiveRefresh[SetDiff[setConn, Set[sender]]] END; closing => BEGIN WriteString["Disconnected from "]; WriteString[pConn.name]; WriteString[" ... "]; SELECT pConn.closeReason FROM remoteClose => WriteString["remoteClose"]; transmissionTimeout => WriteString["transmissionTimeout"]; noRouteToNetwork => WriteString["noRouteToNetwork"]; ENDCASE => ERROR; DestroyConn[pConn]; IF controlState#granted THEN Disconnect END; connecting => BEGIN ENABLE pupVector.StreamClosing => GOTO streamClosing; WriteString["Connection from "]; WriteString[pConn.name]; WriteString[" - "]; pupVector.PrintAddress[@pConn.bs.myPacketStream.remote]; IF NOT Confirm[] THEN GOTO streamClosing; -- Send name PutString[pConn.bs,myName]; pupVector.ByteStreamSendNow[pConn.bs]; -- Send picture EtherOutput[pConn.bs]; CreateIOProcesses[pConn] EXITS streamClosing => DestroyConn[pConn] END; ENDCASE; BitMapDisplay END; IdToConn: PROCEDURE[id: ConnId] RETURNS[pConn: ConnPtr]= BEGIN FOR pConn _ firstConn, pConn.next DO IF pConn=NIL THEN ERROR; IF pConn.id=id THEN EXIT ENDLOOP END; EtherInput: PROCEDURE[bs: PupDefs.ByteStream]= BEGIN OPEN IODefs; eof: BOOLEAN _ FALSE; EndOfFile: PROCEDURE RETURNS[BOOLEAN]= BEGIN RETURN[eof] END; GetWord: PROCEDURE RETURNS[WORD]= BEGIN bytePair: RECORD[left, right: PupDefs.Byte]; bytePair.left _ pupVector.ByteStreamGet[bs]; bytePair.right _ pupVector.ByteStreamGet[bs]; RETURN[LOOPHOLE[bytePair]] END; GetBlock: PROCEDURE[address: POINTER, words: INTEGER]= BEGIN mark: PupDefs.Byte; finger: PupDefs.BlockFinger _ DESCRIPTOR[address, words]; pupVector.ByteStreamGetWholeBlock[bs, @finger ! pupVector.MarkInStream => BEGIN -- A mark=EOF in the stream means end of file. mark _ pupVector.ByteStreamGet[bs]; IF mark=EOF THEN BEGIN eof _ TRUE; CONTINUE END END] END; WriteLine["Receiving picture"]; NewPicture; InputStatus[GetWord]; InputFile[EndOfFile, GetWord, GetBlock]; WriteLine["Finished receiving picture"] END; EtherOutput: PROCEDURE[bs: PupDefs.ByteStream]= BEGIN OPEN IODefs; PutWord: PROCEDURE[word: WORD]= BEGIN bytePair: RECORD[left, right: PupDefs.Byte]; bytePair _ LOOPHOLE[word]; pupVector.ByteStreamPut[bs, bytePair.left]; pupVector.ByteStreamPut[bs, bytePair.right] END; PutBlock: PROCEDURE[address: POINTER, words: INTEGER]= BEGIN finger: PupDefs.BlockFinger _ DESCRIPTOR[address, words]; pupVector.ByteStreamPutBlock[bs, @finger] END; WriteLine["Sending picture"]; OutputStatus[PutWord]; OutputFile[PutWord, PutBlock]; pupVector.ByteStreamPutMark[bs, EOF]; -- indicate end of file WriteLine["Finished sending picture"]; END; OutputServer: PROCEDURE[pConn: ConnPtr]= BEGIN p:OutQueuePtr; finger: PupDefs.BlockFinger; DO IF pConn.outputWaiting THEN BEGIN DO -- delete messages that have no recipients left p _ firstOut; IF p.recipients#emptySet THEN EXIT; firstOut _ p.next; FreeCmd[p.pCmd]; SystemDefs.FreeHeapNode[p] ENDLOOP; DO -- send messages intended for this connection IF InSet[p.recipients, pConn.id] THEN BEGIN IF pConn.output=active OR pConn.output=closing THEN BEGIN finger _ DESCRIPTOR[p.pCmd, SIZE[CmdObj]]; pupVector.ByteStreamPutBlock[pConn.bs, @finger -- potential Yield !pupVector.StreamClosing => BEGIN pConn.output _ closed; CONTINUE END]; WITH p.pCmd SELECT FROM text => PutString[pConn.bs, s]; ENDCASE; END; p.recipients _ SetDiff[p.recipients, Set[pConn.id]] END; p _ p.next; IF p=NIL THEN EXIT ENDLOOP; pConn.outputWaiting _ FALSE; IF pConn.output=active OR pConn.output=closing THEN pupVector.ByteStreamSendNow[pConn.bs !pupVector.StreamClosing => BEGIN pConn.output _ closed; CONTINUE END] END; IF pConn.output#active THEN EXIT; -- no more messages for this connection Yield ENDLOOP; IF pConn.output=closing THEN pupVector.ByteStreamClose[pConn.bs]; pConn.output _ finished END; InputServer: PROCEDURE[pConn: ConnPtr]= BEGIN cmd: CmdObj; mark: PupDefs.Byte; finger: PupDefs.BlockFinger _ DESCRIPTOR[@cmd, SIZE[CmdObj]]; DO pupVector.ByteStreamGetWholeBlock[pConn.bs, @finger -- potential Yield !pupVector.MarkInStream => BEGIN -- A mark=BOF in the stream means beginning of file. mark _ pupVector.ByteStreamGet[pConn.bs]; IF mark=BOF THEN BEGIN pConn.input _ holding; cmd _ [ether[input]]; CONTINUE END END; pupVector.StreamClosing => BEGIN pConn.closeReason _ why; GOTO closing END]; WITH cmd SELECT FROM text => BEGIN GetString[pConn.bs, buffer]; s _ SystemDefs.AllocateHeapString[buffer.length]; StringDefs.AppendString[s, buffer] END; ENDCASE; QueueCmd[@cmd, pConn.id]; WHILE pConn.input=holding DO Yield ENDLOOP; -- wait REPEAT closing => BEGIN pConn.input _ closed; IF pConn.output#finished THEN pConn.output _ closed; IF pConn.closeReason#localClose THEN EtherReport[closing, pConn.id] END ENDLOOP; pConn.input _ finished END; QueueCmd: PROCEDURE[pCmd: CmdPtr, id: ConnId]= BEGIN p: InQueuePtr; CursorCmdPtr: TYPE = POINTER TO cursor CmdObj; IF firstIn#NIL AND pCmd.cmdType=cursor AND lastIn.pCmd.cmdType=cursor THEN BEGIN lastIn.originator _ id; LOOPHOLE[lastIn.pCmd, CursorCmdPtr].point _ LOOPHOLE[pCmd, CursorCmdPtr].point END ELSE BEGIN p _ SystemDefs.AllocateHeapNode[SIZE[InQueueEl]]; p^ _ InQueueEl[NIL, id, CopyCmd[pCmd]]; IF firstIn=NIL THEN firstIn _ p ELSE lastIn.next _ p; lastIn _ p END END; EtherReport: PROCEDURE[etherCmd: EtherCommand, id: ConnId]= BEGIN cmd: CmdObj _ [ether[etherCmd]]; QueueCmd[@cmd, id] END; END. (2048)\343b10B77b11B42b13B59b13B28b4B60b13B75b16B42b16B58b7B45b5B64b9B181b10B159b11B62b10B28b12B57b14B1973b3B70b8B39b5B84b6B53b8B75b5B37b7B75b6B2b6B195b9B292b9B367b5B60b7B82b16B929b7B62b16B59b8B502b14B303b10B73b7B1934b11B102b7B156b7B111b7B130b6B61b16B816b10B516b11B37b8B2b7B361b12B61b10B2b12B18b8B2b7B393b14B44b13B18b8B2b7B704b16B400b6B104b13B257b8B550b17B199b11B527b8B77b8B1369b8B182b10B89b9B57b7B231b8B629b11B63b7B226b8B395b12B1522b11B1219b8B533b11B