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.
13 | Message
: 'a
ClientIdeMessage.t
-> message
14 type message_queue
= message
Lwt_message_queue.t
18 | Failed_to_initialize
of string
20 saved_state_info
: Saved_state_loader.Naming_table_saved_state_info.t
;
22 server_env
: ServerEnv.env
;
23 changed_files_to_process
: Path.Set.t
;
26 message_queue
: message_queue
;
31 Hh_logger.log ("[ide-daemon] " ^^ s
)
33 let set_up_hh_logger_for_client_ide_service ~
(root
: Path.t
): unit =
34 (* Log to a file on disk. Note that calls to `Hh_logger` will always write to
35 `stderr`; this is in addition to that. *)
36 let client_ide_log_fn = (ServerFiles.client_ide_log root
) in
38 Sys.rename
client_ide_log_fn (client_ide_log_fn ^
".old")
42 Hh_logger.set_log
client_ide_log_fn (Out_channel.create
46 EventLogger.init
EventLogger.Event_logger_fake
0.0;
47 log "Starting client IDE service at %s" client_ide_log_fn
49 let load_naming_table_from_saved_state_info
50 (server_env
: ServerEnv.env
)
51 (saved_state_info
: Saved_state_loader.Naming_table_saved_state_info.t
)
52 : ServerEnv.env
Lwt.t
=
54 Saved_state_loader.Naming_table_saved_state_info.(Path.to_string
55 saved_state_info
.naming_table_path
) in
56 let naming_table = Naming_table.load_from_sqlite
57 ~update_reverse_entries
:false
60 log "Loaded naming table from SQLite database at %s" path;
61 let server_env = { server_env with ServerEnv.naming_table } in
68 ~
(naming_table_saved_state_path
: Path.t
option)
70 log "[saved-state] Starting load in root %s" (Path.to_string root
);
73 let%lwt result
= match naming_table_saved_state_path
with
74 | Some naming_table_saved_state_path
->
75 (* Assume that there are no changed files on disk if we're getting
76 passed the path to the saved-state directly, and that the saved-state
77 corresponds to the current state of the world. *)
78 let changed_files = [] in
79 Lwt.return_ok
({ Saved_state_loader.Naming_table_saved_state_info.
80 naming_table_path
= naming_table_saved_state_path
;
83 let%lwt result
= State_loader_lwt.load
85 ~saved_state_type
:Saved_state_loader.Naming_table
in
88 let%lwt new_state
= match result
with
89 | Ok
(saved_state_info
, changed_files) ->
90 log "[saved-state] Naming table path: %s"
91 Saved_state_loader.Naming_table_saved_state_info.(Path.to_string
92 saved_state_info
.naming_table_path
);
94 let%lwt
server_env = load_naming_table_from_saved_state_info
95 env saved_state_info
in
96 log "[saved-state] Load succeeded";
98 Lwt.return
(Initialized
{
102 changed_files_to_process
= Path.Set.of_list
changed_files;
104 | Error load_error
->
105 let message = Saved_state_loader.load_error_to_string load_error
in
106 log "[saved-state] %s" message;
107 Lwt.return
(Failed_to_initialize
message)
111 let stack = Printexc.get_backtrace
() in
113 ~prefix
:"Uncaught exception in client IDE services"
115 Lwt.return
(Failed_to_initialize
(Printf.sprintf
116 "Uncaught exception in client IDE services: %s" stack))
121 ({ ClientIdeMessage.Initialize_from_saved_state.
123 naming_table_saved_state_path
;
124 }: ClientIdeMessage.Initialize_from_saved_state.t
) =
125 set_up_hh_logger_for_client_ide_service ~root
;
127 Relative_path.set_path_prefix
Relative_path.Root root
;
128 let hhi_root = Hhi.get_hhi_root
() in
129 log "Extracted hhi files to directory %s" (Path.to_string
hhi_root);
130 Relative_path.set_path_prefix
Relative_path.Hhi
hhi_root;
131 Relative_path.set_path_prefix
Relative_path.Tmp
(Path.make
"/tmp");
133 let server_args = ServerArgs.default_options ~root
:(Path.to_string root
) in
134 let (server_config
, server_local_config
) = ServerConfig.load
135 ServerConfig.filename
server_args in
137 (* NOTE: We don't want to depend on shared memory in the long-term, since
138 we're only running one process and don't need to share memory with anyone. To
139 remove the shared memory usage here requires refactoring our heaps to never
140 write to shared memory. *)
141 let _: SharedMem.handle
= SharedMem.init
143 (ServerConfig.sharedmem_config server_config
)
145 let bytes_per_word = Sys.word_size
/ 8 in
146 let words_per_mb = 1_000_000 / bytes_per_word in
147 let max_size_in_words = 250 * words_per_mb in
148 Provider_config.set_local_memory_backend ~
max_size_in_words;
150 let genv = ServerEnvBuild.make_genv
155 None
(* no lru_workers *)
157 let server_env = ServerEnvBuild.make_env
genv.ServerEnv.config
in
158 (* We need shallow class declarations so that we can invalidate individual
159 members in a class hierarchy. *)
163 server_env.ServerEnv.tcopt
with
164 GlobalOptions.tco_shallow_class_decl
= true
168 GlobalParserOptions.set
server_env.ServerEnv.popt
;
169 GlobalNamingOptions.set
server_env.ServerEnv.tcopt
;
171 (* Use server_config to modify server_env with the correct symbol index *)
172 let namespace_map = GlobalOptions.po_auto_namespace_map
server_env.ServerEnv.tcopt
in
173 let sienv = SymbolIndex.initialize
176 ~provider_name
:server_local_config
.ServerLocalConfig.symbolindex_search_provider
177 ~quiet
:server_local_config
.ServerLocalConfig.symbolindex_quiet
178 ~savedstate_file_opt
:server_local_config
.ServerLocalConfig.symbolindex_file
182 SearchUtils.sie_log_timings
= true;
186 ServerEnv.local_symbol_table
= ref sienv;
189 let%lwt new_state
= load_saved_state server_env
190 ~root ~
hhi_root ~naming_table_saved_state_path
in
191 log "Serverless IDE has completed initialization";
194 let shutdown (state
: state
): unit Lwt.t
=
197 | Failed_to_initialize
_ ->
198 log "No cleanup to be done";
200 | Initialized
{ hhi_root; _ } ->
201 let hhi_root = Path.to_string
hhi_root in
202 log "Removing hhi directory %s..." hhi_root;
203 Sys_utils.rm_dir_tree
hhi_root;
206 let make_context_from_document_location
207 (server_env: ServerEnv.env
)
208 (document_location
: ClientIdeMessage.document_location
)
209 : (Provider_context.t
* Provider_context.entry
) =
210 let (file_path
, file_input
) = match document_location
with
211 | { ClientIdeMessage.file_contents
= None
; file_path
; _ } ->
213 ServerCommandTypes.FileName
(Path.to_string file_path
) in
214 (file_path
, file_input)
215 | { ClientIdeMessage.file_contents
= Some file_contents
; file_path
; _ } ->
216 let file_input = ServerCommandTypes.FileContent file_contents
in
217 (file_path
, file_input)
219 let file_path = file_path
221 |> Relative_path.create_detect_prefix
in
222 Provider_utils.update_context
223 ~ctx
:(Provider_context.empty ~tcopt
:server_env.ServerEnv.tcopt
)
227 module Handle_message_result
= struct
234 let handle_message: type a
.
236 a
ClientIdeMessage.t
->
237 (state
* a
Handle_message_result.t
) Lwt.t
=
239 let open ClientIdeMessage
in
240 match (state
, message) with
241 | (state
, Shutdown
()) ->
242 let%lwt
() = shutdown state
in
243 Lwt.return
(state
, Handle_message_result.Response
())
245 | ((Failed_to_initialize
_ | Initializing
), File_changed
_) ->
246 (* Should not happen. *)
247 Lwt.return
(state
, Handle_message_result.Error
(
248 "IDE services could not process file change because " ^
249 "it failed to initialize or was still initializing. The caller " ^
250 "should have waited for the IDE services to become ready before " ^
251 "sending file-change notifications."
254 changed_files_to_process
;
256 } as state
), File_changed
path) ->
257 let changed_files_to_process = Path.Set.add
changed_files_to_process path in
258 let state = Initialized
{ state with changed_files_to_process } in
259 Lwt.return
(state, Handle_message_result.Notification
)
261 | (Initializing
, Initialize_from_saved_state param
) ->
262 let%lwt new_state
= initialize param
in
263 Lwt.return
(new_state
, Handle_message_result.Response
())
264 | (Initialized
_, Initialize_from_saved_state
_) ->
265 Lwt.return
(state, Handle_message_result.Error
266 "Tried to initialize when already initialized")
268 | (Initializing
, _) ->
269 Lwt.return
(state, Handle_message_result.Error
270 "IDE services have not yet been initialized")
271 | (Failed_to_initialize error_message
, _) ->
272 Lwt.return
(state, Handle_message_result.Error
(Printf.sprintf
273 "IDE services failed to initialize: %s" error_message
))
275 | (Initialized
{ server_env; _ }, Hover document_location
) ->
277 make_context_from_document_location server_env document_location
in
278 let result = Provider_utils.with_context ~ctx ~f
:(fun () ->
282 ~line
:document_location
.ClientIdeMessage.line
283 ~column
:document_location
.ClientIdeMessage.column
285 Lwt.return
(state, Handle_message_result.Response
result)
288 | (Initialized
{ server_env; _ },
289 Completion
{ ClientIdeMessage.Completion.
290 document_location
= { ClientIdeMessage.
300 |> Relative_path.create_detect_prefix
in
301 let file_content = match file_contents
with
302 | Some file_contents
->
307 |> Sys_utils.cat_no_fail
309 let sienv = !(server_env.ServerEnv.local_symbol_table
) in
310 let matches = ServerAutoComplete.auto_complete_at_position_ctx
315 ~tcopt
:server_env.ServerEnv.tcopt
319 let result = { AutocompleteTypes.
320 completions
= matches.Utils.With_complete_flag.value;
322 is_complete
= matches.Utils.With_complete_flag.is_complete
;
324 Lwt.return
(state, Handle_message_result.Response
result)
326 (* Autocomplete docblock resolve *)
327 | (Initialized
{ server_env; _ }, Completion_resolve param
) ->
328 let open ClientIdeMessage.Completion_resolve
in
329 let start_time = Unix.gettimeofday
() in
330 let result = ServerDocblockAt.go_docblock_for_symbol
335 let sienv = !(server_env.ServerEnv.local_symbol_table
) in
336 if sienv.SearchUtils.sie_log_timings
then begin
338 Hh_logger.log_duration
(Printf.sprintf
"[docblock] Search for [%s] [%s]"
339 param
.symbol
(SearchUtils.show_si_kind param
.kind
)) start_time in
342 Lwt.return
(state, Handle_message_result.Response
result)
344 (* Document highlighting *)
345 | (Initialized
{ server_env; _ }, Document_highlight document_location
) ->
347 make_context_from_document_location server_env document_location
in
348 let results = Provider_utils.with_context ~ctx ~f
:(fun () ->
349 ServerHighlightRefs.go_ctx
352 ~line
:document_location
.line
353 ~column
:document_location
.column
354 ~tcopt
:server_env.ServerEnv.tcopt
356 Lwt.return
(state, Handle_message_result.Response
results)
358 | (Initialized
{ server_env; _ }, Definition document_location
) ->
360 make_context_from_document_location server_env document_location
in
361 let result = Provider_utils.with_context ~ctx ~f
:(fun () ->
362 ServerGoToDefinition.go_ctx
365 ~line
:document_location
.ClientIdeMessage.line
366 ~column
:document_location
.ClientIdeMessage.column
368 Lwt.return
(state, Handle_message_result.Response
result)
370 (* Type Definition *)
371 | (Initialized
{ server_env; _ }, Type_definition document_location
) ->
373 make_context_from_document_location server_env document_location
in
374 let result = Provider_utils.with_context ~ctx ~f
:(fun () ->
375 ServerTypeDefinition.go_ctx
378 ~line
:document_location
.ClientIdeMessage.line
379 ~column
:document_location
.ClientIdeMessage.column
381 Lwt.return
(state, Handle_message_result.Response
result)
384 ~
(out_fd
: Lwt_unix.file_descr
)
385 ~
(message: ClientIdeMessage.message_from_daemon
)
388 Marshal_tools_lwt.to_fd_with_preamble out_fd
message in
393 ~
(in_fd
: Lwt_unix.file_descr
)
394 ~
(out_fd
: Lwt_unix.file_descr
)
396 let rec pump_message_queue (message_queue
: message_queue
): unit Lwt.t
=
398 let%lwt
(message: a
ClientIdeMessage.t
) =
399 Marshal_tools_lwt.from_fd_with_preamble in_fd
in
401 Lwt_message_queue.push message_queue
(Message
message) in
403 | ClientIdeMessage.Shutdown
() ->
405 | _ when not
is_queue_open ->
408 pump_message_queue message_queue
410 let e = Exception.wrap
e in
411 Lwt_message_queue.close message_queue
;
415 let rec handle_messages (t
: t
): unit Lwt.t
=
418 state = Initialized
({ server_env; changed_files_to_process; _ }
420 when (Lwt_message_queue.is_empty message_queue
)
421 && not
(Lwt_unix.readable in_fd
)
422 && not
(Path.Set.is_empty
changed_files_to_process) ->
423 (* Process the next file change, but only if we have no new events to
424 handle. To ensure correctness, we would have to actually process all file
425 change events *before* we processed any other IDE queries. However, we're
426 trying to maximize availability, even if occasionally we give stale
427 results. We can revisit this trade-off later if we decide that the stale
428 results are baffling users. *)
429 let next_file = Path.Set.choose
changed_files_to_process in
430 let changed_files_to_process =
431 Path.Set.remove
changed_files_to_process next_file in
433 ClientIdeIncremental.process_changed_file
server_env next_file in
435 if Path.Set.is_empty
changed_files_to_process
437 let message = ClientIdeMessage.(Notification Done_processing
) in
438 let%lwt
() = write_message ~out_fd ~
message in
443 let state = Initialized
{
446 changed_files_to_process;
448 handle_messages { t
with state }
451 let%lwt
message = Lwt_message_queue.pop t
.message_queue
in
455 | Some
(Message
message) ->
458 let%lwt
(state, response
) = handle_message t
.state message in
460 | Handle_message_result.Notification
->
461 (* No response needed for notifications. *)
463 | Handle_message_result.Response response
->
464 let response = ClientIdeMessage.Response
(Ok
response) in
465 let%lwt
() = write_message ~out_fd ~
message:response in
467 | Handle_message_result.Error
message ->
468 let response = ClientIdeMessage.Response
(Error
message) in
469 let%lwt
() = write_message ~out_fd ~
message:response in
472 let stack = Printexc.get_backtrace
() in
473 Hh_logger.exc ~prefix
:"[ide-daemon] exception: " ~
stack e;
476 handle_messages { t
with state }
480 let message_queue = Lwt_message_queue.create
() in
481 let%lwt
() = handle_messages {
483 state = Initializing
;
485 and () = pump_message_queue message_queue in
488 let e = Exception.wrap
e in
490 "Exception occurred while handling RPC call: %s"
491 (Exception.to_string
e);
494 let daemon_main () (channels
: ('a
, 'b
) Daemon.channel_pair
) : unit =
495 Printexc.record_backtrace
true;
496 let (ic
, oc
) = channels
in
497 let in_fd = Lwt_unix.of_unix_file_descr
(Daemon.descr_of_in_channel ic
) in
498 let out_fd = Lwt_unix.of_unix_file_descr
(Daemon.descr_of_out_channel oc
) in
499 Lwt_main.run
(serve ~
in_fd ~
out_fd)
501 let daemon_entry_point : (unit, unit, unit) Daemon.entry
=
502 Daemon.register_entry_point
"ClientIdeService" daemon_main