diff options
| author | João Távora | 2023-12-14 22:56:33 +0000 |
|---|---|---|
| committer | João Távora | 2023-12-14 23:53:13 +0000 |
| commit | 9e24cde227a1bf2e1f0c005ca16b2a70e704ff5c (patch) | |
| tree | 2644a1f92e40333d0c9e67b713c855fdfb1547dd | |
| parent | af1fe69f05d803a6958f9d8a045d1013e2ce785c (diff) | |
| download | emacs-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.el | 171 |
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. |
| 405 | The following initargs are accepted: | 408 | The 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 |
| 408 | function of no arguments producing one such object. The process | 411 | function producing one such object. If a function, it is passed |
| 409 | represents either a pipe connection to locally running process or | 412 | the `jsonrpc-process-connection' object. The process represents |
| 410 | a stream connection to a network host. The remote endpoint is | 413 | either a pipe connection to locally running process or a stream |
| 411 | expected to understand JSONRPC messages with basic HTTP-style | 414 | connection to a network host. The remote endpoint is expected to |
| 412 | enveloping headers such as \"Content-Length:\". | 415 | understand JSONRPC messages with basic HTTP-style enveloping |
| 416 | headers 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 |
| 415 | connection object, called when the process dies.") | 419 | connection 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. | ||
| 806 | Make a stderr buffer named NAME, forwarding lines prefixed by | ||
| 807 | PREFIX 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 | |||
| 836 | Return function suitable for the :PROCESS initarg of | ||
| 837 | `jsonrpc-process-connection' (which see). | ||
| 838 | |||
| 839 | CONTACT is a list where all the elements are strings except for | ||
| 840 | one, which is usuallky the keyword `:autoport'. | ||
| 841 | |||
| 842 | When the returned function is called it will start a program | ||
| 843 | using a command based on CONTACT, where `:autoport' is | ||
| 844 | substituted by a locally free network port. Thereafter, a | ||
| 845 | network is made to this port. | ||
| 846 | |||
| 847 | Instead of the keyword `:autoport', a cons cell (:autoport | ||
| 848 | FORMAT-FN) is also accepted. In that case FORMAT-FN is passed | ||
| 849 | the port number and should return a string used for the | ||
| 850 | substitution. | ||
| 851 | |||
| 852 | The internal processes and control buffers are named after NAME. | ||
| 853 | |||
| 854 | CONNECT-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 |