1 (* Wrapper over stdin/stdout for handling JSON-RPC *)
2 (* Spec: http://www.jsonrpc.org/specification *)
3 (* Practical readbable guide: https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#base-protocol-json-structures *)
8 (***************************************************************)
9 (* Internal queue functions that run in the daemon process. *)
10 (* The public API for this module comes from Jsonrpc.Make(...) *)
11 (***************************************************************)
13 type internal_queue
= {
14 daemon_in_fd
: Unix.file_descr
; (* fd used by main process to read messages from queue *)
15 messages
: queue_message
Queue.t
;
18 and timestamped_json
= {
19 tj_json
: Hh_json.json
;
24 | Timestamped_json
of timestamped_json
25 | Fatal_exception
of Marshal_tools.remote_exception_data
26 | Recoverable_exception
of Marshal_tools.remote_exception_data
28 and daemon_operation
=
33 (* Try to read a message from the daemon's stdin, which is where all of the
34 editor messages can be read from. May throw if the message is malformed. *)
35 let internal_read_message (reader
: Buffered_line_reader.t
) : timestamped_json
=
36 let message = reader
|> Http_lite.read_message_utf8
in
37 let tj_json = Hh_json.json_of_string
message in
38 let tj_timestamp = Unix.gettimeofday
()
40 { tj_json; tj_timestamp; }
43 (* Reads messages from the editor on stdin, parses them, and sends them to the
45 This runs in a different process because we also timestamp the messages, so
46 we need to read them as soon as they come in. That is, we can't wait for any
47 server computation to finish if we want to get an accurate timestamp. *)
48 let internal_run_daemon'
(oc
: queue_message
Daemon.out_channel
) : unit =
49 let out_fd = Daemon.descr_of_out_channel oc
in
50 let reader = Buffered_line_reader.create
Unix.stdin
in
51 let messages_to_send = Queue.create
() in
55 if Buffered_line_reader.has_buffered_content
reader
58 let read_fds = [Unix.stdin
] in
59 let has_messages_to_send = not
(Queue.is_empty
messages_to_send) in
61 if has_messages_to_send
66 (* Note that if there are no queued messages, this will always block
67 until we're ready to read, rather than returning `Write`, even if
68 stdout is capable of being written to. Furthermore, we will never
69 need to queue a message to be written until we have read
71 let readable_fds, _
, _
= Unix.select
read_fds write_fds [] (-1.0) in
72 let ready_for_read = not
(List.is_empty
readable_fds) in
79 let should_continue = match operation with
82 let timestamped_json = internal_read_message reader in
83 Queue.push
timestamped_json messages_to_send;
86 let message = Printexc.to_string e
in
87 let stack = Printexc.get_backtrace
() in
88 let edata = { Marshal_tools.message; stack; } in
89 let (should_continue, marshal
) = match e
with
90 | Hh_json.Syntax_error _
-> true, Recoverable_exception
edata
91 | _
-> false, Fatal_exception
edata
93 Marshal_tools.to_fd_with_preamble
out_fd marshal
;
97 assert (not
(Queue.is_empty
messages_to_send));
98 let timestamped_json = Queue.pop
messages_to_send in
99 (* We can assume that the entire write will succeed, since otherwise
100 Marshal_tools.to_fd_with_preamble will throw an exception. *)
101 Marshal_tools.to_fd_with_preamble
out_fd (Timestamped_json
timestamped_json);
104 if should_continue then loop ()
108 (* Main function for the daemon process. *)
109 let internal_run_daemon
110 (_dummy_param
: unit)
111 (_ic
, (oc
: queue_message
Daemon.out_channel
)) =
112 Printexc.record_backtrace
true;
114 internal_run_daemon' oc
116 (* An exception that's gotten here is not simply a parse error, but
117 something else, so we should terminate the daemon at this point. *)
118 let message = Printexc.to_string e
in
119 let stack = Printexc.get_backtrace
() in
121 let out_fd = Daemon.descr_of_out_channel oc
in
122 Marshal_tools.to_fd_with_preamble
out_fd
123 (Fatal_exception
{ Marshal_tools.message; stack; })
125 (* There may be a broken pipe, for example. We should just give up on
126 reporting the error. *)
129 let internal_entry_point : (unit, unit, queue_message
) Daemon.entry
=
130 Daemon.register_entry_point
"Jsonrpc" internal_run_daemon
132 let internal_make_queue () : internal_queue
=
133 let handle = Daemon.spawn
135 (* We don't technically need to inherit stdout or stderr, but this might be
136 useful in the event that we throw an unexpected exception in the daemon.
137 It's also useful for print-statement debugging of the daemon. *)
138 (Unix.stdin
, Unix.stdout
, Unix.stderr
)
142 let (ic
, _
) = handle.Daemon.channels
in
144 daemon_in_fd
= Daemon.descr_of_in_channel ic
;
145 messages
= Queue.create
();
149 module Make
(State
: sig type t
end) : sig
150 type kind
= Request
| Notification
| Response
151 val kind_to_string
: kind
-> string
154 json
: Hh_json.json
; (* the json payload *)
155 timestamp
: float; (* time this message arrived at stdin *)
156 (* Following fields are decompositions of 'json'... *)
158 method_
: string; (* mandatory for request+notification; empty otherwise *)
159 id
: Hh_json.json
option; (* mandatory for request+response *)
160 params
: Hh_json.json
option; (* optional for request+notification *)
161 result
: Hh_json.json
option; (* optional for response *)
162 error
: Hh_json.json
option; (* optional for response *)
165 val parse_message
: json
:Hh_json.json
-> timestamp
:float -> message
168 val make_queue
: unit -> queue
(* must call Daemon.entry_point at start of your main *)
169 val get_read_fd
: queue
-> Unix.file_descr
(* can be used for 'select' *)
170 val has_message
: queue
-> bool
171 val get_message
: queue
-> [>
172 | `Message
of message
173 | `Fatal_exception
of Marshal_tools.remote_exception_data
174 | `Recoverable_exception
of Marshal_tools.remote_exception_data
]
176 type on_result
= result
:Hh_json.json
option -> State.t
-> State.t
177 type on_error
= code
:int -> message:string -> data
:Hh_json.json
option -> State.t
-> State.t
178 type cancellation_token
= unit -> unit
180 (* 'respond to_this with_that' is for replying to a JsonRPC request. It will send either *)
181 (* a response or an error depending on whether 'with_that' has an error id in it. *)
182 val respond
: message -> Hh_json.json
-> unit
183 (* notify/request are for initiating JsonRPC messages *)
184 val notify
: string -> Hh_json.json
-> unit
185 val request
: on_result
-> on_error
-> string -> Hh_json.json
-> cancellation_token
187 (* For logging purposes, you can get a copy of which JsonRPC message was last *)
188 (* sent by this module - be it a response, notification, request or cancellation *)
189 val last_sent
: unit -> Hh_json.json
option
190 val clear_last_sent
: unit -> unit
192 (* if the controlling loop received a response message, it should call *)
193 (* into dispatch_response, to trigger the appropriate callback that had *)
194 (* been passed to the corresponding 'request' method. *)
195 val dispatch_response
: message -> State.t
-> State.t
196 (* For logging purposes, when you receive a response message, you can *)
197 (* also see what outgoing request method it is in response to. *)
198 val get_method_for_response
: message -> string
201 type kind
= Request
| Notification
| Response
203 let kind_to_string (kind
: kind
) : string =
205 | Request
-> "Request"
206 | Notification
-> "Notification"
207 | Response
-> "Response"
210 json
: Hh_json.json
; (* the json payload *)
211 timestamp
: float; (* time this message arrived at stdin *)
212 (* Following fields are decompositions of 'json'... *)
214 method_
: string; (* mandatory for request+notification; empty otherwise *)
215 id
: Hh_json.json
option; (* mandatory for request+response *)
216 params
: Hh_json.json
option; (* optional for request+notification *)
217 result
: Hh_json.json
option; (* optional for response *)
218 error
: Hh_json.json
option; (* optional for response *)
222 let parse_message ~
(json
: Hh_json.json
) ~
(timestamp
: float) : message =
223 let id = Hh_json_helpers.try_get_val
"id" json
in
224 let method_opt = Hh_json_helpers.try_get_val
"method" json
225 |> Option.map ~f
:Hh_json.get_string_exn
in
226 let method_ = Option.value method_opt ~default
:"" in (* is easier to consume *)
227 let params = Hh_json_helpers.try_get_val
"params" json
in
228 let result = Hh_json_helpers.try_get_val
"result" json
in
229 let error = Hh_json_helpers.try_get_val
"error" json
in
230 (* Following categorization mostly mirrors that of VSCode except that *)
231 (* VSCode allows number+string+null ID for response, but we allow any ID. *)
232 let kind = match id, method_opt, result, error with
233 | Some _id
, Some _method
, _
, _
-> Request
234 | None
, Some _method
, _
, _
-> Notification
235 | _
, _
, Some _result
, _
-> Response
236 | _
, _
, _
, Some _error
-> Response
237 | _
-> raise
(Hh_json.Syntax_error
"Not JsonRPC")
239 { json
; timestamp
; id; method_; params; result; error; kind; }
242 (************************************************)
243 (* Queue functions that run in the main process *)
244 (************************************************)
246 type queue
= internal_queue
249 internal_make_queue ()
251 let get_read_fd (queue
: queue
) : Unix.file_descr
=
254 (* Read a message into the queue, and return the just-read message. *)
255 let read_single_message_into_queue_blocking (message_queue
: queue
) =
257 try Marshal_tools.from_fd_with_preamble message_queue
.daemon_in_fd
258 with End_of_file
as e
->
259 (* This is different from when the client hangs up. It handles the case
260 that the daemon process exited: for example, if it was killed. *)
261 let message = Printexc.to_string e
in
262 let stack = Printexc.get_backtrace
() in
263 Fatal_exception
{ Marshal_tools.message; stack; }
266 Queue.push
message message_queue
.messages
;
269 let rec read_messages_into_queue_nonblocking (message_queue
: queue
) : unit =
270 let readable_fds, _
, _
= Unix.select
[message_queue
.daemon_in_fd
] [] [] 0.0 in
271 if not
(List.is_empty
readable_fds) then begin
272 (* We're expecting this not to block because we just checked `Unix.select`
273 to make sure that there's something there. *)
274 let message = read_single_message_into_queue_blocking message_queue
in
276 (* Now read any more messages that might be queued up. Only try to read more
277 messages if the daemon is still available to read from. Otherwise, we may
278 infinite loop as a result of `Unix.select` returning that a file
279 descriptor is available to read on. *)
281 | Fatal_exception _
-> ()
282 | _
-> read_messages_into_queue_nonblocking message_queue
;
285 let has_message (queue
: queue
) : bool =
286 read_messages_into_queue_nonblocking queue
;
287 not
(Queue.is_empty queue
.messages
)
289 let get_message (queue
: queue
) =
290 (* Read one in a blocking manner to ensure that we have one. *)
291 if Queue.is_empty queue
.messages
292 then ignore
(read_single_message_into_queue_blocking queue
);
293 (* Then read any others that got queued up so that we can see the maximum
294 number of messages at once for invalidation purposes. *)
295 read_messages_into_queue_nonblocking queue
;
297 let item = Queue.pop queue
.messages
in
299 | Timestamped_json
{tj_json; tj_timestamp;} -> `Message
(parse_message tj_json tj_timestamp)
300 | Fatal_exception data
-> `Fatal_exception data
301 | Recoverable_exception data
-> `Recoverable_exception data
304 (************************************************)
305 (* Output functions for respond+notify *)
306 (************************************************)
308 let last_sent_ref : Hh_json.json
option ref = ref None
310 let clear_last_sent () : unit =
311 last_sent_ref := None
313 let last_sent () : Hh_json.json
option =
316 (* respond: sends either a Response or an Error message, according
317 to whether the json has an error-code or not. *)
319 (in_response_to
: message)
320 (result_or_error
: Hh_json.json
)
323 let is_error = match result_or_error
with
325 Hh_json_helpers.try_get_val
"code" result_or_error
328 let response = JSON_Object
(
329 ["jsonrpc", JSON_String
"2.0"]
331 ["id", Option.value in_response_to
.id ~default
:JSON_Null
]
333 (if is_error then ["error", result_or_error
] else ["result", result_or_error
])
336 last_sent_ref := Some
response;
337 response |> Hh_json.json_to_string
|> Http_lite.write_message stdout
340 (* notify: sends a Notify message *)
341 let notify (method_: string) (params: Hh_json.json
)
344 let message = JSON_Object
[
345 "jsonrpc", JSON_String
"2.0";
346 "method", JSON_String
method_;
350 last_sent_ref := Some
message;
351 message |> Hh_json.json_to_string
|> Http_lite.write_message stdout
354 (************************************************)
355 (* Output functions for request *)
356 (************************************************)
358 type on_result
= result:Hh_json.json
option -> State.t
-> State.t
359 type on_error
= code
:int -> message:string -> data
:Hh_json.json
option -> State.t
-> State.t
360 type cancellation_token
= unit -> unit
362 module Callback
= struct
365 on_result
: on_result
;
370 let requests_counter: IMap.key
ref = ref 0
371 let requests_outstanding: Callback.t
IMap.t
ref = ref IMap.empty
373 (* request: produce a Request message; returns a method you can call to cancel it *)
375 (on_result
: on_result
)
378 (params: Hh_json.json
)
379 : cancellation_token
=
380 incr
requests_counter;
381 let callback = { Callback.method_; on_result
; on_error
; } in
382 let request_id = !requests_counter in
383 requests_outstanding := IMap.add
request_id callback !requests_outstanding;
386 let message = JSON_Object
[
387 "jsonrpc", string_
"2.0";
388 "id", int_
request_id;
389 "method", string_
method_;
393 let cancel_message = JSON_Object
[
394 "jsonrpc", string_
"2.0";
395 "method", string_
"$/cancelRequest";
396 "params", JSON_Object
[
397 "id", int_
request_id;
401 last_sent_ref := Some
message;
402 message |> Hh_json.json_to_string
|> Http_lite.write_message stdout
;
405 last_sent_ref := Some
cancel_message;
406 cancel_message |> Hh_json.json_to_string
|> Http_lite.write_message stdout
410 let get_request_for_response (response: message) =
411 match response.id with
412 | Some
(Hh_json.JSON_Number s
) -> begin
414 let id = int_of_string s
in
415 Option.map
(IMap.get
id !requests_outstanding) ~f
:(fun v
-> (id, v
))
416 with Failure _
-> None
420 let get_method_for_response (response: message) : string =
421 match (get_request_for_response response) with
422 | Some
(_
, callback) -> callback.Callback.method_
425 let dispatch_response
430 let id, on_result
, on_error
= match (get_request_for_response response) with
431 | Some
(id, callback) -> (id, callback.on_result
, callback.on_error
)
432 | None
-> failwith
"response to non-existent id"
434 requests_outstanding := IMap.remove
id !requests_outstanding;
435 if Option.is_some
response.error then
436 let code = Option.bind
response.error (Hh_json_helpers.try_get_val
"code")
437 |> Option.map ~f
:Hh_json.get_number_int_exn
in
438 let message = Option.bind
response.error (Hh_json_helpers.try_get_val
"message")
439 |> Option.map ~f
:Hh_json.get_string_exn
in
440 let data = Option.bind
response.error (Hh_json_helpers.try_get_val
"data") in
441 match code, message, data with
442 | Some
code, Some
message, data -> on_error
code message data state
443 | _
-> failwith
"malformed error response"
445 on_result
response.result state