Skip to content

Commit 81e1184

Browse files
authored
fix: properly log when engine fails to initialize (#244)
* feat: properly respond to and log initialization errors * Allow the engine to start asynchronously It can take many seconds until the engine is available, hence editors might time out the initialization. We still at least send an error $window/logMessage with the initialization error when it happens. * Address review concerns - Also show errors in the client via window/showMessage - Improve logging cases - Only register with nodemapper when node started ok - For now do not introduce full expert test * Reintroduce full server testing with different approach Now we start manually all application children instead of ensuring the application has started * Remove full server testing, too problematic for now * Address a few further review concerns * Expert test is async safe * Log project uri
1 parent b5b001b commit 81e1184

File tree

10 files changed

+254
-79
lines changed

10 files changed

+254
-79
lines changed

apps/expert/lib/expert.ex

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
defmodule Expert do
2+
alias Expert.Project
23
alias Expert.Protocol.Convert
34
alias Expert.Protocol.Id
45
alias Expert.Provider.Handlers
56
alias Expert.State
7+
alias GenLSP.Enumerations
68
alias GenLSP.Requests
79
alias GenLSP.Structures
810

@@ -51,19 +53,33 @@ defmodule Expert do
5153

5254
def handle_request(%GenLSP.Requests.Initialize{} = request, lsp) do
5355
state = assigns(lsp).state
54-
Process.send_after(self(), :default_config, :timer.seconds(5))
5556

56-
case State.initialize(state, request) do
57-
{:ok, response, state} ->
58-
lsp = assign(lsp, state: state)
59-
{:ok, response} = Expert.Protocol.Convert.to_lsp(response)
57+
with {:ok, response, state} <- State.initialize(state, request),
58+
{:ok, response} <- Expert.Protocol.Convert.to_lsp(response) do
59+
Task.Supervisor.start_child(:expert_task_queue, fn ->
60+
config = state.configuration
6061

61-
{:reply, response, lsp}
62+
log_info(lsp, "Starting project")
63+
64+
start_result = Project.Supervisor.start(config.project)
6265

63-
{:error, error} ->
66+
send(Expert, {:engine_initialized, start_result})
67+
end)
68+
69+
{:reply, response, assign(lsp, state: state)}
70+
else
71+
{:error, :already_initialized} ->
6472
response = %GenLSP.ErrorResponse{
6573
code: GenLSP.Enumerations.ErrorCodes.invalid_request(),
66-
message: to_string(error)
74+
message: "Already initialized"
75+
}
76+
77+
{:reply, response, lsp}
78+
79+
{:error, reason} ->
80+
response = %GenLSP.ErrorResponse{
81+
code: GenLSP.Enumerations.ErrorCodes.server_not_initialized(),
82+
message: "Failed to initialize: #{inspect(reason)}"
6783
}
6884

6985
{:reply, response, lsp}
@@ -173,7 +189,7 @@ defmodule Expert do
173189
end
174190
end
175191

176-
def handle_info(:engine_initialized, lsp) do
192+
def handle_info({:engine_initialized, {:ok, _pid}}, lsp) do
177193
state = assigns(lsp).state
178194

179195
new_state = %{state | engine_initialized?: true}
@@ -185,20 +201,33 @@ defmodule Expert do
185201
{:noreply, lsp}
186202
end
187203

188-
def handle_info(:default_config, lsp) do
189-
state = assigns(lsp).state
204+
def handle_info({:engine_initialized, {:error, reason}}, lsp) do
205+
error_message = initialization_error_message(reason)
206+
log_error(lsp, error_message)
190207

191-
if state.configuration == nil do
192-
Logger.warning(
193-
"Did not receive workspace/didChangeConfiguration notification after 5 seconds. " <>
194-
"Using default settings."
195-
)
208+
{:noreply, lsp}
209+
end
196210

197-
{:ok, config} = State.default_configuration(state)
198-
{:noreply, assign(lsp, state: %{state | configuration: config})}
199-
else
200-
{:noreply, lsp}
201-
end
211+
def log_info(lsp \\ get_lsp(), message) do
212+
message = log_prepend_project_root(message, assigns(lsp).state)
213+
214+
Logger.info(message)
215+
GenLSP.info(lsp, message)
216+
end
217+
218+
# When logging errors we also notify the client to display the message
219+
def log_error(lsp \\ get_lsp(), message) do
220+
message = log_prepend_project_root(message, assigns(lsp).state)
221+
222+
Logger.error(message)
223+
GenLSP.error(lsp, message)
224+
225+
GenLSP.notify(lsp, %GenLSP.Notifications.WindowShowMessage{
226+
params: %GenLSP.Structures.ShowMessageParams{
227+
type: Enumerations.MessageType.error(),
228+
message: message
229+
}
230+
})
202231
end
203232

204233
defp apply_to_state(%State{} = state, %{} = request_or_notification) do
@@ -273,4 +302,41 @@ defmodule Expert do
273302
register_options: %Structures.DidChangeWatchedFilesRegistrationOptions{watchers: watchers}
274303
}
275304
end
305+
306+
defp initialization_error_message({:shutdown, {:failed_to_start_child, child, {reason, _}}}) do
307+
case child do
308+
{Project.Node, node_name} ->
309+
node_initialization_message(node_name, reason)
310+
311+
child ->
312+
"Failed to start child #{inspect(child)}: #{inspect(reason)}"
313+
end
314+
end
315+
316+
defp initialization_error_message(reason) do
317+
"Failed to initialize: #{inspect(reason)}"
318+
end
319+
320+
defp node_initialization_message(name, reason) do
321+
case reason do
322+
# NOTE:
323+
# ** (Mix.Error) httpc request failed with: ... Could not install Hex because Mix could not download metadata ...
324+
{:shutdown, {:error, :normal, message}} ->
325+
"Engine #{name} shutdown with error:\n\n#{message}"
326+
327+
{:shutdown, {:node_exit, node_exit}} ->
328+
"Engine #{name} exit with status #{node_exit.status}, last message:\n\n#{node_exit.last_message}"
329+
330+
reason ->
331+
"Failed to start engine #{name}: #{inspect(reason)}"
332+
end
333+
end
334+
335+
defp log_prepend_project_root(message, %State{
336+
configuration: %Expert.Configuration{project: %Forge.Project{} = project}
337+
}) do
338+
"[Project #{project.root_uri}] #{message}"
339+
end
340+
341+
defp log_prepend_project_root(message, _state), do: message
276342
end

