summaryrefslogtreecommitdiff
path: root/lisp/server.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/server.el')
-rw-r--r--lisp/server.el379
1 files changed, 295 insertions, 84 deletions
diff --git a/lisp/server.el b/lisp/server.el
index a6b2742190f..aac3da13e4f 100644
--- a/lisp/server.el
+++ b/lisp/server.el
@@ -106,6 +106,24 @@ Each element is (CLIENTID BUFFERS...) where CLIENTID is a string
that can be given to the server process to identify a client.
When a buffer is marked as \"done\", it is removed from this list.")
+(defvar server-ttys nil
+ "List of current terminal devices used by the server.
+Each element is (CLIENTID TTY) where CLIENTID is a string
+that can be given to the server process to identify a client.
+TTY is the name of the tty device.
+
+When all frames on the device are deleted, the server quits the
+connection to the client, and vice versa.")
+
+(defvar server-frames nil
+ "List of current window-system frames used by the server.
+Each element is (CLIENTID FRAME) where CLIENTID is a string
+that can be given to the server process to identify a client.
+FRAME is the frame that was opened by the client.
+
+When the frame is deleted, the server closes the connection to
+the client, and vice versa.")
+
(defvar server-buffer-clients nil
"List of client ids for clients requesting editing of current buffer.")
(make-variable-buffer-local 'server-buffer-clients)
@@ -168,10 +186,18 @@ are done with it in the server.")
(with-current-buffer "*server*"
(goto-char (point-max))
(insert (current-time-string)
- (if client (format " %s:" client) " ")
+ (if client (format " %s: " client) " ")
string)
(or (bolp) (newline)))))
+(defun server-tty-live-p (tty)
+ "Return non-nil if the tty device named TTY has a live frame."
+ (let (result)
+ (dolist (frame (frame-list) result)
+ (when (and (eq (frame-live-p frame) t)
+ (equal (frame-tty-name frame) tty))
+ (setq result t)))))
+
(defun server-sentinel (proc msg)
(let ((client (assq proc server-clients)))
;; Remove PROC from the list of clients.
@@ -186,9 +212,54 @@ are done with it in the server.")
(or (and server-kill-new-buffers
(not server-existing-buffer))
(server-temp-file-p)))
- (kill-buffer (current-buffer)))))))
+ (kill-buffer (current-buffer)))))
+ (let ((tty (assq (car client) server-ttys)))
+ (when tty
+ (setq server-ttys (delq tty server-ttys))
+ (when (server-tty-live-p (cadr tty))
+ (delete-tty (cadr tty)))))))
(server-log (format "Status changed to %s" (process-status proc)) proc))
+(defun server-handle-delete-tty (tty)
+ "Delete the client connection when the emacsclient terminal device is closed."
+ (dolist (entry server-ttys)
+ (let ((proc (nth 0 entry))
+ (term (nth 1 entry)))
+ (when (equal term tty)
+ (let ((client (assq proc server-clients)))
+ (server-log (format "server-handle-delete-tty, tty %s" tty) (car client))
+ (setq server-ttys (delq entry server-ttys))
+ (delete-process (car client))
+ (when (assq proc server-clients)
+ ;; This seems to be necessary to handle
+ ;; `emacsclient -t -e '(delete-frame)'' correctly.
+ (setq server-clients (delq client server-clients))))))))
+
+(defun server-handle-suspend-tty (tty)
+ "Notify the emacsclient process to suspend itself when its tty device is suspended."
+ (dolist (entry server-ttys)
+ (let ((proc (nth 0 entry))
+ (term (nth 1 entry)))
+ (when (equal term tty)
+ (let ((process (car (assq proc server-clients))))
+ (server-log (format "server-handle-suspend-tty, tty %s" tty) process)
+ (process-send-string process "-suspend \n"))))))
+
+(defun server-handle-delete-frame (frame)
+ "Delete the client connection when the emacsclient frame is deleted."
+ (dolist (entry server-frames)
+ (let ((proc (nth 0 entry))
+ (f (nth 1 entry)))
+ (when (equal frame f)
+ (let ((client (assq proc server-clients)))
+ (server-log (format "server-handle-delete-frame, frame %s" frame) (car client))
+ (setq server-frames (delq entry server-frames))
+ (delete-process (car client))
+ (when (assq proc server-clients)
+ ;; This seems to be necessary to handle
+ ;; `emacsclient -t -e '(delete-frame)'' correctly.
+ (setq server-clients (delq client server-clients))))))))
+
(defun server-select-display (display)
;; If the current frame is on `display' we're all set.
(unless (equal (frame-parameter (selected-frame) 'display) display)
@@ -200,14 +271,14 @@ are done with it in the server.")
;; and select it.
(unless (equal (frame-parameter (selected-frame) 'display) display)
(select-frame
- (make-frame-on-display
- display
+ (make-frame-on-display display)))))
;; This frame is only there in place of an actual "current display"
;; setting, so we want it to be as unobtrusive as possible. That's
;; what the invisibility is for. The minibuffer setting is so that
;; we don't end up displaying a buffer in it (which noone would
;; notice).
- '((visibility . nil) (minibuffer . only)))))))
+ ;; XXX I have found this behaviour to be surprising and annoying. -- Lorentey
+ ;; '((visibility . nil) (minibuffer . only)))))))
(defun server-unquote-arg (arg)
(replace-regexp-in-string
@@ -219,6 +290,19 @@ are done with it in the server.")
(t " ")))
arg t t))
+(defun server-quote-arg (arg)
+ "In NAME, insert a & before each &, each space, each newline, and -.
+Change spaces to underscores, too, so that the return value never
+contains a space."
+ (replace-regexp-in-string
+ "[-&\n ]" (lambda (s)
+ (case (aref s 0)
+ (?& "&&")
+ (?- "&-")
+ (?\n "&n")
+ (?\s "&_")))
+ arg t t))
+
(defun server-ensure-safe-dir (dir)
"Make sure DIR is a directory with no race-condition issues.
Creates the directory if necessary and makes sure:
@@ -256,10 +340,18 @@ Prefix arg means just kill any existing server communications subprocess."
(while server-clients
(let ((buffer (nth 1 (car server-clients))))
(server-buffer-done buffer)))
+ ;; Delete any remaining opened frames of the previous server.
+ (while server-ttys
+ (let ((tty (cadar server-ttys)))
+ (setq server-ttys (cdr server-ttys))
+ (when (server-tty-live-p tty) (delete-tty tty))))
(unless leave-dead
(if server-process
(server-log (message "Restarting server")))
(letf (((default-file-modes) ?\700))
+ (add-to-list 'delete-tty-after-functions 'server-handle-delete-tty)
+ (add-to-list 'suspend-tty-functions 'server-handle-suspend-tty)
+ (add-to-list 'delete-frame-functions 'server-handle-delete-frame)
(setq server-process
(make-network-process
:name "server" :family 'local :server t :noquery t
@@ -291,81 +383,186 @@ PROC is the server process. Format of STRING is \"PATH PATH PATH... \\n\"."
(when prev
(setq string (concat prev string))
(process-put proc 'previous-string nil)))
- ;; If the input is multiple lines,
- ;; process each line individually.
- (while (string-match "\n" string)
- (let ((request (substring string 0 (match-beginning 0)))
- (coding-system (and default-enable-multibyte-characters
- (or file-name-coding-system
- default-file-name-coding-system)))
- client nowait eval
- (files nil)
- (lineno 1)
- (columnno 0))
- ;; Remove this line from STRING.
- (setq string (substring string (match-end 0)))
- (setq client (cons proc nil))
- (while (string-match "[^ ]* " request)
- (let ((arg (substring request (match-beginning 0) (1- (match-end 0)))))
- (setq request (substring request (match-end 0)))
- (cond
- ((equal "-nowait" arg) (setq nowait t))
- ((equal "-eval" arg) (setq eval t))
- ((and (equal "-display" arg) (string-match "\\([^ ]*\\) " request))
- (let ((display (server-unquote-arg (match-string 1 request))))
- (setq request (substring request (match-end 0)))
- (condition-case err
- (server-select-display display)
- (error (process-send-string proc (nth 1 err))
- (setq request "")))))
- ;; ARG is a line number option.
- ((string-match "\\`\\+[0-9]+\\'" arg)
- (setq lineno (string-to-int (substring arg 1))))
- ;; ARG is line number:column option.
- ((string-match "\\`+\\([0-9]+\\):\\([0-9]+\\)\\'" arg)
- (setq lineno (string-to-int (match-string 1 arg))
- columnno (string-to-int (match-string 2 arg))))
- (t
- ;; Undo the quoting that emacsclient does
- ;; for certain special characters.
- (setq arg (server-unquote-arg arg))
- ;; Now decode the file name if necessary.
- (if coding-system
- (setq arg (decode-coding-string arg coding-system)))
- (if eval
- (let ((v (eval (car (read-from-string arg)))))
- (when v
- (with-temp-buffer
- (let ((standard-output (current-buffer)))
- (pp v)
- (process-send-region proc (point-min) (point-max))))))
- ;; ARG is a file name.
- ;; Collapse multiple slashes to single slashes.
- (setq arg (command-line-normalize-file-name arg))
- (push (list arg lineno columnno) files))
- (setq lineno 1)
- (setq columnno 0)))))
- (when files
- (run-hooks 'pre-command-hook)
- (server-visit-files files client nowait)
- (run-hooks 'post-command-hook))
- ;; CLIENT is now a list (CLIENTNUM BUFFERS...)
- (if (null (cdr client))
- ;; This client is empty; get rid of it immediately.
- (progn
- (delete-process proc)
- (server-log "Close empty client" proc))
- ;; We visited some buffer for this client.
- (or nowait (push client server-clients))
- (unless (or isearch-mode (minibufferp))
- (server-switch-buffer (nth 1 client))
- (run-hooks 'server-switch-hook)
- (unless nowait
- (message (substitute-command-keys
- "When done with a buffer, type \\[server-edit]")))))))
- ;; Save for later any partial line that remains.
- (when (> (length string) 0)
- (process-put proc 'previous-string string)))
+ (condition-case err
+ (progn
+ ;; If the input is multiple lines,
+ ;; process each line individually.
+ (while (string-match "\n" string)
+ (let ((request (substring string 0 (match-beginning 0)))
+ (coding-system (and default-enable-multibyte-characters
+ (or file-name-coding-system
+ default-file-name-coding-system)))
+ client nowait newframe display version-checked
+ dontkill ; t if the client should not be killed.
+ registered ; t if the client is already added to server-clients.
+ (files nil)
+ (lineno 1)
+ (columnno 0))
+ ;; Remove this line from STRING.
+ (setq string (substring string (match-end 0)))
+ (setq client (cons proc nil))
+ (while (string-match "[^ ]* " request)
+ (let ((arg (substring request (match-beginning 0) (1- (match-end 0)))))
+ (setq request (substring request (match-end 0)))
+ (cond
+ ;; Check version numbers.
+ ((and (equal "-version" arg) (string-match "\\([0-9.]+\\) " request))
+ (let* ((client-version (match-string 1 request))
+ (truncated-emacs-version (substring emacs-version 0 (length client-version))))
+ (setq request (substring request (match-end 0)))
+ (if (equal client-version truncated-emacs-version)
+ (progn
+ (process-send-string proc "-good-version \n")
+ (setq version-checked t))
+ (error (concat "Version mismatch: Emacs is " truncated-emacs-version ", emacsclient is " client-version)))))
+
+ ((equal "-nowait" arg) (setq nowait t))
+
+ ((and (equal "-display" arg) (string-match "\\([^ ]*\\) " request))
+ (setq display (match-string 1 request)
+ request (substring request (match-end 0))))
+
+ ;; Open a new X frame.
+ ((equal "-window-system" arg)
+ (unless version-checked
+ (error "Protocol error; make sure to use the correct version of emacsclient"))
+ (let ((frame (make-frame-on-display
+ (or display
+ (frame-parameter nil 'display)
+ (getenv "DISPLAY")
+ (error "Please specify display")))))
+ (push (list proc frame) server-frames)
+ (select-frame frame)
+ ;; This makes sure that `emacsclient -w -e '(delete-frame)'' works right.
+ (push client server-clients)
+ (setq registered t
+ newframe t
+ dontkill t)))
+
+ ;; Resume a suspended tty frame.
+ ((equal "-resume" arg)
+ (let ((tty (cadr (assq (car client) server-ttys))))
+ (setq dontkill t)
+ (when tty (resume-tty tty))))
+
+ ;; Suspend the client's frame. (In case we get out of
+ ;; sync, and a C-z sends a SIGTSTP to emacsclient.)
+ ((equal "-suspend" arg)
+ (let ((tty (cadr (assq (car client) server-ttys))))
+ (setq dontkill t)
+ (when tty (suspend-tty tty))))
+
+ ;; Noop; useful for debugging emacsclient.
+ ((and (equal "-ignore" arg) (string-match "\\([^ ]*\\) " request))
+ (setq dontkill t
+ request (substring request (match-end 0))))
+
+ ;; Open a new tty frame at the client. ARG is the name of the pseudo tty.
+ ((and (equal "-tty" arg) (string-match "\\([^ ]*\\) \\([^ ]*\\) " request))
+ (let ((tty (server-unquote-arg (match-string 1 request)))
+ (type (server-unquote-arg (match-string 2 request))))
+ (setq request (substring request (match-end 0)))
+ (unless version-checked
+ (error "Protocol error; make sure to use the correct version of emacsclient"))
+ (let ((frame (make-frame-on-tty tty type)))
+ (push (list (car client) (frame-tty-name frame)) server-ttys)
+ (process-send-string proc (concat "-emacs-pid " (number-to-string (emacs-pid)) "\n"))
+ (select-frame frame)
+ ;; This makes sure that `emacsclient -t -e '(delete-frame)'' works right.
+ (push client server-clients)
+ (setq registered t
+ dontkill t
+ newframe t))))
+
+ ;; ARG is a line number option.
+ ((and (equal "-position" arg) (string-match "\\(\\+[0-9]+\\) " request))
+ (setq request (substring request (match-end 0))
+ lineno (string-to-int (substring (match-string 1 request) 1))))
+
+ ;; ARG is line number:column option.
+ ((and (equal "-position" arg) (string-match "\\+\\([0-9]+\\):\\([0-9]+\\) " request))
+ (setq request (substring request (match-end 0))
+ lineno (string-to-int (match-string 1 request))
+ columnno (string-to-int (match-string 2 request))))
+
+ ;; ARG is a file to load.
+ ((and (equal "-file" arg) (string-match "\\([^ ]+\\) " request))
+ (let ((file (server-unquote-arg (match-string 1 request))))
+ (setq request (substring request (match-end 0)))
+ (if coding-system
+ (setq file (decode-coding-string file coding-system)))
+ (setq file (command-line-normalize-file-name file))
+ (push (list file lineno columnno) files))
+ (setq lineno 1
+ columnno 0))
+
+ ;; ARG is a Lisp expression.
+ ((and (equal "-eval" arg) (string-match "\\([^ ]+\\) " request))
+ (let ((expr (server-unquote-arg (match-string 1 request))))
+ (setq request (substring request (match-end 0)))
+ (if coding-system
+ (setq expr (decode-coding-string expr coding-system)))
+ (let ((v (eval (car (read-from-string expr)))))
+ (when (and (not newframe) v)
+ (with-temp-buffer
+ (let ((standard-output (current-buffer)))
+ (pp v)
+ (process-send-string proc "-print ")
+ (process-send-string
+ proc (server-quote-arg
+ (buffer-substring-no-properties (point-min)
+ (point-max))))
+ (process-send-string proc "\n")))))
+ (setq lineno 1
+ columnno 0)))
+
+ ;; Unknown command.
+ (t (error "Unknown command: %s" arg)))))
+
+ (when files
+ (run-hooks 'pre-command-hook)
+ (server-visit-files files client nowait)
+ (run-hooks 'post-command-hook))
+
+ ;; CLIENT is now a list (CLIENTNUM BUFFERS...)
+
+ ;; Delete the client if necessary.
+ (cond
+ ;; Client requested nowait; return immediately.
+ (nowait
+ (delete-process proc)
+ (server-log "Close nowait client" proc))
+ ;; This client is empty; get rid of it immediately.
+ ((and (not dontkill) (null (cdr client)))
+ (delete-process proc)
+ (server-log "Close empty client" proc))
+ ((not registered)
+ (push client server-clients)))
+
+ ;; We visited some buffer for this client.
+ (cond
+ ((or isearch-mode (minibufferp))
+ nil)
+ ((and newframe (null (cdr client)))
+ (message (substitute-command-keys
+ "When done with this frame, type \\[delete-frame]")))
+ ((not (null (cdr client)))
+ (server-switch-buffer (nth 1 client))
+ (run-hooks 'server-switch-hook)
+ (unless nowait
+ (message (substitute-command-keys
+ "When done with a buffer, type \\[server-edit]")))))))
+
+ ;; Save for later any partial line that remains.
+ (when (> (length string) 0)
+ (process-put proc 'previous-string string)))
+ ;; condition-case
+ (error (ignore-errors
+ (process-send-string
+ proc (concat "-error " (server-quote-arg (error-message-string err))))
+ (setq string "")
+ (server-log (error-message-string err) proc)
+ (delete-process proc)))))
(defun server-goto-line-column (file-line-col)
(goto-line (nth 1 file-line-col))
@@ -439,9 +636,17 @@ FOR-KILLING if non-nil indicates that we are called from `kill-buffer'."
;; If client now has no pending buffers,
;; tell it that it is done, and forget it entirely.
(unless (cdr client)
- (delete-process (car client))
- (server-log "Close" (car client))
- (setq server-clients (delq client server-clients))))
+ (let ((tty (cadr (assq (car client) server-ttys)))
+ (frame (cadr (assq (car client) server-frames))))
+ (cond
+ ;; Be careful, if we delete the process before the
+ ;; tty, then the terminal modes will not be restored
+ ;; correctly.
+ (tty (delete-tty tty))
+ (frame (delete-frame frame))
+ (t (delete-process (car client))
+ (server-log "Close" (car client))
+ (setq server-clients (delq client server-clients)))))))
(setq old-clients (cdr old-clients)))
(if (and (bufferp buffer) (buffer-name buffer))
;; We may or may not kill this buffer;
@@ -508,6 +713,11 @@ specifically for the clients and did not exist before their request for it."
;; using whatever is on disk in that file. -- rms.
(defun server-kill-buffer-query-function ()
(or (not server-buffer-clients)
+ (let ((res t))
+ (dolist (proc server-buffer-clients res)
+ (setq proc (assq proc server-clients))
+ (when (and proc (eq (process-status (car proc)) 'open))
+ (setq res nil))))
(yes-or-no-p (format "Buffer `%s' still has clients; kill it? "
(buffer-name (current-buffer))))))
@@ -569,7 +779,8 @@ Arg NEXT-BUFFER is a suggestion; if it is a live buffer, use it."
;; since we've already effectively done that.
(if (null next-buffer)
(if server-clients
- (server-switch-buffer (nth 1 (car server-clients)) killed-one)
+ (let ((buffer (nth 1 (car server-clients))))
+ (and buffer (server-switch-buffer buffer killed-one)))
(unless (or killed-one (window-dedicated-p (selected-window)))
(switch-to-buffer (other-buffer))
(message "No server buffers remain to edit")))