DIRECTORY Atom USING[ MakeAtom ], Ascii, Commander USING [CommandProc, Register], CommanderOps, Convert, EditedStream, IO, Menus USING [AppendMenuEntry, ChangeNumberOfLines, CreateEntry, FindEntry, MenuEntry, MenuProc, ReplaceMenuEntry], MessageWindow USING [Append], NetworkName, NetworkStream, Process USING [Abort, Detach, EnableAborts, PauseMsec], Rope, TelnetCommands, TelnetOptions, TiogaOps USING [GetCaret, GetRope, Location], TIPLinking, TIPUser USING [InstantiateNewTIPTable, TIPTable], TypeScript USING [BackSpace, ChangeLooks], UserProfile USING [Boolean, ListOfTokens, Token], ViewerClasses USING [MouseButton, Viewer], ViewerEvents USING[ EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc], ViewerIO USING [CreateViewerStreams, GetViewerFromStream, Viewer], ViewerOps USING [AddProp, BlinkViewer, ComputeColumn, FetchProp, FetchViewerClass, PaintViewer], ViewerTools USING [GetSelectionContents]; TelnetViewerImpl: CEDAR MONITOR LOCKS h USING h: Handle IMPORTS Atom, Commander, CommanderOps, Convert, EditedStream, IO, Menus, MessageWindow, NetworkName, NetworkStream, Process, Rope, TiogaOps, TIPLinking, TIPUser, TypeScript, <> UserProfile, ViewerEvents, ViewerIO, ViewerOps, ViewerTools ~ { ROPE: TYPE ~ Rope.ROPE; STREAM: TYPE ~ IO.STREAM; telnetPort: CARDINAL ~ <> 23; toolName: ROPE = "Telnet"; telnetProp: ATOM = Atom.MakeAtom["TelnetProp"]; documentationRope: ROPE ~ "[-p port] [-l] [-d] host\nTelnet\nSwitches:\n -p port number\n -l auto login\n -d do not auto login"; defaultEolSeq: ROPE ~ "\r\l"; -- Direction: TYPE ~ IO.StreamVariety[input..output]; Handle: TYPE ~ REF Object; Object: TYPE ~ MONITORED RECORD [ keyboardStream: STREAM ¬ NIL, viewerStream: STREAM ¬ NIL, networkStream: ARRAY Direction OF STREAM ¬ ALL[NIL], hostName: ROPE ¬ NIL, state: State ¬ disconnected, port: CARD ¬ telnetPort, how: How ¬ command, autoLogin: BOOL ¬ FALSE, pusher: PROCESS ¬ NIL, puller: PROCESS ¬ NIL, optionsWatcher: PROCESS ¬ NIL, nvtState: NVTState, negotiationEvent: CONDITION, lineBuffered: BOOL ¬ TRUE, localEcho: BOOL ¬ TRUE, heSendsGoAheads: BOOL ¬ TRUE, sendGoAheads: BOOL ¬ TRUE, gotGA: BOOL ¬ TRUE, -- it's not clear what to start with extraWills: CARD ¬ 0, extraDos: CARD ¬ 0, eolSeq: ROPE ¬ defaultEolSeq, typescriptBackground: PROCESS ¬ NIL, destructionEvent: ViewerEvents.EventRegistration, trace: TraceBuffer ]; TraceBuffer: TYPE ~ REF TraceBufferRep; TraceBufferRep: TYPE ~ RECORD [ first: NAT, next: NAT, buf: PACKED SEQUENCE size: NAT OF TraceBufferEntry ]; TraceBufferEntry: TYPE ~ PACKED RECORD [ side: Side, char: CHAR ]; Trace: ENTRY PROC [h: Handle, side: Side, char: CHAR] ~ { TraceInternal[h, side, char] }; TraceInternal: INTERNAL PROC [h: Handle, side: Side, char: CHAR] ~ { trace: TraceBuffer ~ h.trace; IF trace # NIL AND trace.size > 1 THEN { next: NAT ¬ trace.next MOD trace.size; trace[next] ¬ [side, char]; trace.next ¬ (next + 1) MOD trace.size; IF trace.first = trace.next THEN trace.first ¬ (trace.first + 1) MOD trace.size; }; }; GetTrace: ENTRY PROC [h: Handle] RETURNS [t: TraceBuffer] ~ { t ¬ h.trace; h.trace ¬ NEW[TraceBufferRep[256]]; }; NVTState: TYPE ~ ARRAY CHAR OF OptionState; OptionState: TYPE ~ PACKED RECORD [ desiredOfMySide: BOOL ¬ FALSE, stateOfMySide: NegotiationState ¬ inactive, desiredOfHisSide: BOOL ¬ FALSE, stateOfHisSide: NegotiationState ¬ inactive ]; NegotiationState: TYPE ~ {inactive, requested, active}; Side: TYPE ~ {mySide, hisSide}; FuncMenuData: TYPE ~ REF FuncMenuDataObject; FuncMenuDataObject: TYPE ~ RECORD [ name: ROPE, value: ROPE, handle: Handle ]; procLine: INT ¬ 0; funcLine: INT ¬ 1; pushMeansFlush: BOOL ¬ FALSE; -- possible semantics problem with ArpaTCP.SendNow not setting the push bit (but IO.Flush waits for acks, ugh...) openTimeout: NetworkStream.Milliseconds ¬ NetworkStream.waitForever; State: TYPE ~ { disconnected, connecting, connected, disconnecting }; How: TYPE ~ { command, connect, login, lfKey }; Worker: TYPE ~ { none, pusher, puller }; CloseAction: TYPE ~ { none, initiate, reply }; TelnetMain: Commander.CommandProc ~ { h: Handle; argv: CommanderOps.ArgumentVector; argv ¬ CommanderOps.Parse[cmd ! CommanderOps.Failed => { msg ¬ errorMsg; result ¬ $Failure; CONTINUE }]; IF argv = NIL THEN RETURN; h ¬ NEW[Object]; TRUSTED { Process.EnableAborts[@(h.negotiationEvent)]; }; msg ¬ ProcessArgs[h, argv]; IF msg # NIL THEN { result ¬ $Failure; RETURN }; TelnetMainInternal[h]; }; TelnetMainInternal: PROC [h: Handle] ~ { v: ViewerClasses.Viewer; [h.keyboardStream, h.viewerStream] ¬ ViewerIO.CreateViewerStreams[name~toolName, <> editedStream~FALSE]; v ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; v.tipTable ¬ telnetTIPTable; EditedStream.SetEcho[h.keyboardStream, NIL]; TypeScript.ChangeLooks[v, 'f]; AddMenuButtons[h]; ViewerOps.AddProp[v, telnetProp, h]; h.destructionEvent ¬ ViewerEvents.RegisterEventProc[proc: NoteDestruction, event: destroy, filter: v, before: TRUE]; TRUSTED {Process.Detach[h.typescriptBackground ¬ FORK TypescriptWatcher[h]]}; IF Rope.Length[h.hostName] > 0 THEN TRUSTED {Process.Detach[FORK DoConnect[h]]}; RETURN; }; AddMenuButtons: PROC [h: Handle] ~ { v: ViewerClasses.Viewer; v ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; AddButton[h, "Another", AnotherProc, "Create another instance of this tool."]; AddButton[h, "Disconnect", DoDisconnect, "Close connection."]; IF UserProfile.Boolean[Rope.Concat[toolName, ".LoginButton"], TRUE] THEN AddButton[h, "Login", LoginProc, "Open connection to selected host and login."]; AddButton[h, "Connect", ConnectProc, "Open connection to selected host."]; AddButton[h, "Break", DoSendBreak, "Send a Break code"]; AddButton[h, "Synch", DoSynch, "Telnet Synch operation"]; AddButton[h, "Interrupt", DoInterrupt, "Telnet Interrupt operation"]; AddButton[h, "FlushLog", FlushLogProc, "Flush the typescript log to the backing file."]; AddButton[h, "ShowOptions", ShowOptionsProc, "Show the current options state."]; IF AddFunctions[h] THEN { ViewerOps.ComputeColumn[v.column]; ViewerOps.PaintViewer[v, all]; } ELSE ViewerOps.PaintViewer[v, menu]; }; ProcessArgs: PROC [h: Handle, argv: CommanderOps.ArgumentVector] RETURNS [msg: ROPE ¬ NIL] ~ { i: INT ¬ 1; h.autoLogin ¬ UserProfile.Boolean[Rope.Concat[toolName, ".AutoLogin"], FALSE]; WHILE i < argv.argc DO len: INT ~ Rope.Length[argv[i]]; IF len = 0 THEN { msg ¬ "Null argument"; GOTO Bad }; IF Rope.Fetch[argv[i], 0] = '- THEN { IF len = 1 THEN { msg ¬ "Bad flag"; GOTO Bad }; SELECT Rope.Fetch[argv[i], 1] FROM 'p => { i ¬ i.SUCC; IF i >= argv.argc THEN GOTO Bad; h.port ¬ Convert.CardFromRope[argv[i] ! Convert.Error => {msg ¬ "Bad Port Number"; GOTO Bad;}]; }; 'l => { h.autoLogin ¬ TRUE; }; 'd => { h.autoLogin ¬ FALSE; }; ENDCASE => { msg ¬ "Bad flag"; GOTO Bad }; } ELSE { IF h.hostName # NIL THEN { msg ¬ "Multiple hosts"; GOTO Bad }; h.hostName ¬ argv[i]; }; i ¬ i.SUCC; ENDLOOP; EXITS Bad => NULL; }; AddFunctions: PROC [h: Handle] RETURNS [someAdded: BOOL ¬ FALSE] ~ { v: ViewerIO.Viewer ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; fName, fValue: ROPE; fButtons: LIST OF ROPE ¬ UserProfile.ListOfTokens[Rope.Concat[toolName, ".FunctionButtons"]]; IF fButtons = NIL OR fButtons.rest = NIL THEN RETURN; Menus.ChangeNumberOfLines[v.menu, funcLine+1]; WHILE fButtons # NIL DO IF fButtons.rest = NIL THEN RETURN; -- odd number of TOKENs, not a pair fName ¬ fButtons.first; fValue ¬ fButtons.rest.first; IF Rope.Length[fName] = 0 THEN LOOP; -- button has no name -> ignore it AddFuncButton[h, fName, fValue]; someAdded ¬ TRUE; fButtons ¬ fButtons.rest.rest; ENDLOOP; }; AddFuncButton: PROC [h: Handle, name, val: ROPE] ~ { v: ViewerIO.Viewer ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; IF v = NIL THEN RETURN; Menus.AppendMenuEntry[v.menu, Menus.CreateEntry[name, FunctionProc, NEW[FuncMenuDataObject ¬ [name, val, h]]], funcLine]; }; AddButton: PROC [h: Handle, name: ROPE, proc: Menus.MenuProc, doc: ROPE, fork: BOOL ¬ TRUE] ~ { v: ViewerIO.Viewer ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; IF v = NIL THEN ERROR; Menus.AppendMenuEntry[v.menu, Menus.CreateEntry[name, proc, h, doc, fork], procLine]; }; PutMsg: PROC [h: Handle, fmt: ROPE, msg: ROPE ¬ NIL] ~ { ENABLE IO.Error => CONTINUE; s: STREAM ~ h.viewerStream; IF (s # NIL) THEN IO.PutF1[s, fmt, IF msg # NIL THEN [rope[msg]] ELSE [null[]]]; }; SetConnecting: ENTRY PROC [h: Handle] RETURNS [ok: BOOL] ~ { ENABLE UNWIND => NULL; IF h.state = disconnected THEN { h.state ¬ connecting; RETURN [TRUE]; } ELSE { RETURN [FALSE] }; }; SetConnected: ENTRY PROC [h: Handle] ~ { ENABLE UNWIND => NULL; v: ViewerIO.Viewer = ViewerIO.GetViewerFromStream[h.viewerStream]; h.state ¬ connected; v.name ¬ Rope.Cat[toolName, ": ", h.hostName]; ViewerOps.PaintViewer[viewer: v, hint: caption]; }; SetDisconnecting: ENTRY PROC [h: Handle] RETURNS [ok: BOOL] ~ { ENABLE UNWIND => NULL; IF h.state = connected THEN { v: ViewerIO.Viewer = ViewerIO.GetViewerFromStream[h.viewerStream]; h.state ¬ disconnecting; v.name ¬ Rope.Cat[toolName, " closing connection to: ", h.hostName]; ViewerOps.PaintViewer[viewer: v, hint: caption]; RETURN [TRUE]; } ELSE { RETURN [FALSE] }; }; SetDisconnected: ENTRY PROC [h: Handle] ~ { ENABLE UNWIND => NULL; v: ViewerIO.Viewer = ViewerIO.GetViewerFromStream[h.viewerStream]; h.state ¬ disconnected; v.name ¬ toolName; ViewerOps.PaintViewer[viewer: v, hint: caption]; PutMsg[h, "\n~~~~~~~~ Connection Closed ~~~~~~~~\n\n"]; }; IsSep: PROC [c: CHAR] RETURNS [BOOL] = { IF c IN [Ascii.NUL..Ascii.SP) OR c = Ascii.DEL THEN RETURN[TRUE]; RETURN[FALSE]; }; FindHostName: PROC [h: Handle] RETURNS [ROPE] ~ { hostName: ROPE ¬ ViewerTools.GetSelectionContents[]; SELECT h.how FROM command => RETURN[h.hostName]; connect, login => RETURN[hostName]; lfKey => RETURN[FindHostFromCaret[]]; ENDCASE; RETURN[NIL]; }; FindHostFromCaret: PROC RETURNS [hostName: ROPE] ~ { caret: TiogaOps.Location; i: INT; caret ¬ TiogaOps.GetCaret[]; hostName ¬ TiogaOps.GetRope[caret.node]; i ¬ caret.where; WHILE i > 0 DO char: CHAR = Rope.Fetch[hostName, i.PRED]; IF IsSep[char] THEN EXIT; i ¬ i.PRED; ENDLOOP; hostName ¬ Rope.Substr[hostName, i, (caret.where - i)]; }; NoteDestruction: ViewerEvents.EventProc = { h: Handle ¬ NARROW[ViewerOps.FetchProp[viewer, telnetProp]]; IF event = destroy AND h # NIL THEN NoteDestructionInternal[h]; }; NoteDestructionInternal: PROC [h: Handle] = { ENABLE UNWIND => NULL; IF h # NIL THEN { TRUSTED {Process.Abort[h.typescriptBackground]}; FinishConnection[h~h, me~none, msg~"Disconnecting.", closeAction~initiate]; ViewerEvents.UnRegisterEventProc[h.destructionEvent, destroy]; }; }; ReplaceAndRepaintMenuEntry: PROC [v: ViewerIO.Viewer, old: Menus.MenuEntry, new: Menus.MenuEntry] = { Menus.ReplaceMenuEntry[ v.menu, old, new]; ViewerOps.PaintViewer[v, menu]; }; FunctionProc: Menus.MenuProc ~ { md: FuncMenuData ¬ NARROW[clientData]; h: Handle ¬ md.handle; v: ViewerIO.Viewer ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; SELECT TRUE FROM ~shift AND ~control => InterpretAndSend[h, md.value]; shift AND ~control => { me: Menus.MenuEntry; selection: ROPE ¬ ViewerTools.GetSelectionContents[]; IF mouseButton # yellow AND Rope.Length[selection] = 0 THEN { MessageWindow.Append["Select something first.", TRUE]; RETURN; }; SELECT mouseButton FROM red => md.value ¬ selection; yellow => MessageWindow.Append[IO.PutFR["Function: \"%g\" = \"%g\"", [rope[md.name]], [rope[md.value]]], TRUE]; blue => { IF Menus.FindEntry[parent.menu, selection] # NIL THEN { MessageWindow.Append["That name alreay in use!", TRUE]; RETURN; }; me ¬ Menus.FindEntry[parent.menu, md.name]; md.name ¬ selection; TRUSTED { Process.Detach[FORK ReplaceAndRepaintMenuEntry[ parent, me, Menus.CreateEntry[md.name, FunctionProc, NEW[FuncMenuDataObject ¬ [md.name, md.value, h]]]]]}; }; ENDCASE; }; control => { me: Menus.MenuEntry; selection: ROPE ¬ ViewerTools.GetSelectionContents[]; IF mouseButton # yellow AND Rope.Length[selection] = 0 THEN { MessageWindow.Append["Select something first.", TRUE]; RETURN; }; SELECT mouseButton FROM red => { IF Rope.Length[selection] > 32 THEN { MessageWindow.Append["Button name too long!", TRUE]; RETURN; }; IF Menus.FindEntry[parent.menu, selection] # NIL THEN { MessageWindow.Append["That name alreay in use!", TRUE]; RETURN; }; Menus.AppendMenuEntry[parent.menu, Menus.CreateEntry[selection, FunctionProc, NEW[FuncMenuDataObject ¬ [selection, "", h]]], funcLine]; MessageWindow.Append[IO.PutFR1["Added function button: \"%g\"", [rope[selection]]], TRUE]; }; yellow => MessageWindow.Append[IO.PutFR["Function: \"%g\" = \"%g\"", [rope[md.name]], [rope[md.value]]], TRUE]; blue => { me ¬ Menus.FindEntry[parent.menu, md.name]; MessageWindow.Append[IO.PutFR1["Deleting function button: \"%g\"", [rope[md.name]]], TRUE]; TRUSTED { Process.Detach[FORK ReplaceAndRepaintMenuEntry[parent, me, NIL]]}; }; ENDCASE; }; ENDCASE; ViewerOps.PaintViewer[v, menu]; }; ConnectProc: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF NOT SetConnecting[h] THEN { ViewerOps.BlinkViewer[parent]; MessageWindow.Append["Can't connect: viewer is busy", TRUE]; RETURN; }; h.how ¬ connect; h.autoLogin ¬ FALSE; TRUSTED {Process.Detach[FORK DoConnect[h]]}; }; LoginProc: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF NOT SetConnecting[h] THEN { ViewerOps.BlinkViewer[parent]; MessageWindow.Append["Can't connect: viewer is busy", TRUE]; RETURN; }; h.how ¬ login; h.autoLogin ¬ TRUE; TRUSTED {Process.Detach[FORK DoConnect[h]]}; }; TryToConnect: PROC [h: Handle, autoLogin: BOOL ¬ FALSE] = { IF NOT SetConnecting[h] THEN { MessageWindow.Append["Can't connect: viewer is busy", TRUE]; RETURN; }; h.how ¬ lfKey; h.autoLogin ¬ autoLogin; TRUSTED {Process.Detach[FORK DoConnect[h]]}; }; DoConnect: PROC [handle: Handle] = { v: ViewerIO.Viewer = ViewerIO.GetViewerFromStream[handle.viewerStream]; hisName: ROPE ¬ NIL; hisAddress: ROPE ¬ NIL; openError: ROPE ¬ NIL; [] ¬ SetConnecting[handle]; { hisName ¬ FindHostName[handle]; IF Rope.IsEmpty[hisName] THEN { PutMsg[handle, "\n\nConnect: please select host name\n\n"]; GOTO Out }; PutMsg[handle, "\nOpening connection to "]; PutMsg[handle, "\"%g\"", hisName]; v.name ¬ IO.PutFR["%g opening connection to %g", [rope[toolName]], [rope[hisName]]]; ViewerOps.PaintViewer[viewer: v, hint: caption]; hisAddress ¬ NetworkName.AddressFromName[family: $ARPA, name: hisName, portHint: Convert.RopeFromCard[handle.port], components: hostAndPort ! NetworkName.Error => {openError ¬ msg; CONTINUE}].addr; IF hisAddress = NIL THEN { PutMsg[handle, Rope.Concat["... address lookup failure: ", openError]]; GOTO Out }; PutMsg[handle, " at \"%g\" ", hisAddress]; v.name ¬ IO.PutFR["%g opening connection to %g at %g", [rope[toolName]], [rope[hisName]], [rope[hisAddress]]]; ViewerOps.PaintViewer[viewer: v, hint: caption]; handle.hostName ¬ hisName; [in: handle.networkStream[input], out: handle.networkStream[output]] ¬ NetworkStream.CreateStreams[protocolFamily: $ARPA, remote: hisAddress, transportClass: $TCP, timeout: 15000! NetworkStream.Error => { openError ¬ msg; CONTINUE; }; NetworkStream.Timeout => { openError ¬ msg; CONTINUE; }; ]; IF openError # NIL THEN { PutMsg[handle, "open failed: %g\n\n", openError]; GOTO Out; }; PutMsg[handle, "connected.\n\n"]; SetConnected[handle]; SetInitialDesiredOptions[handle]; RunConnection[handle]; EXITS Out => NULL; }; SetDisconnected[handle]; }; << WaitUntilConnected: PROC [h: Handle] RETURNS [error: ROPE] ~ { state: ArpaTCPOps.ConnectionState; tcpHandle: ArpaTCPOps.TCPHandle ¬ NARROW[h.networkStream.streamData]; DO state ¬ tcpHandle.state; SELECT state FROM synSent => Process.Pause[1]; synRcvd, established => RETURN[NIL]; ENDCASE => RETURN["unable to establish connection"]; ENDLOOP; }; >> TypescriptWatcher: PROC [h: Handle] = { v: ViewerClasses.Viewer ¬ ViewerIO.GetViewerFromStream[h.viewerStream]; c: CHAR; { ENABLE { IO.EndOfStream, ABORTED, IO.Error => GOTO done; }; DO DO IF h.state = disconnected THEN EXIT ELSE Process.PauseMsec[1500]; ENDLOOP; IF IO.CharsAvail[h.keyboardStream, FALSE] = 0 THEN { Process.PauseMsec[250]; LOOP; }; c ¬ IO.GetChar[h.keyboardStream]; SELECT c FROM Ascii.LF => TryToConnect[h, FALSE]; Ascii.FF => TryToConnect[h, TRUE]; Ascii.BS => TypeScript.BackSpace[v]; ENDCASE => IO.PutChar[h.viewerStream, c]; ENDLOOP; }; EXITS done => NULL; }; SetInitialDesiredOptions: ENTRY PROC [h: Handle] ~ { OPEN TelnetOptions; ENABLE UNWIND => NULL; h.nvtState[echo].desiredOfHisSide ¬ TRUE; h.nvtState[suppressGoAhead].desiredOfHisSide ¬ TRUE; h.nvtState[suppressGoAhead].desiredOfMySide ¬ TRUE; }; NegotiateInitialOptions: PROC [h: Handle] ~ { IF NOT NegotiateOption[h, TelnetOptions.echo, hisSide] THEN { [] ¬ DeactivateOption[h, TelnetOptions.echo, hisSide]; [] ¬ NegotiateOption[h, TelnetOptions.echo, mySide]; }; FOR option: CHAR IN [0C..377C] DO IF h.nvtState[option].desiredOfMySide THEN [] ¬ NegotiateOption[h, option, mySide]; IF h.nvtState[option].desiredOfHisSide THEN [] ¬ NegotiateOption[h, option, hisSide]; ENDLOOP; }; RopeFromNVTState: ENTRY PROC [h: Handle] RETURNS [rope: ROPE] ~ { ENABLE UNWIND => NULL; FOR option: CHAR IN [0C..377C] DO optionState: OptionState ¬ h.nvtState[option]; thisRope: ROPE; IF optionState.desiredOfMySide OR optionState.stateOfMySide # inactive OR optionState.desiredOfHisSide OR optionState.stateOfHisSide # inactive THEN { optionRope: ROPE ¬ RopeFromOption[option]; IF optionRope # NIL THEN thisRope ¬ IO.PutFR["Option: %g (%g)", [rope[optionRope]], [cardinal[option-'\000]]] ELSE thisRope ¬ IO.PutFR1["Option: %g", [cardinal[option-'\000]]]; thisRope ¬ Rope.Concat[thisRope, IO.PutFR["\n mySide[desired: %g, state: %g]", [boolean[optionState.desiredOfMySide]], [rope[RopeFromNegotiationState[optionState.stateOfMySide]]]]]; thisRope ¬ Rope.Concat[thisRope, IO.PutFR["\n hisSide[desired: %g, state: %g]", [boolean[optionState.desiredOfHisSide]], [rope[RopeFromNegotiationState[optionState.stateOfHisSide]]]]]; rope ¬ Rope.Cat[rope, thisRope, "\n"]; }; ENDLOOP; }; RopeFromOption: PROC [option: CHAR] RETURNS [ROPE] ~ { OPEN TelnetOptions; RETURN[SELECT option FROM transmitBinary => "transmitBinary", echo => "echo", suppressGoAhead => "suppressGoAhead", status => "status", timingMark => "timingMark", terminalType => "terminalType", endOfRecord => "endOfRecord", outMrk => "outMrk", naws => "naws", terminalSpeed => "terminalSpeed", toggleFlowControl => "toggleFlowControl", extendedOptionsList => "extendedOptionsList", ENDCASE => NIL]; }; RopeFromNegotiationState: PROC [state: NegotiationState] RETURNS [ROPE] ~ { RETURN[SELECT state FROM inactive => "inactive", active => "active", requested => "requested", ENDCASE => ERROR]; }; RunConnection: PROC [h: Handle] ~ { TRUSTED { h.optionsWatcher ¬ FORK OptionsWatcher[h]; h.pusher ¬ FORK Pusher[h]; h.puller ¬ FORK Puller[h]; NegotiateInitialOptions[h ! IO.EndOfStream, IO.Error, NetworkStream.Error, NetworkStream.Timeout, ABORTED => CONTINUE]; JOIN h.pusher; h.pusher ¬ NIL; JOIN h.puller; h.puller ¬ NIL; Process.Detach[h.optionsWatcher]; Process.Abort[h.optionsWatcher]; h.optionsWatcher ¬ NIL; }; CloseNetworkStreams[h]; }; CloseNetworkStreams: PROC [h: Handle] ~ { FOR d: Direction IN Direction DO IO.Close[h.networkStream[d] ! IO.Error => CONTINUE]; -- probably already closed ENDLOOP; }; SendEndOfMessage: PROC [s: STREAM] ~ Push; Push: PROC [s: STREAM] ~ { IF pushMeansFlush THEN IO.Flush[s] ELSE NetworkStream.SendSoon[s]; }; FinishConnection: PROC [h: Handle, me: Worker, msg: ROPE ¬ NIL, closeAction: CloseAction ¬ none] ~ { IF SetDisconnecting[h] THEN { IF me # pusher THEN TRUSTED { Process.Abort[h.pusher] }; IF me # puller THEN TRUSTED { Process.Abort[h.puller] }; IF msg # NIL THEN PutMsg[h, "\n\n~~~~~~~~ %g ~~~~~~~~\n", msg]; SELECT closeAction FROM initiate, reply => CloseNetworkStreams[h]; ENDCASE; }; }; SplitGvName: PUBLIC PROC[name: ROPE] RETURNS[sn, reg: ROPE] = { length: INT = name.Length[]; FOR i: INT DECREASING IN [0..length) DO IF name.Fetch[i] = '. THEN RETURN[ sn: name.Substr[start: 0, len: i], reg: name.Substr[start: i+1, len: length-(i+1)] ]; ENDLOOP; RETURN[sn: name, reg: NIL]; }; SplitThis: PROC [rope, sep: ROPE] RETURNS [ROPE, ROPE, BOOL] = { lrope: INT ¬ Rope.Length[rope]; lsep: INT ¬ Rope.Length[sep]; fi: INT ¬ Rope.Find[rope, sep, 0, FALSE]; IF fi = -1 THEN RETURN[rope, NIL, FALSE]; RETURN[Rope.Substr[rope, 0, fi], Rope.Substr[rope, fi+lsep], TRUE] }; ReplaceThisRope: PROC [r, d, n: ROPE, all: BOOL ¬ TRUE, case: BOOL ¬ FALSE, pos: INT ¬ 0] RETURNS [ROPE] = { ld: INT ¬ Rope.Length[d]; ln: INT ¬ Rope.Length[n]; DO l: INT ¬ Rope.Length[r]; fi: INT ¬ Rope.Find[r, d, pos, case]; IF fi = -1 THEN EXIT; r ¬ Rope.Replace[r, fi, ld, n]; pos ¬ fi + ln; IF NOT all THEN EXIT; ENDLOOP; RETURN[r]; }; InterpretAndSend: ENTRY PROC [h: Handle, format: ROPE] ~ { ENABLE UNWIND => NULL; delay: BOOL ¬ FALSE; eomSent: BOOL; unixName, xnsPassword, xnsLocalName, xnsDomainName, xnsOrgName: ROPE; selection: ROPE ¬ ViewerTools.GetSelectionContents[]; IF Rope.Length[format] = 0 THEN RETURN; unixName ¬ WITH CommanderOps.GetProp[NIL, $USER] SELECT FROM rope: ROPE => rope ENDCASE => NIL; format ¬ ReplaceThisRope[format, "%unixName", unixName]; format ¬ ReplaceThisRope[format, "%xnsPassword", xnsPassword]; format ¬ ReplaceThisRope[format, "%xnsFullName", Rope.Cat[xnsLocalName, ":", xnsDomainName, ":", xnsOrgName]]; format ¬ ReplaceThisRope[format, "%xnsLocalName", xnsLocalName]; format ¬ ReplaceThisRope[format, "%xnsDomainName", xnsDomainName]; format ¬ ReplaceThisRope[format, "%xnsOrgName", xnsOrgName]; format ¬ ReplaceThisRope[format, "%%", "%"]; format ¬ ReplaceThisRope[format, "%currentSelection", selection]; WHILE format # NIL DO sendThis: ROPE; eomSent ¬ FALSE; [sendThis, format, delay] ¬ SplitThis[format, "%d"]; FOR i: INT IN [0..Rope.Length[sendThis]) DO c: CHAR ¬ Rope.Fetch[sendThis, i]; IF h.state = connected THEN SendChar[h, c] ELSE IO.PutChar[h.viewerStream, c]; IF c = '\n THEN { IF h.state = connected THEN SendEndOfMessage[h.networkStream[output]]; eomSent ¬ TRUE; }; ENDLOOP; IF delay THEN { IF sendThis # NIL AND ~eomSent THEN { IF h.state = connected THEN SendEndOfMessage[h.networkStream[output]]; eomSent ¬ TRUE; }; Process.PauseMsec[2000]; }; ENDLOOP; IF ~eomSent AND h.state = connected THEN SendEndOfMessage[h.networkStream[output]]; }; GetValueFromList: PROC [list: LIST OF ROPE, key: ROPE, eval: PROC [ROPE] RETURNS [ROPE] ¬ NIL] RETURNS [ROPE, BOOL] ~ { thisItem: ROPE; WHILE list # NIL DO IF list.rest = NIL THEN RETURN[NIL, FALSE]; -- odd number of TOKENs, not a pair IF eval = NIL THEN thisItem ¬ list.first ELSE thisItem ¬ eval[list.first]; IF Rope.Equal[thisItem, key, FALSE] THEN RETURN[list.rest.first, TRUE]; list ¬ list.rest.rest; -- move through by pairs ENDLOOP; RETURN[NIL, FALSE]; }; GenerateLoginStuff: PROC [h: Handle] ~ { loginFormat: ROPE; formatFound: BOOL ¬ FALSE; specialLogins: LIST OF ROPE ¬ UserProfile.ListOfTokens[Rope.Concat[toolName, ".SpecialLogins"]]; [loginFormat, formatFound] ¬ GetValueFromList[specialLogins, h.hostName]; IF ~formatFound THEN loginFormat ¬ UserProfile.Token[Rope.Concat[toolName, ".DefaultLogin"], "%unixName\n%d%xnsPassword\n"]; InterpretAndSend[h, loginFormat]; }; LockedSendEndOfMessage: ENTRY PROC [h: Handle, s: STREAM] ~ { ENABLE UNWIND => NULL; SendEndOfMessage[s]; }; LockedSendChar: ENTRY PROC [h: Handle, c: CHAR] ~ { ENABLE UNWIND => NULL; SendChar[h, c]; }; LockedPutRope: ENTRY PROC [h: Handle, r: ROPE, sendNow: BOOL ¬ FALSE] ~ { ENABLE UNWIND => NULL; IO.PutRope[h.networkStream[output], r]; IF sendNow THEN Push[h.networkStream[output]]; }; OptionsWatcher: ENTRY PROC [h: Handle] ~ { OPEN TelnetOptions; ENABLE UNWIND => NULL; DO IF h.nvtState[echo].stateOfHisSide = active THEN h.localEcho ¬ h.lineBuffered ¬ FALSE ELSE h.localEcho ¬ h.lineBuffered ¬ TRUE; IF h.nvtState[suppressGoAhead].stateOfHisSide = active THEN h.heSendsGoAheads ¬ FALSE ELSE h.heSendsGoAheads ¬ TRUE; IF h.nvtState[suppressGoAhead].stateOfMySide = active THEN h.sendGoAheads ¬ FALSE ELSE h.sendGoAheads ¬ TRUE; WAIT h.negotiationEvent; ENDLOOP; }; Pusher: PROC [h: Handle] ~ { msg: ROPE ¬ NIL; closeAction: CloseAction ¬ none; c: CHAR ¬ Ascii.NUL; { ENABLE { IO.EndOfStream, ABORTED => { closeAction ¬ initiate; CONTINUE }; IO.Error => { IF stream = h.networkStream[output] OR stream = h.networkStream[input] THEN msg ¬ "Communication failure." ELSE closeAction ¬ initiate; CONTINUE }; }; IF h.autoLogin THEN GenerateLoginStuff[h]; DO { ENABLE IO.Rubout => { SendCommand[h, TelnetCommands.ip]; CONTINUE }; IF IO.CharsAvail[h.keyboardStream, FALSE] = 0 AND ((NOT h.lineBuffered) OR c = Ascii.CR OR c = Ascii.LF) THEN LockedSendEndOfMessage[h, h.networkStream[output]]; c ¬ IO.GetChar[h.keyboardStream]; IF h.localEcho THEN { SELECT c FROM Ascii.BS => IO.EraseChar[h.viewerStream, '?]; Ascii.CR, Ascii.LF => IO.PutChar[h.viewerStream, '\n]; ENDCASE => IO.PutChar[h.viewerStream, c]; }; IF c = Ascii.CR OR c = Ascii.LF THEN GenerateEOL[h] ELSE LockedSendChar[h, c]; } ENDLOOP; }; FinishConnection[h, puller, msg, closeAction]; }; GenerateEOL: PROC [h: Handle] ~ { LockedPutRope[h, h.eolSeq]; IF h.sendGoAheads THEN SendCommand[h, TelnetCommands.ga]; }; Puller: PROC [h: Handle] ~ { ch: CHAR ¬ Ascii.NUL; col: INTEGER ¬ 0; prevCh: CHAR ¬ Ascii.NUL; msg: ROPE ¬ NIL; closeAction: CloseAction ¬ none; { ENABLE { ABORTED => { closeAction ¬ initiate; CONTINUE }; IO.EndOfStream => { IF stream = h.networkStream[output] THEN msg ¬ "Stream Closed" ELSE closeAction ¬ initiate; CONTINUE }; IO.Error => { IF stream = h.networkStream[output] THEN msg ¬ "Communication failure" ELSE closeAction ¬ initiate; CONTINUE }; NetworkStream.Timeout => { msg ¬ "Stream Timeout"; CONTINUE }; }; DO prevCh ¬ ch; ch ¬ IO.GetChar[h.networkStream[input]]; Trace[h, hisSide, ch]; SELECT ch FROM IN [Ascii.SP..0176C], Ascii.TAB => { IO.PutChar[h.viewerStream, ch]; col ¬ col+1 }; Ascii.LF => IF prevCh = Ascii.CR THEN NULL ELSE { IO.PutChar[h.viewerStream, '\n]; col ¬ 0}; Ascii.CR => IF col # 0 THEN {IO.PutChar[h.viewerStream, '\n]; col ¬ 0}; Ascii.BS => IF col # 0 THEN { IO.EraseChar[h.viewerStream, '?]; col ¬ col-1 }; Ascii.BEL => ViewerOps.BlinkViewer[ViewerIO.GetViewerFromStream[h.viewerStream]]; TelnetCommands.iac => ProcessTelnetCommand[h]; ENDCASE => NULL; ENDLOOP; }; FinishConnection[h, puller, msg, closeAction]; }; ProcessTelnetCommand: PROC [h: Handle] ~ { OPEN TelnetCommands; command: CHAR ¬ IO.GetChar[h.networkStream[input]]; Trace[h, hisSide, command]; SELECT command FROM ayt => LockedPutRope[h, "\n\n", TRUE]; ec => IO.EraseChar[h.viewerStream, '?]; ga => h.gotGA ¬ TRUE; will, wont, do, dont => ProcessTelnetOption[h, command]; ENDCASE => NULL; }; ProcessTelnetOption: ENTRY PROC [h: Handle, command: CHAR] ~ { OPEN TelnetCommands, TelnetOptions; ENABLE UNWIND => NULL; option: CHAR ¬ IO.GetChar[h.networkStream[input]]; optionState: OptionState ¬ h.nvtState[option]; TraceInternal[h, hisSide, option]; SELECT command FROM will => { -- Either an offer on his part to start performing option XXX or a positive acknowledgment of a previous DO request from me. (state = his side) SELECT optionState.stateOfHisSide FROM active => { -- the RFC says you shouldn't send extra/gratuitious "will"s h.extraWills ¬ h.extraWills.SUCC; }; inactive => { -- he's offering to do XXX... IF optionState.desiredOfHisSide THEN { SendOptionInternal[h, do, option]; -- ack it h.nvtState[option].stateOfHisSide ¬ active; } ELSE { SendOptionInternal[h, dont, option]; -- nack it }; }; requested => { -- we've requested so he's acking h.nvtState[option].stateOfHisSide ¬ active; }; ENDCASE => ERROR; }; wont => { -- Negative acknowledgment of a DO request from me. (state = his side) h.nvtState[option].stateOfHisSide ¬ inactive; }; do => { -- Either he is requesting that I begin performing option XXX or a positive acknowledgment of a previous WILL offer by me. (state = my side) SELECT optionState.stateOfMySide FROM active => { -- the RFC says you shouldn't send extra/gratuitious "will"s h.extraDos ¬ h.extraDos.SUCC; }; inactive => { -- he's requesting that I do XXX... IF optionState.desiredOfMySide THEN { SendOptionInternal[h, will, option]; -- ack it h.nvtState[option].stateOfMySide ¬ active; } ELSE { SendOptionInternal[h, wont, option]; -- nack it }; }; requested => { -- he's taking us up on our offer h.nvtState[option].stateOfMySide ¬ active; }; ENDCASE => ERROR; }; dont => { -- Negative acknowledgment of a WILL offer by me. (state = my side) h.nvtState[option].stateOfMySide ¬ inactive; }; ENDCASE => ERROR; BROADCAST h.negotiationEvent; }; NegotiateOption: ENTRY PROC [h: Handle, option: CHAR, side: Side] RETURNS [active: BOOL] ~ { OPEN TelnetCommands; ENABLE UNWIND => NULL; SELECT side FROM mySide => { h.nvtState[option].desiredOfMySide ¬ TRUE; SELECT h.nvtState[option].stateOfMySide FROM inactive => NULL; active => RETURN[TRUE]; ENDCASE => ERROR; h.nvtState[option].stateOfMySide ¬ requested; SendOptionInternal[h, will, option]; DO IF h.nvtState[option].stateOfMySide # requested THEN RETURN[h.nvtState[option].stateOfMySide = active]; WAIT h.negotiationEvent; ENDLOOP; }; hisSide => { h.nvtState[option].desiredOfHisSide ¬ TRUE; SELECT h.nvtState[option].stateOfHisSide FROM inactive => NULL; active => RETURN[TRUE]; ENDCASE => ERROR; h.nvtState[option].stateOfHisSide ¬ requested; SendOptionInternal[h, do, option]; DO IF h.nvtState[option].stateOfHisSide # requested THEN RETURN[h.nvtState[option].stateOfHisSide = active]; WAIT h.negotiationEvent; ENDLOOP; }; ENDCASE => ERROR; }; DeactivateOption: ENTRY PROC [h: Handle, option: CHAR, side: Side] ~ { OPEN TelnetCommands; ENABLE UNWIND => NULL; SELECT side FROM mySide => { h.nvtState[option].desiredOfMySide ¬ FALSE; SELECT h.nvtState[option].stateOfMySide FROM inactive => RETURN; active => NULL; ENDCASE => ERROR; h.nvtState[option].stateOfMySide ¬ inactive; SendOptionInternal[h, wont, option]; }; hisSide => { h.nvtState[option].desiredOfHisSide ¬ FALSE; SELECT h.nvtState[option].stateOfHisSide FROM inactive => RETURN; active => NULL; ENDCASE => ERROR; h.nvtState[option].stateOfHisSide ¬ inactive; SendOptionInternal[h, dont, option]; }; ENDCASE => ERROR; }; SendChar: INTERNAL PROC [h: Handle, char: CHAR] ~ { IO.PutChar[h.networkStream[output], char]; TraceInternal[h, mySide, char]; }; SendOption: ENTRY PROC [h: Handle, command: CHAR, option: CHAR, misc: ROPE ¬ NIL] = { ENABLE UNWIND => NULL; SendOptionInternal[h, command, option, misc]; }; SendOptionInternal: INTERNAL PROC [h: Handle, command: CHAR, option: CHAR, misc: ROPE ¬ NIL] = { ENABLE UNWIND => NULL; SendChar[h, TelnetCommands.iac]; SendChar[h, command]; SendChar[h, option]; FOR i: INT IN [0..Rope.Size[misc]) DO SendChar[h, Rope.Fetch[misc, i]]; ENDLOOP; Push[h.networkStream[output]]; }; SendCommand: ENTRY PROC [h: Handle, command: CHAR, misc: ROPE ¬ NIL] = { ENABLE UNWIND => NULL; SendChar[h, TelnetCommands.iac]; SendChar[h, command]; FOR i: INT IN [0..Rope.Size[misc]) DO SendChar[h, Rope.Fetch[misc, i]]; ENDLOOP; Push[h.networkStream[output]]; }; DoSendBreak: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h.state = connected THEN SendCommand[h, TelnetCommands.brk]; }; DoSynch: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h.state = connected THEN SendSynch[h]; }; SendSynch: ENTRY PROC [h: Handle] = { ENABLE UNWIND => NULL; SendChar[h, TelnetCommands.iac]; NetworkStream.SendAttention[h.networkStream[output], ORD[TelnetCommands.dm]]; Push[h.networkStream[output]]; }; DoInterrupt: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h.state = connected THEN SendCommand[h, TelnetCommands.ip]; }; DoDisconnect: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h.state = connected THEN FinishConnection[h~h, me~none, msg~"Disconnecting.", closeAction~initiate]; }; FlushLogProc: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h.viewerStream # NIL THEN h.viewerStream.Flush[]; }; ShowOptionsProc: Menus.MenuProc ~ { h: Handle ¬ NARROW[clientData]; IF h = NIL THEN RETURN; IF control THEN { buf: TraceBuffer ~ GetTrace[h]; IF buf = NIL THEN { IO.PutRope[h.viewerStream, "\n*** Tracing Enabled ***\n"]; RETURN; }; IO.PutRope[h.viewerStream, " ***\n*** "]; FOR i: NAT ¬ buf.first, (i+1) MOD buf.size UNTIL i = buf.next DO t: TraceBufferEntry ~ buf[i]; IO.PutF[h.viewerStream, "%L%03B %L", [rope[IF t.side = mySide THEN "oBi" ELSE "obI"]], [cardinal[ORD[t.char]]], [rope["OBI"]]]; ENDLOOP; IO.PutRope[h.viewerStream, "***\n"]; RETURN; }; IF h.state # connected THEN RETURN; IO.PutRope[h.viewerStream, Rope.Cat["\n\n<<<< Current Options Status >>>>\n", RopeFromNVTState[h], "<<<< End of Options Status >>>>\n\n"]]; }; AnotherProc: Menus.MenuProc = { h: Handle ¬ NEW[Object]; [] ¬ TelnetMainInternal[h]; }; telnetTIPTable: TIPUser.TIPTable ~ TIPUser.InstantiateNewTIPTable["TelnetViewer.tip"]; [] _ TIPLinking.Append[telnetTIPTable, ViewerOps.FetchViewerClass[$Typescript].tipTable]; Commander.Register["TelnetViewer", TelnetMain, documentationRope, NIL, FALSE]; }... TelnetViewerImpl.mesa Copyright Σ 1987, 1989, 1991, 1992 by Xerox Corporation. All rights reserved. Wes Irish, October 4, 1991 4:43 pm PDT Michael Plass, May 20, 1992 2:47 pm PDT Last tweaked by Mike Spreitzer April 14, 1992 12:57 pm PDT Originally created by Wes Irish with the name TelnetImpl.mesa UserCredentials USING [Get, GetIdentity, Identity], [cmd: Commander.Handle] RETURNS [result: REF, msg] -- hack to do "connect" from command line... [parent, clientData, mouseButton, shift, control] [parent, clientData, mouseButton, shift, control] [parent, clientData, mouseButton, shift, control] IO.PutF1[handle.viewerStream, "%g ", [rope[ConvertExtras.RopeFromArpaAddress[hisAddress]]]]; IF handle.port # telnetPort THEN IO.PutF1[handle.viewerStream, "(port %g) ", [cardinal[handle.port]]]; IO.PutRope[handle.viewerStream, "... "]; tcpInfo _ [ matchForeignAddr: TRUE, foreignAddress: hisAddress, matchForeignPort: TRUE, foreignPort: handle.port, active: TRUE, timeout: openTimeout, matchLocalPort: FALSE]; IF openError = NIL THEN openError _ WaitUntilConnected[handle]; Yuck! ArpaTCP.CreateTCPStream returns an "open" connection without ever hearing from the other end. We'd like to make sure the remote end is there before telling the user "connected". He refuses to echo, so set expectations differently... identity: UserCredentials.Identity; xnsFullName: XNSAuth.Name; unixName _ UserCredentials.Get[].name; identity _ UserCredentials.GetIdentity[]; [xnsFullName, xnsPassword, ] _ XNSAuth.GetIdentityDetails[identity]; xnsLocalName ¬ xnsFullName.object; xnsDomainName ¬ xnsFullName.domain; xnsOrgName ¬ xnsFullName.organization; From RFC 854: In summary, WILL XXX is sent, by either party, to indicate that party's desire (offer) to begin performing option XXX, DO XXX and DON'T XXX being its positive and negative acknowledgments; similarly, DO XXX is sent to indicate a desire (request) that the other party (i.e., the recipient of the DO) begin performing option XXX, WILL XXX and WON'T XXX being the positive and negative acknowledgments. Since the NVT is what is left when no options are enabled, the DON'T and WON'T responses are guaranteed to leave the connection in a state which both ends can handle. Thus, all hosts may implement their TELNET processes to be totally unaware of options that are not supported, simply returning a rejection to (i.e., refusing) any option request that cannot be understood. [parent, clientData, mouseButton, shift, control] [parent, clientData, mouseButton, shift, control] -- Is this right? It is a replacement for this: IO.PutChar[h.networkStream, TelnetCommands.iac]; IO.PutChar[h.networkStream, TelnetCommands.dm]; ArpaTCP.SetUrgent[h.networkStream]; [parent, clientData, mouseButton, shift, control] [parent, clientData, mouseButton, shift, control] Κ*h•NewlineDelimiter –(cedarcode) style™code™Kšœ ΟeœC™NKšœ&™&K™'K™:K˜Kšœ=™=K˜—šΟk ˜ Kšœžœ ˜Kšœ˜Kšœ žœ˜(K˜ Kšœ˜Kšœ ˜ Kšžœ˜Kšœžœg˜rKšΟr œžœŸœ˜K˜ K˜Kšœžœ*˜7Kšœ˜Kšœ˜Kšœ˜Kšœ žœ˜-K˜ Kšœžœ$˜1Kšœ žœ˜*Kšœžœ™3Kšœ žœ ˜1Kšœžœ˜*Kšœ žœH˜ZKšœ žœ4˜BKšœ žœQ˜`Kšœ žœ˜)K˜—šΟnœžœž˜Kšžœžœ ˜Kšžœ7žœ Ÿ œ¨˜ώK˜K˜Kšžœžœžœ˜Kšžœžœžœžœ˜K˜šœ žœžœžœ˜BK˜—Kšœ žœ ˜Kšœ žœ˜/K˜Kšœžœu˜ŒK˜šœžœ Οc ˜*K˜—Kšœ žœžœ˜2Kšœžœžœ˜šœžœž œžœ˜!Kšœžœžœ˜Kšœžœžœ˜Kš œžœ žœžœžœžœ˜4Kšœ žœžœ˜K˜Kšœžœ˜K˜Kšœ žœžœ˜Kšœžœžœ˜Kšœžœžœ˜Kšœžœžœ˜Kšœ˜Kšœž œ˜Kšœžœžœ˜Kšœ žœžœ˜Kšœžœžœ˜Kšœžœžœ˜Kšœžœžœ‘$˜9Kšœ žœ˜Kšœ žœ˜Kšœžœ˜Kšœžœžœ˜$K˜1K˜K˜K˜—Kšœ žœžœ˜'šœžœžœ˜Kšœžœ˜ Kšœœžœ˜ Kš œžœžœžœžœœ œœ˜2Kšœ˜K˜—šœžœžœžœ˜(K˜ Kšœž˜ Kšœ˜K˜—š  œžœžœžœœ ˜YK˜—š   œžœžœžœœ˜DK˜šžœ žœžœžœ˜(Jšœžœžœ ˜&J˜Jšœžœ ˜'Kšžœžœ!žœ ˜PK˜—Kšœ˜K˜—š œžœžœ žœ˜=K˜ Kšœ žœ˜#Kšœ˜K˜—Kš œ žœžœžœžœ ˜+šœ žœžœžœ˜#Kšœžœžœ˜K˜+Kšœžœžœ˜K˜+K˜—Kšœžœ!˜7Kšœžœ˜K˜Kšœžœžœ˜,šœžœžœ˜#Kšœžœ˜ Kšœžœ˜ Kšœ˜K˜—K˜Kšœ žœ˜Kšœ žœ˜K˜K˜Kšœžœžœ‘q˜K˜K˜DK˜Kšœžœ:˜EKšœžœ&˜/Kšœžœ˜(šœ žœ˜.K˜—K˜š  œ˜!Kšœžœ žœ™2K˜Kšœ ˜ K˜"˜Kšœ>žœ˜J—Kšžœžœžœžœ˜Kšœžœ ˜šžœ˜ K˜,Kšœ˜—K˜Kšžœžœžœžœ˜0Kšœ˜Kšœ˜K˜—š œžœ ˜$K˜Kšœ˜KšœQΟy.œžœ˜”K˜1K˜Kšœ'žœ˜,Kšœ˜K˜Kšœ$˜$Kšœnžœ˜tKš‘,™,Kšžœ*žœ˜Mšžœž˜#Kšžœžœ˜,—Kšžœ˜Kšœ˜K˜—šΠbnœžœ˜$Kšœ˜K˜1KšœN˜NKšœ>˜>šžœ<žœž˜HKšœP˜P—KšœJ˜JKšœ8˜8Kšœ9˜9KšœE˜EKšœX˜XKšœP˜Pšžœ˜šžœ˜Kšœ"˜"Kšœ˜Kšœ˜—Kšžœ ˜$—K˜K˜—š   œžœ0žœžœžœ˜^Kšœžœ˜ KšœGžœ˜Nšžœž˜Kšœžœ˜ Kšžœ žœžœ˜4šžœ˜šžœ˜Kšžœ žœžœ˜/šžœž˜"˜Kš œžœžœžœžœ˜,˜%Kšœ-žœ˜9—K˜—˜Kšœžœ˜K˜—˜Kšœžœ˜K˜—Kšžœžœ˜*—Kšœ˜—šžœ˜Kšžœžœžœžœ˜>K˜K˜——Kšœžœ˜ Kšžœ˜—šž˜Kšœžœ˜ —K˜K˜—š   œžœ žœ žœžœ˜DK˜BKšœžœ˜Kšœ žœžœžœG˜]Kš žœ žœžœžœžœžœ˜5Kšœ.˜.šžœ žœž˜Kš žœžœžœžœ‘#˜GK˜K˜Kšžœžœžœ‘"˜HKšœ ˜ Kšœ žœ˜K˜Kšžœ˜—K˜K˜—š  œžœžœ˜4K˜BKšžœžœžœžœ˜KšœDžœ2˜yK˜K˜—š   œžœžœžœžœžœ˜_K˜BKšžœžœžœžœ˜KšœU˜UK˜K˜—š  œžœžœžœžœ˜8Kšžœžœ žœ˜Kšœžœ˜Kšžœžœžœžœžœžœžœ žœ ˜PK˜K˜—š   œžœžœ žœžœ˜˜>K˜—K˜K˜—š œžœE˜eKšœ*˜*Kšœ˜K˜—K˜š  œ˜Kšœ1™1Kšœ˜Kšœžœ ˜&K˜K˜Bšžœžœž˜Kšœžœ+˜5šœžœ˜Kšœ˜Kšœ žœ&˜5šžœžœžœ˜=Kšœ Ÿ œŸΠkrŸ˜6Kšžœ˜K˜—šžœ ž˜K˜Kš œŸžœ"ŸœŸ œŸ€Ÿœ˜ošœ ˜ šžœ+žœžœ˜7Kšœ Ÿ œŸ€Ÿ˜7Kšžœ˜K˜—K˜+K˜šžœ˜ šœž˜KšœQžœ2˜†——K˜—Kšžœ˜—K˜—˜ Kšœ˜Kšœ žœ&˜5šžœžœžœ˜=Kšœ Ÿ œŸ€Ÿ˜6Kšžœ˜K˜—šžœ ž˜šœ˜šžœžœ˜%Kšœ Ÿ œŸ€Ÿ˜4Kšžœ˜K˜—šžœ+žœžœ˜7Kšœ Ÿ œŸ€Ÿ˜7Kšžœ˜K˜—KšœNžœ6˜‡Kš œ Ÿžœ'Ÿœ Ÿ€Ÿ˜ZKšœ˜—Kš œŸžœ"ŸœŸ œŸ€Ÿœ˜ošœ ˜ K˜+Kš œ Ÿžœ*ŸœŸ€Ÿ˜[šžœ˜ šœž˜Kšœ'žœ˜.——K˜—Kšžœ˜—K˜—Kšžœ˜—Kšœ˜K˜K˜—š  œ˜Kšœ1™1K˜Kšœ žœ ˜šžœžœžœ˜Kšœ Ÿœ ŸœŸ˜KšŸ%œŸ€Ÿ˜K˜nK˜@K˜BK˜K˜—šž˜K˜ Kšœžœ!˜(K˜šžœž˜Kšžœžœžœžœ,˜SKšœžœžœžœžœžœžœžœ(˜\Kš œžœžœ žœžœœœœœ˜GKš œžœžœ žœžœ.˜NKšœžœH˜QKšœ.˜.Kšžœžœ˜—Kšžœ˜—K˜—K˜.K˜K˜—š œžœ˜*Kšžœ˜Kšœ žœžœ!˜3K˜šžœ ž˜Kšœ0žœ˜6Kšœžœ˜'Kšœžœ˜K˜9Kšžœžœ˜—K˜K˜—š œžœžœžœ˜>K™”Kšžœ˜#Kšžœžœžœ˜Kšœžœžœ!˜2K˜.K˜"šžœ ž˜šœ ‘˜›šžœž˜&šœ ‘<˜IKšœžœ˜!K˜—šœ‘˜,šžœ˜šžœ˜Kšœ$‘ ˜-K˜+K˜—šžœ˜Kšœ&‘ ˜0K˜——K˜—šœ‘!˜1K˜+K˜—Kšžœžœ˜—K˜—šœ ‘G˜RK˜-Kšœ˜—šœ ‘˜–šžœž˜%šœ ‘<˜IKšœžœ˜K˜—šœ‘#˜2šžœ˜šžœ˜Kšœ&‘ ˜/K˜*K˜—šžœ˜Kšœ&‘ ˜0K˜——K˜—šœ‘!˜1K˜*K˜—Kšžœžœ˜—Kšœ˜—šœ ‘D˜OK˜,Kšœ˜—Kšžœžœ˜—Kšž œ˜K˜K˜—š  œžœžœžœžœ žœ˜\Kšžœ˜Kšžœžœžœ˜šžœž˜˜ Kšœ%žœ˜*šžœ"ž˜,Kšœ žœ˜Kšœ žœžœ˜Kšžœžœ˜—K˜-Kšœ$˜$šž˜Kšžœ.žœžœ,˜gKšžœ˜Kšžœ˜—Kšœ˜—˜ Kšœ&žœ˜+šžœ#ž˜-Kšœ žœ˜Kšœ žœžœ˜Kšžœžœ˜—K˜.Kšœ"˜"šž˜Kšžœ/žœžœ-˜iKšžœ˜Kšžœ˜—K˜—Kšžœžœ˜—K˜K˜—š œžœžœžœ˜FKšžœ˜Kšžœžœžœ˜šžœž˜˜ Kšœ%žœ˜+šžœ"ž˜,Kšœ žœ˜Kšœ žœ˜Kšžœžœ˜—K˜,Kšœ$˜$Kšœ˜—˜ Kšœ&žœ˜,šžœ#ž˜-Kšœ žœ˜Kšœ žœ˜Kšžœžœ˜—K˜-Kšœ$˜$K˜—Kšžœžœ˜—K˜K˜—š œžœžœžœ˜3Kšžœ(˜*K˜Kšœ˜K˜—š  œžœžœžœ žœžœžœ˜UKšžœžœžœ˜Kšœ-˜-K˜—K˜š œžœžœžœ žœžœžœ˜`Kšžœžœžœ˜K˜ K˜K˜šžœžœžœž˜%K˜!Kšžœ˜—Kšœ˜K˜K˜—š £ œžœžœžœžœžœ˜HKšžœžœžœ˜K˜ K˜šžœžœžœž˜%K˜!Kšžœ˜—Kšœ˜K˜K˜—š  œ˜Kšœ1™1K˜Kšœ žœ ˜Kšžœžœ$˜?K˜K˜—š œ˜Kšœ1™1K˜Kšœ žœ ˜Kšžœžœ˜)K˜K˜—š  œžœžœ˜%Kšžœžœžœ˜KšΟw ˜ š§5Πkw§˜MK™/Kšžœ.™0Kšžœ-™/Kšœ#™#—K˜Kšœ˜K˜K˜—š  œ˜Kšœ1™1K˜Kšœ žœ ˜Kšžœžœ#˜>K˜K˜—š  œ˜Kšœ1™1K˜Kšœ žœ ˜KšžœžœL˜gK˜K˜—š £œ˜ Kšœ žœ ˜Kšžœžœžœ˜4K˜K˜—š£œ˜#Kšœ žœ ˜Kšžœžœžœžœ˜šžœ žœ˜J˜šžœžœžœ˜Kšžœ8˜:Kšžœ˜K˜—Kšžœ'˜)š žœžœžœ žœž˜@K˜Kš žœ)žœžœžœžœ˜Kšžœ˜—Kšžœ"˜$Kšžœ˜K˜—Kšžœžœžœ˜#Kšžœ‰˜‹K˜K˜—š£ œ˜Kšœ žœ ˜K˜K˜K˜—K˜VK˜˜YK˜—KšœBžœžœ˜NK˜K˜——…—$ΆŒ