2 * Copyright (c) 2019, Facebook, Inc.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
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
)
20 (** These are messages on ClientIdeDaemon's internal message-queue *)
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
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
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
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
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).
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
143 (** When did we kick off the attempt to load saved-state? *)
144 dcommon
: common_state
;
145 dfiles
: open_files_state
;
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. *)
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 *)
183 message_queue
: message_queue
;
187 let state_to_log_string (state
: state
) : string =
188 let files_to_log_string (files
: open_files_state
) : string =
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
)
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
211 try Sys.rename
client_ide_log_fn (client_ide_log_fn ^
".old")
214 Hh_logger.set_log
client_ide_log_fn;
215 log "Starting client IDE service at %s" client_ide_log_fn
218 ~
(out_fd
: Lwt_unix.file_descr
)
219 ~
(message
: ClientIdeMessage.message_from_daemon
) : unit Lwt.t
=
221 let%lwt
(_
: int) = Marshal_tools_lwt.to_fd_with_preamble out_fd message
in
223 with Unix.Unix_error
(Unix.EPIPE
, _
, _
) -> raise Outfd_write_error
226 (ctx
: Provider_context.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
);
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 *)
240 if Float.(naming_table_load_info
.test_delay
> 0.0) then
241 Lwt_unix.sleep naming_table_load_info
.test_delay
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 *)
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
= ();
259 manifold_path
= "<not provided>";
260 corresponding_rev
= "<not provided>";
261 mergebase_rev
= "<not provided>";
266 State_loader_lwt.load
268 Saved_state_loader.Watchman_options.{ root
; sockname
= None
}
269 ~ignore_hh_version
:false
270 ~saved_state_type
:Saved_state_loader.Naming_table
275 | Ok
{ Saved_state_loader.main_artifacts
; changed_files; _
} ->
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 *)
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
;
300 Saved_state_loader.long_user_message_of_error load_error
;
302 Saved_state_loader.debug_details_of_error load_error
;
303 is_actionable
= Saved_state_loader.is_error_actionable load_error
;
308 Lsp.Error.code
= Lsp.Error.UnknownErrorCode
;
309 message
= reason.ClientIdeMessage.medium_user_message
;
315 Hh_json.string_
reason.ClientIdeMessage.debug_details
);
319 Lwt.return_error
(reason, e)
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)
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
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
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 =
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
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
) :
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");
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
();
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 *)
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
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
423 SymbolIndex.initialize
426 ~namespace_map
:(GlobalOptions.po_auto_namespace_map
tcopt)
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
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'. *)
452 |> List.map ~f
:(fun path ->
453 path |> Path.to_string
|> Relative_path.create_detect_prefix
)
454 |> List.map ~f
:(fun path ->
456 Provider_context.make_entry
458 ~contents
:Provider_context.Raise_exn_on_attempt_to_read
))
459 |> Relative_path.Map.of_list
461 log_debug "initialize1.done";
464 dcommon
= { hhi_root; sienv; popt
; tcopt; local_memory };
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]
478 (out_fd
: Lwt_unix.file_descr
)
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
498 (ClientIdeMessage.Notification
(ClientIdeMessage.Done_init
(Ok
p)))
503 icommon
= dstate
.dcommon
;
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";
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
) :
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. *)
557 (files
: open_files_state
) (path : Relative_path.t
) (contents
: string) :
560 Provider_context.make_entry
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
) :
572 if Relative_path.Map.mem files
.open_files path then
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
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
) :
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. *)
594 (files
: open_files_state
)
595 (document_location
: ClientIdeMessage.document_location
) :
596 open_files_state
* Provider_context.entry =
598 document_location
.ClientIdeMessage.file_path
600 |> Relative_path.create_detect_prefix
604 ( document_location
.ClientIdeMessage.file_contents
,
605 Relative_path.Map.find_opt files
.open_files path )
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
612 ~contents
:(Provider_context.Provided_contents contents
)
614 log_missing_open_file_BUG path;
615 failwith
"Attempted LSP operation on a non-open file"
616 | (Some contents
, Some
entry)
620 (Provider_context.get_file_contents_if_present
entry) ->
622 | (None
, Some
entry) -> entry
623 | (Some contents
, _
) ->
624 Provider_context.make_entry
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. *)
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
645 let update_state_files (state
: state
) (files
: open_files_state
) : state
=
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. *)
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
) ->
675 Hh_logger.Level.set_min_level_file
Hh_logger.Level.Debug
677 Hh_logger.Level.set_min_level_file
Hh_logger.Level.Info
;
678 Lwt.return
(state
, Ok
())
679 | (_
, Shutdown
()) ->
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
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. *)
695 (* following method never throws *)
696 (* TODO(hverr): Figure out how to support 64-bit *)
699 (Provider_context.empty_for_tool
700 ~popt
:dstate.dcommon
.popt
701 ~
tcopt:dstate.dcommon
.tcopt
703 (Provider_backend.Local_memory
dstate.dcommon
.local_memory)
704 ~deps_mode
:Typing_deps_mode.SQLiteMode
)
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
)
714 Lwt.return
(During_init
dstate, Ok
())
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
) ->
731 List.filter
paths ~f
:(fun (Changed_file
path) ->
732 FindUtils.file_filter
path)
734 (* That filtered-out non-hack files *)
738 changed_files_to_process =
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
) ->
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
} ) ->
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
; _
} ) ->
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
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 _
, _
) ->
787 Lsp.Error.code
= Lsp.Error.RequestCancelled
;
788 message = "IDE service has not yet completed init";
792 Lwt.return
(state
, Error
e)
793 | (Failed_init
e, _
) -> Lwt.return
(state
, Error
e)
794 | (Pending_init
, _
) ->
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
806 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
807 ServerHover.go_quarantined
810 ~line
:document_location
.ClientIdeMessage.line
811 ~column
:document_location
.ClientIdeMessage.column
)
813 Lwt.return
(state
, Ok
result)
815 | ( Initialized
istate,
817 { ClientIdeMessage.Completion.document_location
; is_manually_invoked
}
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
822 ServerAutoComplete.go_ctx
825 ~
sienv:istate.icommon
.sienv
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.(
836 ServerDocblockAt.go_docblock_for_symbol
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
850 param
.document_location
.ClientIdeMessage.file_path
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
857 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
858 ServerDocblockAt.go_docblock_ctx
861 ~line
:param
.document_location
.line
862 ~column
:param
.document_location
.column
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
870 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
871 ServerHighlightRefs.go_quarantined
874 ~line
:document_location
.line
875 ~column
:document_location
.column
)
877 Lwt.return
(state
, Ok
results)
879 | (Initialized
istate, Signature_help document_location
) ->
880 let (state
, ctx, entry) = update_file_ctx istate document_location
in
882 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
883 ServerSignatureHelp.go_quarantined
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
894 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
895 ServerGoToDefinition.go_quarantined
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
906 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
907 ServerTypeDefinition.go_quarantined
910 ~line
:document_location
.ClientIdeMessage.line
911 ~column
:document_location
.ClientIdeMessage.column
)
913 Lwt.return
(state
, Ok
result)
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
;
924 let (state
, ctx, entry) = update_file_ctx istate document_location in
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
936 (* Note: we intentionally don't give results from unsaved files *)
937 let ctx = make_empty_ctx istate in
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
=
949 | Initialized
{ ifiles
; _
} ->
950 if Relative_path.Set.is_empty ifiles
.changed_files_to_process then
955 (ClientIdeMessage.Notification
ClientIdeMessage.Done_processing
)
959 let total = ifiles
.changed_files_denominator in
961 total - Relative_path.Set.cardinal ifiles
.changed_files_to_process
967 (ClientIdeMessage.Notification
968 (ClientIdeMessage.Processing_files
969 { ClientIdeMessage.Processing_files.processed; total }))
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) :
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
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
1013 istate.ifiles
.changed_files_denominator
1018 icommon
= { istate.icommon
with sienv };
1022 changed_files_to_process;
1023 changed_files_denominator;
1027 let%lwt
() = write_status ~out_fd
(Initialized
istate) in
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. *)
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)
1052 let%lwt
message = Lwt_message_queue.pop message_queue
in
1053 (match (state
, message) with
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
) =
1066 handle_request message_queue state tracking_id
message
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)
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
) :
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
=
1097 let%lwt
{ ClientIdeMessage.tracking_id
; message } =
1098 Marshal_tools_lwt.from_fd_with_preamble in_fd
1101 Lwt_message_queue.push
1103 (ClientRequest
{ ClientIdeMessage.tracking_id
; message })
1106 | ClientIdeMessage.Shutdown
() -> Lwt.return_unit
1107 | _
when not
is_queue_open -> Lwt.return_unit
1108 | _
-> pump_stdin message_queue
1110 let e = Exception.wrap
e in
1111 Lwt_message_queue.close message_queue
;
1114 let rec handle_messages ({ message_queue
; state
} : t
) : unit Lwt.t
=
1115 let%lwt next_state_opt
=
1118 handle_one_message_exn ~in_fd ~out_fd ~message_queue ~state
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
}
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;
1140 let exn = Exception.wrap
exn in
1141 ClientIdeUtils.log_bug
"fatal clientIdeDaemon" ~
exn ~telemetry
:true;
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 =
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
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
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