aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2018-06-28 13:05:38 +0100
committerJoão Távora2018-06-29 23:05:19 +0100
commit76523866743f39b508e64c34a61af638a4a306b5 (patch)
tree2cdda939bf4381b6b970bb3141078416dcd00b89
parent08594a975a3d95b1c1eae38af608e487e2edfafc (diff)
downloademacs-scratch/add-jsonrpc.tar.gz
emacs-scratch/add-jsonrpc.zip
Add lisp/jsonrpc.elscratch/add-jsonrpc
* lisp/jsonrpc.el: New file * test/lisp/jsonrpc-tests.el: New file
-rw-r--r--lisp/jsonrpc.el738
-rw-r--r--test/lisp/jsonrpc-tests.el242
2 files changed, 980 insertions, 0 deletions
diff --git a/lisp/jsonrpc.el b/lisp/jsonrpc.el
new file mode 100644
index 00000000000..70044320b44
--- /dev/null
+++ b/lisp/jsonrpc.el
@@ -0,0 +1,738 @@
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;; (Library originally extracted from eglot.el, an Emacs LSP client)
25;;
26;; This library implements the JSONRPC 2.0 specification as described
27;; in http://www.jsonrpc.org/. As the name suggests, JSONRPC is a
28;; generic Remote Procedure Call protocol designed around JSON
29;; objects.
30;;
31;; Quoting from the spec: "[JSONRPC] is transport agnostic in that the
32;; concepts can be used within the same process, over sockets, over
33;; http, or in many various message passing environments."
34;;
35;; To model this agnosticism, jsonrpc.el uses objects derived from a
36;; base `jsonrpc-connection' class, which is "abstract" or "virtual"
37;; (in modern OO parlance) and represents the connection to the remote
38;; JSON endpoint. Around this class we can define two distinct APIs:
39;;
40;; 1) A user interface to the JSONRPC _application_, whereby the
41;; `jsonrpc-connection' object is instantiated and used to communicate
42;; with the remote JSONRPC enpoint.
43;;
44;; In this scenario, the JSONRPC application makes the object using
45;; `make-instance' and initiates contacts to the remove endpoint by
46;; passing it to `jsonrpc-notify', `jsonrpc-request' and
47;; `jsonrpc-async-request'. For handling remotely initiated contacts,
48;; which generally come in asynchronously, the `make-instance'
49;; invocation should include `:request-dispatcher' and
50;; `:notification-dispatcher' initargs, which are two functions
51;; receiving the connection object, a symbol naming the JSONRPC
52;; method, and a JSONRPC "params" object.
53;;
54;; The function passed as `:request-dispatcher' handles the remote
55;; endpoint's requests, which expect a reply from the local endpoint.
56;; The function may return locally or non-locally (error). A local
57;; return value should be a JSON object which determines a success
58;; response and is serialized in the JSONRPC "result" object forwarded
59;; to the server. If, however, it uses the `jsonrpc-error' function
60;; to exit non-locally, this responds to the server with a JSONRPC
61;; "error" object instead, the details of which are filled out with
62;; the arguments with whatever was passed to `jsonrpc-error'. A
63;; suitable error reponse is also sent to the server if the function
64;; error unexpectedly with any other error that doesn't originate in a
65;; deliberate call to `jsonrpc-error'.
66;;
67;; 2) A inheritance-based interface to the JSONPRPC _transport
68;; implementation_, whereby `jsonrpc-connection' is subclassed so
69;; users of the user interface can communicate with JSONRPC endpoints
70;; using different underlying transport strategies.
71;;
72;; There are mandatory and optional parts to this API.
73;;
74;; For initiating contacts to the endpoint and replying to it, that
75;; subclass of `jsonrpc-connection' must implement
76;; `jsonrpc-connection-send' method.
77;;
78;; Likewise, for handling the three types remote endpoint's contacts
79;; (responses to requests, remotely initiated requests and remotely
80;; initiated notifications) the transport implementation must arrange
81;; for the function `jsonrpc-connection-receive' to be called after
82;; noticing a new JSONRPC message on the wire (whatever that "wire"
83;; may be).
84;;
85;; Finally, and optionally, the `jsonrpc-connection' subclass should
86;; implement `jsonrpc-shutdown' and `jsonrpc-running-p' if these
87;; concepts apply to the transport.
88;;
89;; For convenience, jsonrpc.el comes built-in with a
90;; `jsonrpc-process-connection' transport implementation that can talk
91;; to local subprocesses (through stdin/stdout) and TCP hosts using
92;; sockets. This uses some basic HTTP-style enveloping headers for
93;; JSON objects sent over the wire. For an example of an application
94;; using this transport scheme on top of JSONRPC, see the Language
95;; Server Protocol
96;; (https://microsoft.github.io/language-server-protocol/specification).
97;; `jsonrpc-process-connection' also implements `jsonrpc-shutdown',
98;; `jsonrpc-running-p'.
99;;
100;;;; About deferred requests and `jsonrpc-connection-p':
101;;
102;; In the user API.
103;;
104;;;; JSON object format:
105;;
106;; JSON objects are exchanged as keyword-value plists: plists are
107;; handed to the dispatcher functions and, likewise, plists should be
108;; given to `jsonrpc-notify', `jsonrpc-request' and
109;; `jsonrpc-async-request'.
110;;
111;; To facilitate handling plists, this library make liberal use of
112;; cl-lib.el and suggests (but doesn't force) its clients to do the
113;; same. A macro `jsonrpc-lambda' can be used to create a lambda for
114;; destructuring a JSON-object like in this example:
115;;
116;; (jsonrpc-async-request
117;; myproc :frobnicate `(:foo "trix")
118;; :success-fn (jsonrpc-lambda (&key bar baz &allow-other-keys)
119;; (message "Server replied back %s and %s!"
120;; bar baz))
121;; :error-fn (jsonrpc-lambda (&key code message _data)
122;; (message "Sadly, server reports %s: %s"
123;; code message)))
124;;
125;;; Code:
126
127(require 'cl-lib)
128(require 'json)
129(require 'eieio)
130(require 'subr-x)
131(require 'warnings)
132(require 'pcase)
133(require 'ert) ; to escape a `condition-case-unless-debug'
134(require 'array) ; xor
135
136
137;;; Public API
138;;;
139;;;###autoload
140(defclass jsonrpc-connection ()
141 ((name
142 :accessor jsonrpc-name
143 :initarg :name
144 :documentation "A name for the connection")
145 (-request-dispatcher
146 :accessor jsonrpc--request-dispatcher
147 :initform #'ignore
148 :initarg :request-dispatcher
149 :documentation "Dispatcher for remotely invoked requests.")
150 (-notification-dispatcher
151 :accessor jsonrpc--notification-dispatcher
152 :initform #'ignore
153 :initarg :notification-dispatcher
154 :documentation "Dispatcher for remotely invoked notifications.")
155 (last-error
156 :accessor jsonrpc-last-error
157 :documentation "Last JSONRPC error message received from endpoint.")
158 (-request-continuations
159 :initform (make-hash-table)
160 :accessor jsonrpc--request-continuations
161 :documentation "A hash table of request ID to continuation lambdas.")
162 (-events-buffer
163 :accessor jsonrpc--events-buffer
164 :documentation "A buffer pretty-printing the JSON-RPC RPC events")
165 (-deferred-actions
166 :initform (make-hash-table :test #'equal)
167 :accessor jsonrpc--deferred-actions
168 :documentation "Map (DEFERRED BUF) to (FN TIMER ID). FN is\
169a saved DEFERRED `async-request' from BUF, to be sent not later\
170than TIMER as ID.")
171 (-next-request-id
172 :initform 0
173 :accessor jsonrpc--next-request-id
174 :documentation "Next number used for a request"))
175 :documentation "Base class representing a JSONRPC connection.
176The following initargs are accepted:
177
178:NAME (mandatory), a string naming the connection
179
180:REQUEST-DISPATCHER (optional), a function of three
181arguments (CONN METHOD PARAMS) for handling JSONRPC requests.
182CONN is a `jsonrpc-connection' object, method is a symbol, and
183PARAMS is a plist representing a JSON object. The function is
184expected to return a JSONRPC result, a plist of (:result
185RESULT) or signal an error of type `jsonrpc-error'.
186
187:NOTIFICATION-DISPATCHER (optional), a function of three
188arguments (CONN METHOD PARAMS) for handling JSONRPC
189notifications. CONN, METHOD and PARAMS are the same as in
190:REQUEST-DISPATCHER.")
191
192;;; API mandatory
193(cl-defgeneric jsonrpc-connection-send (conn &key id method params result error)
194 "Send a JSONRPC message to connection CONN.
195ID, METHOD, PARAMS, RESULT and ERROR. ")
196
197;;; API optional
198(cl-defgeneric jsonrpc-shutdown (conn)
199 "Shutdown the JSONRPC connection CONN.")
200
201;;; API optional
202(cl-defgeneric jsonrpc-running-p (conn)
203 "Tell if the JSONRPC connection CONN is still running.")
204
205;;; API optional
206(cl-defgeneric jsonrpc-connection-ready-p (connection what)
207 "Tell if CONNECTION is ready for WHAT in current buffer.
208If it isn't, a deferrable `jsonrpc-async-request' will be
209deferred to the future. By default, all connections are ready
210for sending requests immediately."
211 (:method (_s _what) ;; by default all connections are ready
212 t))
213
214
215;;; Convenience
216;;;
217(cl-defmacro jsonrpc-lambda (cl-lambda-list &body body)
218 (declare (indent 1) (debug (sexp &rest form)))
219 (let ((e (gensym "jsonrpc-lambda-elem")))
220 `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e))))
221
222(defun jsonrpc-events-buffer (connection)
223 "Get or create JSONRPC events buffer for CONNECTION."
224 (let* ((probe (jsonrpc--events-buffer connection))
225 (buffer (or (and (buffer-live-p probe)
226 probe)
227 (let ((buffer (get-buffer-create
228 (format "*%s events*"
229 (jsonrpc-name connection)))))
230 (with-current-buffer buffer
231 (buffer-disable-undo)
232 (read-only-mode t)
233 (setf (jsonrpc--events-buffer connection) buffer))
234 buffer))))
235 buffer))
236
237(defun jsonrpc-forget-pending-continuations (connection)
238 "Stop waiting for responses from the current JSONRPC CONNECTION."
239 (clrhash (jsonrpc--request-continuations connection)))
240
241(defun jsonrpc-connection-receive (connection message)
242 "Process MESSAGE just received from CONNECTION.
243This function will destructure MESSAGE and call the appropriate
244dispatcher in CONNECTION."
245 (cl-destructuring-bind (&key method id error params result _jsonrpc)
246 message
247 (let (continuations)
248 (jsonrpc--log-event connection message 'server)
249 (setf (jsonrpc-last-error connection) error)
250 (cond
251 (;; A remote request
252 (and method id)
253 (let* ((debug-on-error (and debug-on-error (not (ert-running-test))))
254 (reply
255 (condition-case-unless-debug _ignore
256 (condition-case oops
257 `(:result ,(funcall (jsonrpc--request-dispatcher connection)
258 connection (intern method) params))
259 (jsonrpc-error
260 `(:error
261 (:code
262 ,(or (alist-get 'jsonrpc-error-code (cdr oops)) -32603)
263 :message ,(or (alist-get 'jsonrpc-error-message
264 (cdr oops))
265 "Internal error")))))
266 (error
267 `(:error (:code -32603 :message "Internal error"))))))
268 (apply #'jsonrpc--reply connection id reply)))
269 (;; A remote notification
270 method
271 (funcall (jsonrpc--notification-dispatcher connection)
272 connection (intern method) params))
273 (;; A remote response
274 (setq continuations
275 (and id (gethash id (jsonrpc--request-continuations connection))))
276 (let ((timer (nth 2 continuations)))
277 (when timer (cancel-timer timer)))
278 (remhash id (jsonrpc--request-continuations connection))
279 (if error (funcall (nth 1 continuations) error)
280 (funcall (nth 0 continuations) result)))
281 (;; An abnormal situation
282 id (jsonrpc--warn "No continuation for id %s" id)))
283 (jsonrpc--call-deferred connection))))
284
285
286;;; Contacting the remote endpoint
287;;;
288(defun jsonrpc-error (&rest args)
289 "Error out with FORMAT and ARGS.
290If invoked inside a dispatcher function, this function is suitable
291for replying to the remote endpoint with an error message.
292
293ARGS can be of the form (FORMAT-STRING . MOREARGS) for replying
294with a -32603 error code and a message formed by formatting
295FORMAT-STRING with MOREARGS.
296
297Alternatively ARGS can be plist representing a JSONRPC error
298object, using the keywords `:code', `:message' and `:data'."
299 (if (stringp (car args))
300 (let ((msg
301 (apply #'format-message (car args) (cdr args))))
302 (signal 'jsonrpc-error
303 `(,msg
304 (jsonrpc-error-code . ,32603)
305 (jsonrpc-error-message . ,msg))))
306 (cl-destructuring-bind (&key code message data) args
307 (signal 'jsonrpc-error
308 `(,(format "[jsonrpc] error ")
309 (jsonrpc-error-code . ,code)
310 (jsonrpc-error-message . ,message)
311 (jsonrpc-error-data . ,data))))))
312
313(cl-defun jsonrpc-async-request (connection
314 method
315 params
316 &rest args
317 &key _success-fn _error-fn
318 _timeout-fn
319 _timeout _deferred)
320 "Make a request to CONNECTION, expecting a reply, return immediately.
321The JSONRPC request is formed by METHOD, a symbol, and PARAMS a
322JSON object.
323
324The caller can expect SUCCESS-FN or ERROR-FN to be called with a
325JSONRPC `:result' or `:error' object, respectively. If this
326doesn't happen after TIMEOUT seconds (defaults to
327`jsonrpc-request-timeout'), the caller can expect TIMEOUT-FN to be
328called with no arguments. The default values of SUCCESS-FN,
329ERROR-FN and TIMEOUT-FN simply log the events into
330`jsonrpc-events-buffer'.
331
332If DEFERRED is non-nil, maybe defer the request to a future time
333when the server is thought to be ready according to
334`jsonrpc-connection-ready-p' (which see). The request might
335never be sent at all, in case it is overridden in the meantime by
336a new request with identical DEFERRED and for the same buffer.
337However, in that situation, the original timeout is kept.
338
339Returns nil."
340 (apply #'jsonrpc--async-request-1 connection method params args)
341 nil)
342
343(cl-defun jsonrpc-request (connection method params &key deferred timeout)
344 "Make a request to CONNECTION, wait for a reply.
345Like `jsonrpc-async-request' for CONNECTION, METHOD and PARAMS, but
346synchronous, i.e. doesn't exit until anything
347interesting (success, error or timeout) happens. Furthermore,
348only exit locally (and return the JSONRPC result object) if the
349request is successful, otherwise exit non-locally with an error
350of type `jsonrpc-error'.
351
352DEFERRED is passed to `jsonrpc-async-request', which see."
353 (let* ((tag (cl-gensym "jsonrpc-request-catch-tag")) id-and-timer
354 (retval
355 (unwind-protect ; protect against user-quit, for example
356 (catch tag
357 (setq
358 id-and-timer
359 (jsonrpc--async-request-1
360 connection method params
361 :success-fn (lambda (result) (throw tag `(done ,result)))
362 :error-fn
363 (jsonrpc-lambda
364 (&key code message data)
365 (throw tag `(error (jsonrpc-error-code . ,code)
366 (jsonrpc-error-message . ,message)
367 (jsonrpc-error-data . ,data))))
368 :timeout-fn
369 (lambda ()
370 (throw tag '(error (jsonrpc-error-message . "Timed out"))))
371 :deferred deferred
372 :timeout timeout))
373 (while t (accept-process-output nil 30)))
374 (pcase-let* ((`(,id ,timer) id-and-timer))
375 (remhash id (jsonrpc--request-continuations connection))
376 (remhash (list deferred (current-buffer))
377 (jsonrpc--deferred-actions connection))
378 (when timer (cancel-timer timer))))))
379 (when (eq 'error (car retval))
380 (signal 'jsonrpc-error
381 (cons
382 (format "request id=%s failed:" (car id-and-timer))
383 (cdr retval))))
384 (cadr retval)))
385
386(cl-defun jsonrpc-notify (connection method params)
387 "Notify CONNECTION of something, don't expect a reply."
388 (jsonrpc-connection-send connection
389 :method method
390 :params params))
391
392(defconst jrpc-default-request-timeout 10
393 "Time in seconds before timing out a JSONRPC request.")
394
395
396;;; Specfic to `jsonrpc-process-connection'
397;;;
398;;;###autoload
399(defclass jsonrpc-process-connection (jsonrpc-connection)
400 ((-process
401 :initarg :process :accessor jsonrpc--process
402 :documentation "Process object wrapped by the this connection.")
403 (-expected-bytes
404 :accessor jsonrpc--expected-bytes
405 :documentation "How many bytes declared by server")
406 (-on-shutdown
407 :accessor jsonrpc--on-shutdown
408 :initform #'ignore
409 :initarg :on-shutdown
410 :documentation "Function run when the process dies."))
411 :documentation "A JSONRPC connection over an Emacs process.
412The following initargs are accepted:
413
414:PROCESS (mandatory), a live running Emacs process object or a
415function of no arguments producing one such object. The process
416represents either a pipe connection to locally running process or
417a stream connection to a network host. The remote endpoint is
418expected to understand JSONRPC messages with basic HTTP-style
419enveloping headers such as \"Content-Length:\".
420
421:ON-SHUTDOWN (optional), a function of one argument, the
422connection object, called when the process dies .")
423
424(cl-defmethod initialize-instance ((conn jsonrpc-process-connection) slots)
425 (cl-call-next-method)
426 (let* ((proc (plist-get slots :process))
427 (proc (if (functionp proc) (funcall proc) proc))
428 (buffer (get-buffer-create (format "*%s output*" (process-name proc))))
429 (stderr (get-buffer-create (format "*%s stderr*" (process-name proc)))))
430 (setf (jsonrpc--process conn) proc)
431 (set-process-buffer proc buffer)
432 (process-put proc 'jsonrpc-stderr stderr)
433 (set-process-filter proc #'jsonrpc--process-filter)
434 (set-process-sentinel proc #'jsonrpc--process-sentinel)
435 (with-current-buffer (process-buffer proc)
436 (set-marker (process-mark proc) (point-min))
437 (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t) proc))
438 (process-put proc 'jsonrpc-connection conn)))
439
440(cl-defmethod jsonrpc-connection-send ((connection jsonrpc-process-connection)
441 &rest args
442 &key
443 _id
444 method
445 _params
446 _result
447 _error
448 _partial)
449 "Send MESSAGE, a JSON object, to CONNECTION."
450 (when method
451 (plist-put args :method
452 (cond ((keywordp method) (substring (symbol-name method) 1))
453 ((and method (symbolp method)) (symbol-name method)))))
454 (let* ( (message `(:jsonrpc "2.0" ,@args))
455 (json (jsonrpc--json-encode message))
456 (headers
457 `(("Content-Length" . ,(format "%d" (string-bytes json)))
458 ;; ("Content-Type" . "application/vscode-jsonrpc; charset=utf-8")
459 )))
460 (process-send-string
461 (jsonrpc--process connection)
462 (cl-loop for (header . value) in headers
463 concat (concat header ": " value "\r\n") into header-section
464 finally return (format "%s\r\n%s" header-section json)))
465 (jsonrpc--log-event connection message 'client)))
466
467(defun jsonrpc-process-type (conn)
468 "Return the `process-type' of JSONRPC connection CONN."
469 (process-type (jsonrpc--process conn)))
470
471(cl-defmethod jsonrpc-running-p ((conn jsonrpc-process-connection))
472 "Return non-nil if JSONRPC connection CONN is running."
473 (process-live-p (jsonrpc--process conn)))
474
475(cl-defmethod jsonrpc-shutdown ((conn jsonrpc-process-connection))
476 "Shutdown the JSONRPC connection CONN."
477 (cl-loop
478 with proc = (jsonrpc--process conn)
479 do
480 (delete-process proc)
481 (accept-process-output nil 0.1)
482 while (not (process-get proc 'jsonrpc-sentinel-done))
483 do (jsonrpc--warn
484 "Sentinel for %s still hasn't run, deleting it!" proc)))
485
486(defun jsonrpc-stderr-buffer (conn)
487 "Get CONN's standard error buffer, if any."
488 (process-get (jsonrpc--process conn) 'jsonrpc-stderr))
489
490
491;;; Private stuff
492;;;
493(define-error 'jsonrpc-error "jsonrpc-error")
494
495(defun jsonrpc--json-read ()
496 "Read JSON object in buffer, move point to end of buffer."
497 ;; TODO: I guess we can make these macros if/when jsonrpc.el
498 ;; goes into Emacs core.
499 (cond ((fboundp 'json-parse-buffer) (json-parse-buffer
500 :object-type 'plist
501 :null-object nil
502 :false-object :json-false))
503 (t (let ((json-object-type 'plist))
504 (json-read)))))
505
506(defun jsonrpc--json-encode (object)
507 "Encode OBJECT into a JSON string."
508 (cond ((fboundp 'json-serialize) (json-serialize
509 object
510 :false-object :json-false
511 :null-object nil))
512 (t (let ((json-false :json-false)
513 (json-null nil))
514 (json-encode object)))))
515
516(cl-defun jsonrpc--reply (connection id &key (result nil result-supplied-p) error)
517 "Reply to CONNECTION's request ID with RESULT or ERROR."
518 (jsonrpc-connection-send connection :id id :result result :error error))
519
520(defun jsonrpc--call-deferred (connection)
521 "Call CONNECTION's deferred actions, who may again defer themselves."
522 (when-let ((actions (hash-table-values (jsonrpc--deferred-actions connection))))
523 (jsonrpc--debug connection `(:maybe-run-deferred ,(mapcar #'caddr actions)))
524 (mapc #'funcall (mapcar #'car actions))))
525
526(defun jsonrpc--process-sentinel (proc change)
527 "Called when PROC undergoes CHANGE."
528 (let ((connection (process-get proc 'jsonrpc-connection)))
529 (jsonrpc--debug connection `(:message "Connection state changed" :change ,change))
530 (when (not (process-live-p proc))
531 (with-current-buffer (jsonrpc-events-buffer connection)
532 (let ((inhibit-read-only t))
533 (insert "\n----------b---y---e---b---y---e----------\n")))
534 ;; Cancel outstanding timers
535 (maphash (lambda (_id triplet)
536 (pcase-let ((`(,_success ,_error ,timeout) triplet))
537 (when timeout (cancel-timer timeout))))
538 (jsonrpc--request-continuations connection))
539 (unwind-protect
540 ;; Call all outstanding error handlers
541 (maphash (lambda (_id triplet)
542 (pcase-let ((`(,_success ,error ,_timeout) triplet))
543 (funcall error `(:code -1 :message "Server died"))))
544 (jsonrpc--request-continuations connection))
545 (jsonrpc--message "Server exited with status %s" (process-exit-status proc))
546 (process-put proc 'jsonrpc-sentinel-done t)
547 (delete-process proc)
548 (funcall (jsonrpc--on-shutdown connection) connection)))))
549
550(defun jsonrpc--process-filter (proc string)
551 "Called when new data STRING has arrived for PROC."
552 (when (buffer-live-p (process-buffer proc))
553 (with-current-buffer (process-buffer proc)
554 (let* ((inhibit-read-only t)
555 (connection (process-get proc 'jsonrpc-connection))
556 (expected-bytes (jsonrpc--expected-bytes connection)))
557 ;; Insert the text, advancing the process marker.
558 ;;
559 (save-excursion
560 (goto-char (process-mark proc))
561 (insert string)
562 (set-marker (process-mark proc) (point)))
563 ;; Loop (more than one message might have arrived)
564 ;;
565 (unwind-protect
566 (let (done)
567 (while (not done)
568 (cond
569 ((not expected-bytes)
570 ;; Starting a new message
571 ;;
572 (setq expected-bytes
573 (and (search-forward-regexp
574 "\\(?:.*: .*\r\n\\)*Content-Length: \
575*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n"
576 (+ (point) 100)
577 t)
578 (string-to-number (match-string 1))))
579 (unless expected-bytes
580 (setq done :waiting-for-new-message)))
581 (t
582 ;; Attempt to complete a message body
583 ;;
584 (let ((available-bytes (- (position-bytes (process-mark proc))
585 (position-bytes (point)))))
586 (cond
587 ((>= available-bytes
588 expected-bytes)
589 (let* ((message-end (byte-to-position
590 (+ (position-bytes (point))
591 expected-bytes))))
592 (unwind-protect
593 (save-restriction
594 (narrow-to-region (point) message-end)
595 (let* ((json-message
596 (condition-case-unless-debug oops
597 (jsonrpc--json-read)
598 (error
599 (jsonrpc--warn "Invalid JSON: %s %s"
600 (cdr oops) (buffer-string))
601 nil))))
602 (when json-message
603 ;; Process content in another
604 ;; buffer, shielding proc buffer from
605 ;; tamper
606 (with-temp-buffer
607 (jsonrpc-connection-receive connection
608 json-message)))))
609 (goto-char message-end)
610 (delete-region (point-min) (point))
611 (setq expected-bytes nil))))
612 (t
613 ;; Message is still incomplete
614 ;;
615 (setq done :waiting-for-more-bytes-in-this-message))))))))
616 ;; Saved parsing state for next visit to this filter
617 ;;
618 (setf (jsonrpc--expected-bytes connection) expected-bytes))))))
619
620(cl-defun jsonrpc--async-request-1 (connection
621 method
622 params
623 &rest args
624 &key success-fn error-fn timeout-fn
625 (timeout jrpc-default-request-timeout)
626 (deferred nil))
627 "Does actual work for `jsonrpc-async-request'.
628
629Return a list (ID TIMER). ID is the new request's ID, or nil if
630the request was deferred. TIMER is a timer object set (or nil, if
631TIMEOUT is nil)."
632 (pcase-let* ((buf (current-buffer)) (point (point))
633 (`(,_ ,timer ,old-id)
634 (and deferred (gethash (list deferred buf)
635 (jsonrpc--deferred-actions connection))))
636 (id (or old-id (cl-incf (jsonrpc--next-request-id connection))))
637 (make-timer
638 (lambda ( )
639 (when timeout
640 (run-with-timer
641 timeout nil
642 (lambda ()
643 (remhash id (jsonrpc--request-continuations connection))
644 (remhash (list deferred buf)
645 (jsonrpc--deferred-actions connection))
646 (if timeout-fn (funcall timeout-fn)
647 (jsonrpc--debug
648 connection `(:timed-out ,method :id ,id
649 :params ,params)))))))))
650 (when deferred
651 (if (jsonrpc-connection-ready-p connection deferred)
652 ;; Server is ready, we jump below and send it immediately.
653 (remhash (list deferred buf) (jsonrpc--deferred-actions connection))
654 ;; Otherwise, save in `eglot--deferred-actions' and exit non-locally
655 (unless old-id
656 (jsonrpc--debug connection `(:deferring ,method :id ,id :params
657 ,params)))
658 (puthash (list deferred buf)
659 (list (lambda ()
660 (when (buffer-live-p buf)
661 (with-current-buffer buf
662 (save-excursion (goto-char point)
663 (apply #'jsonrpc-async-request
664 connection
665 method params args)))))
666 (or timer (setq timer (funcall make-timer))) id)
667 (jsonrpc--deferred-actions connection))
668 (cl-return-from jsonrpc--async-request-1 (list id timer))))
669 ;; Really send it
670 ;;
671 (jsonrpc-connection-send connection
672 :id id
673 :method method
674 :params params)
675 (puthash id
676 (list (or success-fn
677 (jsonrpc-lambda (&rest _ignored)
678 (jsonrpc--debug
679 connection (list :message "success ignored"
680 :id id))))
681 (or error-fn
682 (jsonrpc-lambda (&key code message &allow-other-keys)
683 (jsonrpc--debug
684 connection (list
685 :message
686 (format "error ignored, status set (%s)"
687 message)
688 :id id :error code))))
689 (setq timer (funcall make-timer)))
690 (jsonrpc--request-continuations connection))
691 (list id timer)))
692
693(defun jsonrpc--message (format &rest args)
694 "Message out with FORMAT with ARGS."
695 (message "[jsonrpc] %s" (apply #'format format args)))
696
697(defun jsonrpc--debug (server format &rest args)
698 "Debug message for SERVER with FORMAT and ARGS."
699 (jsonrpc--log-event
700 server (if (stringp format)`(:message ,(format format args)) format)))
701
702(defun jsonrpc--warn (format &rest args)
703 "Warning message with FORMAT and ARGS."
704 (apply #'jsonrpc--message (concat "(warning) " format) args)
705 (let ((warning-minimum-level :error))
706 (display-warning 'jsonrpc
707 (apply #'format format args)
708 :warning)))
709
710(defun jsonrpc--log-event (connection message &optional type)
711 "Log a JSONRPC-related event.
712CONNECTION is the current connection. MESSAGE is a JSON-like
713plist. TYPE is a symbol saying if this is a client or server
714originated."
715 (with-current-buffer (jsonrpc-events-buffer connection)
716 (cl-destructuring-bind (&key method id error &allow-other-keys) message
717 (let* ((inhibit-read-only t)
718 (subtype (cond ((and method id) 'request)
719 (method 'notification)
720 (id 'reply)
721 (t 'message)))
722 (type
723 (concat (format "%s" (or type 'internal))
724 (if type
725 (format "-%s" subtype)))))
726 (goto-char (point-max))
727 (let ((msg (format "%s%s%s %s:\n%s\n"
728 type
729 (if id (format " (id:%s)" id) "")
730 (if error " ERROR" "")
731 (current-time-string)
732 (pp-to-string message))))
733 (when error
734 (setq msg (propertize msg 'face 'error)))
735 (insert-before-markers msg))))))
736
737(provide 'jsonrpc)
738;;; jsonrpc.el ends here
diff --git a/test/lisp/jsonrpc-tests.el b/test/lisp/jsonrpc-tests.el
new file mode 100644
index 00000000000..bfdb513ada4
--- /dev/null
+++ b/test/lisp/jsonrpc-tests.el
@@ -0,0 +1,242 @@
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
240
241(provide 'jsonrpc-tests)
242;;; jsonrpc-tests.el ends here