-- 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.