aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2018-06-30 19:06:43 +0100
committerJoão Távora2018-06-30 19:46:06 +0100
commit8af26410a91c3c9679bb0281ddd71f0dd77ec97c (patch)
tree05d2780906fb17ccaaacf953393c15365c7abe05
parent852395bab71cb7032692f3c95e1e4b81a884b66b (diff)
downloademacs-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.texi187
-rw-r--r--etc/NEWS9
-rw-r--r--lisp/jsonrpc.el649
-rw-r--r--test/lisp/jsonrpc-tests.el240
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
5140The @code{jsonrpc} library implements the @acronym{JSONRPC}
5141specification, version 2.0, as it is described in
5142@uref{http://www.jsonrpc.org/}. As the name suggests, JSONRPC is a
5143generic @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
5150Quoting from the @uref{http://www.jsonrpc.org/, spec}, JSONRPC "is
5151transport agnostic in that the concepts can be used within the same
5152process, over sockets, over http, or in many various message passing
5153environments."
5154
5155To model this agnosticism, the @code{jsonrpc} library uses objects of
5156a @code{jsonrpc-connection} class, which represent a connection the
5157remote JSON endpoint (for details on Emacs's object system,
5158@pxref{Top,EIEIO,,eieio,EIEIO}). In modern object-oriented parlance,
5159this class is ``abstract'', i.e. the actual class of a useful
5160connection object used is always a subclass of it. Nevertheless, we
5161can define two distinct API's around the @code{jsonrpc-connection}
5162class:
5163
5164@enumerate
5165
5166@item A user interface for building JSONRPC applications
5167
5168In this scenario, the JSONRPC application instantiates
5169@code{jsonrpc-connection} objects of one of its concrete subclasses
5170using @code{make-instance}. To initiate a contact to the remote
5171endpoint, 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
5174contacts, which generally come in asynchronously, the instantiation
5175should include @code{:request-dispatcher} and
5176@code{:notification-dispatcher} initargs, which are both functions of
51773 arguments: the connection object; a symbol naming the JSONRPC method
5178invoked remotely; and a JSONRPC "params" object.
5179
5180The function passed as @code{:request-dispatcher} is responsible for
5181handling the remote endpoint's requests, which expect a reply from the
5182local endpoint (in this case, the program you're building). Inside
5183that function, you may either return locally (normally) or non-locally
5184(error). A local return value must be a Lisp object serializable as
5185JSON (@pxref{Parsing JSON}). This determines a success response, and
5186the object is forwarded to the server as the JSONRPC "result" object.
5187A non-local return, achieved by calling the function
5188@code{jsonrpc-error}, causes an error response to be sent to the
5189server. The details of the accompanying JSONRPC "error" are filled
5190out with whatever was passed to @code{jsonrpc-error}. A non-local
5191return triggered by an unexpected error of any other type also causes
5192an error response to be sent (unless you have set
5193@code{debug-on-error}, in which case this should land you in the
5194debugger, @pxref{Error Debugging}).
5195
5196@item A inheritance interface for building JSONRPC transport implementations
5197
5198In this scenario, @code{jsonrpc-connection} is subclassed to implement
5199a different underlying transport strategy (for details on how to
5200subclass, @pxref{Inheritance,Inheritance,,eieio}). Users of the
5201application-building interface can then instantiate objects of this
5202concrete class (using the @code{make-instance} function) and connect
5203to JSONRPC endpoints using that strategy.
5204
5205This API has mandatory and optional parts.
5206
5207To allow its users to initiate JSONRPC contacts (notifications or
5208requests) or reply to endpoint requests, the method
5209@code{jsonrpc-connection-send} must be implemented for the subclass.
5210
5211Likewise, for handling the three types of remote contacts (requests,
5212notifications and responses to local requests) the transport
5213implementation must arrange for the function
5214@code{jsonrpc-connection-receive} to be called after noticing a new
5215JSONRPC message on the wire (whatever that "wire" may be).
5216
5217Finally, and optionally, the @code{jsonrpc-connection} subclass should
5218implement @code{jsonrpc-shutdown} and @code{jsonrpc-running-p} if
5219these concepts apply to the transport. If they do, then any system
5220resources (e.g. processes, timers, etc..) used listen for messages on
5221the wire should be released in @code{jsonrpc-shutdown}, i.e. they
5222should 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
5229For convenience, the @code{jsonrpc} library comes built-in with a
5230@code{jsonrpc-process-connection} transport implementation that can
5231talk to local subprocesses (using the standard input and standard
5232output); or TCP hosts (using sockets); or any other remote endpoint
5233that Emacs's process object can represent (@pxref{Processes}).
5234
5235Using this transport, the JSONRPC messages are encoded on the wire as
5236plain text and prefaced by some basic HTTP-style enveloping headers,
5237such as ``Content-Length''.
5238
5239For an example of an application using this transport scheme on top of
5240JSONRPC, see the
5241@uref{https://microsoft.github.io/language-server-protocol/specification,
5242Language Server Protocol}.
5243
5244Along 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
5247initargs as keyword-value pairs to @code{make-instance}:
5248
5249@table @code
5250@item :process
5251Value must be a live process object or a function of no arguments
5252producing one such object. If passed a process object, that is
5253expected to contain an pre-established connection; otherwise, the
5254function is called immediately after the object is made.
5255
5256@item :on-shutdown
5257Value must be a function of a single argument, the
5258@code{jsonrpc-process-connection} object. The function is called
5259after the underlying process object has been deleted (either
5260deliberately by @code{jsonrpc-shutdown} or unexpectedly, because of
5261some external cause).
5262@end table
5263
5264@node JSONRPC JSON object format
5265@subsection JSON object format
5266
5267JSON objects are exchanged as Lisp plists (@pxref{Parsing JSON}):
5268JSON-compatible plists are handed to the dispatcher functions and,
5269likewise, JSON-compatible plists should be given to
5270@code{jsonrpc-notify}, @code{jsonrpc-request} and
5271@code{jsonrpc-async-request}.
5272
5273To facilitate handling plists, this library make liberal use of
5274@code{cl-lib} library and suggests (but doesn't force) its clients to
5275do the same. A macro @code{jsonrpc-lambda} can be used to create a
5276lambda 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
5292In many @acronym{RPC} situations, synchronization between the two
5293communicating endpoints is a matter of correctly designing the RPC
5294application: when synchronization is needed, requests (which are
5295blocking) should be used; when it isn't, notifications should suffice.
5296However, when Emacs acts as one of these endpoints, asynchronous
5297events (e.g. timer- or process-related) may be triggered while there
5298is still uncertainty about the state of the remote endpoint.
5299Furthermore, acting on these events may only sometimes demand
5300synchronization, depending on the event's specific nature.
5301
5302The @code{:deferred} keyword argument to @code{jsonrpc-request} and
5303@code{jsonrpc-async-request} is designed to let the caller indicate
5304that the specific request needs synchronization and its actual
5305issuance may be delayed to the future, until some condition is
5306satisfied. Specifying @code{:deferred} for a request doesn't mean it
5307@emph{will} be delayed, only that it @emph{can} be. If the request
5308isn't sent immediately, @code{jsonrpc} will make renewed efforts to
5309send it at certain key times during communication, such as when
5310receiving or sending other messages to the endpoint.
5311
5312Before any attempt to send the request, the application-specific
5313conditions are checked. Since the @code{jsonrpc} library can't known
5314what these conditions are, the programmer may use the
5315@code{jsonrpc-connection-ready-p} generic function (@pxref{Generic
5316Functions}) to specify them. The default method for this function
5317returns @code{t}, but you can add overriding methods that return
5318@code{nil} in some situations, based on the arguments passed to it,
5319which are the @code{jsonrpc-connection} object (@pxref{JSONRPC
5320Overview}) and whichever value you passed as the @code{:deferred}
5321keyword argument.
5135 5322
5136@node Atomic Changes 5323@node Atomic Changes
5137@section Atomic Change Groups 5324@section Atomic Change Groups
diff --git a/etc/NEWS b/etc/NEWS
index f5332c07828..63c59ae9218 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -579,6 +579,15 @@ This feature uses Tramp and works only on systems which support GVFS,
579i.e. GNU/Linux, roughly spoken. See the chapter "(tramp) Archive file 579i.e. GNU/Linux, roughly spoken. See the chapter "(tramp) Archive file
580names" in the Tramp manual for full documentation of these facilities. 580names" in the Tramp manual for full documentation of these facilities.
581 581
582+++
583** New library for writing JSONRPC applications (https://jsonrpc.org)
584The 'jsonrpc' library enables writing Emacs Lisp applications that
585rely on this protocol. Since the protocol is designed to be
586transport-agnostic, the library provides an API to implement new
587transport strategies as well as a separate API to use them. A
588transport implementation for process-based communication, such as is
589used 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\
77a saved DEFERRED `async-request' from BUF, to be sent not later\
78than 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.
84The following initargs are accepted:
85
86:NAME (mandatory), a string naming the connection
87
88:REQUEST-DISPATCHER (optional), a function of three
89arguments (CONN METHOD PARAMS) for handling JSONRPC requests.
90CONN is a `jsonrpc-connection' object, method is a symbol, and
91PARAMS is a plist representing a JSON object. The function is
92expected to return a JSONRPC result, a plist of (:result
93RESULT) or signal an error of type `jsonrpc-error'.
94
95:NOTIFICATION-DISPATCHER (optional), a function of three
96arguments (CONN METHOD PARAMS) for handling JSONRPC
97notifications. 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.
103ID, 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.
116If it isn't, a request which was passed a value to the
117`:deferred' keyword argument will be deferred to the future.
118WHAT is whatever was passed the as the value to that argument.
119
120By default, all connections are ready for sending all requests
121immediately."
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.
154This function will destructure MESSAGE and call the appropriate
155dispatcher 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.
201If invoked inside a dispatcher function, this function is suitable
202for replying to the remote endpoint with an error message.
203
204ARGS can be of the form (FORMAT-STRING . MOREARGS) for replying
205with a -32603 error code and a message formed by formatting
206FORMAT-STRING with MOREARGS.
207
208Alternatively ARGS can be plist representing a JSONRPC error
209object, 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.
232The JSONRPC request is formed by METHOD, a symbol, and PARAMS a
233JSON object.
234
235The caller can expect SUCCESS-FN or ERROR-FN to be called with a
236JSONRPC `:result' or `:error' object, respectively. If this
237doesn't happen after TIMEOUT seconds (defaults to
238`jsonrpc-request-timeout'), the caller can expect TIMEOUT-FN to be
239called with no arguments. The default values of SUCCESS-FN,
240ERROR-FN and TIMEOUT-FN simply log the events into
241`jsonrpc-events-buffer'.
242
243If DEFERRED is non-nil, maybe defer the request to a future time
244when the server is thought to be ready according to
245`jsonrpc-connection-ready-p' (which see). The request might
246never be sent at all, in case it is overridden in the meantime by
247a new request with identical DEFERRED and for the same buffer.
248However, in that situation, the original timeout is kept.
249
250Returns 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.
256Like `jsonrpc-async-request' for CONNECTION, METHOD and PARAMS,
257but synchronous, i.e. this function doesn't exit until anything
258interesting (success, error or timeout) happens. Furthermore, it
259only exits locally (returning the JSONRPC result object) if the
260request is successful, otherwise exit non-locally with an error
261of type `jsonrpc-error'.
262
263DEFERRED 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.
323The following initargs are accepted:
324
325:PROCESS (mandatory), a live running Emacs process object or a
326function of no arguments producing one such object. The process
327represents either a pipe connection to locally running process or
328a stream connection to a network host. The remote endpoint is
329expected to understand JSONRPC messages with basic HTTP-style
330enveloping headers such as \"Content-Length:\".
331
332:ON-SHUTDOWN (optional), a function of one argument, the
333connection 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
540Return a list (ID TIMER). ID is the new request's ID, or nil if
541the request was deferred. TIMER is a timer object set (or nil, if
542TIMEOUT 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.
623CONNECTION is the current connection. MESSAGE is a JSON-like
624plist. TYPE is a symbol saying if this is a client or server
625originated."
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