HackEventLogger.Memory.profile_if_needed
[hiphop-php.git] / hphp / hack / src / client / ide_service / clientIdeDaemon.ml
blobb8898e22bf59f92ea37d41f9814041942e80fedc
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 (** This is the result type from attempting to load saved-state.
13 In the error case, [stopped_reason] is a human-facing response,
14 and [Lsp.Error.t] contains structured telemetry data. *)
15 type load_saved_state_result =
16 ( Naming_table.t * Saved_state_loader.changed_files,
17 ClientIdeMessage.stopped_reason * Lsp.Error.t )
18 result
20 (** These are messages on ClientIdeDaemon's internal message-queue *)
21 type message =
22 | ClientRequest : 'a ClientIdeMessage.tracked_t -> message
23 (** ClientRequest came from ClientIdeService over stdin;
24 it expects a response. *)
25 | LoadedState : load_saved_state_result -> message
26 (** LoadedState is posted from within ClientIdeDaemon itself once
27 our attempt at loading saved-state has finished; it's picked
28 up by handle_messages. *)
30 type message_queue = message Lwt_message_queue.t
32 exception Outfd_write_error
34 let is_outfd_write_error (exn : Exception.t) : bool =
35 match Exception.unwrap exn with
36 | Outfd_write_error -> true
37 | _ -> false
39 (** istate, "initialized state", is the state the daemon after it has
40 finished initialization (i.e. finished loading saved state),
41 concerning these data-structures:
42 1. forward-naming-table-delta stored in naming_table
43 2. reverse-naming-table-delta-and-cache stored in local_memory
44 3. entries with source text, stored in open_files
45 3. cached ASTs and TASTs, stored in open_files
46 4. shallow-decl-cache, folded-decl-cache, linearization-cache stored in local-memory
48 There are two concepts to understand.
49 1. "Singleton context" (ctx). When processing IDE requests for a file, we create
50 a context object in which that entry's source text and AST and TAST
51 are present in the context, and no others are.
52 2. "Quarantine with respect to an entry". We enter a quarantine while
53 computing the TAST for a singleton context entry. The invariants within
54 the quarantine are different from those without.
56 The key algorithms which read from these data-structures are:
57 1. Ast_provider.get_ast will fetch the cached AST for an entry in ctx, or if
58 the entry is present but as yet lacks an AST then it will parse and cache,
59 or if it's lookinng for the AST of a file not in ctx then it will parse
60 off disk but decline to cache.
61 2. Naming_provider.get_* will get_ast for ctx entry to see if symbol is there.
62 If not it will look in reverse-delta-and-cache or read from sqlite
63 and store the answer back in reverse-delta-and-cache. But if the answer
64 to that fallback was a file in ctx, then it will say that the symbol's
65 not defined.
66 3. Shallow_classes_provider.get_* will look it up in shallow-decl-cache, and otherwise
67 will ask Naming_provider and Ast_provider for the AST, will compute shallow decl,
68 and will store it in shallow-decl-cache
69 4. Linearization_provider.get_* will look it up in linearization-cache. The
70 decl_provider reads and writes linearizations via the linearization_provider.
71 5. Decl_provider.get_* will look it up in folded-decl-cache, computing it if
72 not there using shallow and linearization provider, and store it back in folded-decl-cache
73 6. Tast_provider.compute* is only ever called on entries. It returns the cached
74 TAST if present; otherwise, it runs normal type-checking-and-inference, relies
75 upon all the other providers, and writes the answer back in the entry's TAST cache.
77 The invariants for forward and reverse naming tables:
78 1. These tables only ever reflect truth about disk files; they are unaffected
79 by open_file entries.
80 2. They are updated asynchronously by update_naming_tables_for_changed_file_lwt
81 in response to DidChangeWatchedFile events. Thus, we might be asked to fetch
82 a shallow decl even before the naming-tables have been fully updated.
83 We might for instance read the naming-table and try to fetch a shallow
84 decl from a file that doesn't even exist on disk any more.
86 The invariants for AST, TAST, shallow, folded-decl and linearization caches:
87 1. AST, if present, reflects the AST of its entry's source text,
88 and is a "full" AST (not decl-only), and has errors.
89 2. TAST, if present, reflects the TAST of its entry's source text computed
90 against the on-disk state of all other files
91 3. Outside a quarantine, all entries in shallow cache are correct as of disk
92 (at least as far as asynchronous file updates have been processed).
93 4. Likewise, all entries in folded+linearization caches are correct as
94 of disk.
95 5. We only ever enter quarantine with respect to one single entry.
96 For the duration of the quarantine, an AST for that entry,
97 if present, is correct as of the entry's source text.
98 6. Likewise any shallow decls for an entry are correct as of its source text.
99 Moreover, if shallow decls for an entry are present, then the entry's AST
100 is present and contains those symbols.
101 7. Any shallow decls not for the entry are correct as of disk.
102 8. During quarantine, the shallow-decl of all other files is correct as of disk.
103 9. The entry's TAST, along with every single decl and linearization,
104 are correct as of this entry's source text plus every other file off disk.
106 Here are the algorithms we use that satisfy those invariants.
107 1. Upon a disk-file-change, we invalidate all TASTs (satisfying invariant 2).
108 We use the forward-naming-table to find all "old" symbols that were
109 defined in the file prior to the disk change, and invalidate those
110 shallow decls (satisfying invariant 3). We invalidate all
111 folded+linearization caches (satisfying invariant 4). Invariant 1 is N/A.
112 2. Upon an editor change to a file, we invalidate the entry's AST and TAST
113 (satisfying invariant 1).
114 3. Upon request for a TAST of a file, we create a singleton context for
115 that entry, and enter quarantine as follows. We parse the file and
116 cache its AST and invalidate shallow decls for all symbols inside
117 this new AST (satisfying invariant 6). We invalidate all decls
118 and linearizations (satisfying invariant 9). Subsequent fetches,
119 thanks to the "key algorithms for reading these datastructures" (above)
120 will only cache things in accordance with invariants 6,7,8,9.
121 4. We leave quarantine as follows. We invalidate shallow decls for
122 all symbols in the entry's AST; thanks to invariant 5, this will
123 fulfill invariant 3. We invalidate all decls and linearizations
124 (satisfying invariant 4).
126 type istate = {
127 icommon: common_state;
128 ifiles: open_files_state;
129 naming_table: Naming_table.t;
130 (** the forward-naming-table is constructed during initialize and updated
131 during process_changed_files. It stores an in-memory map of FileInfos that
132 have changed since sqlite. When a file is changed on disk, we need this to
133 know which shallow decls to invalidate. Note: while the forward-naming-table
134 is stored here, the reverse-naming-table is instead stored in ctx. *)
137 (** dstate, "during_init state", is the state the daemon after it has received an
138 init message (and has parsed config files to get popt/tcopt, has initialized
139 glean, as written out hhi files) but before it has loaded saved-state or processed
140 file updates. *)
141 and dstate = {
142 start_time: float;
143 (** When did we kick off the attempt to load saved-state? *)
144 dcommon: common_state;
145 dfiles: open_files_state;
148 and common_state = {
149 hhi_root: Path.t;
150 (** hhi_root files are written during initialize, deleted at shutdown, and
151 refreshed periodically in case the tmp-cleaner has deleted them. *)
152 sienv: SearchUtils.si_env;
153 (** sienv provides autocomplete and find-symbols. It is constructed during
154 initialization and stores a few in-memory structures such as namespace-list,
155 plus in-memory deltas. It is also updated during process_changed_files. *)
156 popt: ParserOptions.t; (** parser options *)
157 tcopt: TypecheckerOptions.t; (** typechecker options *)
158 local_memory: Provider_backend.local_memory;
159 (** Local_memory backend; includes decl caches *)
162 and open_files_state = {
163 open_files: Provider_context.entries;
164 (** all open files, along with caches of their ASTs and TASTs and errors *)
165 changed_files_to_process: Relative_path.Set.t;
166 (** changed_files_to_process is grown during File_changed events, and steadily
167 whittled down one by one in `serve` as we get around to processing them
168 via `process_changed_files`. *)
169 changed_files_denominator: int;
170 (** the user likes to see '5/10' for how many changed files has been processed
171 in the current batch of changes. The denominator counts up for every new file
172 that has to be processed, until the batch ends - i.e. changed_files_to_process
173 becomes empty - and we reset the denominator. *)
176 type state =
177 | Pending_init (** We haven't yet received init request *)
178 | During_init of dstate (** We're working on the init request *)
179 | Initialized of istate (** Finished work on init request *)
180 | Failed_init of Lsp.Error.t (** Failed request, with root cause *)
182 type t = {
183 message_queue: message_queue;
184 state: state;
187 let state_to_log_string (state : state) : string =
188 let files_to_log_string (files : open_files_state) : string =
189 Printf.sprintf
190 "%d open_files; %d changed_files_to_process"
191 (Relative_path.Map.cardinal files.open_files)
192 (Relative_path.Set.cardinal files.changed_files_to_process)
194 match state with
195 | Pending_init -> "Pending_init"
196 | During_init { dfiles; _ } ->
197 Printf.sprintf "During_init(%s)" (files_to_log_string dfiles)
198 | Initialized { ifiles; _ } ->
199 Printf.sprintf "Initialized(%s)" (files_to_log_string ifiles)
200 | Failed_init e -> Printf.sprintf "Failed_init(%s)" e.Lsp.Error.message
202 let log s = Hh_logger.log ("[ide-daemon] " ^^ s)
204 let log_debug s = Hh_logger.debug ("[ide-daemon] " ^^ s)
206 let set_up_hh_logger_for_client_ide_service (root : Path.t) : unit =
207 (* Log to a file on disk. Note that calls to `Hh_logger` will always write to
208 `stderr`; this is in addition to that. *)
209 let client_ide_log_fn = ServerFiles.client_ide_log root in
210 begin
211 try Sys.rename client_ide_log_fn (client_ide_log_fn ^ ".old")
212 with _e -> ()
213 end;
214 Hh_logger.set_log client_ide_log_fn;
215 log "Starting client IDE service at %s" client_ide_log_fn
217 let write_message
218 ~(out_fd : Lwt_unix.file_descr)
219 ~(message : ClientIdeMessage.message_from_daemon) : unit Lwt.t =
220 try%lwt
221 let%lwt (_ : int) = Marshal_tools_lwt.to_fd_with_preamble out_fd message in
222 Lwt.return_unit
223 with Unix.Unix_error (Unix.EPIPE, _, _) -> raise Outfd_write_error
225 let load_saved_state
226 (ctx : Provider_context.t)
227 ~(root : Path.t)
228 ~(naming_table_load_info :
229 ClientIdeMessage.Initialize_from_saved_state.naming_table_load_info
230 option) : load_saved_state_result Lwt.t =
231 log "[saved-state] Starting load in root %s" (Path.to_string root);
232 let%lwt result =
233 try%lwt
234 let%lwt result =
235 match naming_table_load_info with
236 | Some naming_table_load_info ->
237 let open ClientIdeMessage.Initialize_from_saved_state in
238 (* tests may wish to pretend there's a delay *)
239 let%lwt () =
240 if Float.(naming_table_load_info.test_delay > 0.0) then
241 Lwt_unix.sleep naming_table_load_info.test_delay
242 else
243 Lwt.return_unit
245 (* Assume that there are no changed files on disk if we're getting
246 passed the path to the saved-state directly, and that the saved-state
247 corresponds to the current state of the world. *)
248 let changed_files = [] in
249 (* Test hook, for tests that want to get messages in before init *)
250 Lwt.return_ok
252 Saved_state_loader.main_artifacts =
254 Saved_state_loader.Naming_table_info.naming_table_path =
255 naming_table_load_info.path;
257 additional_info = ();
258 changed_files;
259 manifold_path = "<not provided>";
260 corresponding_rev = "<not provided>";
261 mergebase_rev = "<not provided>";
262 is_cached = true;
264 | None ->
265 let%lwt result =
266 State_loader_lwt.load
267 ~watchman_opts:
268 Saved_state_loader.Watchman_options.{ root; sockname = None }
269 ~ignore_hh_version:false
270 ~saved_state_type:Saved_state_loader.Naming_table
272 Lwt.return result
274 match result with
275 | Ok { Saved_state_loader.main_artifacts; changed_files; _ } ->
276 let path =
277 Path.to_string
278 main_artifacts
279 .Saved_state_loader.Naming_table_info.naming_table_path
281 log "[saved-state] Loading naming-table... %s" path;
282 let naming_table = Naming_table.load_from_sqlite ctx path in
283 log "[saved-state] Loaded naming-table.";
284 (* Track how many files we have to change locally *)
285 HackEventLogger.serverless_ide_local_files
286 ~local_file_count:(List.length changed_files);
288 Lwt.return_ok (naming_table, changed_files)
289 | Error load_error ->
290 (* We'll turn that load_error into a user-facing [reason], and a
291 programmatic error [e] for future telemetry *)
292 let reason =
293 ClientIdeMessage.
295 short_user_message =
296 Saved_state_loader.short_user_message_of_error load_error;
297 medium_user_message =
298 Saved_state_loader.medium_user_message_of_error load_error;
299 long_user_message =
300 Saved_state_loader.long_user_message_of_error load_error;
301 debug_details =
302 Saved_state_loader.debug_details_of_error load_error;
303 is_actionable = Saved_state_loader.is_error_actionable load_error;
306 let e =
308 Lsp.Error.code = Lsp.Error.UnknownErrorCode;
309 message = reason.ClientIdeMessage.medium_user_message;
310 data =
311 Some
312 (Hh_json.JSON_Object
314 ( "debug_details",
315 Hh_json.string_ reason.ClientIdeMessage.debug_details );
319 Lwt.return_error (reason, e)
320 with exn ->
321 let exn = Exception.wrap exn in
322 ClientIdeUtils.log_bug "load_exn" ~exn ~telemetry:false;
323 (* We need both a user-facing "reason" and an internal error "e" *)
324 let reason = ClientIdeUtils.make_bug_reason "load_exn" ~exn in
325 let e = ClientIdeUtils.make_bug_error "load_exn" ~exn in
326 Lwt.return_error (reason, e)
328 Lwt.return result
330 let log_startup_time (component : string) (start_time : float) : float =
331 let now = Unix.gettimeofday () in
332 HackEventLogger.serverless_ide_startup ~component ~start_time;
335 let restore_hhi_root_if_necessary (istate : istate) : istate =
336 if Sys.file_exists (Path.to_string istate.icommon.hhi_root) then
337 istate
338 else
339 (* Some processes may clean up the temporary HHI directory we're using.
340 Assume that such a process has deleted the directory, and re-write the HHI
341 files to disk. *)
342 let hhi_root = Hhi.get_hhi_root ~force_write:true () in
344 "Old hhi root %s no longer exists. Creating a new hhi root at %s"
345 (Path.to_string istate.icommon.hhi_root)
346 (Path.to_string hhi_root);
347 Relative_path.set_path_prefix Relative_path.Hhi hhi_root;
348 { istate with icommon = { istate.icommon with hhi_root } }
350 (** Deletes the hhi files we've created. *)
351 let remove_hhi (state : state) : unit =
352 match state with
353 | Pending_init
354 | Failed_init _ ->
356 | During_init { dcommon = { hhi_root; _ }; _ }
357 | Initialized { icommon = { hhi_root; _ }; _ } ->
358 let hhi_root = Path.to_string hhi_root in
359 log "Removing hhi directory %s..." hhi_root;
360 (try Sys_utils.rm_dir_tree hhi_root
361 with exn ->
362 let exn = Exception.wrap exn in
363 ClientIdeUtils.log_bug "remove_hhi" ~exn ~telemetry:true)
365 (** initialize1 is called by handle_request upon receipt of an "init"
366 message from the client. It is synchronous. It sets up global variables and
367 glean. The remainder of init work will happen after we return... our caller
368 handle_request will kick off async work to load saved-state, and once done
369 it will stick a LoadedState message into the queue, and handle_one_message
370 will subsequently pick up that message and call [initialize2]. *)
371 let initialize1 (param : ClientIdeMessage.Initialize_from_saved_state.t) :
372 dstate =
373 log_debug "initialize1";
374 let open ClientIdeMessage.Initialize_from_saved_state in
375 let start_time = Unix.gettimeofday () in
376 HackEventLogger.serverless_ide_set_root param.root;
377 set_up_hh_logger_for_client_ide_service param.root;
379 Relative_path.set_path_prefix Relative_path.Root param.root;
380 let hhi_root = Hhi.get_hhi_root () in
381 log "Extracted hhi files to directory %s" (Path.to_string hhi_root);
382 Relative_path.set_path_prefix Relative_path.Hhi hhi_root;
383 Relative_path.set_path_prefix Relative_path.Tmp (Path.make "/tmp");
385 let server_args =
386 ServerArgs.default_options_with_check_mode ~root:(Path.to_string param.root)
388 let server_args = ServerArgs.set_config server_args param.config in
389 let (server_config, server_local_config) =
390 ServerConfig.load ~silent:true ServerConfig.filename server_args
392 let hhconfig_version =
393 server_config |> ServerConfig.version |> Config_file.version_to_string_opt
395 HackEventLogger.set_hhconfig_version hhconfig_version;
397 Provider_backend.set_local_memory_backend_with_defaults ();
398 let local_memory =
399 match Provider_backend.get () with
400 | Provider_backend.Local_memory local_memory -> local_memory
401 | _ -> failwith "expected local memory backend"
404 (* Use server_config to modify server_env with the correct symbol index *)
405 let genv =
406 ServerEnvBuild.make_genv server_args server_config server_local_config []
408 let init_id = Random_id.short_string () in
409 let { ServerEnv.tcopt; popt; gleanopt; _ } =
410 (* TODO(hverr): Figure out 64-bit mode *)
411 ServerEnvBuild.make_env
412 ~init_id
413 ~deps_mode:Typing_deps_mode.SQLiteMode
414 genv.ServerEnv.config
417 (* We need shallow class declarations so that we can invalidate individual
418 members in a class hierarchy. *)
419 let tcopt = { tcopt with GlobalOptions.tco_shallow_class_decl = true } in
421 let start_time = log_startup_time "basic_startup" start_time in
422 let sienv =
423 SymbolIndex.initialize
424 ~globalrev:None
425 ~gleanopt
426 ~namespace_map:(GlobalOptions.po_auto_namespace_map tcopt)
427 ~provider_name:
428 server_local_config.ServerLocalConfig.symbolindex_search_provider
429 ~quiet:server_local_config.ServerLocalConfig.symbolindex_quiet
430 ~ignore_hh_version:false
431 ~savedstate_file_opt:
432 server_local_config.ServerLocalConfig.symbolindex_file
433 ~workers:None
435 let sienv =
437 sienv with
438 SearchUtils.sie_log_timings = true;
439 SearchUtils.use_ranked_autocomplete = param.use_ranked_autocomplete;
442 if param.use_ranked_autocomplete then AutocompleteRankService.initialize ();
443 let start_time = log_startup_time "symbol_index" start_time in
444 (* We only ever serve requests on files that are open. That's why our caller
445 passes an initial list of open files, the ones already open in the editor
446 at the time we were launched. We don't actually care about their contents
447 at this stage, since updated contents will be delivered upon each request.
448 (and indeed it's pointless to waste time reading existing contents off disk).
449 All we care is that every open file is listed in 'open_files'. *)
450 let open_files =
451 param.open_files
452 |> List.map ~f:(fun path ->
453 path |> Path.to_string |> Relative_path.create_detect_prefix)
454 |> List.map ~f:(fun path ->
455 ( path,
456 Provider_context.make_entry
457 ~path
458 ~contents:Provider_context.Raise_exn_on_attempt_to_read ))
459 |> Relative_path.Map.of_list
461 log_debug "initialize1.done";
463 start_time;
464 dcommon = { hhi_root; sienv; popt; tcopt; local_memory };
465 dfiles =
467 open_files;
468 changed_files_to_process = Relative_path.Set.empty;
469 changed_files_denominator = 0;
473 (** initialize2 is called by handle_one_message upon receipt of a
474 [LoadedState] message. It sends the appropriate message on to the
475 client, and transitions into either [Initialized] or [Failed_init]
476 state. *)
477 let initialize2
478 (out_fd : Lwt_unix.file_descr)
479 (dstate : dstate)
480 (load_state_result : load_saved_state_result) : state Lwt.t =
481 let (_ : float) = log_startup_time "saved_state" dstate.start_time in
482 log_debug "initialize2";
483 match load_state_result with
484 | Ok (naming_table, changed_files) ->
485 let changed_files_to_process =
486 Relative_path.Set.union
487 dstate.dfiles.changed_files_to_process
488 (Relative_path.Set.of_list changed_files)
490 let changed_files_denominator =
491 Relative_path.Set.cardinal changed_files_to_process
493 let p = { ClientIdeMessage.Processing_files.total = 0; processed = 0 } in
494 let%lwt () =
495 write_message
496 ~out_fd
497 ~message:
498 (ClientIdeMessage.Notification (ClientIdeMessage.Done_init (Ok p)))
500 let istate =
502 naming_table;
503 icommon = dstate.dcommon;
504 ifiles =
506 open_files = dstate.dfiles.open_files;
507 changed_files_to_process;
508 changed_files_denominator;
512 log_debug "initialize2.done";
513 Lwt.return (Initialized istate)
514 | Error (reason, e) ->
515 log_debug "initialize2.error";
516 let%lwt () =
517 write_message
518 ~out_fd
519 ~message:
520 (ClientIdeMessage.Notification
521 (ClientIdeMessage.Done_init (Error reason)))
523 remove_hhi (During_init dstate);
524 Lwt.return (Failed_init e)
526 (** An empty ctx with no entries *)
527 let make_empty_ctx (istate : istate) : Provider_context.t =
528 (* TODO(hverr): Support 64-bit *)
529 Provider_context.empty_for_tool
530 ~popt:istate.icommon.popt
531 ~tcopt:istate.icommon.tcopt
532 ~backend:(Provider_backend.Local_memory istate.icommon.local_memory)
533 ~deps_mode:Typing_deps_mode.SQLiteMode
535 (** Constructs a temporary ctx with just one entry. *)
536 let make_singleton_ctx (istate : istate) (entry : Provider_context.entry) :
537 Provider_context.t =
538 let ctx = make_empty_ctx istate in
539 let ctx = Provider_context.add_or_overwrite_entry ~ctx entry in
542 (** This funtion is about papering over a bug. Sometimes, rarely, we're
543 failing to receive DidOpen messages from clientLsp. Our model is to
544 only ever answer IDE requests on open files, so we know we'll eventually
545 reveive a DidClose even for them and be able to clear their TAST cache
546 at that time. But for now, to paper over the bug, we'll call this
547 function to log the event and we'll assume that we just missed a DidOpen. *)
548 let log_missing_open_file_BUG (path : Relative_path.t) : unit =
549 let path = Relative_path.to_absolute path in
550 let message = Printf.sprintf "Error: action on non-open file %s" path in
551 ClientIdeUtils.log_bug message ~telemetry:true
553 (** Opens a file, in response to DidOpen event, by putting in a new
554 entry in open_files, with empty AST and TAST. If the LSP client
555 happened to send us two DidOpens for a file, well, we won't complain. *)
556 let open_file
557 (files : open_files_state) (path : Relative_path.t) (contents : string) :
558 open_files_state =
559 let entry =
560 Provider_context.make_entry
561 ~path
562 ~contents:(Provider_context.Provided_contents contents)
564 let open_files = Relative_path.Map.add files.open_files path entry in
565 { files with open_files }
567 (** Changes a file, in response to DidChange event. For future we
568 might switch ClientIdeDaemon to incremental change events. But for
569 now, this is basically a no-op just with some error checking. *)
570 let change_file (files : open_files_state) (path : Relative_path.t) :
571 open_files_state =
572 if Relative_path.Map.mem files.open_files path then
573 files
574 else
575 (* We'll now mark the file as opened. We'll provide empty contents for now;
576 this doesn't matter since every actual future request for the file will provide
577 actual contents. *)
578 let () = log_missing_open_file_BUG path in
579 open_file files path ""
581 (** Closes a file, in response to DidClose event, by removing the
582 entry in open_files. If the LSP client sents us multile DidCloses,
583 or DidClose for an unopen file, we won't complain. *)
584 let close_file (files : open_files_state) (path : Relative_path.t) :
585 open_files_state =
586 let open_files = Relative_path.Map.remove files.open_files path in
587 { files with open_files }
589 (** Updates an existing opened file, with new contents; if the
590 contents haven't changed then the existing open file's AST and TAST
591 will be left intact; if the file wasn't already open then we
592 throw an exception. *)
593 let update_file
594 (files : open_files_state)
595 (document_location : ClientIdeMessage.document_location) :
596 open_files_state * Provider_context.entry =
597 let path =
598 document_location.ClientIdeMessage.file_path
599 |> Path.to_string
600 |> Relative_path.create_detect_prefix
602 let entry =
603 match
604 ( document_location.ClientIdeMessage.file_contents,
605 Relative_path.Map.find_opt files.open_files path )
606 with
607 | (Some contents, None) ->
608 log_missing_open_file_BUG path;
609 (* TODO(ljw): failwith "Attempted LSP operation on a non-open file" *)
610 Provider_context.make_entry
611 ~path
612 ~contents:(Provider_context.Provided_contents contents)
613 | (None, None) ->
614 log_missing_open_file_BUG path;
615 failwith "Attempted LSP operation on a non-open file"
616 | (Some contents, Some entry)
617 when Option.equal
618 String.equal
619 (Some contents)
620 (Provider_context.get_file_contents_if_present entry) ->
621 entry
622 | (None, Some entry) -> entry
623 | (Some contents, _) ->
624 Provider_context.make_entry
625 ~path
626 ~contents:(Provider_context.Provided_contents contents)
628 let open_files = Relative_path.Map.add files.open_files path entry in
629 ({ files with open_files }, entry)
631 (** like [update_file], but for convenience also produces a ctx for
632 use in typechecking. Also ensures that hhi files haven't been deleted
633 by tmp_cleaner, so that type-checking will succeed. *)
634 let update_file_ctx
635 (istate : istate) (document_location : ClientIdeMessage.document_location) :
636 state * Provider_context.t * Provider_context.entry =
637 let istate = restore_hhi_root_if_necessary istate in
638 let (ifiles, entry) = update_file istate.ifiles document_location in
639 let ctx = make_singleton_ctx istate entry in
640 (Initialized { istate with ifiles }, ctx, entry)
642 (** Simple helper. It updates the [ifiles] or [dfiles] member of Initialized
643 or During_init states, respectively. Will throw if you call it on any other
644 state. *)
645 let update_state_files (state : state) (files : open_files_state) : state =
646 match state with
647 | During_init dstate -> During_init { dstate with dfiles = files }
648 | Initialized istate -> Initialized { istate with ifiles = files }
649 | _ -> failwith ("Update_state_files: unexpected " ^ state_to_log_string state)
651 (** handle_request invariants: Messages are only ever handled serially; we never
652 handle one message while another is being handled. It is a bug if the client sends
653 anything other than [Initialize_from_saved_state] as its first message. Upon
654 receipt+processing of this we transition from [Pre_init] to [During_init]
655 and kick off some async work to load saved state. During this async work, i.e.
656 during [During_init], we are able to handle a few requests but will reject
657 others. Our caller [handle_one_message] is actually the one that transitions
658 us from [During_init] to either [Failed_init] or [Initialized]. Once in one
659 of those states, we never thereafter transition state. *)
660 let handle_request :
661 type a.
662 message_queue ->
663 state ->
664 string ->
665 a ClientIdeMessage.t ->
666 (state * (a, Lsp.Error.t) result) Lwt.t =
667 fun message_queue state _tracking_id message ->
668 let open ClientIdeMessage in
669 match (state, message) with
670 (***********************************************************)
671 (************************* HANDLED IN ANY STATE ************)
672 (***********************************************************)
673 | (_, Verbose_to_file verbose) ->
674 if verbose then
675 Hh_logger.Level.set_min_level_file Hh_logger.Level.Debug
676 else
677 Hh_logger.Level.set_min_level_file Hh_logger.Level.Info;
678 Lwt.return (state, Ok ())
679 | (_, Shutdown ()) ->
680 remove_hhi state;
681 Lwt.return (state, Ok ())
682 (***********************************************************)
683 (************************* INITIALIZATION ******************)
684 (***********************************************************)
685 | (Pending_init, Initialize_from_saved_state param) ->
686 (* Invariant: no message will be sent to us prior to this request,
687 and we must send no message until we've sent this response. *)
688 let open Initialize_from_saved_state in
689 begin
691 let dstate = initialize1 param in
692 (* We're going to kick off the asynchronous part of initializing now.
693 Once it's done, it will appear as a LoadedState message on the queue. *)
694 Lwt.async (fun () ->
695 (* following method never throws *)
696 (* TODO(hverr): Figure out how to support 64-bit *)
697 let%lwt result =
698 load_saved_state
699 (Provider_context.empty_for_tool
700 ~popt:dstate.dcommon.popt
701 ~tcopt:dstate.dcommon.tcopt
702 ~backend:
703 (Provider_backend.Local_memory dstate.dcommon.local_memory)
704 ~deps_mode:Typing_deps_mode.SQLiteMode)
705 ~root:param.root
706 ~naming_table_load_info:param.naming_table_load_info
708 (* if the following push fails, that must be because the queues
709 have been shut down, in which case there's nothing to do. *)
710 let (_succeeded : bool) =
711 Lwt_message_queue.push message_queue (LoadedState result)
713 Lwt.return_unit);
714 Lwt.return (During_init dstate, Ok ())
715 with exn ->
716 let exn = Exception.wrap exn in
717 let e = ClientIdeUtils.make_bug_error "initialize1" ~exn in
718 (* Our caller has an exception handler. But we must handle this ourselves
719 to change state to Failed_init; our caller's handler doesn't change state. *)
720 (* TODO: remove_hhi *)
721 Lwt.return (Failed_init e, Error e)
723 | (_, Initialize_from_saved_state _) ->
724 failwith ("Unexpected init in " ^ state_to_log_string state)
725 (***********************************************************)
726 (************************* CAN HANDLE DURING INIT **********)
727 (***********************************************************)
728 | ( (During_init { dfiles = files; _ } | Initialized { ifiles = files; _ }),
729 Disk_files_changed paths ) ->
730 let paths =
731 List.filter paths ~f:(fun (Changed_file path) ->
732 FindUtils.file_filter path)
734 (* That filtered-out non-hack files *)
735 let files =
737 files with
738 changed_files_to_process =
739 List.fold
740 paths
741 ~init:files.changed_files_to_process
742 ~f:(fun acc (Changed_file path) ->
743 Relative_path.Set.add
745 (Relative_path.create_detect_prefix path));
746 changed_files_denominator =
747 files.changed_files_denominator + List.length paths;
750 Lwt.return (update_state_files state files, Ok ())
751 | ( (During_init { dfiles = files; _ } | Initialized { ifiles = files; _ }),
752 Ide_file_closed file_path ) ->
753 let path =
754 file_path |> Path.to_string |> Relative_path.create_detect_prefix
756 let files = close_file files path in
757 Lwt.return (update_state_files state files, Ok ())
758 | ( (During_init { dfiles = files; _ } | Initialized { ifiles = files; _ }),
759 Ide_file_opened { file_path; file_contents } ) ->
760 let path =
761 file_path |> Path.to_string |> Relative_path.create_detect_prefix
763 let files = open_file files path file_contents in
764 Lwt.return (update_state_files state files, Ok ())
765 | ( (During_init { dfiles = files; _ } | Initialized { ifiles = files; _ }),
766 Ide_file_changed { Ide_file_changed.file_path; _ } ) ->
767 let path =
768 file_path |> Path.to_string |> Relative_path.create_detect_prefix
770 let files = change_file files path in
771 Lwt.return (update_state_files state files, Ok ())
772 (* Document Symbol *)
773 | ( ( During_init { dfiles = files; dcommon = common; _ }
774 | Initialized { ifiles = files; icommon = common; _ } ),
775 Document_symbol document_location ) ->
776 let (files, entry) = update_file files document_location in
777 let result =
778 FileOutline.outline_entry_no_comments ~popt:common.popt ~entry
780 Lwt.return (update_state_files state files, Ok result)
781 (***********************************************************)
782 (************************* UNABLE TO HANDLE ****************)
783 (***********************************************************)
784 | (During_init _, _) ->
785 let e =
787 Lsp.Error.code = Lsp.Error.RequestCancelled;
788 message = "IDE service has not yet completed init";
789 data = None;
792 Lwt.return (state, Error e)
793 | (Failed_init e, _) -> Lwt.return (state, Error e)
794 | (Pending_init, _) ->
795 failwith
796 (Printf.sprintf
797 "unexpected message '%s' in state '%s'"
798 (ClientIdeMessage.t_to_string message)
799 (state_to_log_string state))
800 (***********************************************************)
801 (************************* NORMAL HANDLING AFTER INIT ******)
802 (***********************************************************)
803 | (Initialized istate, Hover document_location) ->
804 let (state, ctx, entry) = update_file_ctx istate document_location in
805 let result =
806 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
807 ServerHover.go_quarantined
808 ~ctx
809 ~entry
810 ~line:document_location.ClientIdeMessage.line
811 ~column:document_location.ClientIdeMessage.column)
813 Lwt.return (state, Ok result)
814 (* Autocomplete *)
815 | ( Initialized istate,
816 Completion
817 { ClientIdeMessage.Completion.document_location; is_manually_invoked }
818 ) ->
819 (* Update the state of the world with the document as it exists in the IDE *)
820 let (state, ctx, entry) = update_file_ctx istate document_location in
821 let result =
822 ServerAutoComplete.go_ctx
823 ~ctx
824 ~entry
825 ~sienv:istate.icommon.sienv
826 ~is_manually_invoked
827 ~line:document_location.line
828 ~column:document_location.column
830 Lwt.return (state, Ok result)
831 (* Autocomplete docblock resolve *)
832 | (Initialized istate, Completion_resolve param) ->
833 let ctx = make_empty_ctx istate in
834 ClientIdeMessage.Completion_resolve.(
835 let result =
836 ServerDocblockAt.go_docblock_for_symbol
837 ~ctx
838 ~symbol:param.symbol
839 ~kind:param.kind
841 Lwt.return (state, Ok result))
842 (* Autocomplete docblock resolve *)
843 | (Initialized istate, Completion_resolve_location param) ->
844 (* We're given a location but it often won't be an opened file.
845 We will only serve autocomplete docblocks as of truth on disk.
846 Hence, we construct temporary entry to reflect the file which
847 contained the target of the resolve. *)
848 let open ClientIdeMessage.Completion_resolve_location in
849 let path =
850 param.document_location.ClientIdeMessage.file_path
851 |> Path.to_string
852 |> Relative_path.create_detect_prefix
854 let ctx = make_empty_ctx istate in
855 let (ctx, entry) = Provider_context.add_entry_if_missing ~ctx ~path in
856 let result =
857 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
858 ServerDocblockAt.go_docblock_ctx
859 ~ctx
860 ~entry
861 ~line:param.document_location.line
862 ~column:param.document_location.column
863 ~kind:param.kind)
865 Lwt.return (state, Ok result)
866 (* Document highlighting *)
867 | (Initialized istate, Document_highlight document_location) ->
868 let (state, ctx, entry) = update_file_ctx istate document_location in
869 let results =
870 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
871 ServerHighlightRefs.go_quarantined
872 ~ctx
873 ~entry
874 ~line:document_location.line
875 ~column:document_location.column)
877 Lwt.return (state, Ok results)
878 (* Signature help *)
879 | (Initialized istate, Signature_help document_location) ->
880 let (state, ctx, entry) = update_file_ctx istate document_location in
881 let results =
882 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
883 ServerSignatureHelp.go_quarantined
884 ~ctx
885 ~entry
886 ~line:document_location.line
887 ~column:document_location.column)
889 Lwt.return (state, Ok results)
890 (* Go to definition *)
891 | (Initialized istate, Definition document_location) ->
892 let (state, ctx, entry) = update_file_ctx istate document_location in
893 let result =
894 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
895 ServerGoToDefinition.go_quarantined
896 ~ctx
897 ~entry
898 ~line:document_location.ClientIdeMessage.line
899 ~column:document_location.ClientIdeMessage.column)
901 Lwt.return (state, Ok result)
902 (* Type Definition *)
903 | (Initialized istate, Type_definition document_location) ->
904 let (state, ctx, entry) = update_file_ctx istate document_location in
905 let result =
906 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
907 ServerTypeDefinition.go_quarantined
908 ~ctx
909 ~entry
910 ~line:document_location.ClientIdeMessage.line
911 ~column:document_location.ClientIdeMessage.column)
913 Lwt.return (state, Ok result)
914 (* Type Coverage *)
915 | (Initialized istate, Type_coverage document_identifier) ->
916 let document_location =
918 file_path = document_identifier.file_path;
919 file_contents = Some document_identifier.file_contents;
920 line = 0;
921 column = 0;
924 let (state, ctx, entry) = update_file_ctx istate document_location in
925 let result =
926 Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () ->
927 ServerColorFile.go_quarantined ~ctx ~entry)
929 Lwt.return (state, Ok result)
930 (* Workspace Symbol *)
931 | (Initialized istate, Workspace_symbol query) ->
932 (* Note: needs reverse-naming-table, hence only works in initialized
933 state: for top-level queries it needs reverse-naming-table to look
934 up positions; for member queries "Foo::bar" it needs it to fetch the
935 decl for Foo. *)
936 (* Note: we intentionally don't give results from unsaved files *)
937 let ctx = make_empty_ctx istate in
938 let result =
939 ServerSearch.go ctx query ~kind_filter:"" istate.icommon.sienv
941 Lwt.return (state, Ok result)
943 let write_status ~(out_fd : Lwt_unix.file_descr) (state : state) : unit Lwt.t =
944 match state with
945 | Pending_init
946 | During_init _
947 | Failed_init _ ->
948 Lwt.return_unit
949 | Initialized { ifiles; _ } ->
950 if Relative_path.Set.is_empty ifiles.changed_files_to_process then
951 let%lwt () =
952 write_message
953 ~out_fd
954 ~message:
955 (ClientIdeMessage.Notification ClientIdeMessage.Done_processing)
957 Lwt.return_unit
958 else
959 let total = ifiles.changed_files_denominator in
960 let processed =
961 total - Relative_path.Set.cardinal ifiles.changed_files_to_process
963 let%lwt () =
964 write_message
965 ~out_fd
966 ~message:
967 (ClientIdeMessage.Notification
968 (ClientIdeMessage.Processing_files
969 { ClientIdeMessage.Processing_files.processed; total }))
971 Lwt.return_unit
973 (** Allow to process the next file change only if we have no new events to
974 handle. To ensure correctness, we would have to actually process all file
975 change events *before* we processed any other IDE queries. However, we're
976 trying to maximize availability, even if occasionally we give stale
977 results. We can revisit this trade-off later if we decide that the stale
978 results are baffling users. *)
979 let should_process_file_change
980 (in_fd : Lwt_unix.file_descr)
981 (message_queue : message_queue)
982 (istate : istate) : bool =
983 Lwt_message_queue.is_empty message_queue
984 && (not (Lwt_unix.readable in_fd))
985 && not (Relative_path.Set.is_empty istate.ifiles.changed_files_to_process)
987 let process_one_file_change (out_fd : Lwt_unix.file_descr) (istate : istate) :
988 istate Lwt.t =
989 let next_file =
990 Relative_path.Set.choose istate.ifiles.changed_files_to_process
992 let changed_files_to_process =
993 Relative_path.Set.remove istate.ifiles.changed_files_to_process next_file
995 let { ClientIdeIncremental.naming_table; sienv; old_file_info; _ } =
996 ClientIdeIncremental.update_naming_tables_for_changed_file
997 ~backend:(Provider_backend.Local_memory istate.icommon.local_memory)
998 ~popt:istate.icommon.popt
999 ~naming_table:istate.naming_table
1000 ~sienv:istate.icommon.sienv
1001 ~path:next_file
1003 Option.iter
1004 old_file_info
1006 (Provider_utils.invalidate_local_decl_caches_for_file
1007 istate.icommon.local_memory);
1008 Provider_utils.invalidate_tast_cache_of_entries istate.ifiles.open_files;
1009 let changed_files_denominator =
1010 if Relative_path.Set.is_empty changed_files_to_process then
1012 else
1013 istate.ifiles.changed_files_denominator
1015 let istate =
1017 naming_table;
1018 icommon = { istate.icommon with sienv };
1019 ifiles =
1021 istate.ifiles with
1022 changed_files_to_process;
1023 changed_files_denominator;
1027 let%lwt () = write_status ~out_fd (Initialized istate) in
1028 Lwt.return istate
1030 (** This function will either process one change that's pending,
1031 or will await as necessary to handle one message. *)
1032 let handle_one_message_exn
1033 ~(in_fd : Lwt_unix.file_descr)
1034 ~(out_fd : Lwt_unix.file_descr)
1035 ~(message_queue : message_queue)
1036 ~(state : state) : state option Lwt.t =
1037 (* The precise order of operations is to help us be responsive
1038 to requests, to never to await if there are pending changes to process,
1039 but also to await for the next thing to do:
1040 (1) If there's a message in [message_queue] then handle it;
1041 (2) Otherwise if there's a message in [in_fd] then await until it
1042 gets pumped into [message_queue] and then handle it;
1043 (3) Otherwise if there are pending file-changes then process them;
1044 (4) otherwise await until the next client request arrives in [in_fd]
1045 and gets pumped into [message_queue] and then handle it. *)
1046 match state with
1047 | Initialized istate
1048 when should_process_file_change in_fd message_queue istate ->
1049 let%lwt istate = process_one_file_change out_fd istate in
1050 Lwt.return_some (Initialized istate)
1051 | _ ->
1052 let%lwt message = Lwt_message_queue.pop message_queue in
1053 (match (state, message) with
1054 | (_, None) ->
1055 Lwt.return_none (* exit loop if message_queue has been closed *)
1056 | (During_init dstate, Some (LoadedState load_state_result)) ->
1057 let%lwt state = initialize2 out_fd dstate load_state_result in
1058 Lwt.return_some state
1059 | (_, Some (LoadedState _)) ->
1060 failwith ("Unexpected LoadedState in " ^ state_to_log_string state)
1061 | (_, Some (ClientRequest { ClientIdeMessage.tracking_id; message })) ->
1062 let unblocked_time = Unix.gettimeofday () in
1063 let%lwt (state, response) =
1064 try%lwt
1065 let%lwt (s, r) =
1066 handle_request message_queue state tracking_id message
1068 Lwt.return (s, r)
1069 with exn ->
1070 (* Our caller has an exception handler which logs the exception.
1071 But we instead must fulfil our contract of responding to the client,
1072 even if we have an exception. Hence we need our own handler here. *)
1073 let exn = Exception.wrap exn in
1074 let e = ClientIdeUtils.make_bug_error "handle_request" ~exn in
1075 Lwt.return (state, Error e)
1077 let%lwt () =
1078 write_message
1079 ~out_fd
1080 ~message:
1081 ClientIdeMessage.(
1082 Response { response; tracking_id; unblocked_time })
1084 Lwt.return_some state)
1086 let serve ~(in_fd : Lwt_unix.file_descr) ~(out_fd : Lwt_unix.file_descr) :
1087 unit Lwt.t =
1088 let rec flush_event_logger () : unit Lwt.t =
1089 let%lwt () = Lwt_unix.sleep 0.5 in
1090 HackEventLogger.Memory.profile_if_needed ();
1091 Lwt.async EventLoggerLwt.flush;
1092 EventLogger.recheck_disk_files ();
1093 flush_event_logger ()
1095 let rec pump_stdin (message_queue : message_queue) : unit Lwt.t =
1096 try%lwt
1097 let%lwt { ClientIdeMessage.tracking_id; message } =
1098 Marshal_tools_lwt.from_fd_with_preamble in_fd
1100 let is_queue_open =
1101 Lwt_message_queue.push
1102 message_queue
1103 (ClientRequest { ClientIdeMessage.tracking_id; message })
1105 match message with
1106 | ClientIdeMessage.Shutdown () -> Lwt.return_unit
1107 | _ when not is_queue_open -> Lwt.return_unit
1108 | _ -> pump_stdin message_queue
1109 with e ->
1110 let e = Exception.wrap e in
1111 Lwt_message_queue.close message_queue;
1112 Exception.reraise e
1114 let rec handle_messages ({ message_queue; state } : t) : unit Lwt.t =
1115 let%lwt next_state_opt =
1116 try%lwt
1117 let%lwt state =
1118 handle_one_message_exn ~in_fd ~out_fd ~message_queue ~state
1120 Lwt.return state
1121 with exn ->
1122 let exn = Exception.wrap exn in
1123 ClientIdeUtils.log_bug "handle_one_message" ~exn ~telemetry:true;
1124 if is_outfd_write_error exn then exit 1;
1125 (* if out_fd is down then there's no use continuing. *)
1126 Lwt.return_some state
1128 match next_state_opt with
1129 | None -> Lwt.return_unit (* exit loop *)
1130 | Some state -> handle_messages { message_queue; state }
1132 try%lwt
1133 let message_queue = Lwt_message_queue.create () in
1134 let flusher_promise = flush_event_logger () in
1135 let%lwt () = handle_messages { message_queue; state = Pending_init }
1136 and () = pump_stdin message_queue in
1137 Lwt.cancel flusher_promise;
1138 Lwt.return_unit
1139 with exn ->
1140 let exn = Exception.wrap exn in
1141 ClientIdeUtils.log_bug "fatal clientIdeDaemon" ~exn ~telemetry:true;
1142 Lwt.return_unit
1144 let daemon_main
1145 (args : ClientIdeMessage.daemon_args)
1146 (channels : ('a, 'b) Daemon.channel_pair) : unit =
1147 Printexc.record_backtrace true;
1148 let (ic, oc) = channels in
1149 let in_fd = Lwt_unix.of_unix_file_descr (Daemon.descr_of_in_channel ic) in
1150 let out_fd = Lwt_unix.of_unix_file_descr (Daemon.descr_of_out_channel oc) in
1151 let daemon_init_id =
1152 Printf.sprintf
1153 "%s.%s"
1154 args.ClientIdeMessage.init_id
1155 (Random_id.short_string ())
1157 HackEventLogger.serverless_ide_init ~init_id:daemon_init_id;
1159 if args.ClientIdeMessage.verbose_to_stderr then
1160 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Debug
1161 else
1162 Hh_logger.Level.set_min_level_stderr Hh_logger.Level.Error;
1163 if args.ClientIdeMessage.verbose_to_file then
1164 Hh_logger.Level.set_min_level_file Hh_logger.Level.Debug
1165 else
1166 Hh_logger.Level.set_min_level_file Hh_logger.Level.Info;
1168 Lwt_main.run (serve ~in_fd ~out_fd)
1170 let daemon_entry_point : (ClientIdeMessage.daemon_args, unit, unit) Daemon.entry
1172 Daemon.register_entry_point "ClientIdeService" daemon_main