diff options
| author | João Távora | 2018-06-30 19:06:43 +0100 |
|---|---|---|
| committer | João Távora | 2018-06-30 19:46:06 +0100 |
| commit | 8af26410a91c3c9679bb0281ddd71f0dd77ec97c (patch) | |
| tree | 05d2780906fb17ccaaacf953393c15365c7abe05 | |
| parent | 852395bab71cb7032692f3c95e1e4b81a884b66b (diff) | |
| download | emacs-8af26410a91c3c9679bb0281ddd71f0dd77ec97c.tar.gz emacs-8af26410a91c3c9679bb0281ddd71f0dd77ec97c.zip | |
Add lisp/jsonrpc.el
* doc/lispref/text.texi (Text): Add JSONRPC.
(JSONRPC): New node.
* etc/NEWS (New Modes and Packages in Emacs 27.1): Mention jsonrpc.el
* lisp/jsonrpc.el: New file.
* test/lisp/jsonrpc-tests.el: New file.
| -rw-r--r-- | doc/lispref/text.texi | 187 | ||||
| -rw-r--r-- | etc/NEWS | 9 | ||||
| -rw-r--r-- | lisp/jsonrpc.el | 649 | ||||
| -rw-r--r-- | test/lisp/jsonrpc-tests.el | 240 |
4 files changed, 1085 insertions, 0 deletions
diff --git a/doc/lispref/text.texi b/doc/lispref/text.texi index 94cd87acf71..5e8601083e5 100644 --- a/doc/lispref/text.texi +++ b/doc/lispref/text.texi | |||
| @@ -62,6 +62,7 @@ the character after point. | |||
| 62 | * GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS. | 62 | * GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS. |
| 63 | * Parsing HTML/XML:: Parsing HTML and XML. | 63 | * Parsing HTML/XML:: Parsing HTML and XML. |
| 64 | * Parsing JSON:: Parsing and generating JSON values. | 64 | * Parsing JSON:: Parsing and generating JSON values. |
| 65 | * JSONRPC:: JSON Remote Procedure Call protocol | ||
| 65 | * Atomic Changes:: Installing several buffer changes atomically. | 66 | * Atomic Changes:: Installing several buffer changes atomically. |
| 66 | * Change Hooks:: Supplying functions to be run when text is changed. | 67 | * Change Hooks:: Supplying functions to be run when text is changed. |
| 67 | @end menu | 68 | @end menu |
| @@ -5132,6 +5133,192 @@ doesn't move point. The arguments @var{args} are interpreted as in | |||
| 5132 | @code{json-parse-string}. | 5133 | @code{json-parse-string}. |
| 5133 | @end defun | 5134 | @end defun |
| 5134 | 5135 | ||
| 5136 | @node JSONRPC | ||
| 5137 | @section JSONRPC communication | ||
| 5138 | @cindex JSON remote procedure call protocol | ||
| 5139 | |||
| 5140 | The @code{jsonrpc} library implements the @acronym{JSONRPC} | ||
| 5141 | specification, version 2.0, as it is described in | ||
| 5142 | @uref{http://www.jsonrpc.org/}. As the name suggests, JSONRPC is a | ||
| 5143 | generic @code{Remote Procedure Call} protocol designed around | ||
| 5144 | @acronym{JSON} objects, which you can convert to and from Lisp objects | ||
| 5145 | (@pxref{Parsing JSON}). | ||
| 5146 | |||
| 5147 | @node JSONRPC Overview | ||
| 5148 | @subsection Overview | ||
| 5149 | |||
| 5150 | Quoting from the @uref{http://www.jsonrpc.org/, spec}, JSONRPC "is | ||
| 5151 | transport agnostic in that the concepts can be used within the same | ||
| 5152 | process, over sockets, over http, or in many various message passing | ||
| 5153 | environments." | ||
| 5154 | |||
| 5155 | To model this agnosticism, the @code{jsonrpc} library uses objects of | ||
| 5156 | a @code{jsonrpc-connection} class, which represent a connection the | ||
| 5157 | remote JSON endpoint (for details on Emacs's object system, | ||
| 5158 | @pxref{Top,EIEIO,,eieio,EIEIO}). In modern object-oriented parlance, | ||
| 5159 | this class is ``abstract'', i.e. the actual class of a useful | ||
| 5160 | connection object used is always a subclass of it. Nevertheless, we | ||
| 5161 | can define two distinct API's around the @code{jsonrpc-connection} | ||
| 5162 | class: | ||
| 5163 | |||
| 5164 | @enumerate | ||
| 5165 | |||
| 5166 | @item A user interface for building JSONRPC applications | ||
| 5167 | |||
| 5168 | In this scenario, the JSONRPC application instantiates | ||
| 5169 | @code{jsonrpc-connection} objects of one of its concrete subclasses | ||
| 5170 | using @code{make-instance}. To initiate a contact to the remote | ||
| 5171 | endpoint, the JSONRPC application passes this object to the functions | ||
| 5172 | @code{jsonrpc-notify'}, @code{jsonrpc-request} and | ||
| 5173 | @code{jsonrpc-async-request}. For handling remotely initiated | ||
| 5174 | contacts, which generally come in asynchronously, the instantiation | ||
| 5175 | should include @code{:request-dispatcher} and | ||
| 5176 | @code{:notification-dispatcher} initargs, which are both functions of | ||
| 5177 | 3 arguments: the connection object; a symbol naming the JSONRPC method | ||
| 5178 | invoked remotely; and a JSONRPC "params" object. | ||
| 5179 | |||
| 5180 | The function passed as @code{:request-dispatcher} is responsible for | ||
| 5181 | handling the remote endpoint's requests, which expect a reply from the | ||
| 5182 | local endpoint (in this case, the program you're building). Inside | ||
| 5183 | that function, you may either return locally (normally) or non-locally | ||
| 5184 | (error). A local return value must be a Lisp object serializable as | ||
| 5185 | JSON (@pxref{Parsing JSON}). This determines a success response, and | ||
| 5186 | the object is forwarded to the server as the JSONRPC "result" object. | ||
| 5187 | A non-local return, achieved by calling the function | ||
| 5188 | @code{jsonrpc-error}, causes an error response to be sent to the | ||
| 5189 | server. The details of the accompanying JSONRPC "error" are filled | ||
| 5190 | out with whatever was passed to @code{jsonrpc-error}. A non-local | ||
| 5191 | return triggered by an unexpected error of any other type also causes | ||
| 5192 | an error response to be sent (unless you have set | ||
| 5193 | @code{debug-on-error}, in which case this should land you in the | ||
| 5194 | debugger, @pxref{Error Debugging}). | ||
| 5195 | |||
| 5196 | @item A inheritance interface for building JSONRPC transport implementations | ||
| 5197 | |||
| 5198 | In this scenario, @code{jsonrpc-connection} is subclassed to implement | ||
| 5199 | a different underlying transport strategy (for details on how to | ||
| 5200 | subclass, @pxref{Inheritance,Inheritance,,eieio}). Users of the | ||
| 5201 | application-building interface can then instantiate objects of this | ||
| 5202 | concrete class (using the @code{make-instance} function) and connect | ||
| 5203 | to JSONRPC endpoints using that strategy. | ||
| 5204 | |||
| 5205 | This API has mandatory and optional parts. | ||
| 5206 | |||
| 5207 | To allow its users to initiate JSONRPC contacts (notifications or | ||
| 5208 | requests) or reply to endpoint requests, the method | ||
| 5209 | @code{jsonrpc-connection-send} must be implemented for the subclass. | ||
| 5210 | |||
| 5211 | Likewise, for handling the three types of remote contacts (requests, | ||
| 5212 | notifications and responses to local requests) the transport | ||
| 5213 | implementation must arrange for the function | ||
| 5214 | @code{jsonrpc-connection-receive} to be called after noticing a new | ||
| 5215 | JSONRPC message on the wire (whatever that "wire" may be). | ||
| 5216 | |||
| 5217 | Finally, and optionally, the @code{jsonrpc-connection} subclass should | ||
| 5218 | implement @code{jsonrpc-shutdown} and @code{jsonrpc-running-p} if | ||
| 5219 | these concepts apply to the transport. If they do, then any system | ||
| 5220 | resources (e.g. processes, timers, etc..) used listen for messages on | ||
| 5221 | the wire should be released in @code{jsonrpc-shutdown}, i.e. they | ||
| 5222 | should only be needed while @code{jsonrpc-running-p} is non-nil. | ||
| 5223 | |||
| 5224 | @end enumerate | ||
| 5225 | |||
| 5226 | @node Process-based JSONRPC connections | ||
| 5227 | @subsection Process-based JSONRPC connections | ||
| 5228 | |||
| 5229 | For convenience, the @code{jsonrpc} library comes built-in with a | ||
| 5230 | @code{jsonrpc-process-connection} transport implementation that can | ||
| 5231 | talk to local subprocesses (using the standard input and standard | ||
| 5232 | output); or TCP hosts (using sockets); or any other remote endpoint | ||
| 5233 | that Emacs's process object can represent (@pxref{Processes}). | ||
| 5234 | |||
| 5235 | Using this transport, the JSONRPC messages are encoded on the wire as | ||
| 5236 | plain text and prefaced by some basic HTTP-style enveloping headers, | ||
| 5237 | such as ``Content-Length''. | ||
| 5238 | |||
| 5239 | For an example of an application using this transport scheme on top of | ||
| 5240 | JSONRPC, see the | ||
| 5241 | @uref{https://microsoft.github.io/language-server-protocol/specification, | ||
| 5242 | Language Server Protocol}. | ||
| 5243 | |||
| 5244 | Along with the mandatory @code{:request-dispatcher} and | ||
| 5245 | @code{:notification-dispatcher} initargs, users of the | ||
| 5246 | @code{jsonrpc-process-connection} class should pass the following | ||
| 5247 | initargs as keyword-value pairs to @code{make-instance}: | ||
| 5248 | |||
| 5249 | @table @code | ||
| 5250 | @item :process | ||
| 5251 | Value must be a live process object or a function of no arguments | ||
| 5252 | producing one such object. If passed a process object, that is | ||
| 5253 | expected to contain an pre-established connection; otherwise, the | ||
| 5254 | function is called immediately after the object is made. | ||
| 5255 | |||
| 5256 | @item :on-shutdown | ||
| 5257 | Value must be a function of a single argument, the | ||
| 5258 | @code{jsonrpc-process-connection} object. The function is called | ||
| 5259 | after the underlying process object has been deleted (either | ||
| 5260 | deliberately by @code{jsonrpc-shutdown} or unexpectedly, because of | ||
| 5261 | some external cause). | ||
| 5262 | @end table | ||
| 5263 | |||
| 5264 | @node JSONRPC JSON object format | ||
| 5265 | @subsection JSON object format | ||
| 5266 | |||
| 5267 | JSON objects are exchanged as Lisp plists (@pxref{Parsing JSON}): | ||
| 5268 | JSON-compatible plists are handed to the dispatcher functions and, | ||
| 5269 | likewise, JSON-compatible plists should be given to | ||
| 5270 | @code{jsonrpc-notify}, @code{jsonrpc-request} and | ||
| 5271 | @code{jsonrpc-async-request}. | ||
| 5272 | |||
| 5273 | To facilitate handling plists, this library make liberal use of | ||
| 5274 | @code{cl-lib} library and suggests (but doesn't force) its clients to | ||
| 5275 | do the same. A macro @code{jsonrpc-lambda} can be used to create a | ||
| 5276 | lambda for destructuring a JSON-object like in this example: | ||
| 5277 | |||
| 5278 | @example | ||
| 5279 | (jsonrpc-async-request | ||
| 5280 | myproc :frobnicate `(:foo "trix") | ||
| 5281 | :success-fn (jsonrpc-lambda (&key bar baz &allow-other-keys) | ||
| 5282 | (message "Server replied back with %s and %s!" | ||
| 5283 | bar baz)) | ||
| 5284 | :error-fn (jsonrpc-lambda (&key code message _data) | ||
| 5285 | (message "Sadly, server reports %s: %s" | ||
| 5286 | code message))) | ||
| 5287 | @end example | ||
| 5288 | |||
| 5289 | @node JSONRPC deferred requests | ||
| 5290 | @subsection Deferred requests | ||
| 5291 | |||
| 5292 | In many @acronym{RPC} situations, synchronization between the two | ||
| 5293 | communicating endpoints is a matter of correctly designing the RPC | ||
| 5294 | application: when synchronization is needed, requests (which are | ||
| 5295 | blocking) should be used; when it isn't, notifications should suffice. | ||
| 5296 | However, when Emacs acts as one of these endpoints, asynchronous | ||
| 5297 | events (e.g. timer- or process-related) may be triggered while there | ||
| 5298 | is still uncertainty about the state of the remote endpoint. | ||
| 5299 | Furthermore, acting on these events may only sometimes demand | ||
| 5300 | synchronization, depending on the event's specific nature. | ||
| 5301 | |||
| 5302 | The @code{:deferred} keyword argument to @code{jsonrpc-request} and | ||
| 5303 | @code{jsonrpc-async-request} is designed to let the caller indicate | ||
| 5304 | that the specific request needs synchronization and its actual | ||
| 5305 | issuance may be delayed to the future, until some condition is | ||
| 5306 | satisfied. Specifying @code{:deferred} for a request doesn't mean it | ||
| 5307 | @emph{will} be delayed, only that it @emph{can} be. If the request | ||
| 5308 | isn't sent immediately, @code{jsonrpc} will make renewed efforts to | ||
| 5309 | send it at certain key times during communication, such as when | ||
| 5310 | receiving or sending other messages to the endpoint. | ||
| 5311 | |||
| 5312 | Before any attempt to send the request, the application-specific | ||
| 5313 | conditions are checked. Since the @code{jsonrpc} library can't known | ||
| 5314 | what these conditions are, the programmer may use the | ||
| 5315 | @code{jsonrpc-connection-ready-p} generic function (@pxref{Generic | ||
| 5316 | Functions}) to specify them. The default method for this function | ||
| 5317 | returns @code{t}, but you can add overriding methods that return | ||
| 5318 | @code{nil} in some situations, based on the arguments passed to it, | ||
| 5319 | which are the @code{jsonrpc-connection} object (@pxref{JSONRPC | ||
| 5320 | Overview}) and whichever value you passed as the @code{:deferred} | ||
| 5321 | keyword argument. | ||
| 5135 | 5322 | ||
| 5136 | @node Atomic Changes | 5323 | @node Atomic Changes |
| 5137 | @section Atomic Change Groups | 5324 | @section Atomic Change Groups |
| @@ -579,6 +579,15 @@ This feature uses Tramp and works only on systems which support GVFS, | |||
| 579 | i.e. GNU/Linux, roughly spoken. See the chapter "(tramp) Archive file | 579 | i.e. GNU/Linux, roughly spoken. See the chapter "(tramp) Archive file |
| 580 | names" in the Tramp manual for full documentation of these facilities. | 580 | names" in the Tramp manual for full documentation of these facilities. |
| 581 | 581 | ||
| 582 | +++ | ||
| 583 | ** New library for writing JSONRPC applications (https://jsonrpc.org) | ||
| 584 | The 'jsonrpc' library enables writing Emacs Lisp applications that | ||
| 585 | rely on this protocol. Since the protocol is designed to be | ||
| 586 | transport-agnostic, the library provides an API to implement new | ||
| 587 | transport strategies as well as a separate API to use them. A | ||
| 588 | transport implementation for process-based communication, such as is | ||
| 589 | used by the Language Server Protocol (LSP), is readily available. | ||
| 590 | |||
| 582 | 591 | ||
| 583 | * Incompatible Lisp Changes in Emacs 27.1 | 592 | * Incompatible Lisp Changes in Emacs 27.1 |
| 584 | 593 | ||
diff --git a/lisp/jsonrpc.el b/lisp/jsonrpc.el new file mode 100644 index 00000000000..8cc853ed5e3 --- /dev/null +++ b/lisp/jsonrpc.el | |||
| @@ -0,0 +1,649 @@ | |||
| 1 | ;;; jsonrpc.el --- JSON-RPC library -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: João Távora <joaotavora@gmail.com> | ||
| 6 | ;; Maintainer: João Távora <joaotavora@gmail.com> | ||
| 7 | ;; Keywords: processes, languages, extensions | ||
| 8 | |||
| 9 | ;; This program is free software; you can redistribute it and/or modify | ||
| 10 | ;; it under the terms of the GNU General Public License as published by | ||
| 11 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 12 | ;; (at your option) any later version. | ||
| 13 | |||
| 14 | ;; This program is distributed in the hope that it will be useful, | ||
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 17 | ;; GNU General Public License for more details. | ||
| 18 | |||
| 19 | ;; You should have received a copy of the GNU General Public License | ||
| 20 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
| 21 | |||
| 22 | ;;; Commentary: | ||
| 23 | |||
| 24 | ;; This library implements the JSONRPC 2.0 specification as described | ||
| 25 | ;; in http://www.jsonrpc.org/. As the name suggests, JSONRPC is a | ||
| 26 | ;; generic Remote Procedure Call protocol designed around JSON | ||
| 27 | ;; objects. To learn how to write JSONRPC programs with this library, | ||
| 28 | ;; see Info node `(elisp)JSONRPC'." | ||
| 29 | ;; | ||
| 30 | ;; This library was originally extracted from eglot.el, an Emacs LSP | ||
| 31 | ;; client, which you should see for an example usage. | ||
| 32 | ;; | ||
| 33 | ;;; Code: | ||
| 34 | |||
| 35 | (require 'cl-lib) | ||
| 36 | (require 'json) | ||
| 37 | (require 'eieio) | ||
| 38 | (require 'subr-x) | ||
| 39 | (require 'warnings) | ||
| 40 | (require 'pcase) | ||
| 41 | (require 'ert) ; to escape a `condition-case-unless-debug' | ||
| 42 | (require 'array) ; xor | ||
| 43 | |||
| 44 | |||
| 45 | ;;; Public API | ||
| 46 | ;;; | ||
| 47 | ;;;###autoload | ||
| 48 | (defclass jsonrpc-connection () | ||
| 49 | ((name | ||
| 50 | :accessor jsonrpc-name | ||
| 51 | :initarg :name | ||
| 52 | :documentation "A name for the connection") | ||
| 53 | (-request-dispatcher | ||
| 54 | :accessor jsonrpc--request-dispatcher | ||
| 55 | :initform #'ignore | ||
| 56 | :initarg :request-dispatcher | ||
| 57 | :documentation "Dispatcher for remotely invoked requests.") | ||
| 58 | (-notification-dispatcher | ||
| 59 | :accessor jsonrpc--notification-dispatcher | ||
| 60 | :initform #'ignore | ||
| 61 | :initarg :notification-dispatcher | ||
| 62 | :documentation "Dispatcher for remotely invoked notifications.") | ||
| 63 | (last-error | ||
| 64 | :accessor jsonrpc-last-error | ||
| 65 | :documentation "Last JSONRPC error message received from endpoint.") | ||
| 66 | (-request-continuations | ||
| 67 | :initform (make-hash-table) | ||
| 68 | :accessor jsonrpc--request-continuations | ||
| 69 | :documentation "A hash table of request ID to continuation lambdas.") | ||
| 70 | (-events-buffer | ||
| 71 | :accessor jsonrpc--events-buffer | ||
| 72 | :documentation "A buffer pretty-printing the JSON-RPC RPC events") | ||
| 73 | (-deferred-actions | ||
| 74 | :initform (make-hash-table :test #'equal) | ||
| 75 | :accessor jsonrpc--deferred-actions | ||
| 76 | :documentation "Map (DEFERRED BUF) to (FN TIMER ID). FN is\ | ||
| 77 | a saved DEFERRED `async-request' from BUF, to be sent not later\ | ||
| 78 | than TIMER as ID.") | ||
| 79 | (-next-request-id | ||
| 80 | :initform 0 | ||
| 81 | :accessor jsonrpc--next-request-id | ||
| 82 | :documentation "Next number used for a request")) | ||
| 83 | :documentation "Base class representing a JSONRPC connection. | ||
| 84 | The following initargs are accepted: | ||
| 85 | |||
| 86 | :NAME (mandatory), a string naming the connection | ||
| 87 | |||
| 88 | :REQUEST-DISPATCHER (optional), a function of three | ||
| 89 | arguments (CONN METHOD PARAMS) for handling JSONRPC requests. | ||
| 90 | CONN is a `jsonrpc-connection' object, method is a symbol, and | ||
| 91 | PARAMS is a plist representing a JSON object. The function is | ||
| 92 | expected to return a JSONRPC result, a plist of (:result | ||
| 93 | RESULT) or signal an error of type `jsonrpc-error'. | ||
| 94 | |||
| 95 | :NOTIFICATION-DISPATCHER (optional), a function of three | ||
| 96 | arguments (CONN METHOD PARAMS) for handling JSONRPC | ||
| 97 | notifications. CONN, METHOD and PARAMS are the same as in | ||
| 98 | :REQUEST-DISPATCHER.") | ||
| 99 | |||
| 100 | ;;; API mandatory | ||
| 101 | (cl-defgeneric jsonrpc-connection-send (conn &key id method params result error) | ||
| 102 | "Send a JSONRPC message to connection CONN. | ||
| 103 | ID, METHOD, PARAMS, RESULT and ERROR. ") | ||
| 104 | |||
| 105 | ;;; API optional | ||
| 106 | (cl-defgeneric jsonrpc-shutdown (conn) | ||
| 107 | "Shutdown the JSONRPC connection CONN.") | ||
| 108 | |||
| 109 | ;;; API optional | ||
| 110 | (cl-defgeneric jsonrpc-running-p (conn) | ||
| 111 | "Tell if the JSONRPC connection CONN is still running.") | ||
| 112 | |||
| 113 | ;;; API optional | ||
| 114 | (cl-defgeneric jsonrpc-connection-ready-p (connection what) | ||
| 115 | "Tell if CONNECTION is ready for WHAT in current buffer. | ||
| 116 | If it isn't, a request which was passed a value to the | ||
| 117 | `:deferred' keyword argument will be deferred to the future. | ||
| 118 | WHAT is whatever was passed the as the value to that argument. | ||
| 119 | |||
| 120 | By default, all connections are ready for sending all requests | ||
| 121 | immediately." | ||
| 122 | (:method (_s _what) ;; by default all connections are ready | ||
| 123 | t)) | ||
| 124 | |||
| 125 | |||
| 126 | ;;; Convenience | ||
| 127 | ;;; | ||
| 128 | (cl-defmacro jsonrpc-lambda (cl-lambda-list &body body) | ||
| 129 | (declare (indent 1) (debug (sexp &rest form))) | ||
| 130 | (let ((e (gensym "jsonrpc-lambda-elem"))) | ||
| 131 | `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) | ||
| 132 | |||
| 133 | (defun jsonrpc-events-buffer (connection) | ||
| 134 | "Get or create JSONRPC events buffer for CONNECTION." | ||
| 135 | (let* ((probe (jsonrpc--events-buffer connection)) | ||
| 136 | (buffer (or (and (buffer-live-p probe) | ||
| 137 | probe) | ||
| 138 | (let ((buffer (get-buffer-create | ||
| 139 | (format "*%s events*" | ||
| 140 | (jsonrpc-name connection))))) | ||
| 141 | (with-current-buffer buffer | ||
| 142 | (buffer-disable-undo) | ||
| 143 | (read-only-mode t) | ||
| 144 | (setf (jsonrpc--events-buffer connection) buffer)) | ||
| 145 | buffer)))) | ||
| 146 | buffer)) | ||
| 147 | |||
| 148 | (defun jsonrpc-forget-pending-continuations (connection) | ||
| 149 | "Stop waiting for responses from the current JSONRPC CONNECTION." | ||
| 150 | (clrhash (jsonrpc--request-continuations connection))) | ||
| 151 | |||
| 152 | (defun jsonrpc-connection-receive (connection message) | ||
| 153 | "Process MESSAGE just received from CONNECTION. | ||
| 154 | This function will destructure MESSAGE and call the appropriate | ||
| 155 | dispatcher in CONNECTION." | ||
| 156 | (cl-destructuring-bind (&key method id error params result _jsonrpc) | ||
| 157 | message | ||
| 158 | (let (continuations) | ||
| 159 | (jsonrpc--log-event connection message 'server) | ||
| 160 | (setf (jsonrpc-last-error connection) error) | ||
| 161 | (cond | ||
| 162 | (;; A remote request | ||
| 163 | (and method id) | ||
| 164 | (let* ((debug-on-error (and debug-on-error (not (ert-running-test)))) | ||
| 165 | (reply | ||
| 166 | (condition-case-unless-debug _ignore | ||
| 167 | (condition-case oops | ||
| 168 | `(:result ,(funcall (jsonrpc--request-dispatcher connection) | ||
| 169 | connection (intern method) params)) | ||
| 170 | (jsonrpc-error | ||
| 171 | `(:error | ||
| 172 | (:code | ||
| 173 | ,(or (alist-get 'jsonrpc-error-code (cdr oops)) -32603) | ||
| 174 | :message ,(or (alist-get 'jsonrpc-error-message | ||
| 175 | (cdr oops)) | ||
| 176 | "Internal error"))))) | ||
| 177 | (error | ||
| 178 | `(:error (:code -32603 :message "Internal error")))))) | ||
| 179 | (apply #'jsonrpc--reply connection id reply))) | ||
| 180 | (;; A remote notification | ||
| 181 | method | ||
| 182 | (funcall (jsonrpc--notification-dispatcher connection) | ||
| 183 | connection (intern method) params)) | ||
| 184 | (;; A remote response | ||
| 185 | (setq continuations | ||
| 186 | (and id (gethash id (jsonrpc--request-continuations connection)))) | ||
| 187 | (let ((timer (nth 2 continuations))) | ||
| 188 | (when timer (cancel-timer timer))) | ||
| 189 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 190 | (if error (funcall (nth 1 continuations) error) | ||
| 191 | (funcall (nth 0 continuations) result))) | ||
| 192 | (;; An abnormal situation | ||
| 193 | id (jsonrpc--warn "No continuation for id %s" id))) | ||
| 194 | (jsonrpc--call-deferred connection)))) | ||
| 195 | |||
| 196 | |||
| 197 | ;;; Contacting the remote endpoint | ||
| 198 | ;;; | ||
| 199 | (defun jsonrpc-error (&rest args) | ||
| 200 | "Error out with FORMAT and ARGS. | ||
| 201 | If invoked inside a dispatcher function, this function is suitable | ||
| 202 | for replying to the remote endpoint with an error message. | ||
| 203 | |||
| 204 | ARGS can be of the form (FORMAT-STRING . MOREARGS) for replying | ||
| 205 | with a -32603 error code and a message formed by formatting | ||
| 206 | FORMAT-STRING with MOREARGS. | ||
| 207 | |||
| 208 | Alternatively ARGS can be plist representing a JSONRPC error | ||
| 209 | object, using the keywords `:code', `:message' and `:data'." | ||
| 210 | (if (stringp (car args)) | ||
| 211 | (let ((msg | ||
| 212 | (apply #'format-message (car args) (cdr args)))) | ||
| 213 | (signal 'jsonrpc-error | ||
| 214 | `(,msg | ||
| 215 | (jsonrpc-error-code . ,32603) | ||
| 216 | (jsonrpc-error-message . ,msg)))) | ||
| 217 | (cl-destructuring-bind (&key code message data) args | ||
| 218 | (signal 'jsonrpc-error | ||
| 219 | `(,(format "[jsonrpc] error ") | ||
| 220 | (jsonrpc-error-code . ,code) | ||
| 221 | (jsonrpc-error-message . ,message) | ||
| 222 | (jsonrpc-error-data . ,data)))))) | ||
| 223 | |||
| 224 | (cl-defun jsonrpc-async-request (connection | ||
| 225 | method | ||
| 226 | params | ||
| 227 | &rest args | ||
| 228 | &key _success-fn _error-fn | ||
| 229 | _timeout-fn | ||
| 230 | _timeout _deferred) | ||
| 231 | "Make a request to CONNECTION, expecting a reply, return immediately. | ||
| 232 | The JSONRPC request is formed by METHOD, a symbol, and PARAMS a | ||
| 233 | JSON object. | ||
| 234 | |||
| 235 | The caller can expect SUCCESS-FN or ERROR-FN to be called with a | ||
| 236 | JSONRPC `:result' or `:error' object, respectively. If this | ||
| 237 | doesn't happen after TIMEOUT seconds (defaults to | ||
| 238 | `jsonrpc-request-timeout'), the caller can expect TIMEOUT-FN to be | ||
| 239 | called with no arguments. The default values of SUCCESS-FN, | ||
| 240 | ERROR-FN and TIMEOUT-FN simply log the events into | ||
| 241 | `jsonrpc-events-buffer'. | ||
| 242 | |||
| 243 | If DEFERRED is non-nil, maybe defer the request to a future time | ||
| 244 | when the server is thought to be ready according to | ||
| 245 | `jsonrpc-connection-ready-p' (which see). The request might | ||
| 246 | never be sent at all, in case it is overridden in the meantime by | ||
| 247 | a new request with identical DEFERRED and for the same buffer. | ||
| 248 | However, in that situation, the original timeout is kept. | ||
| 249 | |||
| 250 | Returns nil." | ||
| 251 | (apply #'jsonrpc--async-request-1 connection method params args) | ||
| 252 | nil) | ||
| 253 | |||
| 254 | (cl-defun jsonrpc-request (connection method params &key deferred timeout) | ||
| 255 | "Make a request to CONNECTION, wait for a reply. | ||
| 256 | Like `jsonrpc-async-request' for CONNECTION, METHOD and PARAMS, | ||
| 257 | but synchronous, i.e. this function doesn't exit until anything | ||
| 258 | interesting (success, error or timeout) happens. Furthermore, it | ||
| 259 | only exits locally (returning the JSONRPC result object) if the | ||
| 260 | request is successful, otherwise exit non-locally with an error | ||
| 261 | of type `jsonrpc-error'. | ||
| 262 | |||
| 263 | DEFERRED is passed to `jsonrpc-async-request', which see." | ||
| 264 | (let* ((tag (cl-gensym "jsonrpc-request-catch-tag")) id-and-timer | ||
| 265 | (retval | ||
| 266 | (unwind-protect ; protect against user-quit, for example | ||
| 267 | (catch tag | ||
| 268 | (setq | ||
| 269 | id-and-timer | ||
| 270 | (jsonrpc--async-request-1 | ||
| 271 | connection method params | ||
| 272 | :success-fn (lambda (result) (throw tag `(done ,result))) | ||
| 273 | :error-fn | ||
| 274 | (jsonrpc-lambda | ||
| 275 | (&key code message data) | ||
| 276 | (throw tag `(error (jsonrpc-error-code . ,code) | ||
| 277 | (jsonrpc-error-message . ,message) | ||
| 278 | (jsonrpc-error-data . ,data)))) | ||
| 279 | :timeout-fn | ||
| 280 | (lambda () | ||
| 281 | (throw tag '(error (jsonrpc-error-message . "Timed out")))) | ||
| 282 | :deferred deferred | ||
| 283 | :timeout timeout)) | ||
| 284 | (while t (accept-process-output nil 30))) | ||
| 285 | (pcase-let* ((`(,id ,timer) id-and-timer)) | ||
| 286 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 287 | (remhash (list deferred (current-buffer)) | ||
| 288 | (jsonrpc--deferred-actions connection)) | ||
| 289 | (when timer (cancel-timer timer)))))) | ||
| 290 | (when (eq 'error (car retval)) | ||
| 291 | (signal 'jsonrpc-error | ||
| 292 | (cons | ||
| 293 | (format "request id=%s failed:" (car id-and-timer)) | ||
| 294 | (cdr retval)))) | ||
| 295 | (cadr retval))) | ||
| 296 | |||
| 297 | (cl-defun jsonrpc-notify (connection method params) | ||
| 298 | "Notify CONNECTION of something, don't expect a reply." | ||
| 299 | (jsonrpc-connection-send connection | ||
| 300 | :method method | ||
| 301 | :params params)) | ||
| 302 | |||
| 303 | (defconst jrpc-default-request-timeout 10 | ||
| 304 | "Time in seconds before timing out a JSONRPC request.") | ||
| 305 | |||
| 306 | |||
| 307 | ;;; Specfic to `jsonrpc-process-connection' | ||
| 308 | ;;; | ||
| 309 | ;;;###autoload | ||
| 310 | (defclass jsonrpc-process-connection (jsonrpc-connection) | ||
| 311 | ((-process | ||
| 312 | :initarg :process :accessor jsonrpc--process | ||
| 313 | :documentation "Process object wrapped by the this connection.") | ||
| 314 | (-expected-bytes | ||
| 315 | :accessor jsonrpc--expected-bytes | ||
| 316 | :documentation "How many bytes declared by server") | ||
| 317 | (-on-shutdown | ||
| 318 | :accessor jsonrpc--on-shutdown | ||
| 319 | :initform #'ignore | ||
| 320 | :initarg :on-shutdown | ||
| 321 | :documentation "Function run when the process dies.")) | ||
| 322 | :documentation "A JSONRPC connection over an Emacs process. | ||
| 323 | The following initargs are accepted: | ||
| 324 | |||
| 325 | :PROCESS (mandatory), a live running Emacs process object or a | ||
| 326 | function of no arguments producing one such object. The process | ||
| 327 | represents either a pipe connection to locally running process or | ||
| 328 | a stream connection to a network host. The remote endpoint is | ||
| 329 | expected to understand JSONRPC messages with basic HTTP-style | ||
| 330 | enveloping headers such as \"Content-Length:\". | ||
| 331 | |||
| 332 | :ON-SHUTDOWN (optional), a function of one argument, the | ||
| 333 | connection object, called when the process dies .") | ||
| 334 | |||
| 335 | (cl-defmethod initialize-instance ((conn jsonrpc-process-connection) slots) | ||
| 336 | (cl-call-next-method) | ||
| 337 | (let* ((proc (plist-get slots :process)) | ||
| 338 | (proc (if (functionp proc) (funcall proc) proc)) | ||
| 339 | (buffer (get-buffer-create (format "*%s output*" (process-name proc)))) | ||
| 340 | (stderr (get-buffer-create (format "*%s stderr*" (process-name proc))))) | ||
| 341 | (setf (jsonrpc--process conn) proc) | ||
| 342 | (set-process-buffer proc buffer) | ||
| 343 | (process-put proc 'jsonrpc-stderr stderr) | ||
| 344 | (set-process-filter proc #'jsonrpc--process-filter) | ||
| 345 | (set-process-sentinel proc #'jsonrpc--process-sentinel) | ||
| 346 | (with-current-buffer (process-buffer proc) | ||
| 347 | (set-marker (process-mark proc) (point-min)) | ||
| 348 | (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t) proc)) | ||
| 349 | (process-put proc 'jsonrpc-connection conn))) | ||
| 350 | |||
| 351 | (cl-defmethod jsonrpc-connection-send ((connection jsonrpc-process-connection) | ||
| 352 | &rest args | ||
| 353 | &key | ||
| 354 | _id | ||
| 355 | method | ||
| 356 | _params | ||
| 357 | _result | ||
| 358 | _error | ||
| 359 | _partial) | ||
| 360 | "Send MESSAGE, a JSON object, to CONNECTION." | ||
| 361 | (when method | ||
| 362 | (plist-put args :method | ||
| 363 | (cond ((keywordp method) (substring (symbol-name method) 1)) | ||
| 364 | ((and method (symbolp method)) (symbol-name method))))) | ||
| 365 | (let* ( (message `(:jsonrpc "2.0" ,@args)) | ||
| 366 | (json (jsonrpc--json-encode message)) | ||
| 367 | (headers | ||
| 368 | `(("Content-Length" . ,(format "%d" (string-bytes json))) | ||
| 369 | ;; ("Content-Type" . "application/vscode-jsonrpc; charset=utf-8") | ||
| 370 | ))) | ||
| 371 | (process-send-string | ||
| 372 | (jsonrpc--process connection) | ||
| 373 | (cl-loop for (header . value) in headers | ||
| 374 | concat (concat header ": " value "\r\n") into header-section | ||
| 375 | finally return (format "%s\r\n%s" header-section json))) | ||
| 376 | (jsonrpc--log-event connection message 'client))) | ||
| 377 | |||
| 378 | (defun jsonrpc-process-type (conn) | ||
| 379 | "Return the `process-type' of JSONRPC connection CONN." | ||
| 380 | (process-type (jsonrpc--process conn))) | ||
| 381 | |||
| 382 | (cl-defmethod jsonrpc-running-p ((conn jsonrpc-process-connection)) | ||
| 383 | "Return non-nil if JSONRPC connection CONN is running." | ||
| 384 | (process-live-p (jsonrpc--process conn))) | ||
| 385 | |||
| 386 | (cl-defmethod jsonrpc-shutdown ((conn jsonrpc-process-connection)) | ||
| 387 | "Shutdown the JSONRPC connection CONN." | ||
| 388 | (cl-loop | ||
| 389 | with proc = (jsonrpc--process conn) | ||
| 390 | do | ||
| 391 | (delete-process proc) | ||
| 392 | (accept-process-output nil 0.1) | ||
| 393 | while (not (process-get proc 'jsonrpc-sentinel-done)) | ||
| 394 | do (jsonrpc--warn | ||
| 395 | "Sentinel for %s still hasn't run, deleting it!" proc))) | ||
| 396 | |||
| 397 | (defun jsonrpc-stderr-buffer (conn) | ||
| 398 | "Get CONN's standard error buffer, if any." | ||
| 399 | (process-get (jsonrpc--process conn) 'jsonrpc-stderr)) | ||
| 400 | |||
| 401 | |||
| 402 | ;;; Private stuff | ||
| 403 | ;;; | ||
| 404 | (define-error 'jsonrpc-error "jsonrpc-error") | ||
| 405 | |||
| 406 | (defun jsonrpc--json-read () | ||
| 407 | "Read JSON object in buffer, move point to end of buffer." | ||
| 408 | ;; TODO: I guess we can make these macros if/when jsonrpc.el | ||
| 409 | ;; goes into Emacs core. | ||
| 410 | (cond ((fboundp 'json-parse-buffer) (json-parse-buffer | ||
| 411 | :object-type 'plist | ||
| 412 | :null-object nil | ||
| 413 | :false-object :json-false)) | ||
| 414 | (t (let ((json-object-type 'plist)) | ||
| 415 | (json-read))))) | ||
| 416 | |||
| 417 | (defun jsonrpc--json-encode (object) | ||
| 418 | "Encode OBJECT into a JSON string." | ||
| 419 | (cond ((fboundp 'json-serialize) (json-serialize | ||
| 420 | object | ||
| 421 | :false-object :json-false | ||
| 422 | :null-object nil)) | ||
| 423 | (t (let ((json-false :json-false) | ||
| 424 | (json-null nil)) | ||
| 425 | (json-encode object))))) | ||
| 426 | |||
| 427 | (cl-defun jsonrpc--reply (connection id &key (result nil result-supplied-p) error) | ||
| 428 | "Reply to CONNECTION's request ID with RESULT or ERROR." | ||
| 429 | (jsonrpc-connection-send connection :id id :result result :error error)) | ||
| 430 | |||
| 431 | (defun jsonrpc--call-deferred (connection) | ||
| 432 | "Call CONNECTION's deferred actions, who may again defer themselves." | ||
| 433 | (when-let ((actions (hash-table-values (jsonrpc--deferred-actions connection)))) | ||
| 434 | (jsonrpc--debug connection `(:maybe-run-deferred ,(mapcar #'caddr actions))) | ||
| 435 | (mapc #'funcall (mapcar #'car actions)))) | ||
| 436 | |||
| 437 | (defun jsonrpc--process-sentinel (proc change) | ||
| 438 | "Called when PROC undergoes CHANGE." | ||
| 439 | (let ((connection (process-get proc 'jsonrpc-connection))) | ||
| 440 | (jsonrpc--debug connection `(:message "Connection state changed" :change ,change)) | ||
| 441 | (when (not (process-live-p proc)) | ||
| 442 | (with-current-buffer (jsonrpc-events-buffer connection) | ||
| 443 | (let ((inhibit-read-only t)) | ||
| 444 | (insert "\n----------b---y---e---b---y---e----------\n"))) | ||
| 445 | ;; Cancel outstanding timers | ||
| 446 | (maphash (lambda (_id triplet) | ||
| 447 | (pcase-let ((`(,_success ,_error ,timeout) triplet)) | ||
| 448 | (when timeout (cancel-timer timeout)))) | ||
| 449 | (jsonrpc--request-continuations connection)) | ||
| 450 | (unwind-protect | ||
| 451 | ;; Call all outstanding error handlers | ||
| 452 | (maphash (lambda (_id triplet) | ||
| 453 | (pcase-let ((`(,_success ,error ,_timeout) triplet)) | ||
| 454 | (funcall error `(:code -1 :message "Server died")))) | ||
| 455 | (jsonrpc--request-continuations connection)) | ||
| 456 | (jsonrpc--message "Server exited with status %s" (process-exit-status proc)) | ||
| 457 | (process-put proc 'jsonrpc-sentinel-done t) | ||
| 458 | (delete-process proc) | ||
| 459 | (funcall (jsonrpc--on-shutdown connection) connection))))) | ||
| 460 | |||
| 461 | (defun jsonrpc--process-filter (proc string) | ||
| 462 | "Called when new data STRING has arrived for PROC." | ||
| 463 | (when (buffer-live-p (process-buffer proc)) | ||
| 464 | (with-current-buffer (process-buffer proc) | ||
| 465 | (let* ((inhibit-read-only t) | ||
| 466 | (connection (process-get proc 'jsonrpc-connection)) | ||
| 467 | (expected-bytes (jsonrpc--expected-bytes connection))) | ||
| 468 | ;; Insert the text, advancing the process marker. | ||
| 469 | ;; | ||
| 470 | (save-excursion | ||
| 471 | (goto-char (process-mark proc)) | ||
| 472 | (insert string) | ||
| 473 | (set-marker (process-mark proc) (point))) | ||
| 474 | ;; Loop (more than one message might have arrived) | ||
| 475 | ;; | ||
| 476 | (unwind-protect | ||
| 477 | (let (done) | ||
| 478 | (while (not done) | ||
| 479 | (cond | ||
| 480 | ((not expected-bytes) | ||
| 481 | ;; Starting a new message | ||
| 482 | ;; | ||
| 483 | (setq expected-bytes | ||
| 484 | (and (search-forward-regexp | ||
| 485 | "\\(?:.*: .*\r\n\\)*Content-Length: \ | ||
| 486 | *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" | ||
| 487 | (+ (point) 100) | ||
| 488 | t) | ||
| 489 | (string-to-number (match-string 1)))) | ||
| 490 | (unless expected-bytes | ||
| 491 | (setq done :waiting-for-new-message))) | ||
| 492 | (t | ||
| 493 | ;; Attempt to complete a message body | ||
| 494 | ;; | ||
| 495 | (let ((available-bytes (- (position-bytes (process-mark proc)) | ||
| 496 | (position-bytes (point))))) | ||
| 497 | (cond | ||
| 498 | ((>= available-bytes | ||
| 499 | expected-bytes) | ||
| 500 | (let* ((message-end (byte-to-position | ||
| 501 | (+ (position-bytes (point)) | ||
| 502 | expected-bytes)))) | ||
| 503 | (unwind-protect | ||
| 504 | (save-restriction | ||
| 505 | (narrow-to-region (point) message-end) | ||
| 506 | (let* ((json-message | ||
| 507 | (condition-case-unless-debug oops | ||
| 508 | (jsonrpc--json-read) | ||
| 509 | (error | ||
| 510 | (jsonrpc--warn "Invalid JSON: %s %s" | ||
| 511 | (cdr oops) (buffer-string)) | ||
| 512 | nil)))) | ||
| 513 | (when json-message | ||
| 514 | ;; Process content in another | ||
| 515 | ;; buffer, shielding proc buffer from | ||
| 516 | ;; tamper | ||
| 517 | (with-temp-buffer | ||
| 518 | (jsonrpc-connection-receive connection | ||
| 519 | json-message))))) | ||
| 520 | (goto-char message-end) | ||
| 521 | (delete-region (point-min) (point)) | ||
| 522 | (setq expected-bytes nil)))) | ||
| 523 | (t | ||
| 524 | ;; Message is still incomplete | ||
| 525 | ;; | ||
| 526 | (setq done :waiting-for-more-bytes-in-this-message)))))))) | ||
| 527 | ;; Saved parsing state for next visit to this filter | ||
| 528 | ;; | ||
| 529 | (setf (jsonrpc--expected-bytes connection) expected-bytes)))))) | ||
| 530 | |||
| 531 | (cl-defun jsonrpc--async-request-1 (connection | ||
| 532 | method | ||
| 533 | params | ||
| 534 | &rest args | ||
| 535 | &key success-fn error-fn timeout-fn | ||
| 536 | (timeout jrpc-default-request-timeout) | ||
| 537 | (deferred nil)) | ||
| 538 | "Does actual work for `jsonrpc-async-request'. | ||
| 539 | |||
| 540 | Return a list (ID TIMER). ID is the new request's ID, or nil if | ||
| 541 | the request was deferred. TIMER is a timer object set (or nil, if | ||
| 542 | TIMEOUT is nil)." | ||
| 543 | (pcase-let* ((buf (current-buffer)) (point (point)) | ||
| 544 | (`(,_ ,timer ,old-id) | ||
| 545 | (and deferred (gethash (list deferred buf) | ||
| 546 | (jsonrpc--deferred-actions connection)))) | ||
| 547 | (id (or old-id (cl-incf (jsonrpc--next-request-id connection)))) | ||
| 548 | (make-timer | ||
| 549 | (lambda ( ) | ||
| 550 | (when timeout | ||
| 551 | (run-with-timer | ||
| 552 | timeout nil | ||
| 553 | (lambda () | ||
| 554 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 555 | (remhash (list deferred buf) | ||
| 556 | (jsonrpc--deferred-actions connection)) | ||
| 557 | (if timeout-fn (funcall timeout-fn) | ||
| 558 | (jsonrpc--debug | ||
| 559 | connection `(:timed-out ,method :id ,id | ||
| 560 | :params ,params))))))))) | ||
| 561 | (when deferred | ||
| 562 | (if (jsonrpc-connection-ready-p connection deferred) | ||
| 563 | ;; Server is ready, we jump below and send it immediately. | ||
| 564 | (remhash (list deferred buf) (jsonrpc--deferred-actions connection)) | ||
| 565 | ;; Otherwise, save in `eglot--deferred-actions' and exit non-locally | ||
| 566 | (unless old-id | ||
| 567 | (jsonrpc--debug connection `(:deferring ,method :id ,id :params | ||
| 568 | ,params))) | ||
| 569 | (puthash (list deferred buf) | ||
| 570 | (list (lambda () | ||
| 571 | (when (buffer-live-p buf) | ||
| 572 | (with-current-buffer buf | ||
| 573 | (save-excursion (goto-char point) | ||
| 574 | (apply #'jsonrpc-async-request | ||
| 575 | connection | ||
| 576 | method params args))))) | ||
| 577 | (or timer (setq timer (funcall make-timer))) id) | ||
| 578 | (jsonrpc--deferred-actions connection)) | ||
| 579 | (cl-return-from jsonrpc--async-request-1 (list id timer)))) | ||
| 580 | ;; Really send it | ||
| 581 | ;; | ||
| 582 | (jsonrpc-connection-send connection | ||
| 583 | :id id | ||
| 584 | :method method | ||
| 585 | :params params) | ||
| 586 | (puthash id | ||
| 587 | (list (or success-fn | ||
| 588 | (jsonrpc-lambda (&rest _ignored) | ||
| 589 | (jsonrpc--debug | ||
| 590 | connection (list :message "success ignored" | ||
| 591 | :id id)))) | ||
| 592 | (or error-fn | ||
| 593 | (jsonrpc-lambda (&key code message &allow-other-keys) | ||
| 594 | (jsonrpc--debug | ||
| 595 | connection (list | ||
| 596 | :message | ||
| 597 | (format "error ignored, status set (%s)" | ||
| 598 | message) | ||
| 599 | :id id :error code)))) | ||
| 600 | (setq timer (funcall make-timer))) | ||
| 601 | (jsonrpc--request-continuations connection)) | ||
| 602 | (list id timer))) | ||
| 603 | |||
| 604 | (defun jsonrpc--message (format &rest args) | ||
| 605 | "Message out with FORMAT with ARGS." | ||
| 606 | (message "[jsonrpc] %s" (apply #'format format args))) | ||
| 607 | |||
| 608 | (defun jsonrpc--debug (server format &rest args) | ||
| 609 | "Debug message for SERVER with FORMAT and ARGS." | ||
| 610 | (jsonrpc--log-event | ||
| 611 | server (if (stringp format)`(:message ,(format format args)) format))) | ||
| 612 | |||
| 613 | (defun jsonrpc--warn (format &rest args) | ||
| 614 | "Warning message with FORMAT and ARGS." | ||
| 615 | (apply #'jsonrpc--message (concat "(warning) " format) args) | ||
| 616 | (let ((warning-minimum-level :error)) | ||
| 617 | (display-warning 'jsonrpc | ||
| 618 | (apply #'format format args) | ||
| 619 | :warning))) | ||
| 620 | |||
| 621 | (defun jsonrpc--log-event (connection message &optional type) | ||
| 622 | "Log a JSONRPC-related event. | ||
| 623 | CONNECTION is the current connection. MESSAGE is a JSON-like | ||
| 624 | plist. TYPE is a symbol saying if this is a client or server | ||
| 625 | originated." | ||
| 626 | (with-current-buffer (jsonrpc-events-buffer connection) | ||
| 627 | (cl-destructuring-bind (&key method id error &allow-other-keys) message | ||
| 628 | (let* ((inhibit-read-only t) | ||
| 629 | (subtype (cond ((and method id) 'request) | ||
| 630 | (method 'notification) | ||
| 631 | (id 'reply) | ||
| 632 | (t 'message))) | ||
| 633 | (type | ||
| 634 | (concat (format "%s" (or type 'internal)) | ||
| 635 | (if type | ||
| 636 | (format "-%s" subtype))))) | ||
| 637 | (goto-char (point-max)) | ||
| 638 | (let ((msg (format "%s%s%s %s:\n%s\n" | ||
| 639 | type | ||
| 640 | (if id (format " (id:%s)" id) "") | ||
| 641 | (if error " ERROR" "") | ||
| 642 | (current-time-string) | ||
| 643 | (pp-to-string message)))) | ||
| 644 | (when error | ||
| 645 | (setq msg (propertize msg 'face 'error))) | ||
| 646 | (insert-before-markers msg)))))) | ||
| 647 | |||
| 648 | (provide 'jsonrpc) | ||
| 649 | ;;; jsonrpc.el ends here | ||
diff --git a/test/lisp/jsonrpc-tests.el b/test/lisp/jsonrpc-tests.el new file mode 100644 index 00000000000..9395ab6ac0a --- /dev/null +++ b/test/lisp/jsonrpc-tests.el | |||
| @@ -0,0 +1,240 @@ | |||
| 1 | ;;; jsonrpc-tests.el --- tests for jsonrpc.el -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: João Távora <joaotavora@gmail.com> | ||
| 6 | ;; Maintainer: João Távora <joaotavora@gmail.com> | ||
| 7 | ;; Keywords: tests | ||
| 8 | |||
| 9 | ;; This program is free software; you can redistribute it and/or modify | ||
| 10 | ;; it under the terms of the GNU General Public License as published by | ||
| 11 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 12 | ;; (at your option) any later version. | ||
| 13 | |||
| 14 | ;; This program is distributed in the hope that it will be useful, | ||
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 17 | ;; GNU General Public License for more details. | ||
| 18 | |||
| 19 | ;; You should have received a copy of the GNU General Public License | ||
| 20 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
| 21 | |||
| 22 | ;;; Commentary: | ||
| 23 | |||
| 24 | ;; About "deferred" tests, `jsonrpc--test-client' has a flag that we | ||
| 25 | ;; test this flag in the this `jsonrpc-connection-ready-p' API method. | ||
| 26 | ;; It holds any `jsonrpc-request's and `jsonrpc-async-request's | ||
| 27 | ;; explicitly passed `:deferred'. After clearing the flag, the held | ||
| 28 | ;; requests are actually sent to the server in the next opportunity | ||
| 29 | ;; (when receiving or sending something to the server). | ||
| 30 | |||
| 31 | ;;; Code: | ||
| 32 | |||
| 33 | (require 'ert) | ||
| 34 | (require 'jsonrpc) | ||
| 35 | (require 'eieio) | ||
| 36 | |||
| 37 | (defclass jsonrpc--test-endpoint (jsonrpc-process-connection) | ||
| 38 | ((scp :accessor jsonrpc--shutdown-complete-p))) | ||
| 39 | |||
| 40 | (defclass jsonrpc--test-client (jsonrpc--test-endpoint) | ||
| 41 | ((hold-deferred :initform t :accessor jsonrpc--hold-deferred))) | ||
| 42 | |||
| 43 | (cl-defmacro jsonrpc--with-emacsrpc-fixture ((endpoint-sym) &body body) | ||
| 44 | (declare (indent 1) (debug t)) | ||
| 45 | (let ((server (gensym "server-")) (listen-server (gensym "listen-server-"))) | ||
| 46 | `(let* (,server | ||
| 47 | (,listen-server | ||
| 48 | (make-network-process | ||
| 49 | :name "Emacs RPC server" :server t :host "localhost" | ||
| 50 | :service 0 | ||
| 51 | :log (lambda (_server client _message) | ||
| 52 | (setq ,server | ||
| 53 | (make-instance | ||
| 54 | 'jsonrpc--test-endpoint | ||
| 55 | :name (process-name client) | ||
| 56 | :process client | ||
| 57 | :request-dispatcher | ||
| 58 | (lambda (_endpoint method params) | ||
| 59 | (unless (memq method '(+ - * / vconcat append | ||
| 60 | sit-for ignore)) | ||
| 61 | (signal 'jsonrpc-error | ||
| 62 | `((jsonrpc-error-message | ||
| 63 | . "Sorry, this isn't allowed") | ||
| 64 | (jsonrpc-error-code . -32601)))) | ||
| 65 | (apply method (append params nil))) | ||
| 66 | :on-shutdown | ||
| 67 | (lambda (conn) | ||
| 68 | (setf (jsonrpc--shutdown-complete-p conn) t))))))) | ||
| 69 | (,endpoint-sym (make-instance | ||
| 70 | 'jsonrpc--test-client | ||
| 71 | "Emacs RPC client" | ||
| 72 | :process | ||
| 73 | (open-network-stream "JSONRPC test tcp endpoint" | ||
| 74 | nil "localhost" | ||
| 75 | (process-contact ,listen-server | ||
| 76 | :service)) | ||
| 77 | :on-shutdown | ||
| 78 | (lambda (conn) | ||
| 79 | (setf (jsonrpc--shutdown-complete-p conn) t))))) | ||
| 80 | (unwind-protect | ||
| 81 | (progn | ||
| 82 | (cl-assert ,endpoint-sym) | ||
| 83 | ,@body | ||
| 84 | (kill-buffer (jsonrpc--events-buffer ,endpoint-sym)) | ||
| 85 | (when ,server | ||
| 86 | (kill-buffer (jsonrpc--events-buffer ,server)))) | ||
| 87 | (unwind-protect | ||
| 88 | (jsonrpc-shutdown ,endpoint-sym) | ||
| 89 | (unwind-protect | ||
| 90 | (jsonrpc-shutdown ,server) | ||
| 91 | (cl-loop do (delete-process ,listen-server) | ||
| 92 | while (progn (accept-process-output nil 0.1) | ||
| 93 | (process-live-p ,listen-server)) | ||
| 94 | do (jsonrpc--message | ||
| 95 | "test listen-server is still running, waiting")))))))) | ||
| 96 | |||
| 97 | (ert-deftest returns-3 () | ||
| 98 | "A basic test for adding two numbers in our test RPC." | ||
| 99 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 100 | (should (= 3 (jsonrpc-request conn '+ [1 2]))))) | ||
| 101 | |||
| 102 | (ert-deftest errors-with--32601 () | ||
| 103 | "Errors with -32601" | ||
| 104 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 105 | (condition-case err | ||
| 106 | (progn | ||
| 107 | (jsonrpc-request conn 'delete-directory "~/tmp") | ||
| 108 | (ert-fail "A `jsonrpc-error' should have been signalled!")) | ||
| 109 | (jsonrpc-error | ||
| 110 | (should (= -32601 (cdr (assoc 'jsonrpc-error-code (cdr err))))))))) | ||
| 111 | |||
| 112 | (ert-deftest signals-an--32603-JSONRPC-error () | ||
| 113 | "Signals an -32603 JSONRPC error." | ||
| 114 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 115 | (condition-case err | ||
| 116 | (progn | ||
| 117 | (jsonrpc-request conn '+ ["a" 2]) | ||
| 118 | (ert-fail "A `jsonrpc-error' should have been signalled!")) | ||
| 119 | (jsonrpc-error | ||
| 120 | (should (= -32603 (cdr (assoc 'jsonrpc-error-code (cdr err))))))))) | ||
| 121 | |||
| 122 | (ert-deftest times-out () | ||
| 123 | "Request for 3-sec sit-for with 1-sec timeout times out." | ||
| 124 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 125 | (should-error | ||
| 126 | (jsonrpc-request conn 'sit-for [3] :timeout 1)))) | ||
| 127 | |||
| 128 | (ert-deftest doesnt-time-out () | ||
| 129 | :tags '(:expensive-test) | ||
| 130 | "Request for 1-sec sit-for with 2-sec timeout succeeds." | ||
| 131 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 132 | (jsonrpc-request conn 'sit-for [1] :timeout 2))) | ||
| 133 | |||
| 134 | (ert-deftest stretching-it-but-works () | ||
| 135 | "Vector of numbers or vector of vector of numbers are serialized." | ||
| 136 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 137 | ;; (vconcat [1 2 3] [3 4 5]) => [1 2 3 3 4 5] which can be | ||
| 138 | ;; serialized. | ||
| 139 | (should (equal | ||
| 140 | [1 2 3 3 4 5] | ||
| 141 | (jsonrpc-request conn 'vconcat [[1 2 3] [3 4 5]]))))) | ||
| 142 | |||
| 143 | (ert-deftest json-el-cant-serialize-this () | ||
| 144 | "Can't serialize a response that is half-vector/half-list." | ||
| 145 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 146 | (should-error | ||
| 147 | ;; (append [1 2 3] [3 4 5]) => (1 2 3 . [3 4 5]), which can't be | ||
| 148 | ;; serialized | ||
| 149 | (jsonrpc-request conn 'append [[1 2 3] [3 4 5]])))) | ||
| 150 | |||
| 151 | (cl-defmethod jsonrpc-connection-ready-p | ||
| 152 | ((conn jsonrpc--test-client) what) | ||
| 153 | (and (cl-call-next-method) | ||
| 154 | (or (not (string-match "deferred" what)) | ||
| 155 | (not (jsonrpc--hold-deferred conn))))) | ||
| 156 | |||
| 157 | (ert-deftest deferred-action-toolate () | ||
| 158 | :tags '(:expensive-test) | ||
| 159 | "Deferred request fails because noone clears the flag." | ||
| 160 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 161 | (should-error | ||
| 162 | (jsonrpc-request conn '+ [1 2] | ||
| 163 | :deferred "deferred-testing" :timeout 0.5) | ||
| 164 | :type 'jsonrpc-error) | ||
| 165 | (should | ||
| 166 | (= 3 (jsonrpc-request conn '+ [1 2] | ||
| 167 | :timeout 0.5))))) | ||
| 168 | |||
| 169 | (ert-deftest deferred-action-intime () | ||
| 170 | :tags '(:expensive-test) | ||
| 171 | "Deferred request barely makes it after event clears a flag." | ||
| 172 | ;; Send an async request, which returns immediately. However the | ||
| 173 | ;; success fun which sets the flag only runs after some time. | ||
| 174 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 175 | (jsonrpc-async-request conn | ||
| 176 | 'sit-for [0.5] | ||
| 177 | :success-fn | ||
| 178 | (lambda (_result) | ||
| 179 | (setf (jsonrpc--hold-deferred conn) nil))) | ||
| 180 | ;; Now wait for an answer to this request, which should be sent as | ||
| 181 | ;; soon as the previous one is answered. | ||
| 182 | (should | ||
| 183 | (= 3 (jsonrpc-request conn '+ [1 2] | ||
| 184 | :deferred "deferred" | ||
| 185 | :timeout 1))))) | ||
| 186 | |||
| 187 | (ert-deftest deferred-action-complex-tests () | ||
| 188 | :tags '(:expensive-test) | ||
| 189 | "Test a more complex situation with deferred requests." | ||
| 190 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 191 | (let (n-deferred-1 | ||
| 192 | n-deferred-2 | ||
| 193 | second-deferred-went-through-p) | ||
| 194 | ;; This returns immediately | ||
| 195 | (jsonrpc-async-request | ||
| 196 | conn | ||
| 197 | 'sit-for [0.1] | ||
| 198 | :success-fn | ||
| 199 | (lambda (_result) | ||
| 200 | ;; this only gets runs after the "first deferred" is stashed. | ||
| 201 | (setq n-deferred-1 | ||
| 202 | (hash-table-count (jsonrpc--deferred-actions conn))))) | ||
| 203 | (should-error | ||
| 204 | ;; This stashes the request and waits. It will error because | ||
| 205 | ;; no-one clears the "hold deferred" flag. | ||
| 206 | (jsonrpc-request conn 'ignore ["first deferred"] | ||
| 207 | :deferred "first deferred" | ||
| 208 | :timeout 0.5) | ||
| 209 | :type 'jsonrpc-error) | ||
| 210 | ;; The error means the deferred actions stash is now empty | ||
| 211 | (should (zerop (hash-table-count (jsonrpc--deferred-actions conn)))) | ||
| 212 | ;; Again, this returns immediately. | ||
| 213 | (jsonrpc-async-request | ||
| 214 | conn | ||
| 215 | 'sit-for [0.1] | ||
| 216 | :success-fn | ||
| 217 | (lambda (_result) | ||
| 218 | ;; This gets run while "third deferred" below is waiting for | ||
| 219 | ;; a reply. Notice that we clear the flag in time here. | ||
| 220 | (setq n-deferred-2 (hash-table-count (jsonrpc--deferred-actions conn))) | ||
| 221 | (setf (jsonrpc--hold-deferred conn) nil))) | ||
| 222 | ;; This again stashes a request and returns immediately. | ||
| 223 | (jsonrpc-async-request conn 'ignore ["second deferred"] | ||
| 224 | :deferred "second deferred" | ||
| 225 | :timeout 1 | ||
| 226 | :success-fn | ||
| 227 | (lambda (_result) | ||
| 228 | (setq second-deferred-went-through-p t))) | ||
| 229 | ;; And this also stashes a request, but waits. Eventually the | ||
| 230 | ;; flag is cleared in time and both requests go through. | ||
| 231 | (jsonrpc-request conn 'ignore ["third deferred"] | ||
| 232 | :deferred "third deferred" | ||
| 233 | :timeout 1) | ||
| 234 | (should second-deferred-went-through-p) | ||
| 235 | (should (eq 1 n-deferred-1)) | ||
| 236 | (should (eq 2 n-deferred-2)) | ||
| 237 | (should (eq 0 (hash-table-count (jsonrpc--deferred-actions conn))))))) | ||
| 238 | |||
| 239 | (provide 'jsonrpc-tests) | ||
| 240 | ;;; jsonrpc-tests.el ends here | ||