apps/expert/lib/expert/application.ex

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,18 @@ defmodule Expert.Application do
6666

6767
ensure_epmd_module!()
6868

69-
children = [
69+
LogFilter.hook_into_logger()
70+
71+
children_spec = children(buffer: buffer_opts)
72+
opts = [strategy: :one_for_one, name: Expert.Supervisor]
73+
74+
Supervisor.start_link(children_spec, opts)
75+
end
76+
77+
def children(opts) do
78+
buffer_opts = Keyword.fetch!(opts, :buffer)
79+
80+
[
7081
{Forge.NodePortMapper, []},
7182
document_store_child_spec(),
7283
{DynamicSupervisor, Expert.Project.DynamicSupervisor.options()},
@@ -81,11 +92,6 @@ defmodule Expert.Application do
8192
dynamic_supervisor: Expert.DynamicSupervisor,
8293
assigns: Expert.Assigns}
8394
]
84-
85-
LogFilter.hook_into_logger()
86-
87-
opts = [strategy: :one_for_one, name: Expert.Supervisor]
88-
Supervisor.start_link(children, opts)
8995
end
9096

9197
@doc false

apps/expert/lib/expert/engine_node.ex

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ defmodule Expert.EngineNode do
55
use Expert.Project.Progress.Support
66

77
defmodule State do
8+
require Logger
9+
810
defstruct [
911
:project,
1012
:port,
1113
:cookie,
1214
:stopped_by,
1315
:stop_timeout,
1416
:started_by,
17+
:last_message,
1518
:status
1619
]
1720

