aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2023-12-14 22:56:33 +0000
committerJoão Távora2023-12-14 23:53:13 +0000
commit9e24cde227a1bf2e1f0c005ca16b2a70e704ff5c (patch)
tree2644a1f92e40333d0c9e67b713c855fdfb1547dd
parentaf1fe69f05d803a6958f9d8a045d1013e2ce785c (diff)
downloademacs-9e24cde227a1bf2e1f0c005ca16b2a70e704ff5c.tar.gz
emacs-9e24cde227a1bf2e1f0c005ca16b2a70e704ff5c.zip
Jsonrpc: add new jsonrpc-autoport-bootstrap helper
This will help Eglot and some other extensions connect to network servers that are started with a call to a local program. * lisp/jsonrpc.el (jsonrpc--process-sentinel): Also delete inferior. (jsonrpc-process-connection): Add -autoport-inferior slot. (initialize-instance jsonrpc-process-connection): Check process-creating function arity. Use jsonrpc-forwarding-buffer (jsonrpc-autoport-bootstrap): New helper. (Version): Bump to 1.0.20.
-rw-r--r--lisp/jsonrpc.el171
1 files changed, 133 insertions, 38 deletions
diff --git a/lisp/jsonrpc.el b/lisp/jsonrpc.el
index dde1c880912..f5db3674366 100644
--- a/lisp/jsonrpc.el
+++ b/lisp/jsonrpc.el
@@ -4,7 +4,7 @@
4 4
5;; Author: João Távora <joaotavora@gmail.com> 5;; Author: João Távora <joaotavora@gmail.com>
6;; Keywords: processes, languages, extensions 6;; Keywords: processes, languages, extensions
7;; Version: 1.0.19 7;; Version: 1.0.20
8;; Package-Requires: ((emacs "25.2")) 8;; Package-Requires: ((emacs "25.2"))
9 9
10;; This is a GNU ELPA :core package. Avoid functionality that is not 10;; This is a GNU ELPA :core package. Avoid functionality that is not
@@ -400,16 +400,20 @@ ignored."
400 :accessor jsonrpc--on-shutdown 400 :accessor jsonrpc--on-shutdown
401 :initform #'ignore 401 :initform #'ignore
402 :initarg :on-shutdown 402 :initarg :on-shutdown
403 :documentation "Function run when the process dies.")) 403 :documentation "Function run when the process dies.")
404 (-autoport-inferior
405 :initform nil
406 :documentation "Used by `jsonrpc-autoport-bootstrap'."))
404 :documentation "A JSONRPC connection over an Emacs process. 407 :documentation "A JSONRPC connection over an Emacs process.
405The following initargs are accepted: 408The following initargs are accepted:
406 409
407:PROCESS (mandatory), a live running Emacs process object or a 410:PROCESS (mandatory), a live running Emacs process object or a
408function of no arguments producing one such object. The process 411function producing one such object. If a function, it is passed
409represents either a pipe connection to locally running process or 412the `jsonrpc-process-connection' object. The process represents
410a stream connection to a network host. The remote endpoint is 413either a pipe connection to locally running process or a stream
411expected to understand JSONRPC messages with basic HTTP-style 414connection to a network host. The remote endpoint is expected to
412enveloping headers such as \"Content-Length:\". 415understand JSONRPC messages with basic HTTP-style enveloping
416headers such as \"Content-Length:\".
413 417
414:ON-SHUTDOWN (optional), a function of one argument, the 418:ON-SHUTDOWN (optional), a function of one argument, the
415connection object, called when the process dies.") 419connection object, called when the process dies.")
@@ -424,37 +428,22 @@ connection object, called when the process dies.")
424 ;; could use a pipe with a process filter instead of 428 ;; could use a pipe with a process filter instead of
425 ;; `after-change-functions'. Alternatively, we need a new initarg 429 ;; `after-change-functions'. Alternatively, we need a new initarg
426 ;; (but maybe not a slot). 430 ;; (but maybe not a slot).
427 (let ((calling-buffer (current-buffer))) 431 (let* ((stderr-buffer-name (format "*%s stderr*" name))
428 (with-current-buffer (get-buffer-create (format "*%s stderr*" name)) 432 (stderr-buffer (jsonrpc--forwarding-buffer stderr-buffer-name "[stderr]" conn))
429 (let ((inhibit-read-only t) 433 (hidden-name (concat " " stderr-buffer-name)))
430 (hidden-name (concat " " (buffer-name)))) 434 ;; If we are correctly coupled to the client, the process now
431 (erase-buffer) 435 ;; created should pick up the `stderr-buffer' just created, which
432 (buffer-disable-undo) 436 ;; we immediately rename
433 (add-hook 437 (setq proc (if (functionp proc)
434 'after-change-functions 438 (if (zerop (cdr (func-arity proc)))
435 (lambda (beg _end _pre-change-len) 439 (funcall proc)
436 (cl-loop initially (goto-char beg) 440 (funcall proc conn))
437 do (forward-line) 441 proc))
438 when (bolp) 442 (with-current-buffer stderr-buffer
439 for line = (buffer-substring 443 (ignore-errors (kill-buffer hidden-name))
440 (line-beginning-position 0) 444 (rename-buffer hidden-name)
441 (line-end-position 0)) 445 (setq buffer-read-only t))
442 do (with-current-buffer (jsonrpc-events-buffer conn) 446 (process-put proc 'jsonrpc-stderr stderr-buffer))
443 (goto-char (point-max))
444 (let ((inhibit-read-only t))
445 (insert (format "[stderr] %s\n" line))))
446 until (eobp)))
447 nil t)
448 ;; If we are correctly coupled to the client, the process
449 ;; now created should pick up the current stderr buffer,
450 ;; which we immediately rename
451 (setq proc (if (functionp proc)
452 (with-current-buffer calling-buffer (funcall proc))
453 proc))
454 (ignore-errors (kill-buffer hidden-name))
455 (rename-buffer hidden-name)
456 (process-put proc 'jsonrpc-stderr (current-buffer))
457 (setq buffer-read-only t))))
458 (setf (jsonrpc--process conn) proc) 447 (setf (jsonrpc--process conn) proc)
459 (set-process-buffer proc (get-buffer-create (format " *%s output*" name))) 448 (set-process-buffer proc (get-buffer-create (format " *%s output*" name)))
460 (set-process-filter proc #'jsonrpc--process-filter) 449 (set-process-filter proc #'jsonrpc--process-filter)
@@ -601,6 +590,7 @@ With optional CLEANUP, kill any associated buffers."
601 (jsonrpc--request-continuations connection)) 590 (jsonrpc--request-continuations connection))
602 (jsonrpc--message "Server exited with status %s" (process-exit-status proc)) 591 (jsonrpc--message "Server exited with status %s" (process-exit-status proc))
603 (delete-process proc) 592 (delete-process proc)
593 (when-let (p (slot-value connection '-autoport-inferior)) (delete-process p))
604 (funcall (jsonrpc--on-shutdown connection) connection))))) 594 (funcall (jsonrpc--on-shutdown connection) connection)))))
605 595
606(cl-defun jsonrpc--process-filter (proc string) 596(cl-defun jsonrpc--process-filter (proc string)
@@ -811,5 +801,110 @@ SUBTYPE tells more about the event."
811 (forward-line 2) 801 (forward-line 2)
812 (point))))))))))))) 802 (point)))))))))))))
813 803
804(defun jsonrpc--forwarding-buffer (name prefix conn)
805 "Helper for `jsonrpc-process-connection' helpers.
806Make a stderr buffer named NAME, forwarding lines prefixed by
807PREFIX to CONN's events buffer."
808 (with-current-buffer (get-buffer-create name)
809 (let ((inhibit-read-only t))
810 (fundamental-mode)
811 (erase-buffer)
812 (buffer-disable-undo)
813 (add-hook
814 'after-change-functions
815 (lambda (beg _end _pre-change-len)
816 (cl-loop initially (goto-char beg)
817 do (forward-line)
818 when (bolp)
819 for line = (buffer-substring
820 (line-beginning-position 0)
821 (line-end-position 0))
822 do (with-current-buffer (jsonrpc-events-buffer conn)
823 (goto-char (point-max))
824 (let ((inhibit-read-only t))
825 (insert (format "%s %s\n" prefix line))))
826 until (eobp)))
827 nil t))
828 (current-buffer)))
829
830
831;;;; More convenience utils
832(cl-defun jsonrpc-autoport-bootstrap (name contact
833 &key connect-args)
834 "Use CONTACT to start network server, then connect to it.
835
836Return function suitable for the :PROCESS initarg of
837`jsonrpc-process-connection' (which see).
838
839CONTACT is a list where all the elements are strings except for
840one, which is usuallky the keyword `:autoport'.
841
842When the returned function is called it will start a program
843using a command based on CONTACT, where `:autoport' is
844substituted by a locally free network port. Thereafter, a
845network is made to this port.
846
847Instead of the keyword `:autoport', a cons cell (:autoport
848FORMAT-FN) is also accepted. In that case FORMAT-FN is passed
849the port number and should return a string used for the
850substitution.
851
852The internal processes and control buffers are named after NAME.
853
854CONNECT-ARGS are passed as additional arguments to
855`open-network-stream'."
856 (lambda (conn)
857 (let* ((port-probe (make-network-process :name "jsonrpc-port-probe-dummy"
858 :server t
859 :host "localhost"
860 :service 0))
861 (port-number (unwind-protect
862 (process-contact port-probe :service)
863 (delete-process port-probe)))
864 (inferior-buffer (jsonrpc--forwarding-buffer
865 (format " *%s inferior output*" name)
866 "[inferior]"
867 conn))
868 (cmd (cl-loop for e in contact
869 if (eq e :autoport) collect (format "%s" port-number)
870 else if (eq (car-safe e) :autoport)
871 collect (funcall (cdr e) port-number)
872 else collect e))
873 inferior np)
874 (unwind-protect
875 (progn
876 (message "[jsonrpc] Attempting to start `%s'"
877 (string-join cmd " "))
878 (setq inferior
879 (make-process
880 :name (format "inferior (%s)" name)
881 :buffer inferior-buffer
882 :noquery t
883 :command cmd))
884 (setq np
885 (cl-loop
886 repeat 10 for i from 0
887 do (accept-process-output nil 0.5)
888 while (process-live-p inferior)
889 do (message
890 "[jsonrpc] %sTrying to connect to localhost:%s (attempt %s)"
891 (if (zerop i) "Started. " "")
892 port-number (1+ i))
893 thereis (ignore-errors
894 (apply #'open-network-stream
895 (format "autostart (%s)" name)
896 nil
897 "localhost" port-number connect-args))))
898 (setf (slot-value conn '-autoport-inferior) inferior)
899 np)
900 (cond ((and (process-live-p np)
901 (process-live-p inferior))
902 (message "[jsonrpc] Done, connected to %s!" port-number))
903 (t
904 (when inferior (delete-process inferior))
905 (when np (delete-process np))
906 (error "[jsonrpc] Could not start and/or connect")))))))
907
908
814(provide 'jsonrpc) 909(provide 'jsonrpc)
815;;; jsonrpc.el ends here 910;;; jsonrpc.el ends here