Unless and until OpenMCL provides alternatives (via window
streams, telnet streams, or some other mechanism) all lisp
processes share a common *TERMINAL-IO*
stream (and
therefore share *DEBUG-IO*
,
*QUERY-IO*
, and other standard and internal
interactive streams.)
It's anticipated that most lisp processes other than the
"Initial" process run mostly in the background. If a background
process writes to the output side of *TERMINAL-IO*
,
that may be a little messy and a little confusing to the user,
but it shouldn't really be catastrophic. All I/O to OpenMCL's
buffered streams goes thru a locking mechanism that prevents the
worst kinds of resource-contention problems.
Although the problems associated with terminal output from multiple processes may be mostly cosmetic, the question of which process receives input from the terminal is likely to be a great deal more important. The stream locking mechanisms can make a confusing situation even worse: competing processes may "steal" terminal input from each other unless locks are held longer than they otherwise need to be, and locks can be held longer than they need to be (as when a process is merely waiting for input to become available on an underlying file descriptor).
Even if background processes rarely need to intentionally read input from the terminal, they may still need to do so in response to errors or other unanticipated situations. There are tradeoffs involved in any solution to this problem. The protocol described below allows background processes which follow it to reliably prompt for and receive terminal input. Background processes which attempt to receive terminal input without following this protocol will likely hang indefinitely while attempting to do so. That's certainly a harsh tradeoff, but since attempts to read terminal input without following this protocol only worked some of the time anyway, it doesn't seem to be an unreasonable one.
In the solution described here (and introduced in OpenMCL 0.9),
the internal stream used to provide terminal input is always
locked by some process (the "owning" process.) The initial
process (the process that typically runs the read-eval-print
loop) owns that stream when it's first created. By using the
macro WITH-TERMINAL-INPUT
, background processes can
temporarily obtain ownership of the terminal and relinquish
ownership to the previous owner when they're done with it.
In OpenMCL, BREAK
, ERROR
,
CERROR
, Y-OR-N-P
,
YES-OR-NO-P
, and
CCL:GET-STRING-FROM-USER
are all defined in terms
of WITH-TERMINAL-INPUT
, as are the :TTY
user-interfaces to STEP
and INSPECT
.
Syntax | with-terminal-input &body forms |
Description |
Requests ownership of the terminal input stream; executes
forms as an implicit PROGN , with
ownership of that stream in effect.
|
Description |
Controls how attempts to obtain ownership of terminal
input are made. When NIL (the default), a message is
printed on *TERMINAL-IO* ; it's expected that
the user will later yield control of the terminal via the
:Y toplevel command. When T, a BREAK condition is
signaled in the owning process; continuing from the break
loop will yield the terminal to the requesting process
(unless the :Y command was already used to do so in the
break loop.)
|
Syntax | (:Y P) |
Description | The :Y command yields control of terminal input to the process whose name or ID matches its argument P. The :Y command can only be used from the initial process; the process denoted by P must have used WITH-TERMINAL-INPUT to request access to the terminal input stream. |
Arguments |
|
? Welcome to OpenMCL Version (Beta: linux) 0.9! ? ? (process-run-function "sleeper" #'(lambda () (sleep 5) (break "broken"))) #<PROCESS sleeper(1) [Enabled] #x3063B33E> ? ;; ;; Process sleeper(1) needs access to terminal input. ;;
This example was run under ILISP; ILISP often gets confused if one tries to enter input and "point" doesn't follow a prompt. Entering a "simple" expression at this point gets it back in synch; that's otherwise not relevant to this example.
() NIL ? (:y 1) ;; ;; process sleeper(1) now controls terminal input ;; > Break in process sleeper(1): broken > While executing: #<Anonymous Function #x3063B276> > Type :GO to continue, :POP to abort. > If continued: Return from BREAK. Type :? for other options. 1 > :b (30C38E30) : 0 "Anonymous Function #x3063B276" 52 (30C38E40) : 1 "Anonymous Function #x304984A6" 376 (30C38E90) : 2 "RUN-PROCESS-INITIAL-FORM" 340 (30C38EE0) : 3 "%RUN-STACK-GROUP-FUNCTION" 768 1 > :pop ;; ;; control of terminal input restored to process Initial(0) ;; ?
If a background process ("A") needs access to the terminal input stream and that stream is owned by another background process ("B"), process "A" announces that fact, then waits until the initial process regains control.
? Welcome to OpenMCL Version (Beta: linux) 0.9! ? ? (process-run-function "sleep-60" #'(lambda () (sleep 60) (break "Huh?"))) #? (process-run-function "sleep-5" #'(lambda () (sleep 5) (break "quicker"))) # ? ;; ;; Process sleep-5(2) needs access to terminal input. ;; () NIL ? (:y 2) ;; ;; process sleep-5(2) now controls terminal input ;; > Break in process sleep-5(2): quicker > While executing: # > Type :GO to continue, :POP to abort. > If continued: Return from BREAK. Type :? for other options. 1 > ;; Process sleep-60(1) will need terminal access when ;; the initial process regains control of it. ;; () NIL 1 > :pop ;; ;; Process sleep-60(1) needs access to terminal input. ;; ;; ;; control of terminal input restored to process Initial(0) ;; ? (:y 1) ;; ;; process sleep-60(1) now controls terminal input ;; > Break in process sleep-60(1): Huh? > While executing: # > Type :GO to continue, :POP to abort. > If continued: Return from BREAK. Type :? for other options. 1 > :pop ;; ;; control of terminal input restored to process Initial(0) ;; ?
This scheme is certainly not bulletproof: imaginative use of PROCESS-INTERRUPT and similar functions might be able to defeat it and deadlock the lisp, and any scenario where several background processes are clamoring for access to the shared terminal input stream at the same time is likely to be confusing and chaotic. (An alternate scheme, where the input focus was magically granted to whatever thread the user was thinking about, was considered and rejected due to technical limitations.)
The longer-term fix would probably involve using network or
window-system streams to give each stream unique instances of
*TERMINAL-IO*
.
Existing code that attempts to read from
*TERMINAL-IO*
from a background process will need
to be changed to use WITH-TERMINAL-INPUT
. Since
that code was probably not working reliably in previous versions
of OpenMCL, this requirement doesn't seem to be too onerous.
Note that WITH-TERMINAL-INPUT
both requests ownership
of the terminal input stream and promises to restore that ownership
to the initial process when it's done with it. An ad hoc use of
READ
or READ-CHAR
doesn't make this promise;
this is the rationale for the restriction on the :Y command.