Let Daemon.spawn stick random stuff into argv
[hiphop-php.git] / hphp / hack / src / utils / jsonrpc.ml
blobe56aee948bbe01ed1a059b62bafc142af996e08b
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 *)
5 open Hh_core
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;
20 tj_timestamp: float;
23 and queue_message =
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 =
29 | Read
30 | Write
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
44 main process.
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
53 let rec loop () =
54 let operation =
55 if Buffered_line_reader.has_buffered_content reader
56 then Read
57 else begin
58 let read_fds = [Unix.stdin] in
59 let has_messages_to_send = not (Queue.is_empty messages_to_send) in
60 let write_fds =
61 if has_messages_to_send
62 then [out_fd]
63 else []
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
70 something. *)
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
73 if ready_for_read
74 then Read
75 else Write
76 end
79 let should_continue = match operation with
80 | Read -> begin
81 try
82 let timestamped_json = internal_read_message reader in
83 Queue.push timestamped_json messages_to_send;
84 true
85 with e ->
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;
94 should_continue
95 end
96 | Write ->
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);
102 true
104 if should_continue then loop ()
106 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
115 with e ->
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; })
124 with _ ->
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
134 ~channel_mode:`pipe
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)
139 internal_entry_point
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
153 type message = {
154 json : Hh_json.json; (* the json payload *)
155 timestamp : float; (* time this message arrived at stdin *)
156 (* Following fields are decompositions of 'json'... *)
157 kind : kind;
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
167 type queue
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
199 end = struct
201 type kind = Request | Notification | Response
203 let kind_to_string (kind: kind) : string =
204 match kind with
205 | Request -> "Request"
206 | Notification -> "Notification"
207 | Response -> "Response"
209 type message = {
210 json : Hh_json.json; (* the json payload *)
211 timestamp : float; (* time this message arrived at stdin *)
212 (* Following fields are decompositions of 'json'... *)
213 kind : kind;
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
248 let make_queue () =
249 internal_make_queue ()
251 let get_read_fd (queue : queue) : Unix.file_descr =
252 queue.daemon_in_fd
254 (* Read a message into the queue, and return the just-read message. *)
255 let read_single_message_into_queue_blocking (message_queue : queue) =
256 let message =
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;
267 message
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. *)
280 match message with
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
298 match item with
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 =
314 !last_sent_ref
316 (* respond: sends either a Response or an Error message, according
317 to whether the json has an error-code or not. *)
318 let respond
319 (in_response_to: message)
320 (result_or_error: Hh_json.json)
321 : unit =
322 let open Hh_json in
323 let is_error = match result_or_error with
324 | JSON_Object _ ->
325 Hh_json_helpers.try_get_val "code" result_or_error
326 |> Option.is_some
327 | _ -> false in
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)
342 : unit =
343 let open Hh_json in
344 let message = JSON_Object [
345 "jsonrpc", JSON_String "2.0";
346 "method", JSON_String method_;
347 "params", params;
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
363 type t = {
364 method_: string;
365 on_result: on_result;
366 on_error: on_error;
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 *)
374 let request
375 (on_result: on_result)
376 (on_error: on_error)
377 (method_: string)
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;
385 let open Hh_json in
386 let message = JSON_Object [
387 "jsonrpc", string_ "2.0";
388 "id", int_ request_id;
389 "method", string_ method_;
390 "params", params;
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;
404 let cancel () =
405 last_sent_ref := Some cancel_message;
406 cancel_message |> Hh_json.json_to_string |> Http_lite.write_message stdout
408 cancel
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
418 | _ -> None
420 let get_method_for_response (response: message) : string =
421 match (get_request_for_response response) with
422 | Some (_, callback) -> callback.Callback.method_
423 | None -> ""
425 let dispatch_response
426 (response: message)
427 (state: State.t)
428 : State.t =
429 let open Callback in
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"
444 else
445 on_result response.result state