sync the repo
[hiphop-php.git] / hphp / hack / src / client / ide_service / clientIdeDaemon.ml
blobf63b745db16a84c198a4590190b8c011ccc73ca4
1 (*
2 * Copyright (c) 2019, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
10 open Hh_prelude
12 (** For debugging. When we get to [at_exit], we'll log what the current activities are. *)
13 let dbg_current_activities : string list SMap.t ref = ref SMap.empty
15 (** Each activity key is associated with up to five most recent timestamped activity values
16 under that key. *)
17 let dbg_set_activity ~(key : string) (value : string) : unit =
18 let history =
19 SMap.find_opt key !dbg_current_activities |> Option.value ~default:[]
21 let history =
22 Printf.sprintf " %s %s" (Unix.gettimeofday () |> Utils.timestring) value
23 :: List.take history 4
25 dbg_current_activities := SMap.add key history !dbg_current_activities;
28 (** This prints a multiline string: for each activity key, the most recent activity values for that key. *)
29 let dbg_dump_activity () : string =
30 SMap.bindings !dbg_current_activities
31 |> List.map ~f:(fun (key, history) ->
32 key :: List.rev history |> String.concat ~sep:"\n")
33 |> String.concat ~sep:"\n"
35 (** These are messages on ClientIdeDaemon's internal message-queue *)
36 type message =
37 | ClientRequest : 'a ClientIdeMessage.tracked_t -> message
38 (** ClientRequest came from ClientIdeService over stdin;
39 it expects a response. *)
40 | GotNamingTable :
41 (ClientIdeInit.init_result, ClientIdeMessage.rich_error) result
42 -> message
43 (** GotNamingTable is posted from within ClientIdeDaemon itself once
44 our attempt at loading saved-state has finished; it's picked
45 up by handle_messages. *)
47 type message_queue = message Lwt_message_queue.t
49 exception Outfd_write_error of string * string
51 let is_outfd_write_error (exn : Exception.t) : bool =
52 match Exception.unwrap exn with
53 | Outfd_write_error _ -> true
54 | _ -> false
56 type common_state = {
57 hhi_root: Path.t;
58 (** hhi_root files are written during initialize, deleted at shutdown, and
59 refreshed periodically in case the tmp-cleaner has deleted them. *)
60 config: ServerConfig.t; [@opaque]
61 local_config: ServerLocalConfig.t; [@opaque]
62 local_memory: Provider_backend.local_memory; [@opaque]
63 (** Local_memory backend; includes decl caches *)
65 [@@deriving show]
67 (** The [entry] caches the TAST+errors; the [Errors.t option] stores what was
68 the most recent version of the errors to have been returned to clientLsp
69 by didOpen/didChange/didClose/codeAction. *)
70 type open_files_state =
71 (Provider_context.entry * Errors.t option ref) Relative_path.Map.t
73 (** istate, "initialized state", is the state the daemon after it has
74 finished initialization (i.e. finished loading saved state),
75 concerning these data-structures:
76 1. forward-naming-table-delta stored in naming_table
77 2. reverse-naming-table-delta-and-cache stored in local_memory
78 3. entries with source text, stored in open_files
79 3. cached ASTs and TASTs, stored in open_files
80 4. shallow-decl-cache, folded-decl-cache stored in local-memory
82 There are two concepts to understand.
83 1. "Singleton context" (ctx). When processing IDE requests for a file, we create
84 a context object in which that entry's source text and AST and TAST
85 are present in the context, and no others are.
86 2. "Quarantine with respect to an entry". We enter a quarantine while
87 computing the TAST for a singleton context entry. The invariants within
88 the quarantine are different from those without.
90 The key algorithms which read from these data-structures are:
91 1. Ast_provider.get_ast will fetch the cached AST for an entry in ctx, or if
92 the entry is present but as yet lacks an AST then it will parse and cache,
93 or if it's lookinng for the AST of a file not in ctx then it will parse
94 off disk but decline to cache.
95 2. Naming_provider.get_* will get_ast for ctx entry to see if symbol is there.
96 If not it will look in reverse-delta-and-cache or read from sqlite
97 and store the answer back in reverse-delta-and-cache. But if the answer
98 to that fallback was a file in ctx, then it will say that the symbol's
99 not defined.
100 3. Decl_provider.get_shallow_class_* will look it up in shallow-decl-cache, and otherwise
101 will ask Naming_provider and Ast_provider for the AST, will compute shallow decl,
102 and will store it in shallow-decl-cache
103 4. Decl_provider.get_* will look it up in folded-decl-cache, computing it if
104 not there using shallow provider, and store it back in folded-decl-cache
105 5. Tast_provider.compute* is only ever called on entries. It returns the cached
106 TAST if present; otherwise, it runs normal type-checking-and-inference, relies
107 upon all the other providers, and writes the answer back in the entry's TAST cache.
109 The invariants for forward and reverse naming tables:
110 1. These tables only ever reflect truth about disk files; they are unaffected
111 by open_file entries.
112 2. They are updated in response to DidChangeWatchedFileEvents.
113 Because watchman and VSCode send those events asynchronously,
114 we might for instance find ourselves being asked to compute a TAST
115 by reading the naming-table and fetching a shallow-decl from a file
116 that doesn't even exist on disk any more (even though we don't yet know it).
118 The invariants for AST, TAST, shallow, and folded-decl caches:
119 1. AST, if present, reflects the AST of its entry's source text,
120 and is a "full" AST (not decl-only), and has errors.
121 2. TAST, if present, reflects the TAST of its entry's source text computed
122 against the on-disk state of all other files
123 3. Outside a quarantine, all entries in shallow cache are correct as of disk
124 (at least as far as asynchronous file updates have been processed).
125 4. Likewise, all entries in folded caches are correct as of disk.
126 5. We only ever enter quarantine with respect to one single entry.
127 For the duration of the quarantine, an AST for that entry,
128 if present, is correct as of the entry's source text.
129 6. Likewise any shallow decls for an entry are correct as of its source text.
130 Moreover, if shallow decls for an entry are present, then the entry's AST
131 is present and contains those symbols.
132 7. Any shallow decls not for the entry are correct as of disk.
133 8. During quarantine, the shallow-decl of all other files is correct as of disk.
134 9. The entry's TAST, along with every single decl,
135 are correct as of this entry's source text plus every other file off disk.
137 Here are the algorithms we use that satisfy those invariants.
138 1. Upon a disk-file-change, we invalidate all TASTs (satisfying invariant 2).
139 We use the forward-naming-table to find all "old" symbols that were
140 defined in the file prior to the disk change, and invalidate those
141 shallow decls (satisfying invariant 3). We invalidate all
142 folded caches (satisfying invariant 4). Invariant 1 is N/A.
143 2. Upon an editor change to a file, we invalidate the entry's AST and TAST
144 (satisfying invariant 1).
145 3. Upon request for a TAST of a file, we create a singleton context for
146 that entry, and enter quarantine as follows. We parse the file and
147 cache its AST and invalidate shallow decls for all symbols inside
148 this new AST (satisfying invariant 6). We invalidate all decls
149 (satisfying invariant 9). Subsequent fetches,
150 thanks to the "key algorithms for reading these datastructures" (above)
151 will only cache things in accordance with invariants 6,7,8,9.
152 4. We leave quarantine as follows. We invalidate shallow decls for
153 all symbols in the entry's AST; thanks to invariant 5, this will
154 fulfill invariant 3. We invalidate all decls (satisfying invariant 4).
156 type istate = {
157 icommon: common_state;
158 iopen_files: open_files_state; [@opaque]
159 naming_table: Naming_table.t; [@opaque]
160 (** the forward-naming-table is constructed during initialize and updated
161 during process_changed_files. It stores an in-memory map of FileInfos that
162 have changed since sqlite. When a file is changed on disk, we need this to
163 know which shallow decls to invalidate. Note: while the forward-naming-table
164 is stored here, the reverse-naming-table is instead stored in ctx. *)
165 sienv: SearchUtils.si_env; [@opaque]
166 (** sienv provides autocomplete and find-symbols. It is constructed during
167 initialize and updated during process_changed_files. It stores a few
168 in-memory structures such as namespace-list, plus in-memory deltas. *)
171 (** dstate, "during_init state", is the state the daemon after it has received an
172 init message (and has parsed config files to get popt/tcopt, has initialized
173 glean, as written out hhi files) but before it has loaded saved-state or processed
174 file updates. *)
175 type dstate = {
176 start_time: float;
177 (** When did we kick off the attempt to load saved-state? *)
178 dcommon: common_state;
179 dopen_files: open_files_state; [@opaque]
180 changed_files_to_process: Relative_path.Set.t;
181 (** [changed_files_to_process] is grown [During_init] upon [Did_change_watched_files changes]
182 and then discharged in [initialize2] before changing to [Initialized] state. *)
184 [@@deriving show]
186 type state =
187 | Pending_init (** We haven't yet received init request *)
188 | During_init of dstate
189 (** We're working on the init request. We're still in
190 the process of loading the saved state. *)
191 | Initialized of istate (** Finished work on init request. *)
192 | Failed_init of ClientIdeMessage.rich_error
193 (** Failed request, with root cause *)
195 type t = {
196 message_queue: message_queue;
197 state: state;
200 let state_to_log_string (state : state) : string =
201 let open_files_to_log_string (open_files : open_files_state) : string =
202 Printf.sprintf "%d open_files" (Relative_path.Map.cardinal open_files)
204 match state with
205 | Pending_init -> "Pending_init"
206 | During_init { dopen_files; changed_files_to_process; _ } ->
207 Printf.sprintf
208 "During_init(%s, %d changed during)"
209 (open_files_to_log_string dopen_files)
210 (Relative_path.Set.cardinal changed_files_to_process)
211 | Initialized { iopen_files; _ } ->
212 Printf.sprintf "Initialized(%s)" (open_files_to_log_string iopen_files)
213 | Failed_init reason ->
214 Printf.sprintf "Failed_init(%s)" reason.ClientIdeMessage.category
216 let log s = Hh_logger.log ("[ide-daemon] " ^^ s)
218 let log_debug s = Hh_logger.debug ("[ide-daemon] " ^^ s)
220 let set_up_hh_logger_for_client_ide_service (root : Path.t) : unit =
221 (* Log to a file on disk. Note that calls to `Hh_logger` will always write to
222 `stderr`; this is in addition to that. *)
223 let client_ide_log_fn = ServerFiles.client_ide_log root in
224 begin
225 try Sys.rename client_ide_log_fn (client_ide_log_fn ^ ".old") with
226 | _e -> ()
227 end;
228 Hh_logger.set_log client_ide_log_fn;
229 log "Starting client IDE service at %s" client_ide_log_fn
231 let write_message
232 ~(out_fd : Lwt_unix.file_descr)
233 ~(message : ClientIdeMessage.message_from_daemon) : unit Lwt.t =
234 try%lwt
235 let%lwt (_ : int) = Marshal_tools_lwt.to_fd_with_preamble out_fd message in
236 Lwt.return_unit
237 with
238 | Unix.Unix_error (Unix.EPIPE, fn, param) ->
239 raise @@ Outfd_write_error (fn, param)
241 let log_startup_time
242 ?(count : int option) (component : string) (start_time : float) : float =
243 let now = Unix.gettimeofday () in
244 HackEventLogger.serverless_ide_startup ?count ~start_time component;
247 let restore_hhi_root_if_necessary (istate : istate) : istate =
248 if Sys.file_exists (Path.to_string istate.icommon.hhi_root) then
249 istate
250 else
251 (* Some processes may clean up the temporary HHI directory we're using.
252 Assume that such a process has deleted the directory, and re-write the HHI
253 files to disk. *)
254 let hhi_root = Hhi.get_hhi_root ~force_write:true () in
256 "Old hhi root %s no longer exists. Creating a new hhi root at %s"
257 (Path.to_string istate.icommon.hhi_root)
258 (Path.to_string hhi_root);
259 Relative_path.set_path_prefix Relative_path.Hhi hhi_root;
260 { istate with icommon = { istate.icommon with hhi_root } }
262 (** Deletes the hhi files we've created. *)
263 let remove_hhi (state : state) : unit =
264 match state with
265 | Pending_init
266 | Failed_init _ ->
268 | During_init { dcommon = { hhi_root; _ }; _ }
269 | Initialized { icommon = { hhi_root; _ }; _ } ->
270 let hhi_root = Path.to_string hhi_root in
271 log "Removing hhi directory %s..." hhi_root;
272 (try Sys_utils.rm_dir_tree hhi_root with
273 | exn ->
274 let e = Exception.wrap exn in
275 ClientIdeUtils.log_bug "remove_hhi" ~e ~telemetry:true)
277 (** Helper called to process a batch of file changes. Updates the naming table, and invalidates the decl and tast caches for the changes. *)
278 let batch_update_naming_table_and_invalidate_caches
279 ~(ctx : Provider_context.t)
280 ~(naming_table : Naming_table.t)
281 ~(sienv : SearchUtils.si_env)
282 ~(local_memory : Provider_backend.local_memory)
283 ~(open_files :
284 (Provider_context.entry * Errors.t option ref) Relative_path.Map.t)
285 (changes : Relative_path.Set.t) : Naming_table.t * SearchUtils.si_env =
286 let start_time = Unix.gettimeofday () in
287 let ClientIdeIncremental.{ changes; naming_table; sienv } =
288 ClientIdeIncremental.update_naming_tables_and_si
289 ~ctx
290 ~naming_table
291 ~sienv
292 ~changes
294 let telemetry =
295 Provider_utils.invalidate_upon_file_changes
296 ~ctx
297 ~local_memory
298 ~changes
299 ~entries:(Relative_path.Map.map open_files ~f:fst)
301 HackEventLogger.ProfileTypeCheck.invalidate
302 ~count:(List.length changes)
303 ~start_time
304 ~path:(List.hd changes |> Option.map ~f:(fun change -> change.FileInfo.path))
305 telemetry;
306 (naming_table, sienv)
308 (** An empty ctx with no entries *)
309 let make_empty_ctx (common : common_state) : Provider_context.t =
310 Provider_context.empty_for_tool
311 ~popt:(ServerConfig.parser_options common.config)
312 ~tcopt:(ServerConfig.typechecker_options common.config)
313 ~backend:(Provider_backend.Local_memory common.local_memory)
314 ~deps_mode:(Typing_deps_mode.InMemoryMode None)
316 (** Constructs a temporary ctx with just one entry. *)
317 let make_singleton_ctx (common : common_state) (entry : Provider_context.entry)
318 : Provider_context.t =
319 let ctx = make_empty_ctx common in
320 let ctx = Provider_context.add_or_overwrite_entry ~ctx entry in
323 (** initialize1 is called by handle_request upon receipt of an "init"
324 message from the client. It is synchronous. It sets up global variables and
325 glean. The remainder of init work will happen after we return... our caller
326 handle_request will kick off async work to load saved-state, and once done
327 it will stick a GotNamingTable message into the queue, and handle_one_message
328 will subsequently pick up that message and call [initialize2]. *)
329 let initialize1 (param : ClientIdeMessage.Initialize_from_saved_state.t) :
330 dstate =
331 log_debug "initialize1";
332 let open ClientIdeMessage.Initialize_from_saved_state in
333 let start_time = Unix.gettimeofday () in
334 HackEventLogger.serverless_ide_set_root param.root;
335 set_up_hh_logger_for_client_ide_service param.root;
337 Relative_path.set_path_prefix Relative_path.Root param.root;
338 let hhi_root = Hhi.get_hhi_root () in
339 log "Extracted hhi files to directory %s" (Path.to_string hhi_root);
340 Relative_path.set_path_prefix Relative_path.Hhi hhi_root;
341 Relative_path.set_path_prefix Relative_path.Tmp (Path.make "/tmp");
343 let server_args =
344 ServerArgs.default_options_with_check_mode ~root:(Path.to_string param.root)
346 let server_args = ServerArgs.set_config server_args param.config in
347 let (config, local_config) = ServerConfig.load ~silent:true server_args in
348 (* Ignore package loading errors for now TODO(jjwu) *)
349 let open GlobalOptions in
350 log "Loading package configuration";
351 let (_, package_info) = PackageConfig.load_and_parse () in
352 let tco = ServerConfig.typechecker_options config in
353 let config =
354 ServerConfig.set_tc_options
355 config
356 { tco with tco_package_info = package_info }
358 HackEventLogger.set_hhconfig_version
359 (ServerConfig.version config |> Config_file.version_to_string_opt);
360 HackEventLogger.set_rollout_flags
361 (ServerLocalConfig.to_rollout_flags local_config);
362 HackEventLogger.set_rollout_group local_config.ServerLocalConfig.rollout_group;
364 Provider_backend.set_local_memory_backend
365 ~max_num_decls:5000
366 ~max_num_folded_class_decls:5000
367 ~max_num_shallow_class_decls:20000;
368 let local_memory =
369 match Provider_backend.get () with
370 | Provider_backend.Local_memory local_memory -> local_memory
371 | _ -> failwith "expected local memory backend"
374 (* We only ever serve requests on files that are open. That's why our caller
375 passes an initial list of open files, the ones already open in the editor
376 at the time we were launched. We don't actually care about their contents
377 at this stage, since updated contents will be delivered upon each request.
378 (and indeed it's pointless to waste time reading existing contents off disk).
379 All we care is that every open file is listed in 'open_files'. *)
380 let open_files =
381 param.open_files
382 |> List.map ~f:(fun path ->
383 path |> Path.to_string |> Relative_path.create_detect_prefix)
384 |> List.map ~f:(fun path ->
385 ( path,
386 ( Provider_context.make_entry
387 ~path
388 ~contents:Provider_context.Raise_exn_on_attempt_to_read,
389 ref None ) ))
390 |> Relative_path.Map.of_list
392 let start_time =
393 log_startup_time
394 "initialize1"
395 ~count:(List.length param.open_files)
396 start_time
398 log_debug "initialize1.done";
400 start_time;
401 dcommon = { hhi_root; config; local_config; local_memory };
402 dopen_files = open_files;
403 changed_files_to_process = Relative_path.Set.empty;
406 (** initialize2 is called by handle_one_message upon receipt of a
407 [GotNamingTable] message. It sends the appropriate message on to the
408 client, and transitions into either [Initialized] or [Failed_init]
409 state. *)
410 let initialize2
411 (out_fd : Lwt_unix.file_descr)
412 (dstate : dstate)
413 (init_result :
414 (ClientIdeInit.init_result, ClientIdeMessage.rich_error) result) :
415 state Lwt.t =
416 let start_time = log_startup_time "load_naming_table" dstate.start_time in
417 log_debug "initialize2";
418 match init_result with
419 | Ok { ClientIdeInit.naming_table; sienv; changed_files } ->
420 let changed_files_to_process =
421 Relative_path.Set.union
422 dstate.changed_files_to_process
423 (Relative_path.Set.of_list changed_files)
424 |> Relative_path.Set.filter ~f:FindUtils.path_filter
426 let (naming_table, sienv) =
427 batch_update_naming_table_and_invalidate_caches
428 ~ctx:(make_empty_ctx dstate.dcommon)
429 ~naming_table
430 ~sienv
431 ~local_memory:dstate.dcommon.local_memory
432 ~open_files:dstate.dopen_files
433 changed_files_to_process
435 let istate =
437 naming_table;
438 sienv;
439 icommon = dstate.dcommon;
440 iopen_files = dstate.dopen_files;
443 (* Note: Done_init is needed to (1) transition clientIdeService state, (2) cause
444 clientLsp to know to ask for squiggles to be refreshed on open files. *)
445 let%lwt () =
446 write_message
447 ~out_fd
448 ~message:
449 (ClientIdeMessage.Notification (ClientIdeMessage.Done_init (Ok ())))
451 let count = Relative_path.Set.cardinal changed_files_to_process in
452 let (_ : float) = log_startup_time "initialize2" start_time ~count in
453 log_debug "initialize2.done";
454 Lwt.return (Initialized istate)
455 | Error reason ->
456 log_debug "initialize2.error";
457 let%lwt () =
458 write_message
459 ~out_fd
460 ~message:
461 (ClientIdeMessage.Notification
462 (ClientIdeMessage.Done_init (Error reason)))
464 remove_hhi (During_init dstate);
465 Lwt.return (Failed_init reason)
467 (** This funtion is about papering over a bug. Sometimes, rarely, we're
468 failing to receive DidOpen messages from clientLsp. Our model is to
469 only ever answer IDE requests on open files, so we know we'll eventually
470 reveive a DidClose even for them and be able to clear their TAST cache
471 at that time. But for now, to paper over the bug, we'll call this
472 function to log the event and we'll assume that we just missed a DidOpen. *)
473 let log_missing_open_file_BUG (reason : string) (path : Relative_path.t) : unit
475 let path = Relative_path.to_absolute path in
476 let message =
477 Printf.sprintf "Error: action on non-open file [%s] %s" reason path
479 ClientIdeUtils.log_bug message ~telemetry:true
481 (** Registers the file in response to DidOpen or DidChange,
482 during the During_init state, by putting in a new
483 entry in open_files, with empty AST and TAST. If the LSP client
484 happened to send us two DidOpens for a file, or DidChange before DidOpen,
485 well, we won't complain. *)
486 let open_or_change_file_during_init
487 (dstate : dstate) (path : Relative_path.t) (contents : string) : dstate =
488 let entry =
489 Provider_context.make_entry
490 ~path
491 ~contents:(Provider_context.Provided_contents contents)
493 let dopen_files =
494 Relative_path.Map.add dstate.dopen_files ~key:path ~data:(entry, ref None)
496 { dstate with dopen_files }
498 (** Closes a file, in response to DidClose event, by removing the
499 entry in open_files. If the LSP client sents us multile DidCloses,
500 or DidClose for an unopen file, we won't complain. *)
501 let close_file (open_files : open_files_state) (path : Relative_path.t) :
502 open_files_state =
503 if not (Relative_path.Map.mem open_files path) then
504 log_missing_open_file_BUG "close-without-open" path;
505 Relative_path.Map.remove open_files path
507 (** Updates an existing opened file, with new contents; if the
508 contents haven't changed then the existing open file's AST and TAST
509 will be left intact. *)
510 let update_file
511 (open_files : open_files_state) (document : ClientIdeMessage.document) :
512 open_files_state * Provider_context.entry * Errors.t option ref =
513 let path =
514 document.ClientIdeMessage.file_path
515 |> Path.to_string
516 |> Relative_path.create_detect_prefix
518 let contents = document.ClientIdeMessage.file_contents in
519 let (entry, published_errors) =
520 match Relative_path.Map.find_opt open_files path with
521 | None ->
522 (* This is a common scenario although I'm not quite sure why *)
523 ( Provider_context.make_entry
524 ~path
525 ~contents:(Provider_context.Provided_contents contents),
526 ref None )
527 | Some (entry, published_errors)
528 when Option.equal
529 String.equal
530 (Some contents)
531 (Provider_context.get_file_contents_if_present entry) ->
532 (* we can just re-use the existing entry; contents haven't changed *)
533 (entry, published_errors)
534 | Some _ ->
535 (* We'll create a new entry; existing entry caches, if present, will be dropped
536 But first, need to clear the Fixme cache. This is a global cache
537 which is updated as a side-effect of the Ast_provider. *)
538 Fixme_provider.remove_batch (Relative_path.Set.singleton path);
539 ( Provider_context.make_entry
540 ~path
541 ~contents:(Provider_context.Provided_contents contents),
542 ref None )
544 let open_files =
545 Relative_path.Map.add open_files ~key:path ~data:(entry, published_errors)
547 (open_files, entry, published_errors)
549 (** like [update_file], but for convenience also produces a ctx for
550 use in typechecking. Also ensures that hhi files haven't been deleted
551 by tmp_cleaner, so that type-checking will succeed. *)
552 let update_file_ctx (istate : istate) (document : ClientIdeMessage.document) :
553 istate * Provider_context.t * Provider_context.entry * Errors.t option ref =
554 let istate = restore_hhi_root_if_necessary istate in
555 let (iopen_files, entry, published_errors) =
556 update_file istate.iopen_files document
558 let ctx = make_singleton_ctx istate.icommon entry in
559 ({ istate with iopen_files }, ctx, entry, published_errors)
561 (** We avoid showing typing errors if there are parsing errors. *)
562 let get_user_facing_errors
563 ~(ctx : Provider_context.t) ~(entry : Provider_context.entry) : Errors.t =
564 let (_, ast_errors) =
565 Ast_provider.compute_parser_return_and_ast_errors
566 ~popt:(Provider_context.get_popt ctx)
567 ~entry
569 if Errors.is_empty ast_errors then
570 let { Tast_provider.Compute_tast_and_errors.errors = all_errors; _ } =
571 Tast_provider.compute_tast_and_errors_quarantined ~ctx ~entry
573 all_errors
574 else
575 ast_errors
577 (** Computes the Errors.t for what's on disk at a given path.
578 We provide [istate] just in case we can benefit from a cached answer. *)
579 let get_errors_for_path (istate : istate) (path : Relative_path.t) : Errors.t =
580 let disk_content_opt =
581 Sys_utils.cat_or_failed (Relative_path.to_absolute path)
583 let cached_entry_opt = Relative_path.Map.find_opt istate.iopen_files path in
584 let entry_opt =
585 match (disk_content_opt, cached_entry_opt) with
586 | (None, _) ->
587 (* if the disk file is absent (e.g. it was deleted prior to the user closing it),
588 then we naturally can't compute errors for it. *)
589 None
590 | ( Some disk_content,
591 Some
592 ( ({
593 Provider_context.contents =
594 Provider_context.(
595 Contents_from_disk str | Provided_contents str);
597 } as entry),
598 _ ) )
599 when String.equal disk_content str ->
600 (* file on disk was the same as what we currently have in the entry, and
601 the entry very likely already has errors computed for it, so as an optimization
602 we'll re-use errors from that entry. *)
603 Some entry
604 | (Some disk_content, _) ->
605 (* file on disk is different from what we have in the entry, e.g. because the
606 user closed a modified file, so compute errors from the disk content. *)
607 Some
608 (Provider_context.make_entry
609 ~path
610 ~contents:(Provider_context.Provided_contents disk_content))
612 match entry_opt with
613 | None ->
614 (* file couldn't be read off disk (maybe absent); therefore, by definition, no errors *)
615 Errors.empty
616 | Some entry ->
617 (* Here we'll get either cached errors from the cached entry, or will recompute errors
618 from the partially cached entry, or will compute errors from the file on disk. *)
619 let ctx = make_singleton_ctx istate.icommon entry in
620 get_user_facing_errors ~ctx ~entry
622 (** handle_request invariants: Messages are only ever handled serially; we never
623 handle one message while another is being handled. It is a bug if the client sends
624 anything other than [Initialize_from_saved_state] as its first message. Upon
625 receipt+processing of this we transition from [Pre_init] to [During_init]
626 and kick off some async work to prepare the naming table. During this async work, i.e.
627 during [During_init], we are able to handle a few requests but will reject
628 others. Important: files may change during [During_init], and it's important that we keep track of and eventually index these changed files.
630 Our caller [handle_one_message] is actually the one that transitions
631 us from [During_init] to either [Failed_init] or [Initialized]. Once in one
632 of those states, we never thereafter transition state. *)
633 let handle_request
634 (type a)
635 (message_queue : message_queue)
636 (state : state)
637 (_tracking_id : string)
638 (message : a ClientIdeMessage.t) : state * (a, Lsp.Error.t) result =
639 let open ClientIdeMessage in
640 match (state, message) with
641 (***********************************************************)
642 (************************* HANDLED IN ANY STATE ************)
643 (***********************************************************)
644 | (_, Verbose_to_file verbose) ->
645 if verbose then
646 Hh_logger.Level.set_min_level_file Hh_logger.Level.Debug
647 else
648 Hh_logger.Level.set_min_level_file Hh_logger.Level.Info;
649 (state, Ok ())
650 | (_, Shutdown ()) ->
651 remove_hhi state;
652 (state, Ok ())
653 (***********************************************************)
654 (************************* INITIALIZATION ******************)
655 (***********************************************************)
656 | (Pending_init, Initialize_from_saved_state param) -> begin
657 (* Invariant: no message will be sent to us prior to this request,
658 and we must send no message until we've sent this response. *)
660 let dstate = initialize1 param in
661 (* We're going to kick off the asynchronous part of initializing now.
662 Once it's done, it will appear as a GotNamingTable message on the queue. *)
663 Lwt.async (fun () ->
664 let%lwt naming_table_result =
665 ClientIdeInit.init
666 ~config:dstate.dcommon.config
667 ~local_config:dstate.dcommon.local_config
668 ~param
669 ~hhi_root:dstate.dcommon.hhi_root
670 ~local_memory:dstate.dcommon.local_memory
672 (* if the following push fails, that must be because the queues
673 have been shut down, in which case there's nothing to do. *)
674 let (_succeeded : bool) =
675 Lwt_message_queue.push
676 message_queue
677 (GotNamingTable naming_table_result)
679 Lwt.return_unit);
680 log "Finished saved state initialization. State: %s" (show_dstate dstate);
681 (During_init dstate, Ok ())
682 with
683 | exn ->
684 let e = Exception.wrap exn in
685 let reason = ClientIdeUtils.make_rich_error "initialize1" ~e in
686 (* Our caller has an exception handler. But we must handle this ourselves
687 to change state to Failed_init; our caller's handler doesn't change state. *)
688 (* TODO: remove_hhi *)
689 (Failed_init reason, Error (ClientIdeUtils.to_lsp_error reason))
691 | (_, Initialize_from_saved_state _) ->
692 failwith ("Unexpected init in " ^ state_to_log_string state)
693 (***********************************************************)
694 (************************* CAN HANDLE DURING INIT **********)
695 (***********************************************************)
696 | (During_init dstate, Did_change_watched_files changes) ->
697 (* While init is happening, we accumulate changes in [changed_files_to_process].
698 Once naming-table has been loaded, then [initialize2] will process+discharge all these
699 accumulated changes. *)
700 let changed_files_to_process =
701 Relative_path.Set.union dstate.changed_files_to_process changes
703 (During_init { dstate with changed_files_to_process }, Ok ())
704 | (Initialized istate, Did_change_watched_files changes) ->
705 let (naming_table, sienv) =
706 batch_update_naming_table_and_invalidate_caches
707 ~ctx:(make_empty_ctx istate.icommon)
708 ~naming_table:istate.naming_table
709 ~sienv:istate.sienv
710 ~local_memory:istate.icommon.local_memory
711 ~open_files:istate.iopen_files
712 changes
714 let istate = { istate with naming_table; sienv } in
715 (Initialized istate, Ok ())
716 (* didClose *)
717 | (During_init dstate, Did_close file_path) ->
718 let path =
719 file_path |> Path.to_string |> Relative_path.create_detect_prefix
721 ( During_init
722 { dstate with dopen_files = close_file dstate.dopen_files path },
723 Ok [] )
724 | (Initialized istate, Did_close file_path) ->
725 let path =
726 file_path |> Path.to_string |> Relative_path.create_detect_prefix
728 let errors = get_errors_for_path istate path |> Errors.sort_and_finalize in
729 let diagnostics =
730 List.map
731 errors
732 ~f:ClientIdeMessage.diagnostic_of_finalized_error_without_related_hints
734 ( Initialized
735 { istate with iopen_files = close_file istate.iopen_files path },
736 Ok diagnostics )
737 (* didOpen or didChange *)
738 | (During_init dstate, Did_open_or_change { file_path; file_contents }) ->
739 let path =
740 file_path |> Path.to_string |> Relative_path.create_detect_prefix
742 let dstate = open_or_change_file_during_init dstate path file_contents in
743 (During_init dstate, Ok ())
744 | (Initialized istate, Did_open_or_change document) ->
745 let (iopen_files, _entry, _errors) =
746 update_file istate.iopen_files document
748 (Initialized { istate with iopen_files }, Ok ())
749 (* Pull diagnostics *)
750 | (Initialized istate, Diagnostics document) ->
751 let (istate, ctx, entry, published_errors_ref) =
752 update_file_ctx istate document
754 let errors = get_user_facing_errors ~ctx ~entry in
755 published_errors_ref := Some errors;
756 let errors = Errors.sort_and_finalize errors in
757 let diagnostics = Ide_diagnostics.convert ~ctx ~entry errors in
758 (Initialized istate, Ok diagnostics)
759 (* Document Symbol *)
760 | (During_init dstate, Document_symbol document) ->
761 let (dopen_files, entry, _) = update_file dstate.dopen_files document in
762 let result =
763 FileOutline.outline_entry_no_comments
764 ~popt:(ServerConfig.parser_options dstate.dcommon.config)
765 ~entry
767 (During_init { dstate with dopen_files }, Ok result)
768 | (Initialized istate, Document_symbol document) ->
769 let (iopen_files, entry, _) = update_file istate.iopen_files document in
770 let result =
771 FileOutline.outline_entry_no_comments
772 ~popt:(ServerConfig.parser_options istate.icommon.config)
773 ~entry
775 (Initialized { istate with iopen_files }, Ok result)
776 (***********************************************************)
777 (************************* UNABLE TO HANDLE ****************)
778 (***********************************************************)
779 | (During_init dstate, _) ->
780 let e =
782 Lsp.Error.code = Lsp.Error.RequestCancelled;
783 message = "IDE service has not yet completed init";
784 data = None;
787 (During_init dstate, Error e)
788 | (Failed_init reason, _) ->
789 (Failed_init reason, Error (ClientIdeUtils.to_lsp_error reason))
790 | (Pending_init, _) ->
791 failwith
792 (Printf.sprintf
793 "unexpected message '%s' in state '%s'"
794 (ClientIdeMessage.t_to_string message)
795 (state_to_log_string state))
796 (***********************************************************)
797 (************************* NORMAL HANDLING AFTER INIT ******)
798 (***********************************************************)
799 | (Initialized istate, Hover (document, { line; column })) ->
800 let (istate, ctx, entry, _) = update_file_ctx istate document in
801 let result =
802 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
803 Ide_hover.go_quarantined ~ctx ~entry ~line ~column)
805 (Initialized istate, Ok result)
806 | ( Initialized istate,
807 Go_to_implementation (document, { line; column }, document_list) ) ->
808 let (istate, ctx, entry, _) = update_file_ctx istate document in
809 let (istate, result) =
810 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
811 match ServerFindRefs.go_from_file_ctx ~ctx ~entry ~line ~column with
812 | Some (_name, action)
813 when not @@ ServerGoToImpl.is_searchable ~action ->
814 (istate, ClientIdeMessage.Invalid_symbol_impl)
815 | Some (name, action) ->
817 1) For all open files that we know about in ClientIDEDaemon, uesd the
818 cached TASTs to return positions of implementations
819 2) Return this list, alongside the name and action
820 3) ClientLSP, upon receiving, will shellout to hh_server
821 and reject all server-provided positions for files that ClientIDEDaemon
822 knew about, under the assumption that our cached TAST provides edited
823 file info, if applicable.
825 let (istate, single_file_positions) =
826 List.fold
827 ~f:(fun (istate, accum) document ->
828 let (istate, ctx, _entry, _errors) =
829 update_file_ctx istate document
831 let stringified_path =
832 Path.to_string document.ClientIdeMessage.file_path
834 let filename =
835 Relative_path.create_detect_prefix stringified_path
837 let single_file_pos =
838 ServerGoToImpl.go_for_single_file
839 ~ctx
840 ~action
841 ~naming_table:istate.naming_table
842 ~filename
843 |> ServerFindRefs.to_absolute
844 |> List.map ~f:snd
846 let urikey =
847 Lsp_helpers.path_string_to_lsp_uri
848 stringified_path
849 ~default_path:stringified_path
852 let updated_map =
853 Lsp.UriMap.add urikey single_file_pos accum
855 (istate, updated_map))
856 document_list
857 ~init:(istate, Lsp.UriMap.empty)
859 ( istate,
860 ClientIdeMessage.Go_to_impl_success
861 (name, action, single_file_positions) )
862 | None -> (istate, ClientIdeMessage.Invalid_symbol_impl))
864 (Initialized istate, Ok result)
865 (* textDocument/rename *)
866 | ( Initialized istate,
867 Rename (document, { line; column }, new_name, document_list) ) ->
868 let (istate, ctx, entry, _errors) = update_file_ctx istate document in
869 let (istate, result) =
870 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
871 match
872 ServerFindRefs.go_from_file_ctx_with_symbol_definition
873 ~ctx
874 ~entry
875 ~line
876 ~column
877 with
878 | None -> (istate, ClientIdeMessage.Not_renameable_position)
879 | Some (_definition, action) when ServerFindRefs.is_local action ->
880 let res =
881 match ServerRename.go_for_localvar ctx action new_name with
882 | Ok (Some patch_list) ->
883 ClientIdeMessage.Rename_success
884 { shellout = None; local = patch_list }
885 | Ok None ->
886 ClientIdeMessage.Rename_success { shellout = None; local = [] }
887 | Error action ->
888 let str =
889 Printf.sprintf
890 "ClientIDEDaemon failed to rename for localvar %s"
891 (ServerCommandTypes.Find_refs.show_action action)
893 log "%s" str;
894 failwith "ClientIDEDaemon failed to rename for a localvar"
896 (istate, res)
897 | Some (symbol_definition, action) ->
898 let (istate, single_file_patches) =
899 List.fold
900 ~f:(fun (istate, accum) document ->
901 let (istate, ctx, _entry, _errors) =
902 update_file_ctx istate document
904 let filename =
905 Path.to_string document.ClientIdeMessage.file_path
906 |> Relative_path.create_detect_prefix
908 let single_file_patches =
909 ServerRename.go_for_single_file
911 ~find_refs_action:action
912 ~filename
913 ~symbol_definition
914 ~new_name
915 ~naming_table:istate.naming_table
917 let patches =
918 match single_file_patches with
919 | Ok patches -> patches
920 | Error _ -> []
922 let patch_list = List.rev_append patches accum in
923 (istate, patch_list))
924 ~init:(istate, [])
925 document_list
927 ( istate,
928 ClientIdeMessage.Rename_success
930 shellout = Some (symbol_definition, action);
931 local = single_file_patches;
933 (* not a localvar, must defer to hh_server *))
935 (Initialized istate, Ok result)
936 (* textDocument/references - localvar only *)
937 | ( Initialized istate,
938 Find_references (document, { line; column }, document_list) ) ->
939 let open Result.Monad_infix in
940 (* Update the state of the world with the document as it exists in the IDE *)
941 let (istate, ctx, entry, _) = update_file_ctx istate document in
942 let (istate, result) =
943 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
944 match ServerFindRefs.go_from_file_ctx ~ctx ~entry ~line ~column with
945 | Some (name, action) when ServerFindRefs.is_local action ->
946 let result =
947 ServerFindRefs.go_for_localvar ctx action
948 >>| ServerFindRefs.to_absolute
950 let result =
951 match result with
952 | Ok ide_result ->
953 let lsp_uri_map =
954 begin
955 match ide_result with
956 | [] ->
957 (* If we find-refs on a localvar via right-click, is it possible that it doesn't return references?
958 It's possible some nondeterminism changed the cached TAST,
959 but assert that it's a failure for now
961 let err =
962 Printf.sprintf
963 "FindRefs returned an empty list of positions for localvar %s"
964 name
966 log "%s" err;
967 HackEventLogger.invariant_violation_bug err;
968 failwith err
969 | positions ->
970 let filename =
971 List.hd_exn positions |> snd |> Pos.filename
973 let uri =
974 Lsp_helpers.path_string_to_lsp_uri
975 ~default_path:filename
976 filename
978 Lsp.UriMap.add uri positions Lsp.UriMap.empty
981 let () =
982 if lsp_uri_map |> Lsp.UriMap.values |> List.length = 1 then
984 else
985 (* Can a localvar cross file boundaries? I sure hope not. *)
986 let err =
987 Printf.sprintf
988 "Found more than one file when executing find refs for localvar %s"
989 name
991 log "%s" err;
992 HackEventLogger.invariant_violation_bug err;
993 failwith err
995 ClientIdeMessage.Find_refs_success
997 full_name = name;
998 action = None;
999 hint_suffixes = [];
1000 open_file_results = lsp_uri_map;
1002 | Error _action ->
1003 let err =
1004 Printf.sprintf "Failed to find refs for localvar %s" name
1006 log "%s" err;
1007 HackEventLogger.invariant_violation_bug err;
1008 failwith err
1010 (istate, result)
1011 (* clientLsp should raise if we return a LocalVar action *)
1012 | None ->
1013 (* Clicking a line+col that isn't a symbol *)
1014 (istate, ClientIdeMessage.Invalid_symbol)
1015 | Some (name, action) ->
1016 (* Not a localvar, so we do the following:
1017 1) For all open files that we know about in ClientIDEDaemon, uesd the
1018 cached TASTs to return positions of references
1019 2) Return this list, alongside the name and action
1020 3) ClientLSP, upon receiving, will shellout to hh_server
1021 and reject all server-provided positions for files that ClientIDEDaemon
1022 knew about, under the assumption that our cached TAST provides edited
1023 file info, if applicable.
1025 let (istate, single_file_refs) =
1026 List.fold
1027 ~f:(fun (istate, accum) document ->
1028 let (istate, ctx, _entry, _errors) =
1029 update_file_ctx istate document
1031 let stringified_path =
1032 Path.to_string document.ClientIdeMessage.file_path
1034 let filename =
1035 Relative_path.create_detect_prefix stringified_path
1037 let single_file_ref =
1038 ServerFindRefs.go_for_single_file
1039 ~ctx
1040 ~action
1041 ~filename
1042 ~name
1043 ~naming_table:istate.naming_table
1044 |> ServerFindRefs.to_absolute
1046 let urikey =
1047 Lsp_helpers.path_string_to_lsp_uri
1048 stringified_path
1049 ~default_path:stringified_path
1052 let updated_map =
1053 Lsp.UriMap.add urikey single_file_ref accum
1055 (istate, updated_map))
1056 document_list
1057 ~init:(istate, Lsp.UriMap.empty)
1059 let sienv_ref = ref istate.sienv in
1060 let hints =
1061 SymbolIndex.find_refs ~sienv_ref ~action ~max_results:100
1063 let hint_suffixes =
1064 Option.value_map hints ~default:[] ~f:(fun hints ->
1065 List.filter_map hints ~f:(fun path ->
1066 if Relative_path.is_root (Relative_path.prefix path) then
1067 Some (Relative_path.suffix path)
1068 else
1069 None))
1071 ( { istate with sienv = !sienv_ref },
1072 ClientIdeMessage.Find_refs_success
1074 full_name = name;
1075 action = Some action;
1076 hint_suffixes;
1077 open_file_results = single_file_refs;
1078 } ))
1080 (Initialized istate, Ok result)
1081 (* Autocomplete *)
1082 | ( Initialized istate,
1083 Completion
1084 (document, { line; column }, { ClientIdeMessage.is_manually_invoked })
1085 ) ->
1086 (* Update the state of the world with the document as it exists in the IDE *)
1087 let (istate, ctx, entry, _) = update_file_ctx istate document in
1088 let sienv_ref = ref istate.sienv in
1089 let result =
1090 ServerAutoComplete.go_ctx
1091 ~ctx
1092 ~entry
1093 ~sienv_ref
1094 ~is_manually_invoked
1095 ~line
1096 ~column
1097 ~naming_table:istate.naming_table
1099 let istate = { istate with sienv = !sienv_ref } in
1100 (Initialized istate, Ok result)
1101 (* Autocomplete docblock resolve *)
1102 | ( Initialized istate,
1103 Completion_resolve Completion_resolve.{ fullname = symbol; kind } ) ->
1104 HackEventLogger.completion_call ~method_name:"Completion_resolve";
1105 let ctx = make_empty_ctx istate.icommon in
1106 let result = ServerDocblockAt.go_docblock_for_symbol ~ctx ~symbol ~kind in
1107 let signature = ServerAutoComplete.get_signature ctx symbol in
1108 (Initialized istate, Ok Completion_resolve.{ docblock = result; signature })
1109 (* Autocomplete docblock resolve *)
1110 | ( Initialized istate,
1111 Completion_resolve_location (file_path, fullname, { line; column }, kind)
1112 ) ->
1113 (* We're given a location but it often won't be an opened file.
1114 We will only serve autocomplete docblocks as of truth on disk.
1115 Hence, we construct temporary entry to reflect the file which
1116 contained the target of the resolve. *)
1117 HackEventLogger.completion_call ~method_name:"Completion_resolve_location";
1118 let path =
1119 file_path |> Path.to_string |> Relative_path.create_detect_prefix
1121 let ctx = make_empty_ctx istate.icommon in
1122 let (ctx, entry) = Provider_context.add_entry_if_missing ~ctx ~path in
1123 let result =
1124 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1125 ServerDocblockAt.go_docblock_ctx ~ctx ~entry ~line ~column ~kind)
1127 let (Full_name s) = fullname in
1128 let signature = ServerAutoComplete.get_signature ctx s in
1129 (Initialized istate, Ok Completion_resolve.{ docblock = result; signature })
1130 (* Document highlighting *)
1131 | (Initialized istate, Document_highlight (document, { line; column })) ->
1132 let (istate, ctx, entry, _) = update_file_ctx istate document in
1133 let results =
1134 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1135 Ide_highlight_refs.go_quarantined ~ctx ~entry ~line ~column)
1137 (Initialized istate, Ok results)
1138 (* Signature help *)
1139 | (Initialized istate, Signature_help (document, { line; column })) ->
1140 let (istate, ctx, entry, _) = update_file_ctx istate document in
1141 let results =
1142 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1143 ServerSignatureHelp.go_quarantined ~ctx ~entry ~line ~column)
1145 (Initialized istate, Ok results)
1146 (* AutoClose *)
1147 | (Initialized istate, AutoClose (document, { line; column })) ->
1148 let (istate, ctx, entry, _) = update_file_ctx istate document in
1149 let close_tag = AutocloseTags.go_xhp_close_tag ~ctx ~entry ~line ~column in
1150 (Initialized istate, Ok close_tag)
1151 (* Code actions (refactorings, quickfixes) *)
1152 | (Initialized istate, Code_action (document, range)) ->
1153 let (istate, ctx, entry, published_errors_ref) =
1154 update_file_ctx istate document
1157 let results =
1158 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1159 Code_actions_services.go ~ctx ~entry ~range)
1162 (* We'll take this opportunity to make sure we've returned the latest errors.
1163 Why only return errors from didOpen,didChange,didClose,codeAction, and not also all
1164 the other methods like "hover" which might have recomputed TAST+errors? -- simplicity,
1165 mainly -- it's simpler to perform+handle this logic in just a few places rather than
1166 everywhere, and also because codeAction is called so frequently (e.g. upon changing
1167 tabs) that it's the best opportunity we have. *)
1168 let errors = get_user_facing_errors ~ctx ~entry in
1169 let errors_opt =
1170 match !published_errors_ref with
1171 | Some published_errors when phys_equal published_errors errors ->
1172 (* If the previous errors we returned are physically equal, that's an indication
1173 that the entry's TAST+errors hasn't been recomputed since last we returned errors
1174 back to clientLsp, so no need to do anything.
1175 And we actively WANT to do nothing in this case, since codeAction is called so frequently --
1176 e.g. every time the caret moves -- and we wouldn't want errors to be republished that
1177 frequently. *)
1178 None
1179 | Some _
1180 | None ->
1181 (* [Some _] -> This case indicates either that we'd previously returned errors back to clientLsp
1182 but the TAST+errors has changed since then, e.g. maybe the TAST+errors were invalidated
1183 due to a decl change, and some other action like hover recomputed the TAST+errors but
1184 didn't return them to clientLsp (because hover doesn't return errors), and so it's
1185 fallen to us to send them back. Note: only didOpen,didChange,didClose,codeAction
1186 ever return errors back to clientLsp. *)
1187 (* [None] -> This case indicates that we don't have a record of previous errors returned back to clientLsp.
1188 Might happen because a decl change invalidated TAST+errors and we are the first action since
1189 the decl change. Or because for some reason didOpen didn't arrive prior to codeAction. *)
1190 published_errors_ref := Some errors;
1191 Some (Errors.sort_and_finalize errors)
1193 (Initialized istate, Ok (results, errors_opt))
1194 (* Code action resolve (refactorings, quickfixes) *)
1195 | ( Initialized istate,
1196 Code_action_resolve { document; range; resolve_title; use_snippet_edits }
1197 ) ->
1198 let (istate, ctx, entry, _) = update_file_ctx istate document in
1200 let result =
1201 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1202 Code_actions_services.resolve
1203 ~ctx
1204 ~entry
1205 ~range
1206 ~resolve_title
1207 ~use_snippet_edits)
1209 (Initialized istate, Ok result)
1210 (* Go to definition *)
1211 | (Initialized istate, Definition (document, { line; column })) ->
1212 let (istate, ctx, entry, _) = update_file_ctx istate document in
1213 let result =
1214 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1215 ServerGoToDefinition.go_quarantined ~ctx ~entry ~line ~column)
1217 (Initialized istate, Ok result)
1218 (* Type Definition *)
1219 | (Initialized istate, Type_definition (document, { line; column })) ->
1220 let (istate, ctx, entry, _) = update_file_ctx istate document in
1221 let result =
1222 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
1223 ServerTypeDefinition.go_quarantined ~ctx ~entry ~line ~column)
1225 (Initialized istate, Ok result)
1226 (* Workspace Symbol *)
1227 | (Initialized istate, Workspace_symbol query) ->
1228 (* Note: needs reverse-naming-table, hence only works in initialized
1229 state: for top-level queries it needs reverse-naming-table to look
1230 up positions; for member queries "Foo::bar" it needs it to fetch the
1231 decl for Foo. *)
1232 (* Note: we intentionally don't give results from unsaved files *)
1233 let ctx = make_empty_ctx istate.icommon in
1234 let sienv_ref = ref istate.sienv in
1235 let result = Ide_search.go ctx query ~kind_filter:"" sienv_ref in
1236 let istate = { istate with sienv = !sienv_ref } in
1237 (Initialized istate, Ok result)
1239 (** Awaits until the next message is available, and handles it *)
1240 let handle_one_message_exn
1241 ~(out_fd : Lwt_unix.file_descr)
1242 ~(message_queue : message_queue)
1243 ~(state : state) : state option Lwt.t =
1244 dbg_set_activity ~key:"handle" "popping";
1245 let%lwt message = Lwt_message_queue.pop message_queue in
1246 dbg_set_activity
1247 ~key:"handle"
1248 (match message with
1249 | None -> "none"
1250 | Some (GotNamingTable (Ok _)) -> "got_naming_table_ok"
1251 | Some (GotNamingTable (Error _)) -> "got_naming_table_err"
1252 | Some (ClientRequest message) ->
1253 ClientIdeMessage.tracked_t_to_string message);
1254 match (state, message) with
1255 | (_, None) ->
1256 Lwt.return_none (* exit loop if message_queue has been closed *)
1257 | (During_init dstate, Some (GotNamingTable naming_table_result)) ->
1258 let%lwt state = initialize2 out_fd dstate naming_table_result in
1259 Lwt.return_some state
1260 | (_, Some (GotNamingTable _)) ->
1261 failwith ("Unexpected GotNamingTable in " ^ state_to_log_string state)
1262 | (_, Some (ClientRequest { ClientIdeMessage.tracking_id; message })) ->
1263 let unblocked_time = Unix.gettimeofday () in
1264 HackEventLogger.serverless_ide_set_tracking_id tracking_id;
1265 (* Our caller has an exception handler which logs the exception.
1266 But we instead must fulfil our contract of responding to the client,
1267 even if we have an exception. Hence we need our own handler here. *)
1268 let (state, response) =
1269 try handle_request message_queue state tracking_id message with
1270 | WorkerCancel.Worker_should_exit as exn ->
1271 let e = Exception.wrap exn in
1272 (* When is this exception raised? several places during Typing_toplevel
1273 inner-loops call [WorkerCancel.raise_if_stop_requested]. So: it will
1274 be raised during [Tast_provider.compute_tast*] shortly after
1275 ClientLsp has called [WorkerCancel.stop_workers], which it does
1276 if it sees $/cancelRequest LSP notification on the incoming queue. *)
1277 let stack = Exception.get_backtrace_string e |> Exception.clean_stack in
1278 let lsp_error =
1280 Lsp.Error.code = Lsp.Error.RequestCancelled;
1281 message = Exception.get_ctor_string e;
1282 data = Some (Hh_json.JSON_Object [("stack", Hh_json.string_ stack)]);
1285 (state, Error lsp_error)
1286 | exn ->
1287 let e = Exception.wrap exn in
1288 let reason = ClientIdeUtils.make_rich_error "handle_request" ~e in
1289 (state, Error (ClientIdeUtils.to_lsp_error reason))
1291 dbg_set_activity ~key:"handle" "write_response";
1292 let%lwt () =
1293 write_message
1294 ~out_fd
1295 ~message:
1296 ClientIdeMessage.(Response { response; tracking_id; unblocked_time })
1298 dbg_set_activity ~key:"handle" "written_response";
1299 Lwt.return_some state
1301 let serve ~(in_fd : Lwt_unix.file_descr) ~(out_fd : Lwt_unix.file_descr) :
1302 unit Lwt.t =
1303 let rec flush_event_logger () : unit Lwt.t =
1304 dbg_set_activity ~key:"flush" "sleep";
1305 let%lwt () = Lwt_unix.sleep 0.5 in
1306 HackEventLogger.Memory.profile_if_needed ();
1307 dbg_set_activity ~key:"flush" "flush";
1308 Lwt.async EventLoggerLwt.flush;
1309 dbg_set_activity ~key:"flush" "recheck";
1310 EventLogger.recheck_disk_files ();
1311 flush_event_logger ()
1313 let rec pump_stdin (message_queue : message_queue) : unit Lwt.t =
1314 dbg_set_activity ~key:"pump" "loop";
1315 let%lwt (message, is_queue_open) =
1316 try%lwt
1317 dbg_set_activity ~key:"pump" "from_fd";
1318 let%lwt { ClientIdeMessage.tracking_id; message } =
1319 Marshal_tools_lwt.from_fd_with_preamble in_fd
1321 dbg_set_activity ~key:"pump" "push";
1322 let is_queue_open =
1323 Lwt_message_queue.push
1324 message_queue
1325 (ClientRequest { ClientIdeMessage.tracking_id; message })
1327 Lwt.return (message, is_queue_open)
1328 with
1329 | End_of_file ->
1330 (* There are two proper ways to close clientIdeDaemon: (1) by sending it a Shutdown message
1331 over the FD which is handled above, or (2) by closing the FD which is handled here. Neither
1332 path is considered anomalous and neither will raise an exception.
1333 Note that closing the message-queue is how we tell handle_messages loop to terminate. *)
1334 dbg_set_activity ~key:"pump" "eof";
1335 Lwt_message_queue.close message_queue;
1336 Lwt.return (ClientIdeMessage.Shutdown (), false)
1337 | exn ->
1338 let e = Exception.wrap exn in
1339 dbg_set_activity ~key:"pump" ("exn " ^ Exception.get_ctor_string e);
1340 Lwt_message_queue.close message_queue;
1341 Exception.reraise e
1343 match message with
1344 | ClientIdeMessage.Shutdown () -> Lwt.return_unit
1345 | _ when not is_queue_open -> Lwt.return_unit
1346 | _ ->
1347 (* Care! The following call should be tail recursive otherwise we'll get huge callstacks.
1348 The [@tailcall] attribute would normally enforce this, but doesn't help the presence of lwt. *)
1349 (pump_stdin [@tailcall]) message_queue
1351 let rec handle_messages ({ message_queue; state } : t) : unit Lwt.t =
1352 dbg_set_activity ~key:"handle" "loop";
1353 let%lwt next_state_opt =
1354 try%lwt
1355 let%lwt state = handle_one_message_exn ~out_fd ~message_queue ~state in
1356 dbg_set_activity ~key:"handle" "done";
1357 Lwt.return state
1358 with
1359 | exn ->
1360 let e = Exception.wrap exn in
1361 let is_write_error = is_outfd_write_error e in
1362 dbg_set_activity
1363 ~key:"handle"
1364 (Printf.sprintf
1365 "exn %s; is_write_error=%b"
1366 (Exception.get_ctor_string e)
1367 is_write_error);
1368 ClientIdeUtils.log_bug "handle_one_message" ~e ~telemetry:true;
1369 if is_write_error then exit 1;
1370 (* if out_fd is down then there's no use continuing. *)
1371 dbg_set_activity ~key:"handle" "exn continue";
1372 Lwt.return_some state
1374 match next_state_opt with
1375 | None -> Lwt.return_unit (* exit loop *)
1376 | Some state ->
1377 (* Care! The following call should be tail recursive otherwise we'll get huge callstacks.
1378 The [@tailcall] attribute would normally enforce this, but doesn't help the presence of lwt. *)
1379 (handle_messages [@tailcall]) { message_queue; state }
1381 try%lwt
1382 dbg_set_activity ~key:"main" "serve";
1383 let message_queue = Lwt_message_queue.create () in
1384 let flusher_promise = flush_event_logger () in
1385 let%lwt () = handle_messages { message_queue; state = Pending_init }
1386 and () = pump_stdin message_queue in
1387 dbg_set_activity ~key:"main" "ending";
1388 Lwt.cancel flusher_promise;
1389 Lwt.return_unit
1390 with
1391 | exn ->
1392 let e = Exception.wrap exn in
1393 ClientIdeUtils.log_bug "fatal clientIdeDaemon" ~e ~telemetry:true;
1394 dbg_set_activity ~key:"main" ("exception " ^ Exception.get_ctor_string e);
1395 Lwt.return_unit
1397 let daemon_main
1398 (args : ClientIdeMessage.daemon_args)
1399 (channels : ('a, 'b) Daemon.channel_pair) : unit =
1400 Folly.ensure_folly_init ();
1401 Printexc.record_backtrace true;
1402 dbg_set_activity ~key:"main" "daemon_main";
1403 let (ic, oc) = channels in
1404 let in_fd = Lwt_unix.of_unix_file_descr (Daemon.descr_of_in_channel ic) in
1405 let out_fd = Lwt_unix.of_unix_file_descr (Daemon.descr_of_out_channel oc) in
1406 let daemon_init_id =
1407 Printf.sprintf
1408 "%s.%s"
1409 args.ClientIdeMessage.init_id
1410 (Random_id.short_string ())
1412 HackEventLogger.serverless_ide_init ~init_id:daemon_init_id;
1414 Typing_log.out_channel := stderr;
1415 (* where 'hh_show' goes *)
1416 if args.ClientIdeMessage.verbose_to_stderr then
1417 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Debug
1418 else
1419 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Error;
1420 if args.ClientIdeMessage.verbose_to_file then
1421 Hh_logger.Level.set_min_level_file Hh_logger.Level.Debug
1422 else
1423 Hh_logger.Level.set_min_level_file Hh_logger.Level.Info;
1425 (* in hh_shared.c, worker_id=0 is used for main process, and _id=1 for the first worker. *)
1426 SharedMem.connect args.ClientIdeMessage.shm_handle ~worker_id:1;
1428 Stdlib.at_exit (fun () ->
1430 let activities = dbg_dump_activity () in
1431 Hh_logger.log "SERVERLESS_IDE_EXIT\n%s" activities;
1432 HackEventLogger.serverless_ide_exit activities
1433 with
1434 | _ -> ());
1436 dbg_set_activity ~key:"main" "run_main";
1437 Lwt_utils.run_main (fun () -> serve ~in_fd ~out_fd);
1438 dbg_set_activity ~key:"main" "done";
1439 Hh_logger.log "SERVERLESS_IDE_DONE(ok)";
1440 HackEventLogger.serverless_ide_done None
1441 with
1442 | exn ->
1443 let e = Exception.wrap exn in
1444 dbg_set_activity ~key:"main" ("exn " ^ Exception.get_ctor_string e);
1445 Hh_logger.log
1446 "SERVERLESS_IDE_DONE(exn)\n%s"
1447 (Exception.to_string e |> Exception.clean_stack);
1448 HackEventLogger.serverless_ide_done (Some e)
1450 let daemon_entry_point : (ClientIdeMessage.daemon_args, unit, unit) Daemon.entry
1452 Daemon.register_entry_point "ClientIdeService" daemon_main
1454 module Test = struct
1455 type env = istate
1457 let init ~custom_config ~naming_sqlite =
1458 let config =
1459 Option.value custom_config ~default:ServerConfig.default_config
1461 let local_config = ServerLocalConfig.default in
1462 let tcopt = ServerConfig.typechecker_options config in
1463 let popt = ServerConfig.parser_options config in
1464 Provider_backend.set_local_memory_backend_with_defaults_for_test ();
1465 let local_memory =
1466 match Provider_backend.get () with
1467 | Provider_backend.Local_memory local_memory -> local_memory
1468 | _ -> failwith "expected local memory backend"
1470 let sienv =
1471 SymbolIndex.initialize
1472 ~gleanopt:(ServerConfig.glean_options config)
1473 ~namespace_map:tcopt.GlobalOptions.po.ParserOptions.auto_namespace_map
1474 ~provider_name:
1475 local_config.ServerLocalConfig.ide_symbolindex_search_provider
1476 ~quiet:local_config.ServerLocalConfig.symbolindex_quiet
1478 let ctx =
1479 Provider_context.empty_for_tool
1480 ~popt
1481 ~tcopt
1482 ~backend:(Provider_backend.Local_memory local_memory)
1483 ~deps_mode:(Typing_deps_mode.InMemoryMode None)
1485 let naming_table =
1486 Naming_table.load_from_sqlite ctx (Path.to_string naming_sqlite)
1489 naming_table;
1490 sienv;
1491 icommon = { hhi_root = Path.make "/"; config; local_config; local_memory };
1492 iopen_files = Relative_path.Map.empty;
1495 let index istate changes =
1496 Hh_logger.log
1497 "--> [index] %s"
1498 (Relative_path.Set.elements changes
1499 |> List.map ~f:Relative_path.suffix
1500 |> String.concat ~sep:" ");
1501 let (naming_table, sienv) =
1502 batch_update_naming_table_and_invalidate_caches
1503 ~ctx:(make_empty_ctx istate.icommon)
1504 ~naming_table:istate.naming_table
1505 ~sienv:istate.sienv
1506 ~local_memory:istate.icommon.local_memory
1507 ~open_files:istate.iopen_files
1508 changes
1510 { istate with naming_table; sienv }
1512 let handle istate message =
1513 Hh_logger.log "--> %s" (ClientIdeMessage.t_to_string message);
1514 let message_queue = Lwt_message_queue.create () in
1515 match
1516 handle_request message_queue (Initialized istate) "tracking_id" message
1517 with
1518 | (Initialized istate, Ok response) -> (istate, response)
1519 | (_, Error { Lsp.Error.code; message; data }) ->
1520 let msg =
1521 Printf.sprintf
1522 "handle_request %s: %s %s"
1523 (Lsp.Error.show_code code)
1524 message
1525 (Option.value_map data ~default:"" ~f:Hh_json.json_to_multiline)
1527 failwith msg
1528 | (_, Ok _) ->
1529 let msg = Printf.sprintf "handle_request ended in bad state" in
1530 failwith msg