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