441 lines
16 KiB
Common Lisp
441 lines
16 KiB
Common Lisp
;;; swank-repl.lisp --- Server side part of the Lisp listener.
|
||
;;
|
||
;; License: public domain
|
||
(in-package swank)
|
||
|
||
(defpackage swank-repl
|
||
(:use cl swank/backend)
|
||
(:export *send-repl-results-function*)
|
||
(:import-from
|
||
swank
|
||
|
||
*default-worker-thread-bindings*
|
||
|
||
*loopback-interface*
|
||
|
||
add-hook
|
||
*connection-closed-hook*
|
||
|
||
eval-region
|
||
with-buffer-syntax
|
||
|
||
connection
|
||
connection.socket-io
|
||
connection.repl-results
|
||
connection.user-input
|
||
connection.user-output
|
||
connection.user-io
|
||
connection.trace-output
|
||
connection.dedicated-output
|
||
connection.env
|
||
|
||
multithreaded-connection
|
||
mconn.active-threads
|
||
mconn.repl-thread
|
||
mconn.auto-flush-thread
|
||
use-threads-p
|
||
|
||
*emacs-connection*
|
||
default-connection
|
||
with-connection
|
||
|
||
send-to-emacs
|
||
*communication-style*
|
||
handle-requests
|
||
wait-for-event
|
||
make-tag
|
||
thread-for-evaluation
|
||
socket-quest
|
||
|
||
authenticate-client
|
||
encode-message
|
||
|
||
auto-flush-loop
|
||
clear-user-input
|
||
|
||
current-thread-id
|
||
cat
|
||
with-struct*
|
||
with-retry-restart
|
||
with-bindings
|
||
|
||
package-string-for-prompt
|
||
find-external-format-or-lose
|
||
|
||
defslimefun
|
||
|
||
;; FIXME: those should be exported from swank-repl only, but how to
|
||
;; do that whithout breaking init files?
|
||
*use-dedicated-output-stream*
|
||
*dedicated-output-stream-port*
|
||
*globally-redirect-io*))
|
||
|
||
(in-package swank-repl)
|
||
|
||
(defvar *use-dedicated-output-stream* nil
|
||
"When T swank will attempt to create a second connection to Emacs
|
||
which is used just to send output.")
|
||
|
||
(defvar *dedicated-output-stream-port* 0
|
||
"Which port we should use for the dedicated output stream.")
|
||
|
||
(defvar *dedicated-output-stream-buffering*
|
||
(if (eq *communication-style* :spawn) t nil)
|
||
"The buffering scheme that should be used for the output stream.
|
||
Valid values are nil, t, :line")
|
||
|
||
(defvar *globally-redirect-io* :started-from-emacs
|
||
"When T globally redirect all standard streams to Emacs.
|
||
When :STARTED-FROM-EMACS redirect when launched by M-x slime")
|
||
|
||
(defun globally-redirect-io-p ()
|
||
(case *globally-redirect-io*
|
||
((t) t)
|
||
(:started-from-emacs swank-loader:*started-from-emacs*)))
|
||
|
||
(defun open-streams (connection properties)
|
||
"Return the 5 streams for IO redirection:
|
||
DEDICATED-OUTPUT INPUT OUTPUT IO REPL-RESULTS"
|
||
(let* ((input-fn
|
||
(lambda ()
|
||
(with-connection (connection)
|
||
(with-simple-restart (abort-read
|
||
"Abort reading input from Emacs.")
|
||
(read-user-input-from-emacs)))))
|
||
(dedicated-output (if *use-dedicated-output-stream*
|
||
(open-dedicated-output-stream
|
||
connection
|
||
(getf properties :coding-system))))
|
||
(in (make-input-stream input-fn))
|
||
(out (or dedicated-output
|
||
(make-output-stream (make-output-function connection))))
|
||
(io (make-two-way-stream in out))
|
||
(repl-results (swank:make-output-stream-for-target connection
|
||
:repl-result)))
|
||
(typecase connection
|
||
(multithreaded-connection
|
||
(setf (mconn.auto-flush-thread connection)
|
||
(make-auto-flush-thread out))))
|
||
(values dedicated-output in out io repl-results)))
|
||
|
||
(defun make-output-function (connection)
|
||
"Create function to send user output to Emacs."
|
||
(lambda (string)
|
||
(with-connection (connection)
|
||
(send-to-emacs `(:write-string ,string)))))
|
||
|
||
(defun open-dedicated-output-stream (connection coding-system)
|
||
"Open a dedicated output connection to the Emacs on SOCKET-IO.
|
||
Return an output stream suitable for writing program output.
|
||
|
||
This is an optimized way for Lisp to deliver output to Emacs."
|
||
(let ((socket (socket-quest *dedicated-output-stream-port* nil))
|
||
(ef (find-external-format-or-lose coding-system)))
|
||
(unwind-protect
|
||
(let ((port (local-port socket)))
|
||
(encode-message `(:open-dedicated-output-stream ,port
|
||
,coding-system)
|
||
(connection.socket-io connection))
|
||
(let ((dedicated (accept-connection
|
||
socket
|
||
:external-format ef
|
||
:buffering *dedicated-output-stream-buffering*
|
||
:timeout 30)))
|
||
(authenticate-client dedicated)
|
||
(close-socket socket)
|
||
(setf socket nil)
|
||
dedicated))
|
||
(when socket
|
||
(close-socket socket)))))
|
||
|
||
(defmethod thread-for-evaluation ((connection multithreaded-connection)
|
||
(id (eql :find-existing)))
|
||
(or (car (mconn.active-threads connection))
|
||
(find-repl-thread connection)))
|
||
|
||
(defmethod thread-for-evaluation ((connection multithreaded-connection)
|
||
(id (eql :repl-thread)))
|
||
(find-repl-thread connection))
|
||
|
||
(defun find-repl-thread (connection)
|
||
(cond ((not (use-threads-p))
|
||
(current-thread))
|
||
(t
|
||
(let ((thread (mconn.repl-thread connection)))
|
||
(cond ((not thread) nil)
|
||
((thread-alive-p thread) thread)
|
||
(t
|
||
(setf (mconn.repl-thread connection)
|
||
(spawn-repl-thread connection "new-repl-thread"))))))))
|
||
|
||
(defun spawn-repl-thread (connection name)
|
||
(spawn (lambda ()
|
||
(with-bindings *default-worker-thread-bindings*
|
||
(repl-loop connection)))
|
||
:name name))
|
||
|
||
(defun repl-loop (connection)
|
||
(handle-requests connection))
|
||
|
||
;;;;; Redirection during requests
|
||
;;;
|
||
;;; We always redirect the standard streams to Emacs while evaluating
|
||
;;; an RPC. This is done with simple dynamic bindings.
|
||
|
||
(defslimefun create-repl (target &key coding-system)
|
||
(assert (eq target nil))
|
||
(let ((conn *emacs-connection*))
|
||
(initialize-streams-for-connection conn `(:coding-system ,coding-system))
|
||
(with-struct* (connection. @ conn)
|
||
(setf (@ env)
|
||
`((*standard-input* . ,(@ user-input))
|
||
,@(unless (globally-redirect-io-p)
|
||
`((*standard-output* . ,(@ user-output))
|
||
(*trace-output* . ,(or (@ trace-output) (@ user-output)))
|
||
(*error-output* . ,(@ user-output))
|
||
(*debug-io* . ,(@ user-io))
|
||
(*query-io* . ,(@ user-io))
|
||
(*terminal-io* . ,(@ user-io))))))
|
||
(maybe-redirect-global-io conn)
|
||
(add-hook *connection-closed-hook* 'update-redirection-after-close)
|
||
(typecase conn
|
||
(multithreaded-connection
|
||
(setf (mconn.repl-thread conn)
|
||
(spawn-repl-thread conn "repl-thread"))))
|
||
(list (package-name *package*)
|
||
(package-string-for-prompt *package*)))))
|
||
|
||
(defun initialize-streams-for-connection (connection properties)
|
||
(multiple-value-bind (dedicated in out io repl-results)
|
||
(open-streams connection properties)
|
||
(setf (connection.dedicated-output connection) dedicated
|
||
(connection.user-io connection) io
|
||
(connection.user-output connection) out
|
||
(connection.user-input connection) in
|
||
(connection.repl-results connection) repl-results)
|
||
connection))
|
||
|
||
(defun read-user-input-from-emacs ()
|
||
(let ((tag (make-tag)))
|
||
(force-output)
|
||
(send-to-emacs `(:read-string ,(current-thread-id) ,tag))
|
||
(let ((ok nil))
|
||
(unwind-protect
|
||
(prog1 (caddr (wait-for-event `(:emacs-return-string ,tag value)))
|
||
(setq ok t))
|
||
(unless ok
|
||
(send-to-emacs `(:read-aborted ,(current-thread-id) ,tag)))))))
|
||
|
||
;;;;; Listener eval
|
||
|
||
(defvar *listener-eval-function* 'repl-eval)
|
||
|
||
(defvar *listener-saved-value* nil)
|
||
|
||
(defslimefun listener-save-value (slimefun &rest args)
|
||
"Apply SLIMEFUN to ARGS and save the value.
|
||
The saved value should be visible to all threads and retrieved via
|
||
LISTENER-GET-VALUE."
|
||
(setq *listener-saved-value* (apply slimefun args))
|
||
t)
|
||
|
||
(defslimefun listener-get-value ()
|
||
"Get the last value saved by LISTENER-SAVE-VALUE.
|
||
The value should be produced as if it were requested through
|
||
LISTENER-EVAL directly, so that spacial variables *, etc are set."
|
||
(listener-eval (let ((*package* (find-package :keyword)))
|
||
(write-to-string '*listener-saved-value*))))
|
||
|
||
(defslimefun listener-eval (string &key (window-width nil window-width-p))
|
||
(if window-width-p
|
||
(let ((*print-right-margin* window-width))
|
||
(funcall *listener-eval-function* string))
|
||
(funcall *listener-eval-function* string)))
|
||
|
||
(defslimefun clear-repl-variables ()
|
||
(let ((variables '(*** ** * /// // / +++ ++ +)))
|
||
(loop for variable in variables
|
||
do (setf (symbol-value variable) nil))))
|
||
|
||
(defvar *send-repl-results-function* 'send-repl-results-to-emacs)
|
||
|
||
(defun repl-eval (string)
|
||
(clear-user-input)
|
||
(with-buffer-syntax ()
|
||
(with-retry-restart (:msg "Retry SLIME REPL evaluation request.")
|
||
(track-package
|
||
(lambda ()
|
||
(multiple-value-bind (values last-form) (eval-region string)
|
||
(setq *** ** ** * * (car values)
|
||
/// // // / / values
|
||
+++ ++ ++ + + last-form)
|
||
(funcall *send-repl-results-function* values))))))
|
||
nil)
|
||
|
||
(defun track-package (fun)
|
||
(let ((p *package*))
|
||
(unwind-protect (funcall fun)
|
||
(unless (eq *package* p)
|
||
(send-to-emacs (list :new-package (package-name *package*)
|
||
(package-string-for-prompt *package*)))))))
|
||
|
||
(defun send-repl-results-to-emacs (values)
|
||
(finish-output)
|
||
(if (null values)
|
||
(send-to-emacs `(:write-string "; No value" :repl-result))
|
||
(dolist (v values)
|
||
(send-to-emacs `(:write-string ,(cat (prin1-to-string v) #\newline)
|
||
:repl-result)))))
|
||
|
||
(defslimefun redirect-trace-output (target)
|
||
(setf (connection.trace-output *emacs-connection*)
|
||
(swank:make-output-stream-for-target *emacs-connection* target))
|
||
nil)
|
||
|
||
|
||
|
||
;;;; IO to Emacs
|
||
;;;
|
||
;;; This code handles redirection of the standard I/O streams
|
||
;;; (`*standard-output*', etc) into Emacs. The `connection' structure
|
||
;;; contains the appropriate streams, so all we have to do is make the
|
||
;;; right bindings.
|
||
|
||
;;;;; Global I/O redirection framework
|
||
;;;
|
||
;;; Optionally, the top-level global bindings of the standard streams
|
||
;;; can be assigned to be redirected to Emacs. When Emacs connects we
|
||
;;; redirect the streams into the connection, and they keep going into
|
||
;;; that connection even if more are established. If the connection
|
||
;;; handling the streams closes then another is chosen, or if there
|
||
;;; are no connections then we revert to the original (real) streams.
|
||
;;;
|
||
;;; It is slightly tricky to assign the global values of standard
|
||
;;; streams because they are often shadowed by dynamic bindings. We
|
||
;;; solve this problem by introducing an extra indirection via synonym
|
||
;;; streams, so that *STANDARD-INPUT* is a synonym stream to
|
||
;;; *CURRENT-STANDARD-INPUT*, etc. We never shadow the "current"
|
||
;;; variables, so they can always be assigned to affect a global
|
||
;;; change.
|
||
|
||
;;;;; Global redirection setup
|
||
|
||
(defvar *saved-global-streams* '()
|
||
"A plist to save and restore redirected stream objects.
|
||
E.g. the value for '*standard-output* holds the stream object
|
||
for *standard-output* before we install our redirection.")
|
||
|
||
(defun setup-stream-indirection (stream-var &optional stream)
|
||
"Setup redirection scaffolding for a global stream variable.
|
||
Supposing (for example) STREAM-VAR is *STANDARD-INPUT*, this macro:
|
||
|
||
1. Saves the value of *STANDARD-INPUT* in `*SAVED-GLOBAL-STREAMS*'.
|
||
|
||
2. Creates *CURRENT-STANDARD-INPUT*, initially with the same value as
|
||
*STANDARD-INPUT*.
|
||
|
||
3. Assigns *STANDARD-INPUT* to a synonym stream pointing to
|
||
*CURRENT-STANDARD-INPUT*.
|
||
|
||
This has the effect of making *CURRENT-STANDARD-INPUT* contain the
|
||
effective global value for *STANDARD-INPUT*. This way we can assign
|
||
the effective global value even when *STANDARD-INPUT* is shadowed by a
|
||
dynamic binding."
|
||
(let ((current-stream-var (prefixed-var '#:current stream-var))
|
||
(stream (or stream (symbol-value stream-var))))
|
||
;; Save the real stream value for the future.
|
||
(setf (getf *saved-global-streams* stream-var) stream)
|
||
;; Define a new variable for the effective stream.
|
||
;; This can be reassigned.
|
||
(proclaim `(special ,current-stream-var))
|
||
(set current-stream-var stream)
|
||
;; Assign the real binding as a synonym for the current one.
|
||
(let ((stream (make-synonym-stream current-stream-var)))
|
||
(set stream-var stream)
|
||
(set-default-initial-binding stream-var `(quote ,stream)))))
|
||
|
||
(defun prefixed-var (prefix variable-symbol)
|
||
"(PREFIXED-VAR \"FOO\" '*BAR*) => SWANK::*FOO-BAR*"
|
||
(let ((basename (subseq (symbol-name variable-symbol) 1)))
|
||
(intern (format nil "*~A-~A" (string prefix) basename) :swank)))
|
||
|
||
(defvar *standard-output-streams*
|
||
'(*standard-output* *error-output* *trace-output*)
|
||
"The symbols naming standard output streams.")
|
||
|
||
(defvar *standard-input-streams*
|
||
'(*standard-input*)
|
||
"The symbols naming standard input streams.")
|
||
|
||
(defvar *standard-io-streams*
|
||
'(*debug-io* *query-io* *terminal-io*)
|
||
"The symbols naming standard io streams.")
|
||
|
||
(defun init-global-stream-redirection ()
|
||
(when (globally-redirect-io-p)
|
||
(cond (*saved-global-streams*
|
||
(warn "Streams already redirected."))
|
||
(t
|
||
(mapc #'setup-stream-indirection
|
||
(append *standard-output-streams*
|
||
*standard-input-streams*
|
||
*standard-io-streams*))))))
|
||
|
||
(defun globally-redirect-io-to-connection (connection)
|
||
"Set the standard I/O streams to redirect to CONNECTION.
|
||
Assigns *CURRENT-<STREAM>* for all standard streams."
|
||
(dolist (o *standard-output-streams*)
|
||
(set (prefixed-var '#:current o)
|
||
(connection.user-output connection)))
|
||
;; FIXME: If we redirect standard input to Emacs then we get the
|
||
;; regular Lisp top-level trying to read from our REPL.
|
||
;;
|
||
;; Perhaps the ideal would be for the real top-level to run in a
|
||
;; thread with local bindings for all the standard streams. Failing
|
||
;; that we probably would like to inhibit it from reading while
|
||
;; Emacs is connected.
|
||
;;
|
||
;; Meanwhile we just leave *standard-input* alone.
|
||
#+NIL
|
||
(dolist (i *standard-input-streams*)
|
||
(set (prefixed-var '#:current i)
|
||
(connection.user-input connection)))
|
||
(dolist (io *standard-io-streams*)
|
||
(set (prefixed-var '#:current io)
|
||
(connection.user-io connection))))
|
||
|
||
(defun revert-global-io-redirection ()
|
||
"Set *CURRENT-<STREAM>* to *REAL-<STREAM>* for all standard streams."
|
||
(dolist (stream-var (append *standard-output-streams*
|
||
*standard-input-streams*
|
||
*standard-io-streams*))
|
||
(set (prefixed-var '#:current stream-var)
|
||
(getf *saved-global-streams* stream-var))))
|
||
|
||
;;;;; Global redirection hooks
|
||
|
||
(defvar *global-stdio-connection* nil
|
||
"The connection to which standard I/O streams are globally redirected.
|
||
NIL if streams are not globally redirected.")
|
||
|
||
(defun maybe-redirect-global-io (connection)
|
||
"Consider globally redirecting to CONNECTION."
|
||
(when (and (globally-redirect-io-p) (null *global-stdio-connection*)
|
||
(connection.user-io connection))
|
||
(unless *saved-global-streams*
|
||
(init-global-stream-redirection))
|
||
(setq *global-stdio-connection* connection)
|
||
(globally-redirect-io-to-connection connection)))
|
||
|
||
(defun update-redirection-after-close (closed-connection)
|
||
"Update redirection after a connection closes."
|
||
(check-type closed-connection connection)
|
||
(when (eq *global-stdio-connection* closed-connection)
|
||
(if (and (default-connection) (globally-redirect-io-p))
|
||
;; Redirect to another connection.
|
||
(globally-redirect-io-to-connection (default-connection))
|
||
;; No more connections, revert to the real streams.
|
||
(progn (revert-global-io-redirection)
|
||
(setq *global-stdio-connection* nil)))))
|
||
|
||
(provide :swank-repl)
|