-- ChatImpl.mesa -- Stolen from Laurel Chat.mesa -- Larry Stewart, April 6, 1983 6:43 pm -- Warren Teitelman, November 3, 1982 5:53 pm -- Last Edited by: Maxwell, January 25, 1983 3:09 pm DIRECTORY Ascii USING [LF], Commander USING [CommandProc, Register], ConvertUnsafe USING [AppendRope], FileIO USING [Open, OpenFailed], Inline USING [BITAND], IO, List USING [Length, Remove], Menus USING [AppendMenuEntry, CreateEntry, FindEntry, MenuEntry, MenuProc, ReplaceMenuEntry], PupDefs USING [PupAddress, PupPackageMake, PupPackageDestroy], PupStream USING [GetPupAddress, PupByteStreamCreate, PupNameTrouble, SecondsToTocks, StreamClosing], PupTypes USING [telnetSoc], Rope USING [Cat, Fetch, Find, Length, ROPE, Substr], Runtime USING [BoundsFault, GetBcdTime], Stream USING [CompletionCode, GetBlock, Handle, InputOptions, PutByte, PutChar, SendNow, SetInputOptions, SetSST, SubSequenceType, TimeOut], TiogaOps USING [GetCaret, GetRope, Location], TIPUser USING [InstantiateNewTIPTable, RegisterTIPPredicate, TIPPredicate, TIPTable], TypeScript USING [ChangeLooks, Create, InsertCharAtFrontOfBuffer, TS], UECP USING [Argv, Parse], UserCredentials USING [GetUserCredentials], ViewerClasses, ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc], ViewerIO USING [CreateViewerStreams], ViewerOps USING [AddProp, DestroyViewer, FetchProp, PaintViewer], ViewerTools USING [GetSelectedViewer, GetSelectionContents, SetSelection]; ChatImpl: CEDAR MONITOR LOCKS h.LOCK USING h: Handle IMPORTS Commander, ConvertUnsafe, FileIO, Inline, IO, List, Menus, PupDefs, PupStream, Rope, Runtime, Stream, TiogaOps, TIPUser, TypeScript, UECP, UserCredentials, ViewerEvents, ViewerIO, ViewerOps, ViewerTools SHARES Menus, ViewerClasses = BEGIN Handle: TYPE = REF ChatInstanceRecord; State: TYPE = {idle, starting, running, closing, destroy}; DisconnectChar: CHAR = 220C; AbortChar: CHAR = 221C; ConnectChar: CHAR = 222C; LoginChar: CHAR = 223C; RemoteCloseChar: CHAR = 224C; setLineWidth: Stream.SubSequenceType = 2; setPageLength: Stream.SubSequenceType = 3; timingMark: Stream.SubSequenceType = 5; timingMarkReply: Stream.SubSequenceType = 6; ChatInstanceRecord: TYPE = MONITORED RECORD [ ts: TypeScript.TS, -- the primary typescript state: State _ idle, lorc: CHAR _ 'c, logFileName: Rope.ROPE, logStream: IO.STREAM, keyFile: Rope.ROPE, argv: UECP.Argv, pleaseStop: BOOL _ FALSE, uToSStopped: BOOL _ FALSE, sToUStopped: BOOL _ FALSE, inDestroy: BOOL _ FALSE, serverToUserProcess: PROCESS, userToServerProcess: PROCESS, serverName: Rope.ROPE _ "Ivy", useOldHost: BOOL _ FALSE, keyStream: IO.STREAM, destroyOnClose: BOOL _ FALSE, setSelection: BOOL _ FALSE, in: IO.STREAM, origOut: IO.STREAM, out: IO.STREAM, tipTable: TIPUser.TIPTable, serverStream: Stream.Handle _ NIL, oldSplit: Menus.MenuEntry _ NIL ]; logFileNumber: INT _ 0; chatInstanceList: LIST OF REF ANY _ NIL; destroyEvent: ViewerEvents.EventRegistration _ NIL; closeEvent: ViewerEvents.EventRegistration _ NIL; -- This procedure is FORKed by the UserToServer process at the time a -- connection is opened. It is JOINED whenever the connection is closed. -- It also goes away if the Chat viewer is destroyed. ServerToUser: PROC [h: Handle] = TRUSTED {{ buffer: STRING _ [400]; why: Stream.CompletionCode; mySST: Stream.SubSequenceType; DO ENABLE { ABORTED => GOTO Cleanup; PupStream.StreamClosing => { IF NOT h.pleaseStop THEN TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: RemoteCloseChar]; GOTO Cleanup; }; IO.Error => GOTO Cleanup; }; buffer.length _ 0; [buffer.length, why, mySST] _ Stream.GetBlock[h.serverStream, [@buffer.text, 0, buffer.maxlength] ! Stream.TimeOut => { IF h.pleaseStop OR h.state # running THEN GOTO Cleanup ELSE RESUME; }]; IF h.pleaseStop OR h.state # running THEN GOTO Cleanup; IF h.out.UserAbort[] THEN ERROR; FOR i: NAT IN [0.. buffer.length) DO IF buffer[i]#Ascii.LF THEN h.out.PutChar[Inline.BITAND[buffer[i], 177B]]; ENDLOOP; IF why = sstChange AND mySST = timingMark THEN Stream.SetSST[h.serverStream, timingMarkReply]; ENDLOOP; EXITS Cleanup => h.sToUStopped _ TRUE; }; }; InitialNegotiations: PROC [h: Handle] = TRUSTED { Stream.SetSST[h.serverStream, setLineWidth]; Stream.PutByte[h.serverStream, 0]; --Stream.SetSST[h.serverStream, setPageLength]; --Stream.PutByte[h.serverStream, 255]; kludge to minimize BELLS from Juniper IF h.lorc='l OR h.lorc='x THEN { name, password: Rope.ROPE; [name: name, password: password] _ UserCredentials.GetUserCredentials[]; SendStringToServer[h, "Login "]; SendStringToServer[h, name]; IF h.lorc='l AND Rope.Find[s1: name, s2: "."] = -1 THEN { SendStringToServer[h, ".PA"]; }; SendStringToServer[h, " "]; SendStringToServer[h, password]; SendStringToServer[h, " \n"]; }; }; SendStringToServer: PROC[h: Handle, s: Rope.ROPE] = TRUSTED { FOR i: INT IN [0 .. s.Length[]) DO Stream.PutChar[h.serverStream, s.Fetch[i]]; ENDLOOP; Stream.SendNow[h.serverStream]; }; FinishStringWithErrorMsg: PROC [h: Handle, errorMsg: STRING] = { IF errorMsg # NIL THEN h.out.PutF[": %s.\n", IO.string[errorMsg]] ELSE h.out.PutF[".\n"]; }; -- UserAbort is active because Chat.TIP is not activated until h.state = running (due to -- the TIP predicate), so control-DEL sets UserAbort. -- However, while in this routine, UserAbort is used only to stop taking characters from -- the keyStream, not to close the connection. If the user types control-DEL here, she -- will have to type it again to really close the connection. FromKeys: PROC [h: Handle] = TRUSTED { count: NAT _ 0; { WHILE h.keyStream # NIL AND ~h.keyStream.EndOf[] DO IF h.in.UserAbort[] OR h.pleaseStop OR h.state # starting THEN GOTO CloseKeyStream; Stream.PutChar[h.serverStream, h.keyStream.GetChar[]]; count _ count + 1; IF count >= 50 THEN { Stream.SendNow[h.serverStream]; count _ 0; }; ENDLOOP; GOTO CloseKeyStream; EXITS CloseKeyStream => { IF h.in.UserAbort[] THEN h.in.ResetUserAbort[]; IF h.keyStream # NIL THEN{ h.keyStream.Close[]; h.keyStream _ NIL; }; IF count # 0 THEN Stream.SendNow[h.serverStream]; }; }; }; StartUp: PROC [h: Handle] = {{ ENABLE UNWIND => h.state _ idle; h.state _ starting; IF NOT h.useOldHost OR h.serverName.Length[] = 0 THEN h.serverName _ FindHostName[h]; h.useOldHost _ FALSE; h.pleaseStop _ FALSE; h.sToUStopped _ FALSE; h.uToSStopped _ FALSE; IF h.setSelection THEN ViewerTools.SetSelection[h.ts, NIL]; h.setSelection _ FALSE; h.in.ResetUserAbort[]; h.out.PutF["\nViewers Chat of %t.\n", IO.time[Runtime.GetBcdTime[]]]; IF h.logStream # NIL THEN h.out.PutF["Log file: %g\n", IO.rope[h.logFileName]] ELSE h.out.PutF["No log file.\n"]; OpenConnection[h]; IF h.serverStream = NIL THEN { h.state _ idle; RETURN; }; SetName[h, Rope.Cat["Chat ", h.serverName]]; InitialNegotiations[h]; FromKeys[h]; h.state _ running; h.serverToUserProcess _ FORK ServerToUser[h]; }; }; FindHostName: PROC [h: Handle] RETURNS [host: Rope.ROPE] = { caret: TiogaOps.Location; r: Rope.ROPE; i: INT; r _ ViewerTools.GetSelectionContents[]; IF r.Length[] > 1 THEN RETURN[r]; caret _ TiogaOps.GetCaret[]; r _ TiogaOps.GetRope[caret.node]; i _ caret.where; WHILE i > 0 DO char: CHARACTER = Rope.Fetch[r, i - 1]; IF -- char # '* AND -- NOT ChatTokenProc[char] = other THEN EXIT; i _ i -1; ENDLOOP; host _ Rope.Substr[base: r, start: i, len: caret.where - i]; }; -- This procedure includes '+ in order to handle Pup names of the form -- dls+100004 ChatTokenProc: IO.BreakProc -- [char: CHAR] RETURNS [IO.CharClass] -- = TRUSTED { IF IO.TokenProc[char] = other THEN RETURN [other]; IF char = '+ THEN RETURN [other]; RETURN [sepr]; }; OpenConnection: PROC [h: Handle] = TRUSTED { connect: STRING _ [64]; addr: PupDefs.PupAddress; PupDefs.PupPackageMake[]; { connect.length _ 0; h.out.ResetUserAbort[]; ConvertUnsafe.AppendRope[to: connect, from: h.serverName ! Runtime.BoundsFault => { h.out.PutF[" ... name too long!"]; GOTO Return; } ]; h.out.PutF["Opening connection to %g ... ", IO.rope[h.serverName]]; addr.socket _ PupTypes.telnetSoc; -- default value PupStream.GetPupAddress [@addr, connect ! PupStream.PupNameTrouble => { h.out.ResetUserAbort[]; h.out.PutF["PUP name error"]; FinishStringWithErrorMsg[h, e]; GOTO Return; }]; h.serverStream _ PupStream.PupByteStreamCreate [addr, PupStream.SecondsToTocks[1] ! PupStream.StreamClosing => { h.out.ResetUserAbort[]; h.out.PutF["Can't connect"]; FinishStringWithErrorMsg[h, text]; GOTO Return; }]; Stream.SetInputOptions[h.serverStream, Stream.InputOptions[TRUE,FALSE,FALSE,FALSE,FALSE]]; h.out.PutF["open.\n"]; EXITS Return => NULL; }; }; CloseConnection: PROC [h: Handle, print: BOOL] = TRUSTED { h.pleaseStop _ TRUE; IF h.serverStream # NIL THEN { IF print THEN h.out.PutF["\nClosing connection to %s", IO.rope[h.serverName] ! IO.Error => CONTINUE]; h.serverStream.delete[h.serverStream]; h.serverStream _ NIL; -- could cause pointer faults! IF print THEN h.out.PutF[" ... Closed\n" ! IO.Error => CONTINUE]; PupDefs.PupPackageDestroy[]; }; IF h.keyStream # NIL THEN { h.keyStream.Close[]; h.keyStream _ NIL; }; IF h.state = running THEN JOIN h.serverToUserProcess; IF h.logStream # NIL THEN h.logStream.Flush[]; h.state _ idle; SetName[h, "Chat"]; }; SetName: PROC [h: Handle, r: Rope.ROPE] = { InternalSetName: PROC [v: ViewerClasses.Viewer] = { v.name _ r; ViewerOps.PaintViewer[viewer: v, hint: caption]; }; EnumerateSplits[h.ts, InternalSetName ! ANY => CONTINUE]; }; ChatMain: Commander.CommandProc = TRUSTED { h: Handle _ NEW[ChatInstanceRecord _ []]; chatTipTable: TIPUser.TIPTable _ TIPUser.InstantiateNewTIPTable["Chat.TIP"]; execOut: IO.STREAM _ cmd.out; switchChar: CHAR; i: NAT _ 2; h.argv _ UECP.Parse[cmd.commandLine]; h.lorc _ 'l; -- default GV login WHILE i < h.argv.argc DO IF h.argv[i].Length[] > 1 THEN SELECT h.argv[i].Fetch[0] FROM '- => { switchChar _ h.argv[i].Fetch[1]; SELECT switchChar FROM 'd => h.destroyOnClose _ TRUE; 'k => { IF i+1 < h.argv.argc THEN { h.keyStream _ IO.RIS[h.argv[i+1]]; i _ i + 1; }; }; ENDCASE => h.lorc _ switchChar; }; '> => h.logFileName _ Rope.Substr[base: h.argv[i], start: 1]; '< => h.keyFile _ Rope.Substr[base: h.argv[i], start: 1]; ENDCASE => execOut.PutF["chat: unknown command: %s.\n", IO.rope[h.argv[i]]]; i _ i + 1; ENDLOOP; IF h.logFileName.Length[] = 0 THEN { h.logFileName _ IO.PutFR["Chat%d.log", IO.int[logFileNumber]]; logFileNumber _ logFileNumber + 1; }; IF h.keyFile.Length[] > 0 THEN h.keyStream _ FileIO.Open[fileName: h.keyFile ! FileIO.OpenFailed => { execOut.PutF["Chat:"]; IO.PutSignal[execOut]; execOut.PutF[", %s\n", IO.rope[h.keyFile]]; CONTINUE; }]; -- iconic unless command line not empty h.ts _ TypeScript.Create[info: [name: "Chat", iconic: h.argv.argc <= 1 OR h.lorc = 'i], paint: TRUE]; TypeScript.ChangeLooks[h.ts, 'f]; h.ts.file _ h.logFileName; h.ts.icon _ typescript; -- create log file h.logStream _ FileIO.Open[fileName: h.logFileName, accessOptions: overwrite, raw: TRUE ! FileIO.OpenFailed => { execOut.PutF["Chat:"]; IO.PutSignal[execOut]; execOut.PutF[", %s\n", IO.rope[h.logFileName]]; CONTINUE; }]; -- plug in Chat TIP table. chatTipTable.link _ h.ts.tipTable; chatTipTable.opaque _ FALSE; h.tipTable _ h.ts.tipTable _ chatTipTable; [in: h.in, out: h.origOut] _ ViewerIO.CreateViewerStreams[name: "Chat.log", viewer: h.ts, editedStream: FALSE]; h.out _ h.origOut; IF h.logStream # NIL THEN h.out _ IO.CreateDribbleStream[stream: h.origOut, dribbleTo: h.logStream, flushEveryNChars: 256]; [] _ IO.SetEcho[h.in, NIL]; IF h.argv.argc > 1 THEN { h.serverName _ h.argv[1]; h.useOldHost _ TRUE; -- IF -i then we are just initializing the host name, else really connect IF h.lorc = 'i THEN h.lorc _ 'c ELSE TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: IF h.lorc = 'c THEN ConnectChar ELSE LoginChar]; }; IF List.Length[chatInstanceList] = 0 THEN { -- closeEvent _ ViewerEvents.RegisterEventProc[proc: MyClose, event: close]; destroyEvent _ ViewerEvents.RegisterEventProc[proc: MyDestroy, event: destroy] }; chatInstanceList _ CONS[h, chatInstanceList]; Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Disconnect", proc: MyDisconnect, clientData: h, fork: TRUE, documentation: "Close Ethernet connection"]]; Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Login", proc: MyLogin, clientData: h, documentation: "Open Ethernet Connection to selected host and send Login sequence"]]; Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Connect", proc: MyConnect, clientData: h, documentation: "Open Ethernet Connection to selected host"]]; Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "BreakKey", proc: MyBreakKey, clientData: h, documentation: "Transmit Ascii.NULL (DLS interprets as RS-232 Line Break)"]]; Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "FlushLog", proc: MyFlushLog, clientData: h, documentation: "Flush log file to disk."]]; h.oldSplit _ Menus.FindEntry[menu: h.ts.menu, entryName: "Split"]; Menus.ReplaceMenuEntry[menu: h.ts.menu, oldEntry: h.oldSplit, newEntry: Menus.CreateEntry[name: "Split", proc: MySplit, fork: TRUE, clientData: h, documentation: "Split window"]]; ViewerOps.AddProp[h.ts, $ChatToolData, h]; ViewerOps.PaintViewer[viewer: h.ts, hint: all]; h.userToServerProcess _ FORK UserToServer[h]; }; UserToServer: PROC [h: Handle] = TRUSTED {{ char: CHAR; count: NAT _ 0; DO ENABLE { ABORTED => ERROR; -- ever happen? IO.UserAborted => { h.out.ResetUserAbort[]; h.out.PutF["\nUserAbort!\n"]; CONTINUE; }; IO.Error => { -- last viewer destroyed ! -- Close connection and exit CloseConnection[h, FALSE]; GOTO Die; }; PupStream.StreamClosing => { IF h.serverStream # NIL THEN { h.out.PutF["\nConnection being closed by %s", IO.rope[h.serverName]]; FinishStringWithErrorMsg[h, text]; CloseConnection[h, FALSE]; IF h.destroyOnClose THEN { ViewerOps.DestroyViewer[h.ts]; GOTO Die; }; }; CONTINUE; }; }; char _ h.in.GetChar[]; IF h.inDestroy THEN { CloseConnection[h, FALSE]; GOTO Die; }; SELECT char FROM AbortChar => { IF h.state = running THEN CloseConnection[h, TRUE]; h.out.ResetUserAbort[]; }; DisconnectChar => { IF h.state = running THEN CloseConnection[h, TRUE]; }; RemoteCloseChar => { ERROR PupStream.StreamClosing[why: remoteClose, text: NIL]; }; ConnectChar => { IF h.state = idle THEN { h.lorc _ 'c; StartUp[h]; count _ 0; }; }; LoginChar => { IF h.state = idle THEN { h.lorc _ 'l; StartUp[h]; count _ 0; }; }; ENDCASE => { SELECT h.state FROM running => { Stream.PutChar[h.serverStream, char]; IF count >= 50 OR NOT h.in.CharsAvail[] THEN { Stream.SendNow[h.serverStream]; count _ 0; } ELSE count _ count + 1; }; ENDCASE => h.out.PutChar[char]; }; ENDLOOP; EXITS Die => { IF h.logStream # NIL THEN h.logStream.Close[]; }; }; }; -- This EventProc exits only to keep h.ts pointing to a valid copy of the typescript -- viewer. It is only needed for the use of TypeScript.InsertCharAtFrontOfBuffer. MyDestroy: ViewerEvents.EventProc = { h: Handle _ NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]]; IF h = NIL THEN RETURN; IF NumSplit[viewer] = 1 THEN { -- last one h.inDestroy _ TRUE; -- next line is a crock to avoid signal in TypeScripts, see McGregor -- Process.Pause[Process.SecondsToTicks[2]]; -- h.out.Close[]; breaks viewers destroy!! chatInstanceList _ List.Remove[h, chatInstanceList]; IF List.Length[chatInstanceList] = 0 THEN { ViewerEvents.UnRegisterEventProc[proc: destroyEvent, event: destroy]; -- ViewerEvents.UnRegisterEventProc[proc: closeEvent, event: close]; }; } ELSE IF NumSplit[viewer] > 1 THEN { IF viewer = h.ts THEN { Another: PROC [v: ViewerClasses.Viewer] = { IF v # viewer THEN h.ts _ v; }; EnumerateSplits[viewer, Another]; }; }; }; MyClose: ViewerEvents.EventProc = { h: Handle _ NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]]; IF h = NIL THEN RETURN; TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: DisconnectChar]; }; MyConnect: Menus.MenuProc = { viewer: TypeScript.TS _ NARROW[parent]; h: Handle _ NARROW[clientData]; h.ts _ viewer; -- "primary" copy h.useOldHost _ mouseButton # red; TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: ConnectChar]; }; MyLogin: Menus.MenuProc = { viewer: TypeScript.TS _ NARROW[parent]; h: Handle _ NARROW[clientData]; h.ts _ viewer; -- "primary" copy h.useOldHost _ mouseButton # red; TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: LoginChar]; }; MyDisconnect: Menus.MenuProc = { h: Handle _ NARROW[clientData]; TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: DisconnectChar]; }; MyBreakKey: Menus.MenuProc = TRUSTED { h: Handle _ NARROW[clientData]; IF h.state # running THEN RETURN; IF h.serverStream # NIL THEN { Stream.PutChar[h.serverStream, '\000]; Stream.SendNow[h.serverStream]; }; }; MyFlushLog: Menus.MenuProc = { h: Handle _ NARROW[clientData]; IF h.logStream # NIL THEN h.logStream.Flush[]; }; MySplit: Menus.MenuProc = { h: Handle _ NARROW[clientData]; CheckChatProperties: PROC [v: ViewerClasses.Viewer] = { IF ViewerOps.FetchProp[v, $ChatToolData] = NIL THEN ViewerOps.AddProp[v, $ChatToolData, h]; v.tipTable _ h.tipTable; }; h.oldSplit.proc[parent: parent, clientData: h.oldSplit.clientData, mouseButton: mouseButton, shift: shift, control: control]; EnumerateSplits[NARROW[parent, ViewerClasses.Viewer], CheckChatProperties]; }; ConnectionOpen: TIPUser.TIPPredicate --PROC RETURNS [BOOLEAN]-- = { h: Handle; viewer: ViewerClasses.Viewer _ ViewerTools.GetSelectedViewer[]; IF viewer=NIL THEN RETURN [FALSE]; -- no primary selection h _ NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]]; IF h=NIL THEN RETURN [FALSE]; -- not a chat tool RETURN [h.state = running]; -- connection open? }; EnumerateSplits: PROC [v: ViewerClasses.Viewer, p: PROC [v: ViewerClasses.Viewer]] = { v2: ViewerClasses.Viewer _ v; IF v = NIL THEN RETURN; DO p[v2]; IF v2.link = NIL OR v2.link = v THEN RETURN; v2 _ v2.link; ENDLOOP; }; NumSplit: PROC [v: ViewerClasses.Viewer] RETURNS [count: INT _ 0] = { Counter: PROC [v2: ViewerClasses.Viewer] = { count _ count + 1; }; EnumerateSplits[v, Counter]; }; Init: PROC = { Commander.Register[key: "Chat", proc: ChatMain, doc: "Pup User Telnet, see Chat.doc"]; TIPUser.RegisterTIPPredicate[$ConnectionOpen, ConnectionOpen]; }; -- main program for chat Init[]; END. March 31, 1982 9:27 pm, Stewart, copied from Laurel Chat April 1, 1982 4:06 pm, Stewart, own viewer class & TIP table April 4, 1982 5:18 pm, Stewart, Menu April 6, 1982 10:27 pm, Stewart, command line stuff 18-Apr-82 17:42:24, Use TypeScript again June 6, 1982 4:44 pm, Stewart, fix Menu instantiation to add Split June 6, 1982 8:08 pm, Stewart, using own Split code until viewers copies PropList September 21, 1982 4:26 pm, Stewart, Cedar 3.4 January 23, 1983 10:18 pm, Stewart, Cedar 3.5, major rework January 24, 1983 5:49 pm, Stewart, Cedar 3.6 jJStartOfExpansion* -- [c: CHAR] RETURNS [IO.BreakAction] --  JN&N