<<>> <> <> <> <> <> <> <> <> <> DIRECTORY Args, Ascii, BridgeExec, BridgeComm, BridgeDriver, Commander, CommanderOps, Convert, IO, NetworkName, NetworkStream, Process, RefTab, RefText, Rope, SymTab, SystemNames, TypeScript, UserProfile, ViewerIO ; BridgeDriverImpl: CEDAR MONITOR LOCKS h USING h: Handle IMPORTS Ascii, Args, BridgeExec, BridgeComm, Commander, CommanderOps, Convert, IO, NetworkName, NetworkStream, Process, RefTab, RefText, Rope, SymTab, SystemNames, TypeScript, UserProfile, ViewerIO EXPORTS BridgeDriver ~ { NetworkStreamPair: TYPE ~ BridgeExec.NetworkStreamPair; ROPE: TYPE ~ Rope.ROPE; Session: TYPE ~ BridgeExec.Session; STREAM: TYPE ~ IO.STREAM; <> bridgeWKSRope: ROPE ¬ "3001"; initialTransportClassRope: ROPE ~ Rope.Translate[ base: UserProfile.Token[key: "Bridge.DefaultTransport", default: "ARPA"], translator: Upper]; defaultTransportClass: ATOM ¬ Convert.AtomFromRope[initialTransportClassRope]; defaultInitialPutTimeout: NetworkStream.Milliseconds ¬ 20000; defaultInitialGetTimeout: NetworkStream.Milliseconds ¬ 40000; bridgeNetworkStreamTransportClass: ATOM ~ $SPP; Handle: TYPE ~ REF Object; Object: TYPE ~ MONITORED RECORD [ session: Session ¬ NIL, listener: NetworkStream.Listener ¬ NIL, mgrStreams: NetworkStreamPair ¬ [NIL, NIL], userName: ROPE, passwd: ROPE ]; sessionTab: SymTab.Ref ¬ SymTab.Create[]; transportTab: RefTab.Ref ¬ RefTab.Create[]; Upper: PROC [old: CHAR] RETURNS [new: CHAR] ~ { RETURN [Ascii.Upper[old]]; }; SessionNameFromHostNameEtc: PUBLIC PROC [hostName: ROPE, etc: ROPE] RETURNS [sessionName: ROPE] ~ { RETURN [Rope.Cat[hostName, "(", etc, ")"]]; }; HostNameFromSessionName: PUBLIC PROC [sessionName: ROPE] RETURNS [hostName: ROPE] ~ { pos: INT ¬ Rope.SkipTo[s~sessionName, pos~0, skip~"("]; IF (pos < Rope.Length[sessionName]) AND (pos > 0) THEN { WHILE (pos > 0) AND (Rope.Fetch[sessionName, pos-1] = ' ) DO pos ¬ pos - 1; ENDLOOP; }; RETURN [Rope.Substr[sessionName, 0, pos]] }; CloseConnectionWrapper: PROC [nsp: NetworkStreamPair] ~ { BridgeComm.CloseConnection[nsp, initiate]; }; SessionWorker: NetworkStream.ListenerWorkerProc -- [listener, in, out] -- ~ { h: Handle ~ NARROW[listener.listenerWorkerClientData]; msg: CHAR ¬ 'X; rawArg, cmdName, cookedArg: ROPE; pos: INT; nsp: NetworkStreamPair ¬ [in, out]; [msg, rawArg] ¬ BridgeComm.GetMsg[nsp ! BridgeComm.Error => CONTINUE]; IF msg # 'E THEN GOTO Bad; pos ¬ Rope.SkipTo[s~rawArg, pos~0, skip~argSeparator]; cmdName ¬ Rope.Substr[rawArg, 0, pos]; pos ¬ Rope.SkipOver[s~rawArg, pos~pos, skip~argSeparator]; pos ¬ Rope.SkipOver[s~rawArg, pos~pos, skip~" "]; cookedArg ¬ Rope.Substr[rawArg, pos]; { len: INT ~ Rope.Length[cookedArg]; IF len > 0 AND Rope.Equal[Rope.Substr[cookedArg, len-1, 1], argSeparator] THEN cookedArg ¬ Rope.Substr[cookedArg, 0, len-1]; }; IF BridgeExec.CreateInstance[cmdName, nsp, cookedArg, h.session, CloseConnectionWrapper] = NIL THEN GOTO Bad; EXITS Bad => { IO.Close[in]; IO.Close[out] } }; defaultCmd: ROPE ¬ ""; StartSession: PUBLIC PROC [sessionName: ROPE, nameAndPasswordProc: BridgeDriver.NameAndPasswordProc, cmd: ROPE ¬ NIL, transportClass: ATOM ¬ NIL] RETURNS [excuse: ROPE ¬ NIL] ~ { h: Handle; userName: ROPE; passwd: ROPE; FetchOrCreateHandle: SymTab.UpdateAction -- [found, val] RETURNS [op, new] -- ~ { handles: LIST OF Handle ¬ IF found THEN NARROW[val] ELSE NIL; FOR tail: LIST OF Handle ¬ handles, tail.rest UNTIL tail = NIL DO IF Rope.Equal[tail.first.userName, userName] THEN { h ¬ tail.first; <> <> <> op ¬ none; RETURN; }; ENDLOOP; h ¬ NEW[Object ¬ [userName~userName, passwd~passwd]]; handles ¬ CONS[h, handles]; -- work around compiler bug new ¬ handles; op ¬ store; RETURN; }; StartSessionInner: ENTRY PROC [h: Handle] ~ { ENABLE UNWIND => NULL; IF h.session = NIL THEN { hisName, theRemote, logonMsg, ansArg: ROPE ¬ NIL; ansChar: CHAR; hisName ¬ HostNameFromSessionName[sessionName]; [addr~theRemote] ¬ NetworkName.AddressFromName[transportClass, hisName, bridgeWKSRope, hostAndPort, NIL ! NetworkName.Error => {theRemote ¬ NIL; CONTINUE }]; IF Rope.IsEmpty[theRemote] THEN { excuse ¬ "name lookup failed"; GOTO CantCreate }; [h.mgrStreams.in, h.mgrStreams.out] ¬ NetworkStream.CreateStreams[ protocolFamily~transportClass, remote~theRemote, transportClass~bridgeNetworkStreamTransportClass, timeout~defaultInitialGetTimeout ! NetworkStream.Error => CONTINUE]; IF h.mgrStreams.in = NIL THEN { excuse ¬ "Can't connect"; GOTO CantCreate }; h.session ¬ BridgeExec.CreateSession[sessionName]; h.listener ¬ NetworkStream.CreateListener[ protocolFamily~transportClass, transportClass~bridgeNetworkStreamTransportClass, listenerWorkerProc~SessionWorker, listenerWorkerClientData~h]; logonMsg ¬ Rope.Cat[NetworkStream.GetListenerInfo[h.listener].local, " ", userName, " ", passwd]; { ENABLE NetworkStream.Error, IO.Error => { excuse ¬ "I/O error sending logon: "; GOTO CantCreate }; IO.PutRope[h.mgrStreams.out, logonMsg]; NetworkStream.SendEndOfMessage[h.mgrStreams.out]; }; [ansChar, ansArg] ¬ BridgeComm.GetMsg[h.mgrStreams ! BridgeComm.Error => { excuse ¬ Rope.Concat["I/O error reading logon response: ", msg]; GOTO CantCreate; } ]; excuse ¬ SELECT ansChar FROM 'Y => NIL, 'N, 'Q => ansArg, ENDCASE => "Protocol error"; IF excuse # NIL THEN GOTO CantCreate; EXITS CantCreate => { KillSessionInternal[h, sessionName]; RETURN } }; IF BridgeExec.SessionIsDead[h.session] THEN { excuse ¬ "Session is dead"; RETURN }; BridgeComm.PutMsgWithAck[h.mgrStreams, 'E, (IF Rope.IsEmpty[cmd] THEN defaultCmd ELSE cmd) ! BridgeComm.Error => { excuse ¬ "Can't send cmd"; CONTINUE }]; }; IF nameAndPasswordProc # NIL THEN [userName, passwd] ¬ nameAndPasswordProc[] ELSE [userName, passwd] ¬ GetCurrentCredentials[ HostNameFromSessionName[sessionName], TRUE ]; IF Rope.IsEmpty[userName] THEN RETURN["missing user name"]; IF transportClass = NIL THEN transportClass ¬ defaultTransportClass; SymTab.Update[sessionTab, sessionName, FetchOrCreateHandle]; -- h ¬ handle IF excuse # NIL THEN RETURN; StartSessionInner[h]; }; KillSessionInternal: INTERNAL PROC [h: Handle, sessionName: ROPE] ~ { DoDelete: SymTab.UpdateAction -- [found, val] RETURNS [op, new] -- ~ { newHandles: LIST OF Handle ¬ NIL; IF NOT found THEN RETURN [none, NIL]; FOR handles: LIST OF Handle ¬ NARROW[val], handles.rest UNTIL handles = NIL DO IF handles.first # h THEN newHandles ¬ CONS[handles.first, newHandles]; ENDLOOP; IF newHandles # NIL THEN RETURN [store, newHandles] ELSE RETURN [delete, NIL]; }; SymTab.Update[sessionTab, sessionName, DoDelete]; IF h.session # NIL THEN { BridgeExec.DestroySession[h.session]; h.session ¬ NIL; }; IF h.listener # NIL THEN { NetworkStream.DestroyListener[h.listener]; h.listener ¬ NIL; }; IF h.mgrStreams # [NIL, NIL] THEN { NetworkStream.SetTimeout[h.mgrStreams.in, 7500, FALSE ! NetworkStream.Error => CONTINUE]; NetworkStream.SetTimeout[h.mgrStreams.out, 7500, FALSE ! NetworkStream.Error => CONTINUE]; BridgeComm.PutMsg[h.mgrStreams, 'Q, "" ! BridgeComm.Error => CONTINUE]; BridgeComm.CloseConnection[h.mgrStreams]; h.mgrStreams ¬ [NIL, NIL]; } }; EntryKillSession: ENTRY PROC [h: Handle, sessionName: ROPE] ~ { ENABLE UNWIND => NULL; KillSessionInternal[h, sessionName]; }; KillSession: PUBLIC PROC [sessionName: ROPE, userName: ROPE ¬ NIL] RETURNS [excuse: ROPE] ~ { handles: LIST OF Handle; h: Handle; excuse ¬ "No session with that name."; DO handles ¬ NARROW[SymTab.Fetch[sessionTab, sessionName].val]; WHILE handles # NIL DO IF (h ¬ handles.first) = NIL THEN ERROR; IF userName = NIL OR Rope.Equal[userName, h.userName] THEN EXIT; handles ¬ handles.rest; ENDLOOP; IF handles = NIL THEN EXIT; excuse ¬ NIL; EntryKillSession[h, sessionName]; ENDLOOP; }; EnumerateSessions: PUBLIC PROC [procToApply: BridgeDriver.EachSession] ~ { EachSession: SymTab.EachPairAction -- [key, val] RETURNS [quit] -- ~ { FOR handles: LIST OF Handle ¬ NARROW[val], handles.rest UNTIL handles = NIL DO h: Handle ~ handles.first; listener: NetworkStream.Listener ~ h.listener; class: ATOM ¬ IF listener # NIL THEN NetworkStream.GetListenerInfo[listener].protocolFamily ELSE $NONE; IF procToApply[sessionName: key, userName: h.userName, class: class] THEN RETURN[quit: TRUE]; ENDLOOP; }; [] ¬ SymTab.Pairs[sessionTab, EachSession]; }; UpdateDefaultTransportClass: PUBLIC PROC [newClass: ATOM ¬ NIL] RETURNS [oldClass: ATOM] ~ { oldClass ¬ defaultTransportClass; IF newClass # NIL THEN defaultTransportClass ¬ newClass; }; secondsBetweenKeepalives: INT ¬ 300; Daemon: PROC ~ { EachSessionInner: ENTRY PROC [h: Handle] ~ { ENABLE UNWIND => NULL; IF h.mgrStreams # [NIL, NIL] THEN BridgeComm.PutMsgWithAck[h.mgrStreams, 'Z, Convert.RopeFromCard[2*secondsBetweenKeepalives] ! BridgeComm.Error => CONTINUE]; }; EachSession: SymTab.EachPairAction ~ { FOR handles: LIST OF Handle ¬ NARROW[val], handles.rest UNTIL handles = NIL DO EachSessionInner[handles.first]; ENDLOOP; }; DO Process.PauseMsec[INT[1000]*secondsBetweenKeepalives]; [] ¬ SymTab.Pairs[sessionTab, EachSession]; ENDLOOP; }; FixUserNameForUnix: PUBLIC PROC [name: ROPE] RETURNS [fixedName: ROPE] ~ { len: INT ~ MIN [8, Rope.Length[name], name.Index[pos1: 0, s2: "."]]; text: REF TEXT ~ RefText.ObtainScratch[len]; FOR i: INT IN [0..len) DO text[i] ¬ name.Fetch[i]; ENDLOOP; text.length ¬ len; fixedName ¬ Rope.FromRefText[text]; RefText.ReleaseScratch[text]; }; argSeparator: ROPE ~ "\r"; CmdFromRopes: PROC [r1, r2, r3, r4, r5: ROPE ¬ NIL] RETURNS [cmd: ROPE] ~ { list: LIST OF ROPE ¬ NIL; IF r5 # NIL THEN list ¬ CONS[r5, list]; IF r4 # NIL THEN list ¬ CONS[r4, list]; IF r3 # NIL THEN list ¬ CONS[r3, list]; IF r2 # NIL THEN list ¬ CONS[r2, list]; IF r1 # NIL THEN list ¬ CONS[r1, list]; RETURN [CmdFromListOfRope[list]]; }; CmdFromListOfRope: PUBLIC PROC [list: LIST OF ROPE] RETURNS [cmd: ROPE ¬ NIL] ~ { FOR each: LIST OF ROPE ¬ list, each.rest WHILE each # NIL DO IF cmd = NIL THEN cmd ¬ each.first ELSE cmd ¬ Rope.Cat[cmd, argSeparator, each.first]; ENDLOOP; }; ListOfRopeFromCmd: PUBLIC PROC [cmd: ROPE] RETURNS [list: LIST OF ROPE ¬ NIL] ~ { separatorIndex: INT; temp: ROPE ¬ cmd; len: INT ¬ Rope.Length[cmd]; IF len > 0 THEN { IF Rope.EqualSubstrs[s1~temp, start1~len-1, len1~1, s2~argSeparator] THEN { temp ¬ Rope.Substr[base~temp, len~len-1]; }; }; WHILE (separatorIndex ¬ Rope.FindBackward[temp, argSeparator]) >= 0 DO list ¬ CONS[ Rope.Substr[base~temp, start~separatorIndex+1], list ]; temp ¬ Rope.Substr[base~temp, len~separatorIndex]; ENDLOOP; IF NOT Rope.IsEmpty[temp] THEN list ¬ CONS[ temp, list ]; }; CredsEntry: TYPE ~ REF CredsEntryObject; CredsEntryObject: TYPE ~ RECORD [ name: ROPE, password: ROPE ]; credsTab: SymTab.Ref ¬ SymTab.Create[case~FALSE]; GetCurrentCredentials: PUBLIC PROC [machineName: ROPE, useGV: BOOL] RETURNS [name: ROPE ¬ NIL, password: ROPE ¬ NIL] ~ { credsEntry: CredsEntry; key: ROPE; IF Rope.IsEmpty[machineName] THEN machineName ¬ "*"; WITH SymTab.Fetch[credsTab, machineName].val SELECT FROM it: CredsEntry => {name ¬ it.name; password ¬ it.password}; ENDCASE => WITH SymTab.Fetch[credsTab, "*"].val SELECT FROM it: CredsEntry => {name ¬ it.name; password ¬ it.password}; ENDCASE; IF name = NIL AND useGV THEN { remoteMachine: ROPE ~ HostNameFromSessionName[sessionName~machineName]; userProfileKey: ROPE ~ Rope.Cat["Bridge.", machineName, ".userName"]; name ¬ SystemNames.UserName[]; name ¬ UserProfile.Token[key~userProfileKey, default~name]; }; name ¬ FixUserNameForUnix[name]; <> <> <<};>> }; SetCurrentCredentials: PUBLIC PROC [machineName: ROPE, name: ROPE, password: ROPE] ~ { credsEntry: CredsEntry; key: ROPE; SELECT TRUE FROM Rope.IsEmpty[machineName] => key ¬ "*"; ENDCASE => key ¬ machineName; credsEntry ¬ NEW[CredsEntryObject ¬ [name, password]]; [] ¬ SymTab.Store[credsTab, key, credsEntry]; }; SmashBridgeCredentialsIfUserChanged: UserProfile.ProfileChangedProc ~ { IF reason = newUser THEN credsTab ¬ SymTab.Create[case~FALSE]; }; BridgeStart: ROPE ~ "BridgeStart"; BridgeStartShort: ROPE ~ "Bridge"; BridgeStartDoc: ROPE ~ " [-i]\nStart a Cornell Unix Bridge Session.\n-i => prompt for username and password.\n"; BridgeStartUsage: ROPE ~ Rope.Concat["Usage: BridgeStart ", BridgeStartDoc]; PromptForLine: PROC [ stdin: STREAM, stdout: STREAM, prompt: ROPE, echo: BOOL ¬ TRUE] RETURNS [line: ROPE ¬ NIL] ~ { lookHidden: IO.Value ~ IO.rope["h"]; lookShiftHidden: IO.Value ~ IO.rope["H"]; IO.PutF1[stdout, " %g: ", [rope[prompt]]]; IF NOT echo THEN { IO.PutF1[stdout, "%l", lookHidden]; }; BEGIN ENABLE UNWIND => IF NOT echo THEN stdout.PutF1["%l", lookShiftHidden]; BEGIN ENABLE BEGIN IO.EndOfStream => CONTINUE; IO.Rubout => { IF NOT echo THEN stdout.PutF1["%l", lookShiftHidden]; stdout.PutRope[" \n"]; stdin.Reset[]; CONTINUE; }; END; line ¬ IO.GetLineRope[stdin]; END; IF line = NIL THEN { IF NOT echo THEN stdout.PutF1["%l", lookShiftHidden]; RETURN; }; IF NOT echo THEN { viewer: ViewerIO.Viewer ~ ViewerIO.GetViewerFromStream[stdout]; IO.PutF1[stdout, "%l", lookShiftHidden]; IF TypeScript.IsATypeScript[viewer] THEN { TypeScript.BackSpace[viewer, line.Length[] + 1]; IO.PutRope[stdout, "****\n"]; stdin.Reset[]; }; }; END; }; DoBridgeStart: Commander.CommandProc ~ { sessionName, iFlag: Args.Arg; bridgeCmd, name, password: ROPE; SupplyNameAndPassword: BridgeDriver.NameAndPasswordProc -- RETURNS [userName, passwd] -- ~ { RETURN [name, password]; }; IF Args.NArgs[cmd] = 0 THEN RETURN [$Failure, BridgeStartUsage]; [sessionName, iFlag] ¬ Args.ArgsGet[cmd: cmd, format: "%s-i%b", caseSensitive: FALSE ! Args.Error => {msg ¬ reason; CONTINUE}]; IF msg # NIL THEN RETURN [result~$Failure, msg~msg]; IF NOT iFlag.bool THEN { [name, password] ¬ GetCurrentCredentials[sessionName.rope, TRUE]; }; IF Rope.IsEmpty[name] THEN { name ¬ PromptForLine[stdin~cmd.in, stdout~cmd.out, prompt~"user name", echo~TRUE]; password ¬ NIL; }; name ¬ FixUserNameForUnix[name]; IF Rope.IsEmpty[password] THEN { password ¬ PromptForLine[stdin~cmd.in, stdout~cmd.out, prompt~"password", echo~FALSE]; }; bridgeCmd ¬ CmdFromListOfRope[LIST["RTTY", "LoginSh"]]; msg ¬ StartSession[sessionName.rope, SupplyNameAndPassword, bridgeCmd, NIL]; IF msg # NIL THEN RETURN[result~$Failure, msg~msg]; }; BridgeChangeTransport: ROPE ~ "BridgeChangeTransport"; BridgeChangeTransportDoc: ROPE ~ "\nChange the transport used by bridge."; BridgeChangeTransportUsage: ROPE ~ Rope.Concat["Usage: BridgeChangeTransport [XNS|TCP]", BridgeChangeTransportDoc]; TransportIsRegistered: PROC [desiredFamily: ATOM] RETURNS [isRegistered: BOOL ¬ FALSE] ~ { EachClass: NetworkStream.EnumerateCallbackProc -- [protocolFamily, transportClass] RETURNS [continue] -- ~ { IF transportClass # bridgeNetworkStreamTransportClass THEN ERROR; IF protocolFamily = desiredFamily THEN { isRegistered ¬ TRUE; continue ¬ FALSE; } ELSE { continue ¬ TRUE; } }; NetworkStream.Enumerate[families~NIL, classes~bridgeNetworkStreamTransportClass, proc~EachClass]; }; DoBridgeChangeTransport: Commander.CommandProc ~ { <> transportClassArg: Args.Arg; oldTransportClass, newTransportClass: ATOM; IF Args.NArgs[cmd] # 1 THEN RETURN[$Failure, BridgeChangeTransportUsage]; [transportClassArg] ¬ Args.ArgsGet[cmd~cmd, format~"%s", caseSensitive~FALSE ! Args.Error => {result ¬ $Failure; msg ¬ reason; GOTO Out }]; newTransportClass ¬ Convert.AtomFromRope[transportClassArg.rope]; IF NOT TransportIsRegistered[newTransportClass] THEN { result ¬ $Failure; msg ¬ "Transport not registered"; GOTO Out }; oldTransportClass ¬ UpdateDefaultTransportClass[newTransportClass]; msg ¬ Rope.Concat["transport was ", Convert.RopeFromAtom[oldTransportClass]]; EXITS Out => NULL; }; BridgeCredentials: ROPE ~ "BridgeCredentials"; BridgeCredentialsDoc: ROPE ~ "\n[name, [machine]] Supply explicit Bridge credentials for machine"; BridgeCredentialsUsage: ROPE ~ Rope.Concat["Usage: BridgeCredentials ", BridgeCredentialsDoc]; DoBridgeCredentials: Commander.CommandProc ~ { <> argv: CommanderOps.ArgumentVector ¬ CommanderOps.Parse[cmd ! CommanderOps.Failed => {msg ¬ errorMsg; GO TO oops}]; argc: NAT ¬ argv.argc; name: ROPE ¬ IF argc > 1 THEN argv[1] ELSE NIL; machine: ROPE ¬ IF argc > 2 THEN argv[2] ELSE NIL; password: ROPE ¬ NIL; IF argc > 3 THEN GO TO usage; IF Rope.IsEmpty[name] THEN { name ¬ PromptForLine[stdin~cmd.in, stdout~cmd.out, prompt~"name", echo~TRUE]; }; IF Rope.IsEmpty[password] THEN { password ¬ PromptForLine[stdin~cmd.in, stdout~cmd.out, prompt~"password", echo~FALSE]; }; IF Rope.IsEmpty[machine] THEN machine ¬ "*"; SetCurrentCredentials[machine, name, password]; msg ¬ Rope.Cat["Credentials set for Bridge sessions to ", machine, ".\n"]; EXITS usage => msg ¬ BridgeCredentialsUsage; oops => result ¬ $Failure; }; BridgePrintTransport: ROPE ~ "BridgePrintTransport"; BridgePrintTransportDoc: ROPE ~ "\nPrint the default transport currently being used by bridge."; BridgePrintTransportUsage: ROPE ~ Rope.Concat["Usage: BridgePrintTransport", BridgePrintTransportDoc]; DoBridgePrintTransport: Commander.CommandProc ~ { <> IF Args.NArgs[cmd] # 0 THEN RETURN[$Failure, BridgePrintTransportUsage]; msg ¬ Rope.Concat["Transport is ", Convert.RopeFromAtom[defaultTransportClass]]; RETURN[result: result, msg: msg]; }; BridgeList: ROPE ~ "BridgeList"; BridgeListDoc: ROPE ~ "\nList active Bridge sessions."; BridgeListUsage: ROPE ~ Rope.Concat["Usage: BridgeList ", BridgeListDoc]; DoBridgeList: Commander.CommandProc ~ { PrintSession: BridgeDriver.EachSession ~ { <> IO.PutF[cmd.out, "%g on %g (%g)\n", [rope[userName]], [rope[sessionName]], [atom[class]]] }; IF Args.NArgs[cmd] # 0 THEN RETURN[$Failure, BridgeListUsage]; EnumerateSessions[PrintSession]; }; BridgeKill: ROPE ~ "BridgeKill"; BridgeKillDoc: ROPE ~ " [ ]\nKill a Bridge session."; BridgeKillUsage: ROPE ~ Rope.Concat["Usage: BridgeKill ", BridgeKillDoc]; DoBridgeKill: Commander.CommandProc ~ { sessionName, userName: Args.Arg; excuse: ROPE; [sessionName, userName] ¬ Args.ArgsGet[cmd, "%s[s" ! Args.Error => {msg ¬ reason; CONTINUE}]; IF msg # NIL THEN RETURN[$Failure, BridgeKillUsage]; excuse ¬ KillSession[sessionName.rope, IF userName.ok THEN userName.rope ELSE NIL]; IF excuse # NIL THEN RETURN[$Failure, excuse]; }; TRUSTED { Process.Detach[FORK Daemon[]] }; UserProfile.CallWhenProfileChanges[SmashBridgeCredentialsIfUserChanged]; Commander.Register[ key: BridgeChangeTransport, proc: DoBridgeChangeTransport, doc: BridgeChangeTransportDoc]; Commander.Register[ key: BridgePrintTransport, proc: DoBridgePrintTransport, doc: BridgePrintTransportDoc]; Commander.Register[ key: BridgeList, proc: DoBridgeList, doc: BridgeListDoc]; Commander.Register[ key: BridgeKill, proc: DoBridgeKill, doc: BridgeKillDoc]; Commander.Register[ key: BridgeStart, proc: DoBridgeStart, doc: BridgeStartDoc]; Commander.Register[ key: BridgeStartShort, proc: DoBridgeStart, doc: BridgeStartDoc]; Commander.Register[ key: BridgeCredentials, proc: DoBridgeCredentials, doc: BridgeCredentialsDoc]; }...