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 (** 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
17 let dbg_set_activity ~
(key
: string) (value : string) : unit =
19 SMap.find_opt key
!dbg_current_activities |> Option.value ~default
:[]
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 *)
37 | ClientRequest
: 'a
ClientIdeMessage.tracked_t
-> message
38 (** ClientRequest came from ClientIdeService over stdin;
39 it expects a response. *)
41 (ClientIdeInit.init_result
, ClientIdeMessage.rich_error
) result
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
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 *)
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
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).
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
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. *)
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 *)
196 message_queue
: message_queue
;
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
)
205 | Pending_init
-> "Pending_init"
206 | During_init
{ dopen_files
; changed_files_to_process
; _
} ->
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
225 try Sys.rename
client_ide_log_fn (client_ide_log_fn ^
".old") with
228 Hh_logger.set_log
client_ide_log_fn;
229 log "Starting client IDE service at %s" client_ide_log_fn
232 ~
(out_fd
: Lwt_unix.file_descr
)
233 ~
(message
: ClientIdeMessage.message_from_daemon
) : unit Lwt.t
=
235 let%lwt
(_
: int) = Marshal_tools_lwt.to_fd_with_preamble out_fd message
in
238 | Unix.Unix_error
(Unix.EPIPE
, fn
, param
) ->
239 raise
@@ Outfd_write_error
(fn
, param
)
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
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
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 =
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
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
)
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
295 Provider_utils.invalidate_upon_file_changes
299 ~entries
:(Relative_path.Map.map open_files ~f
:fst
)
301 HackEventLogger.ProfileTypeCheck.invalidate
302 ~count
:(List.length changes
)
304 ~path
:(List.hd changes
|> Option.map ~f
:(fun change
-> change
.FileInfo.path
))
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
) :
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");
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
354 ServerConfig.set_tc_options
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
366 ~max_num_folded_class_decls
:5000
367 ~max_num_shallow_class_decls
:20000;
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'. *)
382 |> List.map ~f
:(fun path
->
383 path
|> Path.to_string
|> Relative_path.create_detect_prefix
)
384 |> List.map ~f
:(fun path
->
386 ( Provider_context.make_entry
388 ~contents
:Provider_context.Raise_exn_on_attempt_to_read
,
390 |> Relative_path.Map.of_list
395 ~count
:(List.length param
.open_files)
398 log_debug "initialize1.done";
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]
411 (out_fd
: Lwt_unix.file_descr
)
414 (ClientIdeInit.init_result
, ClientIdeMessage.rich_error
) result
) :
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
)
431 ~
local_memory:dstate
.dcommon
.local_memory
432 ~
open_files:dstate
.dopen_files
433 changed_files_to_process
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. *)
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)
456 log_debug "initialize2.error";
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
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
=
489 Provider_context.make_entry
491 ~contents
:(Provider_context.Provided_contents contents
)
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
) :
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. *)
511 (open_files : open_files_state
) (document
: ClientIdeMessage.document
) :
512 open_files_state
* Provider_context.entry * Errors.t
option ref =
514 document
.ClientIdeMessage.file_path
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
522 (* This is a common scenario although I'm not quite sure why *)
523 ( Provider_context.make_entry
525 ~
contents:(Provider_context.Provided_contents
contents),
527 | Some
(entry, published_errors
)
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
)
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
541 ~
contents:(Provider_context.Provided_contents
contents),
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)
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
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
585 match (disk_content_opt, cached_entry_opt) with
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. *)
590 | ( Some disk_content
,
593 Provider_context.contents =
595 Contents_from_disk str
| Provided_contents str
);
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. *)
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. *)
608 (Provider_context.make_entry
610 ~
contents:(Provider_context.Provided_contents disk_content
))
614 (* file couldn't be read off disk (maybe absent); therefore, by definition, no errors *)
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. *)
635 (message_queue
: message_queue
)
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
) ->
646 Hh_logger.Level.set_min_level_file
Hh_logger.Level.Debug
648 Hh_logger.Level.set_min_level_file
Hh_logger.Level.Info
;
650 | (_
, Shutdown
()) ->
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. *)
664 let%lwt naming_table_result
=
666 ~
config:dstate.dcommon
.config
667 ~local_config
:dstate.dcommon
.local_config
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
677 (GotNamingTable naming_table_result
)
680 log "Finished saved state initialization. State: %s" (show_dstate
dstate);
681 (During_init
dstate, Ok
())
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
710 ~
local_memory:istate.icommon
.local_memory
711 ~
open_files:istate.iopen_files
714 let istate = { istate with naming_table
; sienv
} in
715 (Initialized
istate, Ok
())
717 | (During_init
dstate, Did_close file_path
) ->
719 file_path
|> Path.to_string
|> Relative_path.create_detect_prefix
722 { dstate with dopen_files = close_file dstate.dopen_files path },
724 | (Initialized
istate, Did_close file_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
732 ~f
:ClientIdeMessage.diagnostic_of_finalized_error_without_related_hints
735 { istate with iopen_files
= close_file istate.iopen_files
path },
737 (* didOpen or didChange *)
738 | (During_init
dstate, Did_open_or_change
{ file_path
; file_contents
}) ->
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
763 FileOutline.outline_entry_no_comments
764 ~popt
:(ServerConfig.parser_options
dstate.dcommon
.config)
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
771 FileOutline.outline_entry_no_comments
772 ~popt
:(ServerConfig.parser_options
istate.icommon
.config)
775 (Initialized
{ istate with iopen_files
}, Ok
result)
776 (***********************************************************)
777 (************************* UNABLE TO HANDLE ****************)
778 (***********************************************************)
779 | (During_init
dstate, _
) ->
782 Lsp.Error.code
= Lsp.Error.RequestCancelled
;
783 message = "IDE service has not yet completed init";
787 (During_init
dstate, Error
e)
788 | (Failed_init
reason, _
) ->
789 (Failed_init
reason, Error
(ClientIdeUtils.to_lsp_error
reason))
790 | (Pending_init
, _
) ->
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
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
) =
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
835 Relative_path.create_detect_prefix
stringified_path
837 let single_file_pos =
838 ServerGoToImpl.go_for_single_file
841 ~naming_table
:istate.naming_table
843 |> ServerFindRefs.to_absolute
847 Lsp_helpers.path_string_to_lsp_uri
849 ~default_path
:stringified_path
853 Lsp.UriMap.add
urikey single_file_pos accum
855 (istate, updated_map))
857 ~init
:(istate, Lsp.UriMap.empty
)
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 () ->
872 ServerFindRefs.go_from_file_ctx_with_symbol_definition
878 | None
-> (istate, ClientIdeMessage.Not_renameable_position
)
879 | Some
(_definition
, action
) when ServerFindRefs.is_local action
->
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
}
886 ClientIdeMessage.Rename_success
{ shellout
= None
; local
= [] }
890 "ClientIDEDaemon failed to rename for localvar %s"
891 (ServerCommandTypes.Find_refs.show_action action
)
894 failwith
"ClientIDEDaemon failed to rename for a localvar"
897 | Some
(symbol_definition
, action
) ->
898 let (istate, single_file_patches
) =
900 ~f
:(fun (istate, accum
) document
->
901 let (istate, ctx, _entry
, _errors
) =
902 update_file_ctx istate document
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
915 ~naming_table
:istate.naming_table
918 match single_file_patches with
919 | Ok
patches -> patches
922 let patch_list = List.rev_append
patches accum
in
923 (istate, patch_list))
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
->
947 ServerFindRefs.go_for_localvar
ctx action
948 >>| ServerFindRefs.to_absolute
955 match ide_result
with
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
963 "FindRefs returned an empty list of positions for localvar %s"
967 HackEventLogger.invariant_violation_bug
err;
971 List.hd_exn positions
|> snd
|> Pos.filename
974 Lsp_helpers.path_string_to_lsp_uri
975 ~default_path
:filename
978 Lsp.UriMap.add
uri positions
Lsp.UriMap.empty
982 if lsp_uri_map |> Lsp.UriMap.values
|> List.length
= 1 then
985 (* Can a localvar cross file boundaries? I sure hope not. *)
988 "Found more than one file when executing find refs for localvar %s"
992 HackEventLogger.invariant_violation_bug
err;
995 ClientIdeMessage.Find_refs_success
1000 open_file_results
= lsp_uri_map;
1004 Printf.sprintf
"Failed to find refs for localvar %s" name
1007 HackEventLogger.invariant_violation_bug
err;
1011 (* clientLsp should raise if we return a LocalVar action *)
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
) =
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
1035 Relative_path.create_detect_prefix
stringified_path
1037 let single_file_ref =
1038 ServerFindRefs.go_for_single_file
1043 ~naming_table
:istate.naming_table
1044 |> ServerFindRefs.to_absolute
1047 Lsp_helpers.path_string_to_lsp_uri
1049 ~default_path
:stringified_path
1053 Lsp.UriMap.add
urikey single_file_ref accum
1055 (istate, updated_map))
1057 ~init
:(istate, Lsp.UriMap.empty
)
1059 let sienv_ref = ref istate.sienv
in
1061 SymbolIndex.find_refs ~
sienv_ref ~action ~max_results
:100
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)
1071 ( { istate with sienv
= !sienv_ref },
1072 ClientIdeMessage.Find_refs_success
1075 action
= Some action
;
1077 open_file_results
= single_file_refs
;
1080 (Initialized
istate, Ok
result)
1082 | ( Initialized
istate,
1084 (document
, { line
; column
}, { ClientIdeMessage.is_manually_invoked
})
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
1090 ServerAutoComplete.go_ctx
1094 ~is_manually_invoked
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
)
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";
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
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
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
1142 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
1143 ServerSignatureHelp.go_quarantined ~
ctx ~
entry ~line ~column
)
1145 (Initialized
istate, Ok
results)
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
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
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
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
}
1198 let (istate, ctx, entry, _
) = update_file_ctx istate document
in
1201 Provider_utils.respect_but_quarantine_unsaved_changes ~
ctx ~f
:(fun () ->
1202 Code_actions_services.resolve
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
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
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
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
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
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
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)
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";
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
) :
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
) =
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";
1323 Lwt_message_queue.push
1325 (ClientRequest
{ ClientIdeMessage.tracking_id
; message })
1327 Lwt.return
(message, is_queue_open)
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)
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
;
1344 | ClientIdeMessage.Shutdown
() -> Lwt.return_unit
1345 | _
when not
is_queue_open -> Lwt.return_unit
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
=
1355 let%lwt state
= handle_one_message_exn ~out_fd ~message_queue ~state
in
1356 dbg_set_activity ~key
:"handle" "done";
1360 let e = Exception.wrap exn
in
1361 let is_write_error = is_outfd_write_error e in
1365 "exn %s; is_write_error=%b"
1366 (Exception.get_ctor_string
e)
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 *)
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
}
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;
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);
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 =
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
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
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
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
1443 let e = Exception.wrap exn
in
1444 dbg_set_activity ~key
:"main" ("exn " ^
Exception.get_ctor_string
e);
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
1457 let init ~custom_config ~naming_sqlite
=
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
();
1466 match Provider_backend.get
() with
1467 | Provider_backend.Local_memory
local_memory -> local_memory
1468 | _
-> failwith
"expected local memory backend"
1471 SymbolIndex.initialize
1472 ~gleanopt
:(ServerConfig.glean_options
config)
1473 ~namespace_map
:tcopt.GlobalOptions.po
.ParserOptions.auto_namespace_map
1475 local_config.ServerLocalConfig.ide_symbolindex_search_provider
1476 ~quiet
:local_config.ServerLocalConfig.symbolindex_quiet
1479 Provider_context.empty_for_tool
1482 ~backend
:(Provider_backend.Local_memory
local_memory)
1483 ~deps_mode
:(Typing_deps_mode.InMemoryMode None
)
1486 Naming_table.load_from_sqlite
ctx (Path.to_string naming_sqlite
)
1491 icommon
= { hhi_root = Path.make
"/"; config; local_config; local_memory };
1492 iopen_files
= Relative_path.Map.empty
;
1495 let index istate changes
=
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
1506 ~
local_memory:istate.icommon
.local_memory
1507 ~
open_files:istate.iopen_files
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
1516 handle_request message_queue (Initialized
istate) "tracking_id" message
1518 | (Initialized
istate, Ok response
) -> (istate, response
)
1519 | (_
, Error
{ Lsp.Error.code
; message; data
}) ->
1522 "handle_request %s: %s %s"
1523 (Lsp.Error.show_code code
)
1525 (Option.value_map data ~default
:"" ~f
:Hh_json.json_to_multiline
)
1529 let msg = Printf.sprintf
"handle_request ended in bad state" in