<> <> <> <> <> <> <> <<>> <> <<>> DIRECTORY Atom, BasicTime, Buttons, Carets, CodeControl, Commander, CommanderOps, Convert, EchoStream, FS, HostAndTerminalOps, Imager, ImagerBackdoor, ImagerBitmapContext, ImagerColor, ImagerForkContext, ImagerPath, ImagerSample, ImagerSender, IO, IOClasses, IOErrorFormatting, KeyMapping, KeySyms1, KeySymsCedar, KeySymsKB, KeySymsPrincOpsConvention, KeyTypes, MessageWindow, NetAddressing, NetworkStream, Process, Real, RealFns, RefTab, RemoteEventTime, RemoteImagerDataTypes, RemoteViewersHost, RemoteViewersHostBackdoor, Rope, SF, SimpleFeedback, TerminalLocation, TerminalReceiver, Termination, TIPKeyboards, UserInput, UserInputGetActions, UserInputOps, ViewerClasses, ViewerLocks, ViewerOps, ViewerPrivate, ViewerSpecs, ViewersWorld, ViewersWorldClasses, ViewersWorldInitializations, WindowManager; RemoteViewersHostImpl: CEDAR MONITOR IMPORTS Atom, BasicTime, Buttons, Carets, CodeControl, Commander, CommanderOps, Convert, EchoStream, FS, HostAndTerminalOps, Imager, ImagerBackdoor, ImagerBitmapContext, ImagerColor, ImagerForkContext, ImagerSample, ImagerSender, IO, IOClasses, IOErrorFormatting, KeyMapping, MessageWindow, NetAddressing, NetworkStream, Process, Real, RealFns, RefTab, RemoteEventTime, Rope, SimpleFeedback, TerminalLocation, TerminalReceiver, Termination, TIPKeyboards, UserInputGetActions, UserInputOps, ViewerLocks, ViewerOps, ViewerPrivate, ViewerSpecs, ViewersWorld, ViewersWorldClasses, ViewersWorldInitializations EXPORTS RemoteViewersHost, RemoteViewersHostBackdoor = BEGIN OPEN RemoteImagerDataTypes, HAT:HostAndTerminalOps, NA:NetAddressing, RET:RemoteEventTime, TL:TerminalLocation, VWC:ViewersWorldClasses; ROPE: TYPE ~ Rope.ROPE; LocPair: TYPE ~ RECORD [cancl, pretty: REF TL.RemoteLocation]; PushData: TYPE ~ REF PushDataPrivate; PushDataPrivate: TYPE ~ RECORD [ rloc: LocPair, coding, sendTiming, loggingNet, loggingPlain, begun: BOOL ¬ FALSE, netInStream: IO.STREAM ¬ NIL, netOutStream: IO.STREAM ¬ NIL, --the raw seething network stream netLogStream: IO.STREAM ¬ NIL, --log of what goes to netOutStream codedStream: IO.STREAM ¬ NIL, --forks to netOutStream and netLogStream codingStream: IO.STREAM ¬ NIL, --compresses to codedStream plainLogStream: IO.STREAM ¬ NIL, --log of plaintext output outStream: IO.STREAM ¬ NIL, --forks to plainLogStream and codingStream ish: ImagerSender.Handle ¬ NIL, logFlushClock: NATURAL ¬ 0, version: BYTE ¬ 0, mp: MousePosition ¬ [0, FALSE, 0], cutBuffers: CutBufferList ¬ NIL, cutBufferChange: CONDITION ]; CutBufferList: TYPE ~ LIST OF RECORD [buffer: ATOM, key: CARD, data: ROPE]; myVersion: BYTE ¬ 17; myOldestCompatibleVersion: BYTE ¬ 10; codingMethod: ROPE ¬ "B4KS"; compressable: BOOL ¬ TRUE; reject, sendTiming: BOOL ¬ FALSE; debug: BOOL ¬ TRUE; debugInput: BOOL ¬ FALSE; logInput: BOOL ¬ FALSE; logNet, logPlain: BOOL ¬ FALSE; sizeChangeable: BOOL ¬ TRUE; adjustTime: BOOL ¬ FALSE; replyToTimeProbes: BOOL ¬ TRUE; beingAdvised: BOOL ¬ FALSE; --advice temporarily decomissioned July 15, 1991 cc: BOOL ¬ FALSE; --cc temporarily decommissioned well before July 16, 1991 inputLogFmt: ROPE ¬ "-vux:/tmp/RemoteViewersInput/%g.log"; netLogFmt: ROPE ¬ "-vux:/tmp/ViewersTap/%g.netBytes"; plainLogFmt: ROPE ¬ "-vux:/tmp/ViewersTap/%g.plainBytes"; undefinedOnly: TL.LocState ¬ TL.CreateSingleLocState[[undefined[]], [undefined[]]]; localOnly: TL.LocState ¬ TL.CreateSingleLocState[[local[]], [local[]]]; greeting: ROPE ¬ "Radically new RemoteViewersHost of Groundhog Day!"; change: CONDITION; started: BOOL ¬ FALSE; locState: TL.LocState ¬ undefinedOnly; comingState: TL.LocState ¬ locState; primaryPD: PushData ¬ NIL; allPD: TL.LocSet--canonical loc -> PushData-- ¬ TL.CreateLocSet[FALSE]; screenSettingses: ScreenSettingses ¬ MakeInitialScreenSettingses[]; backingSm: Imager.SampleMap ¬ NIL; curHot: VECI ¬ [0, 0]; curCursorPattern: CursorArray ¬ ALL[0]; curCursorPatternName: ATOM ¬ NIL; inputReceiver: PROCESS ¬ NIL; connClose, connBreak: TL.Why ¬ [BasicTime.Now[], NIL]; Spy: PROC [IO.STREAM, NA.Address] ¬ NIL; vw: ViewersWorld.Ref ¬ NIL; userInput: UserInput.Handle ¬ NIL; lcAdded: BOOL ¬ FALSE; fileStream: IO.STREAM ¬ NIL; gmt1: BasicTime.GMT ¬ BasicTime.Now[].Update[-3600] --hope clocks disagree by less than 1 hr--; et1: RET.EventTime ¬ RET.FromEGMT[[gmt1, 0]]; et0: RET.EventTime ¬ et1.Sub[RET.SmallConsCC[0, 1]]; MakeInitialScreenSettingses: PROC RETURNS [sss: ScreenSettingses] ~ { sss ¬ NEW [ScreenSettingsesPrivate[1]]; sss[0] ¬ [methods: LIST["black and white"], sizes: LIST[[1000, 1000]]]; RETURN}; keyMapping: KeyMapping.Mapping ¬ CreateKeyTable[]; CreateKeyTable: PROC RETURNS [KeyMapping.Mapping] ~ { kt: KeyMapping.KeyTable ¬ NEW [KeyMapping.KeyTableRep ¬ ALL[NIL]]; FOR kn: PrincOpsKeyName IN PrincOpsKeyName DO ks0, ks1: KeyTypes.KeySym; kd: REF KeyMapping.KeySymsRep; SELECT kn FROM STUFF => {ks0 ¬ KeySymsKB.Next; ks1 ¬ KeySymsKB.R12}; USERABORT => {ks0 ¬ KeySymsCedar.Swat; ks1 ¬ KeySymsKB.R15}; Arrow => {ks0 ¬ KeySymsKB.LeftArrow; ks1 ¬ KeySymsKB.UpArrow}; Dash => {ks0 ¬ KeySyms1.Hyphen; ks1 ¬ KeySyms1.LowLine}; EXPAND => {ks0 ¬ KeySymsKB.RightMeta; ks1 ¬ KeySymsPrincOpsConvention.Expand}; A11 => {ks0 ¬ KeySymsKB.LeftAlt; ks1 ¬ KeySymsPrincOpsConvention.A11}; A8 => {ks0 ¬ ks1 ¬ KeySymsPrincOpsConvention.Paste}; --TIPKeyboards already says PASTE is used for LineFeed! ENDCASE => [ks0, ks1] ¬ TIPKeyboards.KeySymsFromKeyCode[VAL[kn.ORD]]; kd ¬ NEW [KeyMapping.KeySymsRep[IF ks0#ks1 THEN 2 ELSE 1]]; kd[0] ¬ ks0; IF ks0#ks1 THEN kd[1] ¬ ks1; kt[VAL[kn.ORD]] ¬ kd; ENDLOOP; RETURN [KeyMapping.NewMapping[kt, VAL[PrincOpsKeyName.LAST.ORD]]]}; lc: TL.Client ~ NEW [TL.ClientPrivate ¬ [ NoteChange: NoteChange, data: NIL]]; NoteChange: PROC [client: TL.Client, x: TL.LocState] ~ {[] ¬ SetLoc[x, FALSE]}; SetLoc: PROC [to: TL.LocState, asAdvice: BOOL] ~ { WithLock: ENTRY PROC ~ { ENABLE UNWIND => NULL; TRUSTED {comingState ¬ to}; RETURN}; WithLock[]; IF debug THEN SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $FYI, "At %g, SetLoc[to: %g, asAdvice: %g].", LIST[[time[BasicTime.Now[]]], [rope[TL.FormatLocState[to]]], [boolean[asAdvice]] ]]; {old: Process.Priority ~ Process.GetPriority[]; Process.SetPriority[Process.priorityClient3]; TRUSTED {Process.Detach[FORK EffectChange[]]}; Process.SetPriority[old]; RETURN}}; OpenOrder: TYPE ~ RECORD [primary: BOOL, rloc: LocPair]; EffectChange: PROC ~ { opens: LIST OF OpenOrder ¬ NIL; InnerEffect: ENTRY PROC ~ { ENABLE UNWIND => NULL; to: TL.LocState ~ comingState; wasLocal, isLocal: BOOL ¬ TRUE; CloseOld: INTERNAL PROC [key, val: REF ANY] RETURNS [quit: BOOL ¬ FALSE] --RefTab.EachPairAction-- ~ { rl: REF TL.Location ~ NARROW[key]; IF to.allCancl.Fetch[key].found THEN RETURN; WITH rl SELECT FROM x: REF undefined TL.Location => wasLocal ¬ wasLocal; x: REF local TL.Location => wasLocal ¬ wasLocal; x: REF TL.RemoteLocation => { curPD: PushData ~ NARROW[val]; CloseStream[curPD]; IF NOT allPD.Delete[key] THEN ERROR; wasLocal ¬ FALSE}; ENDCASE => ERROR; }; AddNew: INTERNAL PROC [key, val: REF ANY] RETURNS [quit: BOOL ¬ FALSE] --RefTab.EachPairAction-- ~ { rl: REF TL.Location ~ NARROW[key]; cancl: TL.Location ~ rl­.Canonicalize[]; k2: REF TL.Location ~ NEW [TL.Location ¬ cancl]; found: BOOL; pda: REF ANY; [found, pda] ¬ allPD.Fetch[k2]; IF found THEN { IF cancl.LocsEqual[locState.primaryCancl] AND pda#NIL --pda=NIL means OpenStream is already coming, and that will set primaryPD-- THEN primaryPD ¬ NARROW[pda]; RETURN}; WITH rl SELECT FROM x: REF undefined TL.Location => BROADCAST change; x: REF local TL.Location => BROADCAST change; x: REF TL.RemoteLocation => { IF NOT allPD.Insert[k2, NIL] THEN ERROR; opens ¬ CONS[[primary: locState.primaryCancl.LocsEqual[cancl], rloc: [cancl: NARROW[k2], pretty: NARROW[rl]]], opens]; isLocal ¬ FALSE}; ENDCASE => ERROR; }; IF to.LocStateEqual[locState] AND started THEN RETURN; started ¬ TRUE; IF allPD.Pairs[CloseOld] THEN ERROR; TRUSTED {locState ¬ to}; IF locState.allPretty.Pairs[AddNew] THEN ERROR; RETURN}; Carets.SuspendCarets[]; ViewerLocks.CallUnderViewerTreeLock[InnerEffect]; --AMN ViewerPrivate.WaitForPaintingToFinish[]; Carets.ResumeCarets[]; FOR ol: LIST OF OpenOrder ¬ opens, ol.rest WHILE ol#NIL DO OpenStream[ol.first.rloc, ol.first.primary, ol.rest=NIL, [BasicTime.Now[], "change"], NIL]; ENDLOOP; RETURN}; SetSizes: PROC [] ~ { InnerPaint: PROC = { FOR screen: NAT IN [0 .. 1--A1S--) DO size: VECI ~ screenSettingses[screen].sizes.first; ViewersWorld.SetSize[vw, size.x, size.y, IF screen=0 THEN NIL ELSE ERROR]; --calls ViewerPrivate.SetCreator and ViewerOps.PaintEverything ENDLOOP; RETURN}; Carets.SuspendCarets[]; ViewerLocks.CallUnderViewerTreeLock[InnerPaint]; ViewerPrivate.WaitForPaintingToFinish[]; Carets.ResumeCarets[]; RETURN}; SetSpy: PUBLIC ENTRY PROC [spy: PROC [IO.STREAM, NA.Address]] ~ { ENABLE UNWIND => NULL; Spy ¬ spy; <> RETURN}; MaybeSpy: ENTRY PROC [pd: PushData] ~ { ENABLE UNWIND => NULL; pd.begun ¬ TRUE; MaybeSpyInternal[pd]; RETURN}; MaybeSpyInternal: INTERNAL PROC [pd: PushData] ~ { IF Spy#NIL AND pd#NIL AND pd.begun THEN TRUSTED {Process.Detach[FORK Spy[pd.netInStream, pd.rloc.pretty.addr]]}; RETURN}; AdviceFrom: PROC [name: ROPE] RETURNS [problem: ROPE] ~ { addr: NA.Address; {ENABLE NA.Error => {problem ¬ NA.FormatError[codes, msg]; GOTO Skipit}; addr ¬ NA.ParseAddress[name]; [] ¬ NA.ToNnAddress[addr]}; {pl: TL.Location ~ [remote[addr]]; cl: TL.Location ~ pl.Canonicalize[]; ls1: TL.LocState ¬ TL.GetLocState[].CopyLocState[]; ls1.LocStateIncrement[cl, pl]; {ls2: TL.LocState ¬ [primaryCancl: cl, primaryPretty: pl, allCancl: ls1.allCancl, allPretty: ls1.allPretty]; SetLoc[ls2, TRUE]; RETURN [NIL]}}; EXITS Skipit => name ¬ name}; StopAdvice: PROC RETURNS [problem: ROPE] ~ {SetLoc[localOnly, FALSE]; RETURN [NIL]}; ISNoteBreak: PROC [data: REF ANY, consumer: IO.STREAM, why: ROPE] ~ { pd: PushData ~ NARROW[data]; now: BasicTime.GMT ~ BasicTime.Now[]; TRUSTED {Process.Detach[FORK NoteBreakAt[pd, now, why]]}; RETURN}; NoteBreak: PROC [curPD: PushData, why: ROPE] ~ { now: BasicTime.GMT ~ BasicTime.Now[]; NoteBreakAt[curPD, now, why]; RETURN}; NoteBreakAt: PROC [curPD: PushData, now: BasicTime.GMT, why: ROPE] ~ { SetBreak: ENTRY PROC ~ { ENABLE UNWIND => NULL; connBreak ¬ [now, why]; RETURN}; MaybeReopen: ENTRY PROC ~ { ENABLE UNWIND => NULL; IF allPD.Fetch[curPD.rloc.cancl].val = curPD THEN TRUSTED { Process.Detach[FORK SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $FYI, "Re-opening connection to %g after %g-second delay.", LIST[ [rope[TL.FormatLoc[curPD.rloc.pretty­]]], [real[retryPause/1000.0]] ]]]; Process.Detach[FORK OpenStream[curPD.rloc, curPD.rloc.cancl­.LocsEqual[locState.primaryCancl], TRUE, [now, why], curPD]]}; RETURN}; SetBreak[]; TRUSTED {Process.Detach[FORK SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $FYI, "Connection to %g broken at %g (%g).", LIST[ [rope[TL.FormatLoc[curPD.rloc.pretty­]]], [time[now]], [rope[why]] ]]]}; Process.PauseMsec[retryPause]; MaybeReopen[]; RETURN}; retryPause: INT ¬ 5000; CheckIn: ENTRY PROC [rloc: LocPair, primary: BOOL, why: TL.Why, dedPD: PushData] RETURNS [PushData] ~ { ENABLE UNWIND => NULL; curPD: PushData ¬ NARROW[allPD.Fetch[rloc.cancl].val]; IF curPD#NIL THEN CloseStream[curPD]; IF rloc.cancl.kind # remote THEN ERROR; IF NOT locState.allCancl.Fetch[rloc.cancl].found THEN RETURN [NIL]; curPD ¬ NEW [PushDataPrivate ¬ [rloc]]; TRUSTED {Process.InitializeCondition[@curPD.cutBufferChange, Process.SecondsToTicks[5]]}; IF primary THEN primaryPD ¬ curPD; [] ¬ allPD.Store[rloc.cancl, curPD]; RETURN [curPD]}; CheckOut: ENTRY PROC [pd: PushData, sss: ScreenSettingses] RETURNS [ok: BOOL] ~ { ENABLE UNWIND => NULL; BROADCAST change; IF NOT allPD.Fetch[pd.rloc.cancl].found THEN {CloseStream[pd]; RETURN [FALSE]}; IF locState.primaryCancl.LocsEqual[pd.rloc.cancl­] THEN screenSettingses ¬ sss; RETURN [TRUE]}; OpenStream: PROC [rloc: LocPair, primary, final: BOOL, why: TL.Why, dedPD: PushData] ~ { rejection: ROPE; sss: ScreenSettingses; {pd: PushData ~ CheckIn[rloc, primary, why, dedPD]; IF debug THEN SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $FYI, "At %g, OpenStream[rloc: %g, primary: %g].", LIST[ [time[BasicTime.Now[]]], [rope[IF pd#NIL THEN TL.FormatLoc[rloc.pretty­] ELSE "NIL"]], [boolean[primary]] ]]; IF pd=NIL THEN RETURN; {forFile: ROPE ~ IO.PutFR["%g-%g", [rope[NA.FormatAddress[rloc.pretty.addr, FALSE]]], [rope[rloc.pretty.addr.socket]] ]; nwsAddr: ROPE ~ pd.rloc.cancl.addr.ToNnAddress[!NA.Error => { connClose ¬ [BasicTime.Now[], NA.FormatError[codes, msg]]; TRUSTED {Process.Detach[FORK NoteReject[beingAdvised, rloc.pretty­, connClose, FALSE]]}; GOTO escape}].addr; IF logNet THEN pd.netLogStream ¬ FS.StreamOpen[fileName: IO.PutFR1[netLogFmt, [rope[forFile]] ], accessOptions: create, keep: 10 !FS.Error => CONTINUE]; IF logPlain THEN pd.plainLogStream ¬ FS.StreamOpen[fileName: IO.PutFR1[plainLogFmt, [rope[forFile]] ], accessOptions: create, keep: 10 !FS.Error => CONTINUE]; pd.loggingNet ¬ pd.netLogStream#NIL; pd.loggingPlain ¬ pd.plainLogStream#NIL; [pd.netInStream, pd.netOutStream] ¬ NetworkStream.CreateStreams[protocolFamily: pd.rloc.cancl.addr.protocolFamily, remote: nwsAddr, transportClass: $basicStream !NetworkStream.Error => { connClose ¬ [BasicTime.Now[], FmtNSErr[codes, msg]]; TRUSTED {Process.Detach[FORK NoteReject[beingAdvised, pd.rloc.pretty­, connClose, FALSE]]}; GOTO escape}]; pd.outStream ¬ pd.codingStream ¬ pd.codedStream ¬ IF pd.loggingNet THEN IOClasses.CreateDribbleOutputStream[output1: pd.netOutStream, output2: pd.netLogStream] ELSE pd.netOutStream; {ENABLE { IO.Error => {NoteBreak[pd, IOErrorFormatting.FormatError[ec, details, msg]]; GOTO escape}; IO.EndOfStream => {NoteBreak[pd, "IO.EndOfStream"]; GOTO escape}; CodeControl.BadCodingMethod => { NoteBreak[pd, IO.PutFR["Lookup of coding method %g raises BadCodingMethod[%g]", [rope[codingMethod]], [rope[errorMsg]] ]]; compressable ¬ FALSE; GOTO escape}; }; [rejection, pd.coding, pd.sendTiming, pd.version] ¬ Good[pd.netInStream, pd.netOutStream, compressable, sendTiming]; IF rejection=NIL THEN sss ¬ ImagerSender.Prelude[pd.netInStream]; }; TRUSTED {Process.Detach[FORK NoteReject[beingAdvised, pd.rloc.pretty­, [BasicTime.Now[], rejection], pd.coding]]}; IF rejection#NIL THEN { connClose ¬ [BasicTime.Now[], rejection]; pd.netInStream.Close[!IO.Error => CONTINUE]; pd.netOutStream.Close[!IO.Error => CONTINUE]; IF pd.loggingPlain THEN pd.plainLogStream.Close[!IO.Error => CONTINUE]; IF pd.loggingNet THEN pd.netLogStream.Close[!IO.Error => CONTINUE]; GOTO escape}; IF pd.coding THEN pd.codingStream ¬ CodeControl.CreateEncodingStream[pd.codedStream, codingMethod]; IF pd.loggingPlain THEN pd.outStream ¬ IOClasses.CreateDribbleOutputStream[output1: pd.codingStream, output2: pd.plainLogStream] ELSE pd.outStream ¬ pd.codingStream; IF pd.loggingPlain THEN pd.plainLogStream.PutChar[VAL[pd.version]]; MaybeSpy[pd]; pd.ish ¬ ImagerSender.Begin[ pd.outStream, pd.netOutStream, IF pd.netLogStream#NIL THEN pd.netLogStream ELSE pd.netOutStream, IF pd.plainLogStream#NIL THEN pd.plainLogStream ELSE pd.netOutStream, pd.version, pd.sendTiming, Push, ISNoteBreak, SampleScreen, 1, NIL, pd]; IF CheckOut[pd, sss] THEN { TRUSTED {Process.Detach[inputReceiver ¬ FORK Decode[pd, forFile]]}; IF final THEN SetSizes[]; } ELSE rejection ¬ rejection; EXITS escape => NULL; }}}; CloseStream: INTERNAL PROC [pd: PushData] ~ { ENABLE IO.Error => CONTINUE; IF pd.codingStream=NIL THEN RETURN; IF pd.loggingPlain THEN IO.Close[pd.plainLogStream, FALSE]; IF pd.coding THEN IO.Close[pd.codedStream, FALSE]; IF pd.loggingNet THEN IO.Close[pd.netLogStream, FALSE]; IO.Close[pd.netOutStream, TRUE]; --can't close a coding stream asynchronously with writing RETURN}; SampleScreen: PROC [data: REF ANY, screen: NATURAL] RETURNS [ScreenSetting] ~ { SELECT screen FROM 0 => RETURN [["BlackAndWhite", [ViewerSpecs.bwScreenWidth, ViewerSpecs.bwScreenHeight], left]]; ENDCASE => ERROR}; NoteReject: PROC [beingAdvised: BOOL, opLoc: TL.Location, break: TL.Why, coding: BOOL] ~ { SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $Error, IF break.explanation=NIL THEN IF beingAdvised THEN "%g Accepting advice from %g.%g" ELSE IF coding THEN "%g Accepting %g as coding terminal.%g" ELSE "%g Accepting %g as plaintext terminal.%g" ELSE IF beingAdvised THEN "%g Giving up on advice from %g 'cause %g." ELSE "%g Giving up on %g as terminal 'cause %g.", LIST[ [rope[ShortFmtTime[break.time]]], [rope[TL.FormatLoc[opLoc]]], [rope[break.explanation]] ]]; IF break.explanation#NIL THEN SELECT beingAdvised FROM FALSE => TL.Abandon[opLoc, break]; TRUE => [] ¬ SetLoc[localOnly, FALSE]; ENDCASE => ERROR; }; Good: PROC [inStream, outStream: IO.STREAM, willingToCompress, maySendTiming: BOOL] RETURNS [rejection: ROPE ¬ "bug", doCompress, sendTiming: BOOL ¬ TRUE, version: BYTE ¬ 0] ~ { ENABLE IO.Error, IO.EndOfStream => {rejection ¬ "IO.Error during initial negotiations"; CONTINUE}; hisPassword, hisCode, hisReject: CHAR; hisVersion, hisOldestCompatibleVersion: BYTE; ctlVersion: NAT; [ctlVersion, rejection] ¬ TL.StartCommand[inStream, outStream, 'V, TRUE]; IF rejection#NIL THEN RETURN; outStream.PutChar[CHAR.LAST]; outStream.PutChar[VAL[myVersion]]; outStream.PutChar[VAL[myOldestCompatibleVersion]]; outStream.Flush[]; hisPassword ¬ inStream.GetChar[]; IF hisPassword#CHAR.LAST THEN RETURN ["terminal didn't properly open initial negotiations"]; hisVersion ¬ inStream.GetChar[].ORD; hisOldestCompatibleVersion ¬ inStream.GetChar[].ORD; IF hisVersion < myOldestCompatibleVersion OR myVersion < hisOldestCompatibleVersion THEN RETURN [IO.PutFLR[ "[%g..%g] <> [%g..%g] (terminal's version range vs. this host's)", LIST[ [cardinal[hisOldestCompatibleVersion]], [cardinal[hisVersion]], [cardinal[myOldestCompatibleVersion]], [cardinal[myVersion]]]]]; version ¬ MIN[hisVersion, myVersion]; outStream.PutChar[IF willingToCompress THEN 'C ELSE 'P]; outStream.PutChar[IF reject THEN 'R ELSE 'A]; IF hisVersion >= 12 THEN outStream.PutChar[IF maySendTiming THEN 'T ELSE 'N]; outStream.Flush[]; hisCode ¬ inStream.GetChar[]; hisReject ¬ inStream.GetChar[]; IF reject OR (SELECT hisReject FROM 'R => TRUE, 'A => FALSE, ENDCASE => ERROR) THEN RETURN ["terminal or host is rejecting"]; rejection ¬ NIL; doCompress ¬ willingToCompress AND hisCode='C; sendTiming ¬ hisVersion >= 12 AND maySendTiming; RETURN}; Decode: PROC [pd: PushData, forFile: ROPE] ~ { in: IO.STREAM ¬ pd.netInStream; inputLog: IO.STREAM ¬ NIL; et: EventTime ¬ RET.noEventTime; Repaint: PROC ~ TRUSTED {Process.Detach[FORK ViewerOps.PaintEverything[]]}; TakeTimeReply: PROC [org, mid: EventTime, descToo: BOOL, desc: TerminalReceiver.EventDesc] ~ {NULL}; TakeCutBuffer: ENTRY PROC [buffer: ATOM, key: CARD, data: ROPE] ~ { ENABLE UNWIND => NULL; pd.cutBuffers ¬ CONS[[buffer, key, data], pd.cutBuffers]; NOTIFY pd.cutBufferChange; RETURN}; InsertAction: PROC [ab: TerminalReceiver.ActionBody] = TRUSTED { IF debugInput THEN { SimpleFeedback.PutFL[$RemoteViewersHost, begin, $Input, "%g[t:%g, dt:%g, dv:%g, dy:%g", LIST[ [atom[ab.kind]], [cardinal[ab.eventTime]], [integer[ab.deltaTime]], [atom[IF ab.device#NIL THEN NARROW[ab.device] ELSE $NIL]], [atom[IF ab.display#NIL THEN NARROW[ab.display] ELSE $NIL]] ]]; SELECT ab.kind FROM $Key => SimpleFeedback.PutFL[$RemoteViewersHost, end, $Input, ", down:%g, kc:%g, ks:%g]", LIST[ [boolean[ab.down]], [cardinal[ab.keyCode.ORD]], [cardinal[ab.preferredSym]] ]]; $IntegerPosition, $Position => SimpleFeedback.PutFL[$RemoteViewersHost, end, $Input, ", x:%g, y:%g, rx:%g, ry:%g]", LIST[ [integer[ab.x]], [integer[ab.y]], [real[ab.rx]], [real[ab.ry]] ]]; ENDCASE => SimpleFeedback.Append[$RemoteViewersHost, end, $Input, "]"]; }; et ¬ et0.Add[RET.SmallConsCC[0, ab.eventTime]]; SELECT ab.kind FROM $IntegerPosition => pd.mp ¬ [mouseX: ab.x, mouseY: ab.y, color: DisplayToScreen[NARROW[ab.display]]#0]; $Position => pd.mp ¬ [mouseX: Real.Round[ab.rx], mouseY: Real.Round[ab.ry], color: DisplayToScreen[NARROW[ab.display]]#0]; $Key => IF ab.down AND pd.version > 10 THEN ImagerSender.SendTimeReply[pd.ish, et, RET.ReadEventTime[], [pd.mp, [contents: keyDown[value: VAL[ab.keyCode.ORD]]] ]]; ENDCASE => NULL; IF pd=primaryPD THEN UserInputGetActions.InsertInputActionBody[userInput, ab]; }; IF logInput THEN { inputLog ¬ FS.StreamOpen[IO.PutFR1[inputLogFmt, [rope[forFile]] ], create !FS.Error => CONTINUE]; IF inputLog#NIL THEN in ¬ EchoStream.CreateEchoStream[in: pd.netInStream, out: inputLog]; }; TerminalReceiver.Decode[in, inputLog, et1, pd.version, InsertAction, Repaint, TakeTimeReply, TakeCutBuffer, adjustTime !UNWIND => IF inputLog#NIL THEN inputLog.Close[!IO.Error => CONTINUE] ]; IF inputLog#NIL THEN inputLog.Close[!IO.Error => CONTINUE]; RETURN}; GetStats: PROC [flush: ImagerSender.Handle] RETURNS [deltaBits, totalBits, deltaSeconds: CARD, avgBitRate: REAL] = { newTime: BasicTime.GMT; totalBits ¬ ImagerSender.GetTotal[flush: flush]; newTime ¬ BasicTime.Now[]; deltaBits ¬ totalBits - lastTotal; lastTotal ¬ totalBits; deltaSeconds ¬ BasicTime.Period[lastTime, newTime]; lastTime ¬ newTime; avgBitRate ¬ IF deltaSeconds # 0 THEN REAL[deltaBits]/deltaSeconds ELSE 0; }; lastTime: BasicTime.GMT ¬ BasicTime.Now[]; lastTotal: CARD ¬ 0; MyCreate: PROC [screenServerData: REF ANY, screen: ViewerPrivate.Screen] RETURNS [c: Imager.Context] --ViewerPrivate.ContextCreatorProc-- = { size: SF.Vec ~ [s: ViewerSpecs.bwScreenHeight, f: ViewerSpecs.bwScreenWidth]; sm: Imager.SampleMap ¬ backingSm; IF sm=NIL OR ImagerSample.GetSize[sm] # size THEN backingSm ¬ sm ¬ ImagerSample.NewSampleMap[box: [[0, 0], size], bitsPerSample: 1]; {smc: Imager.Context ~ ImagerBitmapContext.Create[deviceSpaceSize: size, scanMode: [slow: down, fast: right], surfaceUnitsPerInch: [72.0, 72.0], pixelUnits: TRUE, fontCacheName: $Bitmap]; rcs: LIST OF Imager.Context ¬ NIL; i: INT ¬ 0; BuildSender: PROC [key, val: REF ANY] RETURNS [quit: BOOL ¬ FALSE] --RefTab.EachPairAction-- ~ { rl: REF TL.Location ~ NARROW[key]; pd: PushData ~ NARROW[val]; ci: Imager.Context; SELECT rl.kind FROM local, undefined => RETURN; remote => IF pd=NIL OR pd.ish=NIL --haven't opened yet, this context will be discarded after open-- THEN RETURN; ENDCASE => ERROR; ci ¬ ImagerSender.Create[pd.ish, screen.ORD]; IF ci=NIL THEN RETURN; {j: INT ¬ i ¬ i+1; rcs ¬ CONS[ci, rcs]; WHILE j MOD 2 = 0 DO c1: Imager.Context ~ rcs.first; c2: Imager.Context ~ rcs.rest.first; rcs ¬ rcs.rest; rcs.first ¬ ImagerForkContext.Create[c1, c2]; j ¬ j/2; ENDLOOP; RETURN}}; ImagerBitmapContext.SetBitmap[smc, sm]; IF allPD.Pairs[BuildSender] THEN ERROR; IF rcs=NIL THEN RETURN [smc]; WHILE rcs.rest#NIL DO c1: Imager.Context ~ rcs.first; c2: Imager.Context ~ rcs.rest.first; rcs ¬ rcs.rest; rcs.first ¬ ImagerForkContext.Create[c1, c2]; ENDLOOP; c ¬ ImagerForkContext.Create[smc, rcs.first]; IF NOT (cc OR beingAdvised) THEN ImagerForkContext.FakeANoImage[c, TRUE]; RETURN}}; SetCursorPattern: ENTRY PROC [screenServerData: REF, deltaX, deltaY: INTEGER, cursorPattern: CursorArray, patternName: ATOM ¬ $Unnamed, cursor: REF ¬ NIL] --VWC.SetCursorPatternProc-- ~ { ENABLE UNWIND => NULL; curHot ¬ [deltaX, deltaY]; curCursorPattern ¬ cursorPattern; curCursorPatternName ¬ patternName; IF primaryPD=NIL THEN RETURN; ImagerSender.SendInterminalSetting[primaryPD.ish, [CursorPattern[cursorPattern]]]; ImagerSender.SendInterminalSetting[primaryPD.ish, [CursorOffset[deltaX, deltaY]]]; RETURN}; GetCursorPattern: ENTRY PROC [screenServerData: REF, cursor: REF ¬ NIL] RETURNS [deltaX, deltaY: INTEGER, cursorPattern: CursorArray, patternName: ATOM] ~ { ENABLE UNWIND => NULL; RETURN [curHot.x, curHot.y, curCursorPattern, curCursorPatternName]}; SetBigCursorPattern: VWC.SetBigCursorPatternProc ~ {ERROR}; GetBigCursorPattern: VWC.GetBigCursorPatternProc ~ {ERROR}; IsBigCursorPattern: VWC.IsBigCursorPatternProc ~ {RETURN [FALSE]}; BigCursorsSupported: VWC.BigCursorsSupportedProc ~ {RETURN [FALSE]}; SetCursorColor: VWC.SetCursorColorProc ~ {RETURN}; GetCursorColor: VWC.GetCursorColorProc ~ {RETURN [Imager.black]}; SetMousePosition: PROC [screenServerData: REF, x, y: INTEGER, display: REF ¬ NIL, device: REF ¬ NIL] --VWC.SetMousePositionProc-- ~ { pd: PushData ~ primaryPD; IF pd#NIL AND pd.version > 15 THEN ImagerSender.SendInterminalSetting[pd.ish, [MousePosition[[mouseX: MAX[MIN[x, INT16.LAST], INT16.FIRST], mouseY: MAX[MIN[y, 16383], -16383], color: FALSE<>]]]] ELSE device ¬ device}; GetMousePosition: PROC [screenServerData: REF, device: REF ¬ NIL] RETURNS [x, y: INTEGER, display: REF ¬ NIL] --VWC.GetMousePositionProc-- ~ { pd: PushData ~ primaryPD; IF pd#NIL THEN RETURN [pd.mp.mouseX, pd.mp.mouseY, NIL<>]; RETURN [0, 0, NIL]}; SetCutBuffer: PROC [screenServerData: REF, buffer: ATOM, data: ROPE] --VWC.SetCutBufferProc-- ~ { pd: PushData ~ primaryPD; IF pd#NIL AND pd.version>=17 THEN ImagerSender.SendCutBuffer[pd.ish, buffer, data]; RETURN}; GetCutBuffer: ENTRY PROC [screenServerData: REF, buffer: ATOM] RETURNS [data: ROPE] --VWC.GetCutBufferProc-- ~ { ENABLE UNWIND => NULL; pd: PushData ~ primaryPD; IF pd#NIL AND pd.version>=17 THEN { bkey: CARD ~ cutBufferKey ¬ cutBufferKey+1; t1, t2: BasicTime.GMT; ImagerSender.RequestCutBuffer[pd.ish, buffer, cutBufferKey]; t1 ¬ BasicTime.Now[]; DO prev: CutBufferList ¬ NIL; cur: CutBufferList ¬ pd.cutBuffers; WHILE cur#NIL DO IF cur.first.buffer=buffer AND cur.first.key=bkey THEN { IF prev#NIL THEN prev.rest ¬ cur.rest ELSE pd.cutBuffers ¬ cur.rest; RETURN [cur.first.data]}; prev ¬ cur; cur ¬ cur.rest; ENDLOOP; t2 ¬ BasicTime.Now[]; IF BasicTime.Period[from: t1, to: t2] >= cutBufferTimeout THEN RETURN [NIL]; WAIT pd.cutBufferChange; ENDLOOP; }; RETURN [NIL]}; cutBufferKey: CARD ¬ 0; cutBufferTimeout: INT ¬ 10; Blink: PROC [screenServerData: REF, display: REF ¬ NIL, frequency: CARDINAL ¬ 750, duration: CARDINAL ¬ 500] --VWC.BlinkProc-- ~ { BlinkPD: PROC [key, val: REF ANY] RETURNS [quit: BOOL ¬ FALSE] ~ { pd: PushData ~ NARROW[val]; ImagerSender.BlinkBWDisplay[pd.ish]; RETURN}; IF allPD.Pairs[BlinkPD] THEN ERROR; RETURN}; GetDeviceSize: ENTRY PROC [screenServerData: REF, display: REF ¬ NIL] RETURNS [w, h: NAT] --VWC.GetDeviceSizeProc-- ~ { ENABLE UNWIND => NULL; sss: ScreenSettingses ~ screenSettingses; RETURN [sss[0].sizes.first.x, sss[0].sizes.first.y]}; AllocateColorMapIndex: PROC [screenServerData: REF, display: REF ¬ NIL, revokeIndex: VWC.RevokeColorMapIndexProc, clientData: REF ¬ NIL] RETURNS [index: CARDINAL] --VWC.AllocateColorMapIndexProc-- ~ { ERROR ViewersWorld.outOfColormapEntries--I can't allocate any color map indices--}; FreeColorMapIndex: PROC [screenServerData: REF, index: CARDINAL, display: REF ¬ NIL] ~ {RETURN}; SetColorMapEntry: PROC [screenServerData: REF, index: CARDINAL, display: REF ¬ NIL, red, green, blue: INTEGER] ~ {RETURN}; GetColorMapEntry: PROC [screenServerData: REF, index: CARDINAL, display: REF ¬ NIL] RETURNS [red, green, blue: INTEGER] ~ {ERROR}; Push: PROC [data: REF ANY] = { pd: PushData ~ NARROW[data]; IF pd.coding THEN IO.Flush[pd.codingStream]; IF (pd.loggingNet OR pd.loggingPlain) AND (pd.logFlushClock ¬ pd.logFlushClock+1)=logFlushPeriod THEN { IF pd.loggingNet THEN IO.Flush[pd.netLogStream]; IF pd.loggingPlain THEN IO.Flush[pd.plainLogStream]; pd.logFlushClock ¬ 0}; NetworkStream.SendSoon[pd.netOutStream, soon]; RETURN}; logFlushPeriod: NATURAL ¬ 25; soon: CARDINAL--milliseconds-- ¬ 0; ShortFmtTime: PROC [time: BasicTime.GMT] RETURNS [ROPE] ~ { up: BasicTime.Unpacked ~ BasicTime.Unpack[time]; RETURN [IO.PutFLR["%g/%g %g:%02g:%02g", LIST[ [cardinal[up.month.ORD+1]], [cardinal[up.day]], [cardinal[up.hour]], [cardinal[up.minute]], [cardinal[up.second]] ]]]}; DescribeStream: PROC [s: IO.STREAM] RETURNS [ROPE] ~ { IF s=NIL THEN RETURN ["NIL"]; WITH s.streamData SELECT FROM x: NetworkStream.NetworkStreamData => { pf, tc: ATOM; local, remote: ROPE; [pf, local, remote, tc] ¬ NetworkStream.GetStreamInfo[s]; RETURN IO.PutFLR["NetworkStream[rem=%g, loc=%g, pf=%g, tc=%g]", LIST[ [rope[remote]], [rope[local]], [atom[pf]], [atom[tc]] ]]}; ENDCASE => { class: ATOM ~ s.GetInfo[].class; RETURN IO.PutFR["%g[%bB]", [atom[class]], [integer[LOOPHOLE[s]]] ]}}; FmtNSErr: PROC [codes: LIST OF ATOM, msg: ROPE] RETURNS [ROPE] ~ { out: IO.STREAM ~ IO.ROS[]; out.PutRope["NetworkStream.Error["]; FOR codes ¬ codes, codes.rest WHILE codes#NIL DO out.PutF1["$%g, ", [atom[codes.first]]]; ENDLOOP; out.PutF1["\"%q\"]", [rope[msg]]]; RETURN [out.RopeFromROS[]]}; displayPrefix: ROPE ~ "Display"; DisplayToScreen: PROC [display: ATOM] RETURNS [screen: NAT] ~ { SELECT display FROM $Display0, $Main => RETURN [0]; $Display1, $Color => RETURN [1]; ENDCASE => { r: ROPE ~ Atom.GetPName[display]; IF displayPrefix.IsPrefix[r] THEN { nr: ROPE ~ r.Substr[start: displayPrefix.Length[]]; i: INT ~ Convert.IntFromRope[nr !Convert.Error => GOTO No]; IF i IN [0..NAT.LAST] THEN RETURN [i]; EXITS No => screen ¬ 0}; ERROR}}; Start: PUBLIC PROC ~ { bwSize: VECI ~ screenSettingses[0].sizes.first; class: VWC.ViewersWorldClass ~ NEW [VWC.ViewersWorldClassObj ¬ [ setCursorPattern: SetCursorPattern, getCursorPattern: GetCursorPattern, setBigCursorPattern: SetBigCursorPattern, getBigCursorPattern: GetBigCursorPattern, isBigCursorPattern: IsBigCursorPattern, bigCursorsSupported: BigCursorsSupported, setCursorColor: SetCursorColor, getCursorColor: GetCursorColor, setMousePosition: SetMousePosition, getMousePosition: GetMousePosition, setCutBuffer: SetCutBuffer, getCutBuffer: GetCutBuffer, blink: Blink, getDeviceSize: GetDeviceSize, creator: MyCreate, allocateColorMapIndex: AllocateColorMapIndex, freeColorMapIndex: FreeColorMapIndex, setColorMapEntry: SetColorMapEntry, getColorMapEntry: GetColorMapEntry]]; vw ¬ VWC.CreateViewersWorld[class, keyMapping]; ViewersWorld.SetSize[vw, bwSize.x, bwSize.y]; userInput ¬ ViewersWorld.GetInputHandle[vw]; UserInputOps.SetAbsoluteTime[handle: userInput, epochTimeStamp: [1], epochGMT: gmt1]; IF NOT lcAdded THEN {lcAdded ¬ TRUE; lc.AddClient[]}; ViewersWorldInitializations.StartInstallation[vw]; MessageWindow.Append[greeting, TRUE]; [] ¬ Buttons.Create[info: [name: "Exit"], proc: ExitClick, documentation: exitDoc]; [] ¬ Buttons.Create[info: [name: "Disco"], proc: DiscoClick]; [] ¬ Buttons.Create[info: [name: "Repaint"], proc: RepaintClick]; ViewerPrivate.CheckForEmergencySaveAllEdits[]; ViewersWorldInitializations.FinishInstallation[vw]; RETURN}; exitDoc: ROPE ~ "Exit Cedar"; ExitClick: ViewerClasses.ClickProc ~ { Process.SetPriority[Process.priorityClient3]; TRUSTED {Process.Detach[FORK AnimateShutdown[]]}; Process.PauseMsec[shutDownMsec]; Termination.QuitWorld[userMsg: "Cedar terminated because user clicked `Exit'", interceptable: FALSE]; }; shutDownMsec: NAT ¬ 5000; nSteps: NAT ¬ 6; AnimateShutdown: PROC ~ { ctx: Imager.Context ~ MyCreate[NIL, ViewerPrivate.Screen.FIRST]; cx: REAL ~ ViewerSpecs.bwScreenWidth/2.0; cy: REAL ~ ViewerSpecs.bwScreenHeight/2.0; r: REAL ¬ RealFns.SqRt[cx*cx + cy*cy]; ShutItDown: PROC ~ { r ¬ r; FOR i: NAT DECREASING IN [0..nSteps) DO StepPath: ImagerPath.PathProc ~ { Rect: PROC [f: REAL] ~ { rf: REAL ~ r*f; moveTo[[cx-rf, cy-rf]]; lineTo[[cx+rf, cy-rf]]; lineTo[[cx+rf, cy+rf]]; lineTo[[cx-rf, cy+rf]]; lineTo[[cx-rf, cy-rf]]; }; Circ: PROC [f: REAL] ~ { rf: REAL ~ r*f; moveTo[[cx-rf, cy]]; arcTo[[cx+rf, cy], [cx-rf, cy]]; RETURN}; Circ[REAL[i+1]/nSteps]; IF i>0 THEN Circ[REAL[i]/nSteps]; RETURN}; ctx.SetColor[ImagerColor.ColorFromHSV[REAL[i]/nSteps, 1.0, 1.0]]; ctx.MaskFill[StepPath, TRUE]; ENDLOOP; Process.PauseMsec[shutDownMsec]; RETURN}; ImagerBackdoor.ViewReset[ctx]; ViewerLocks.CallUnderViewerTreeLock[ShutItDown]; RETURN}; DiscoClick: ViewerClasses.ClickProc ~ { locState: TL.LocState ~ TL.GetLocState[]; SimpleFeedback.PutFL[$RemoteViewersHost, oneLiner, $FYI, "At %g, disconnecting from %g.", LIST[[time[BasicTime.Now[]]], [rope[TL.FormatLocState[locState]]] ]]; Process.PauseMsec[1000]; TL.SetState[undefinedOnly]; RETURN}; RepaintClick: ViewerClasses.ClickProc ~ { ViewerOps.PaintEverything[]; RETURN}; optionDesc: ROPE ¬ "((+|-)(logInput|logPlain|logNet|code|reject|sendTiming) | -codeMethod )* --- set flg(s)"; optionUsage: ROPE ¬ Rope.Concat["RemoteViewersHostOption ", optionDesc]; OptionCmd: PROC [cmd: Commander.Handle] RETURNS [result: REF ANY ¬ NIL, msg: ROPE ¬ NIL] ~ { argv: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd]; Set: PROC [name: ROPE, sense: BOOL] RETURNS [BOOL] ~ { SELECT TRUE FROM name.Equal["logInput", FALSE] => logInput ¬ sense; name.Equal["logPlain", FALSE] => logPlain ¬ sense; name.Equal["logNet", FALSE] => logNet ¬ sense; name.Equal["code", FALSE] => compressable ¬ sense; name.Equal["reject", FALSE] => reject ¬ sense; name.Equal["sendTiming", FALSE] => sendTiming ¬ sense; ENDCASE => RETURN [TRUE]; RETURN [FALSE]}; i: NAT ¬ 1; IF argv.argc<1 THEN RETURN [$Null, optionUsage]; WHILE i < argv.argc DO SELECT TRUE FROM argv[i].Length = 0 => RETURN [$Failure, optionUsage]; argv[i].Equal["-codeMethod", FALSE] => IF (i ¬ i.SUCC) < argv.argc THEN codingMethod ¬ argv[i] ELSE RETURN [$Failure, optionUsage]; argv[i].Fetch[0] = '+ => IF Set[argv[i].Substr[1], TRUE] THEN RETURN [$Failure, optionUsage]; argv[i].Fetch[0] = '- => IF Set[argv[i].Substr[1], FALSE] THEN RETURN [$Failure, optionUsage]; ENDCASE => RETURN [$Failure, optionUsage]; i ¬ i.SUCC; ENDLOOP; cmd.out.PutFL["RemoteViewersHost options are: logInput=%g, logPlain=%g, logNet=%g, code=%g, codeMethod=%g, reject=%g, sendTiming=%g.\n", LIST[ [boolean[logInput]], [boolean[logPlain]], [boolean[logNet]], [boolean[compressable]], [rope[codingMethod]], [boolean[reject]], [boolean[sendTiming]] ]]; RETURN}; TRUSTED { ProcessSize: NAT = BYTES[PROCESS]; pb: PACKED ARRAY [0..ProcessSize) OF BYTE ¬ LOOPHOLE[Process.GetCurrent[]]; msg: IO.STREAM ¬ IO.ROS[]; FOR i: NAT IN [0..ProcessSize) DO msg.PutF1["%02x", [cardinal[pb[i]]] ] ENDLOOP; IF debug THEN SimpleFeedback.PutF[$RemoteViewersHost, oneLiner, $FYI, "RemoteViewersHost starting in process %g.", [rope[msg.RopeFromROS[]]] ]; Process.InitializeCondition[@change, Process.SecondsToTicks[120]]; Process.EnableAborts[@change]; }; HAT.SetProtocolVersionRangeForSide[Host, "Viewers", [myOldestCompatibleVersion, myVersion]]; Commander.Register["RemoteViewersHostOption", OptionCmd, optionDesc]; Start[]; END.