@@ -47,9 +50,14 @@ defmodule Expert.EngineNode do
4750
# If we start distribution manually after all the code is loaded,
4851
# everything works fine.
4952
"""
50-
{:ok, _} = Node.start(:"#{Project.node_name(state.project)}", :longnames)
51-
#{Forge.NodePortMapper}.register()
52-
IO.puts(\"ok\")
53+
node_start = Node.start(:"#{Project.node_name(state.project)}", :longnames)
54+
case node_start do
55+
{:ok, _} ->
56+
#{Forge.NodePortMapper}.register()
57+
IO.puts(\"ok\")
58+
{:error, reason} ->
59+
IO.puts(\"error starting node:\n \#{inspect(reason)}\")
60+
end
5361
"""
5462
| path_append_arguments(paths)
5563
]
@@ -103,6 +111,28 @@ defmodule Expert.EngineNode do
103111
end
104112
end
105113

114+
def on_exit_status(%__MODULE__{} = state, exit_status) do
115+
stop_reason =
116+
case exit_status do
117+
0 ->
118+
project = state.project
119+
Logger.info("Engine for #{project.root_uri} shut down")
120+
121+
:shutdown
122+
123+
_error_status ->
124+
Logger.error(
125+
"Engine shut down unexpectedly, node exited with status #{exit_status}). Last message: #{state.last_message}"
126+
)
127+
128+
{:shutdown, {:node_exit, %{status: exit_status, last_message: state.last_message}}}
129+
end
130+
131+
new_state = %{state | status: :stopped}
132+
133+
{stop_reason, new_state}
134+
end
135+
106136
def maybe_reply_to_stopper(%State{stopped_by: stopped_by} = state)
107137
when is_tuple(stopped_by) do
108138
GenServer.reply(state.stopped_by, :ok)
@@ -373,9 +403,18 @@ defmodule Expert.EngineNode do
373403
end
374404

375405
@impl true
376-
def handle_info({_port, {:data, message}}, %State{} = state) do
377-
Logger.debug("Node port message: #{to_string(message)}")
378-
{:noreply, state}
406+
def handle_info({_port, {:exit_status, exit_status}}, %State{} = state) do
407+
{stop_reason, state} = State.on_exit_status(state, exit_status)
408+
409+
{:stop, stop_reason, state}
410+
end
411+
412+
@impl true
413+
def handle_info({_port, {:data, data}}, %State{} = state) do
414+
message = to_string(data)
415+
Logger.debug("Node port message: #{message}")
416+
417+
{:noreply, %{state | last_message: message}}
379418
end
380419

381420
@impl true

apps/expert/lib/expert/port.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ defmodule Expert.Port do
118118
opts
119119
end
120120

121-
Port.open({:spawn_executable, launcher}, [:stderr_to_stdout | opts])
121+
Port.open({:spawn_executable, launcher}, [:stderr_to_stdout, :exit_status] ++ opts)
122122
end
123123

