-- File: TerminalMultiplexImpl.mesa -- Last edited by Levin: March 16, 1983 10:50 am DIRECTORY DebuggerSwap USING [canSwap], DisplayFace USING [ Background, Connect, Disconnect, hasBuffer, pagesForBitmap, SetBackground, SetCursorPattern, TurnOff, TurnOn], File USING [Capability, Create, nullCapability], Heap USING [systemZone], Inline USING [LongDiv], Keys USING [DownUp, KeyBits], MouseFace USING [SetPosition], PilotFileTypes USING [tAnonymousFile], Process USING [Detach, Milliseconds, MsecToTicks, Seconds, SetPriority, Ticks], ProcessOperations USING [Enter, Exit], ProcessPriorities USING [priorityClientHigh, priorityFrameFault, priorityRealTime], Runtime USING [GlobalFrame, Interrupt], RuntimeInternal USING [WorryCallDebugger], Space USING [CopyIn, CopyOut, Handle, nullHandle, VMPageNumber], SpecialSpace USING [MakeCodeResident, MakeGlobalFrameResident, MakeResident, MakeSwappable], System USING [GetGreenwichMeanTime], TerminalMultiplex USING [ InputAction, InputController, SwapAction, Terminal, TerminalDesired, TerminalSwapNotifier], -- EXPORTS only UserTerminal USING [ Background, Coordinate, cursor, CursorArray, screenHeight, screenWidth, SetBackground, SetCursorPattern, keyboard, mouse, State], UserTerminalImpl USING [background, bitmapBase, bitmapSpace, LOCK, saveCursor, state], Volume USING [systemID]; TerminalMultiplexImpl: MONITOR IMPORTS DebuggerSwap, DisplayFace, File, Heap, Inline, MouseFace, Process, ProcessOperations, Runtime, RuntimeInternal, Space, SpecialSpace, System, UserTerminal, Volume EXPORTS TerminalMultiplex SHARES UserTerminalImpl = BEGIN OPEN TerminalMultiplex; -- Gentle reader, -- If kludges give you a sensuous thrill, you will like this module. -- If you are offended by explicit, adult material, do not read further, -- and send a message to your local system wizard indicating -- that you do not want to receive any future listings of a similar character. --*******************************-- --* *-- --* Types and Related Constants *-- --* *-- --*******************************-- TerminalInfo: TYPE = LONG POINTER TO TerminalInfoRecord; TerminalInfoRecord: TYPE = RECORD [ state: TerminalStateRecord ← [], notifierList: Notifier ← NIL, inputController: InputController ← NIL]; TerminalState: TYPE = LONG POINTER TO TerminalStateRecord; TerminalStateRecord: TYPE = RECORD [ backingFile: File.Capability ← File.nullCapability, bitmapBase: LONG POINTER ← NIL, bitmapSpace: Space.Handle ← Space.nullHandle, background: UserTerminal.Background ← white, state: UserTerminal.State ← disconnected, cursor: ARRAY [0..16) OF WORD ← ALL[0], cursorPosition: UserTerminal.Coordinate ← [x: UserTerminal.screenWidth/2, y: UserTerminal.screenHeight/2], mousePosition: UserTerminal.Coordinate ← [x: UserTerminal.screenWidth/2, y: UserTerminal.screenHeight/2]]; VirtualTerminals: TYPE = ARRAY Terminal OF TerminalInfo; KeysRecord: TYPE = RECORD [ type: SELECT OVERLAID * FROM bits => [bit: Keys.KeyBits], words => [word: ARRAY [0..keysWords) OF WORD], ENDCASE]; Notifier: TYPE = LONG POINTER TO NotifierItem; NotifierItem: TYPE = RECORD [ next, prev: Notifier, notifierProc: TerminalSwapNotifier]; keysWords: CARDINAL = SIZE[Keys.KeyBits]; firstStarKeysWord: CARDINAL = 5; --********************-- --* *-- --* Global Variables *-- --* *-- --********************-- vt: VirtualTerminals; currentTerminal: Terminal ← primary; nextTerminal: Terminal; lockDepth: NAT ← 1; -- Note: swaps initially prevented debuggerLockDepth: NAT ← 1; terminalChanging: BOOLEAN ← FALSE; terminalStateChange: CONDITION ← [timeout: 0]; AlreadyRegistered: ERROR = CODE; --*********************-- --* *-- --* Public Procedures *-- --* *-- --*********************-- SelectTerminal: PUBLIC PROC [terminal: TerminalDesired ← swap, lock: BOOLEAN ← FALSE] RETURNS [worked: BOOLEAN] = { RETURN[NotifySwap[requested: terminal, lock: lock, wait: TRUE]]}; PreventSwaps: PUBLIC ENTRY PROC = { WaitUntilTerminalStable[]; lockDepth ← lockDepth + 1}; PermitSwaps: PUBLIC ENTRY PROC = { IF lockDepth > 0 THEN lockDepth ← lockDepth - 1}; PreventDebuggerSwaps: PUBLIC ENTRY PROC = { WaitUntilTerminalStable[]; debuggerLockDepth ← debuggerLockDepth + 1}; PermitDebuggerSwaps: PUBLIC ENTRY PROC = { IF debuggerLockDepth > 0 THEN debuggerLockDepth ← debuggerLockDepth - 1}; CurrentTerminal: PUBLIC ENTRY PROC RETURNS[terminal: Terminal, lockCount: NAT, debuggerLockCount: NAT] = { WaitUntilTerminalStable[]; RETURN[currentTerminal, lockDepth, debuggerLockDepth]}; RegisterInputController: PUBLIC PROC [controller: InputController] = { PreventSwaps[]; IF vt[currentTerminal].inputController ~= NIL THEN ERROR AlreadyRegistered; vt[currentTerminal].inputController ← controller; PermitSwaps[]}; UnregisterInputController: PUBLIC PROC RETURNS [controller: InputController] = { PreventSwaps[]; controller ← vt[currentTerminal].inputController; vt[currentTerminal].inputController ← NIL; PermitSwaps[]}; RegisterNotifier: PUBLIC PROC [proc: TerminalSwapNotifier] = { notifier: Notifier ← Heap.systemZone.NEW[NotifierItem]; head: Notifier; PreventSwaps[]; IF (head ← vt[currentTerminal].notifierList) = NIL THEN notifier↑ ← [next: notifier, prev: notifier, notifierProc: proc] ELSE { notifier↑ ← [next: head, prev: head.prev, notifierProc: proc]; head.prev.next ← notifier; head.prev ← notifier}; vt[currentTerminal].notifierList ← notifier; PermitSwaps[]}; UnregisterNotifier: PUBLIC PROC [proc: TerminalSwapNotifier] = { head, notifier: Notifier; PreventSwaps[]; IF (head ← notifier ← vt[currentTerminal].notifierList) = NIL THEN RETURN; DO IF notifier.notifierProc = proc THEN { notifier.prev.next ← notifier.next; notifier.next.prev ← notifier.prev; IF notifier = head THEN vt[currentTerminal].notifierList ← IF notifier.next = notifier THEN NIL ELSE notifier.next; Heap.systemZone.FREE[@notifier]; EXIT}; IF (notifier ← notifier.next) = head THEN EXIT; ENDLOOP; Heap.systemZone.FREE[@notifier]; PermitSwaps[]}; --*******************-- --* *-- --* Synchronization *-- --* *-- --*******************-- NotifySwap: ENTRY PROCEDURE [requested: TerminalDesired, lock: BOOLEAN, wait: BOOLEAN] RETURNS [worked: BOOLEAN] = { WaitUntilTerminalStable[]; IF lockDepth > 0 THEN RETURN[FALSE]; nextTerminal ← IF requested = swap THEN IF currentTerminal = primary THEN alternate ELSE primary ELSE requested; IF nextTerminal ~= currentTerminal THEN { terminalChanging ← TRUE; BROADCAST terminalStateChange}; IF wait THEN WaitUntilTerminalStable[]; IF lock THEN lockDepth ← lockDepth + 1; RETURN[TRUE]}; WaitUntilTerminalStable: INTERNAL PROCEDURE = INLINE { WHILE terminalChanging DO WAIT terminalStateChange ENDLOOP}; WaitForTerminalChanging: ENTRY PROCEDURE RETURNS [new: Terminal] = INLINE { UNTIL terminalChanging DO WAIT terminalStateChange ENDLOOP; RETURN[nextTerminal]}; NotifyNewTerminal: ENTRY PROCEDURE = INLINE { currentTerminal ← nextTerminal; terminalChanging ← FALSE; BROADCAST terminalStateChange}; --**********************-- --* *-- --* Multiplexor Proper *-- --* *-- --**********************-- Multiplexor: PROCEDURE = BEGIN -- To avoid having to trust InputControllers and SwapNotifiers too much, the -- actual multiplixor runs at priorityClientHigh. Naturally, this doesn't -- guarantee that a misbehaving client foreground process won't lock it out, -- but running at a higher priority would require residency in all the stuff -- we call. DoList: PROC [head: Notifier, action: SwapAction] = { item: Notifier ← head; IF head = NIL THEN RETURN; DO item.notifierProc[action]; IF (item ← IF action = coming THEN item.next ELSE item.prev) = head THEN EXIT; ENDLOOP}; Process.SetPriority[ProcessPriorities.priorityClientHigh]; DO new: TerminalInfo ← vt[WaitForTerminalChanging[]]; old: TerminalInfo ← vt[currentTerminal]; IF old.inputController ~= NIL THEN old.inputController[disable]; DoList[new.notifierList, coming]; FlipTerminal[old: @old.state, new: @new.state]; DoList[old.notifierList, going]; IF new.inputController ~= NIL THEN new.inputController[enable]; NotifyNewTerminal[]; ENDLOOP; END; ut: POINTER TO FRAME [UserTerminalImpl] ← LOOPHOLE[Runtime.GlobalFrame[UserTerminal.SetCursorPattern]]; FlipTerminal: PROCEDURE [old, new: TerminalState] = INLINE BEGIN -- This procedure is a first class crock. Its purpose is to switch display, mouse, -- and cursor information beneath UserTerminalImpl. Basically, it crams -- new values directly into the variables of UserTerminalImpl's global frame (using -- its monitor lock), then calls DisplayFace directly to cause the changes to take -- effect. All this is needed because most of these variables are write-only, and -- because UserTerminalImpl thinks there is only one bitmap in the universe at a time. UNTIL ProcessOperations.Enter[@ut.LOCK] DO NULL ENDLOOP; old↑ ← [old.backingFile, ut.bitmapBase, ut.bitmapSpace, ut.background, ut.state, ut.saveCursor, UserTerminal.cursor↑, UserTerminal.mouse↑]; SELECT old.state FROM on => { DisplayFace.TurnOff[]; IF DisplayFace.hasBuffer THEN Space.CopyOut[old.bitmapSpace, [old.backingFile, 0]] ELSE SpecialSpace.MakeSwappable[old.bitmapSpace]; DisplayFace.Disconnect[]; }; off => { IF DisplayFace.hasBuffer THEN Space.CopyOut[old.bitmapSpace, [old.backingFile, 0]]; DisplayFace.Disconnect[]; }; disconnected => NULL; ENDCASE; [backingFile: , bitmapBase: ut.bitmapBase, bitmapSpace: ut.bitmapSpace, background: ut.background, state: ut.state, cursor: ut.saveCursor, cursorPosition: UserTerminal.cursor↑, mousePosition: ] ← new↑; MouseFace.SetPosition[[new.mousePosition.x, new.mousePosition.y]]; DisplayFace.SetBackground[IF new.background = white THEN white ELSE black]; DisplayFace.SetCursorPattern[@new.cursor]; SELECT new.state FROM on => { DisplayFace.Connect[Space.VMPageNumber[new.bitmapSpace]]; IF DisplayFace.hasBuffer THEN Space.CopyIn[new.bitmapSpace, [new.backingFile, 0]] ELSE SpecialSpace.MakeResident[new.bitmapSpace]; DisplayFace.TurnOn[]; }; off => { DisplayFace.Connect[Space.VMPageNumber[new.bitmapSpace]]; IF DisplayFace.hasBuffer THEN Space.CopyIn[new.bitmapSpace, [new.backingFile, 0]]; }; disconnected => NULL; ENDCASE; ProcessOperations.Exit[@ut.LOCK]; END; --********************-- --* *-- --* Keyboard Watcher *-- --* *-- --********************-- clockProbeInterval: Process.Seconds = 60; wakeUpMS: Process.Milliseconds = 300; wakeUpInterval: Process.Ticks = Process.MsecToTicks[wakeUpMS]; wakeUpsPerClockProbe: CARDINAL = Inline.LongDiv[LONG[clockProbeInterval]*1000, wakeUpMS]; KeyboardWatcher: PROCEDURE = { -- Note: This procedure is forked as a separate process by the main -- body code. It watches for the key combinations that request a terminal -- swap and a call on the debugger. Since this process runs at high -- priority, everything it touches must be pinned. keyboard: LONG POINTER TO Keys.KeyBits = LOOPHOLE[UserTerminal.keyboard]; Pause: ENTRY PROCEDURE = INLINE {WAIT aShortTime}; DebuggerEnabled: ENTRY PROCEDURE RETURNS [BOOLEAN] = INLINE {RETURN[debuggerLockDepth = 0]}; aShortTime: CONDITION ← [timeout: wakeUpInterval]; wakeUps: CARDINAL ← 0; originalCanSwap: BOOL = DebuggerSwap.canSwap; swapKeys, ctrlSwatKeys, remoteDebuggerKeys, oldKeys, newKeys: KeysRecord; swapKeys.bit ← ALL[Keys.DownUp[up]]; swapKeys.bit[Ctrl] ← down; ctrlSwatKeys.word ← swapKeys.word; swapKeys.bit[RightShift] ← swapKeys.bit[LeftShift] ← ctrlSwatKeys.bit[Spare3] ← down; remoteDebuggerKeys ← ctrlSwatKeys; remoteDebuggerKeys.bit[Spare1] ← down; Process.SetPriority[ProcessPriorities.priorityRealTime]; oldKeys.bit ← keyboard↑; DO changed: BOOLEAN ← FALSE; swap: BOOLEAN ← TRUE; goToDebugger, goToRemoteDebugger: BOOLEAN ← DebuggerEnabled[]; newKeys.bit ← keyboard↑; FOR word: CARDINAL IN [0..firstStarKeysWord) DO thisWord: WORD = newKeys.word[word]; IF thisWord ~= oldKeys.word[word] THEN changed ← TRUE; IF swap AND (thisWord ~= swapKeys.word[word]) THEN swap ← FALSE; IF goToDebugger AND (thisWord ~= ctrlSwatKeys.word[word]) THEN goToDebugger ← FALSE; IF goToRemoteDebugger AND (thisWord ~= remoteDebuggerKeys.word[word]) THEN goToRemoteDebugger ← FALSE; ENDLOOP; IF changed THEN { oldKeys ← newKeys; SELECT TRUE FROM swap => [] ← NotifySwap[swap, FALSE, FALSE]; goToDebugger => { -- Note: This means "go to the default (world swap) debugger", which may be -- either local or remote depending on the state of affairs at initialization time. DebuggerSwap.canSwap ← originalCanSwap; Runtime.Interrupt[]; }; goToRemoteDebugger => { DebuggerSwap.canSwap ← FALSE; Runtime.Interrupt[]; }; ENDCASE; }; IF wakeUps = wakeUpsPerClockProbe THEN {[] ← System.GetGreenwichMeanTime[]; wakeUps ← 0} ELSE wakeUps ← wakeUps + 1; Pause[]; ENDLOOP}; WatcherOfLastResort: ENTRY PROCEDURE = { -- Note: This procedure is forked as a separate process by the main -- body code. It watches for the key combinations that request a panic -- call on the debugger. Since this process runs at the highest priority -- everything it touches must be pinned and it can't even call a procedure -- (RuntimeInternal.WorryCallDebugger is a fixed frame). We do this exercise -- in masochism to survive even if others insist on writing code to run at -- priority 6, but forget to pin everything they should. keyboard: LONG POINTER TO Keys.KeyBits = LOOPHOLE[UserTerminal.keyboard]; oneTick: CONDITION ← [timeout: wakeUpInterval]; Process.SetPriority[ProcessPriorities.priorityFrameFault]; -- Look Ma, no procedure calls... DO WAIT oneTick; IF keyboard[Ctrl] = down AND keyboard[LeftShift] = down AND keyboard[Spare3] = down AND debuggerLockDepth = 0 THEN { oldCanSwap: BOOL = DebuggerSwap.canSwap; IF keyboard[Spare1] = down THEN DebuggerSwap.canSwap ← FALSE; RuntimeInternal.WorryCallDebugger["Whew!"L]; DebuggerSwap.canSwap ← oldCanSwap; }; ENDLOOP}; --*************-- --* *-- --* Main Body *-- --* *-- --*************-- Initialize: PROCEDURE = BEGIN current: TerminalState; vt[primary] ← Heap.systemZone.NEW[TerminalInfoRecord ← [state: [cursor: [001600B, 003700B, 007740B, 005640B, 001600B, 003700B, 007740B, 015660B, 011620B, 003700B, 007740B, 015660B, 031630B, 023710B, 003700B, 007740B]]]]; vt[alternate] ← Heap.systemZone.NEW[TerminalInfoRecord ← [state: [cursor: [160016B, 015660B, 002100B, 105242B, 113322B, 060034B, 021610B, 062514B, 120412B, 021610B, 062514B, 120412B, 111222B, 030030B, 046544B, 041204B]]]]; vt[special] ← Heap.systemZone.NEW[TerminalInfoRecord ← [state: [cursor: [001600B, 003700B, 007740B, 005640B, 001600B, 003700B, 007740B, 015660B, 011620B, 003700B, 007740B, 015660B, 031630B, 023710B, 003700B, 007740B]]]]; IF DisplayFace.hasBuffer THEN { -- we must create backing files for saving bitmaps during terminal swaps FOR t: Terminal IN Terminal DO vt[t].state.backingFile ← File.Create[Volume.systemID, DisplayFace.pagesForBitmap, PilotFileTypes.tAnonymousFile]; ENDLOOP}; current ← @vt[currentTerminal].state; Process.Detach[FORK Multiplexor[]]; -- The initial UserTerminal state is assumed to be disconnected. MouseFace.SetPosition[[current.mousePosition.x, current.mousePosition.y]]; [] ← UserTerminal.SetBackground[current.background]; UserTerminal.SetCursorPattern[current.cursor]; UserTerminal.cursor↑ ← current.cursorPosition; -- The only code that needs to be resident is: -- KeyboardWatcher, NotifySwap, WatcherOfLastResort -- however, until we package this thing, it is sufficient -- to pin the whole module (which isn't very big anyway). SpecialSpace.MakeCodeResident[TerminalMultiplexImpl]; SpecialSpace.MakeGlobalFrameResident[TerminalMultiplexImpl]; Process.Detach[FORK WatcherOfLastResort[]]; Process.Detach[FORK KeyboardWatcher[]]; END; Initialize[]; END.