124124
@doc """

apps/expert/lib/expert/state.ex

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ defmodule Expert.State do
3535
%__MODULE__{}
3636
end
3737

38-
# TODO: this function has a side effect (starting the project supervisor)
39-
# that i think might be better off in the calling function
4038
def initialize(
4139
%__MODULE__{initialized?: false} = state,
4240
%Requests.Initialize{
@@ -50,15 +48,9 @@ defmodule Expert.State do
5048
end
5149

5250
config = Configuration.new(event.root_uri, event.capabilities, client_name)
53-
new_state = %__MODULE__{state | configuration: config, initialized?: true}
54-
Logger.info("Starting project at uri #{config.project.root_uri}")
5551

5652
response = initialize_result()
57-
58-
Task.Supervisor.start_child(:expert_task_queue, fn ->
59-
{:ok, _pid} = Project.Supervisor.start(config.project)
60-
send(Expert, :engine_initialized)
61-
end)
53+
new_state = %__MODULE__{state | configuration: config, initialized?: true}
6254

6355
{:ok, response, new_state}
6456
end
@@ -67,10 +59,6 @@ defmodule Expert.State do
6759
{:error, :already_initialized}
6860
end
6961

70-
def default_configuration(%__MODULE__{configuration: config}) do
71-
Configuration.default(config)
72-
end
73-
7462
def apply(%__MODULE__{initialized?: false}, request) do
7563
Logger.error("Received #{request.method} before server was initialized")
7664
{:error, :not_initialized}

apps/expert/test/expert/engine_node_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,20 @@ defmodule Expert.EngineNodeTest do
6464
assert_receive {:stopped, 1}
6565
assert_receive {:lsp_log, "Couldn't find an elixir executable for project" <> _}
6666
end
67+
68+
test "shuts down with error message if exited with error code", %{project: project} do
69+
{:ok, _node_name, node_pid} = EngineNode.start(project)
70+
71+
Process.monitor(node_pid)
72+
73+
exit_status = 127
74+
75+
send(node_pid, {nil, {:exit_status, exit_status}})
76+
77+
assert_receive {:DOWN, _ref, :process, ^node_pid, exit_reason}
78+
79+
assert {:shutdown, {:node_exit, node_exit}} = exit_reason
80+
assert %{status: ^exit_status, last_message: last_message} = node_exit
81+
assert is_binary(last_message)
82+
end
6783
end

apps/expert/test/expert/project/diagnostics_test.exs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule Expert.Project.DiagnosticsTest do
1313

1414
import Forge.EngineApi.Messages
1515
import Forge.Test.Fixtures
16+
import Expert.Test.Protocol.TransportSupport
1617

1718
setup do
1819
project = project()
@@ -37,24 +38,6 @@ defmodule Expert.Project.DiagnosticsTest do
3738
struct(Diagnostic.Result, values)
3839
end
3940

40-
def with_patched_transport(_) do
41-
test = self()
42-
43-
patch(GenLSP, :notify_server, fn _, message ->
44-
send(test, {:transport, message})
45-
end)
46-
47-
patch(GenLSP, :notify, fn _, message ->
48-
send(test, {:transport, message})
49-
end)
50-
51-
patch(GenLSP, :request, fn _, message ->
52-
send(test, {:transport, message})
53-
end)
54-
55-
:ok
56-
end
57-
5841
defp open_file(project, contents) do
5942
uri = file_uri(project, "lib/project.ex")
6043
:ok = Document.Store.open(uri, contents, 0)

apps/expert/test/expert/project/progress_test.exs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Expert.Project.ProgressTest do
99

1010
import Forge.Test.Fixtures
1111
import Forge.EngineApi.Messages
12+
import Expert.Test.Protocol.TransportSupport
1213

1314
use ExUnit.Case
1415
use Patch
@@ -44,20 +45,6 @@ defmodule Expert.Project.ProgressTest do
4445
project_progress(label: label, message: message, stage: stage)
4546
end
4647

47-
def with_patched_transport(_) do
48-
test = self()
49-
50-
patch(GenLSP, :notify, fn _, message ->
51-
send(test, {:transport, message})
52-
end)
53-
54-
patch(GenLSP, :request, fn _, message ->
55-
send(test, {:transport, message})
56-
end)
57-
58-
:ok
59-
end
60-
6148
def with_work_done_progress_support(_) do
6249
patch(Configuration, :client_support, fn :work_done_progress -> true end)
6350
:ok

0 commit comments

Comments
 (0)