Skip to content

Commit c93445d

Browse files
committed
feat: properly respond to and log initialization errors
1 parent 0ff78f1 commit c93445d

File tree

9 files changed

+138
-53
lines changed

9 files changed

+138
-53
lines changed

apps/expert/lib/expert.ex

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,25 @@ defmodule Expert do
5353
state = assigns(lsp).state
5454
Process.send_after(self(), :default_config, :timer.seconds(5))
5555

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)
60-
61-
{:reply, response, lsp}
56+
with {:ok, response, state} <- State.initialize(state, request),
57+
{:ok, response} <- Expert.Protocol.Convert.to_lsp(response) do
58+
lsp = assign(lsp, state: state)
6259

63-
{:error, error} ->
64-
response = %GenLSP.ErrorResponse{
65-
code: GenLSP.Enumerations.ErrorCodes.invalid_request(),
66-
message: to_string(error)
67-
}
60+
{:reply, response, lsp}
61+
else
62+
{:error, {:shutdown, {:failed_to_start_child, _, _}}} ->
63+
{:reply,
64+
%GenLSP.ErrorResponse{
65+
code: GenLSP.Enumerations.ErrorCodes.server_not_initialized(),
66+
message: "Failed to start node"
67+
}, lsp}
6868

69-
{:reply, response, lsp}
69+
{:error, reason} ->
70+
{:reply,
71+
%GenLSP.ErrorResponse{
72+
code: GenLSP.Enumerations.ErrorCodes.server_not_initialized(),
73+
message: to_string(reason)
74+
}, lsp}
7075
end
7176
end
7277

apps/expert/lib/expert/engine_node.ex

Lines changed: 28 additions & 1 deletion
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

@@ -103,6 +106,19 @@ defmodule Expert.EngineNode do
103106
end
104107
end
105108

109+
def on_exit_status(%__MODULE__{} = state, exit_status) do
110+
if exit_status > 0 do
111+
GenLSP.error(
112+
Expert.get_lsp(),
113+
"Node exited with status #{exit_status}, last message: #{to_string(state.last_message)}"
114+
)
115+
else
116+
Logger.debug("Node exited with status 0")
117+
end
118+
119+
%{state | status: :stopped}
120+
end
121+
106122
def maybe_reply_to_stopper(%State{stopped_by: stopped_by} = state)
107123
when is_tuple(stopped_by) do
108124
GenServer.reply(state.stopped_by, :ok)
@@ -252,6 +268,10 @@ defmodule Expert.EngineNode do
252268
Logger.debug("Building engine: #{to_string(data)}")
253269
wait_for_engine(port, data)
254270

271+
{^port, {:exit_status, status}} ->
272+
Logger.debug("Engine build script exited with status: #{to_string(status)}")
273+
{:error, {:exit_status, status}, last_line}
274+
255275
{:EXIT, ^port, reason} ->
256276
Logger.error("Engine build script exited with reason: #{inspect(reason)} #{last_line}")
257277
{:error, reason, last_line}
@@ -372,10 +392,17 @@ defmodule Expert.EngineNode do
372392
{:stop, :shutdown, state}
373393
end
374394

395+
@impl true
396+
def handle_info({_port, {:exit_status, exit_status}}, %State{} = state) do
397+
state = State.on_exit_status(state, exit_status)
398+
399+
{:stop, :shutdown, state}
400+
end
401+
375402
@impl true
376403
def handle_info({_port, {:data, message}}, %State{} = state) do
377404
Logger.debug("Node port message: #{to_string(message)}")
378-
{:noreply, state}
405+
{:noreply, %{state | last_message: message}}
379406
end
380407

381408
@impl true

apps/expert/lib/expert/port.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ defmodule Expert.Port do
112112
opts
113113
end
114114

115-
Port.open({:spawn_executable, launcher}, [:stderr_to_stdout | opts])
115+
Port.open({:spawn_executable, launcher}, [:stderr_to_stdout, :exit_status] ++ opts)
116116
end
117117

118118
@doc """

apps/expert/lib/expert/state.ex

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,15 @@ defmodule Expert.State do
5050
end
5151

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

56-
response = initialize_result()
57-
58-
Task.Supervisor.start_child(:expert_task_queue, fn ->
59-
{:ok, _pid} = Project.Supervisor.start(config.project)
55+
with {:ok, _pid} <- Project.Supervisor.start(config.project) do
6056
send(Expert, :engine_initialized)
61-
end)
57+
new_state = %__MODULE__{state | configuration: config, initialized?: true}
58+
response = initialize_result()
6259

63-
{:ok, response, new_state}
60+
{:ok, response, new_state}
61+
end
6462
end
6563

6664
def initialize(%__MODULE__{initialized?: true}, %Requests.Initialize{}) do

apps/expert/test/expert/engine_node_test.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Expert.EngineNodeTest do
44

55
import Forge.Test.EventualAssertions
66
import Forge.Test.Fixtures
7+
import Expert.Test.Protocol.TransportSupport
78

89
use ExUnit.Case, async: false
910
use Patch
@@ -64,4 +65,34 @@ defmodule Expert.EngineNodeTest do
6465
assert_receive {:stopped, 1}
6566
assert_receive {:lsp_log, "Couldn't find an elixir executable for project" <> _}
6667
end
68+
69+
test "sends a message to client if exited with error code", %{project: project} do
70+
with_patched_transport()
71+
72+
{:ok, _node_name, node_pid} = EngineNode.start(project)
73+
74+
project_alive? = project |> EngineNode.name() |> Process.whereis() |> Process.alive?()
75+
assert project_alive?
76+
77+
exit_status = 127
78+
79+
send(node_pid, {nil, {:exit_status, exit_status}})
80+
81+
error_message_type = GenLSP.Enumerations.MessageType.error()
82+
83+
assert_receive {:transport,
84+
%GenLSP.Notifications.WindowLogMessage{
85+
params: %GenLSP.Structures.LogMessageParams{
86+
type: ^error_message_type,
87+
message: message
88+
}
89+
}}
90+
91+
assert String.starts_with?(
92+
message,
93+
"Node exited with status #{exit_status}, last message:"
94+
)
95+
96+
assert_eventually Process.whereis(EngineNode.name(project)) == nil, :timer.seconds(5)
97+
end
6798
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_supports?, fn :work_done_progress -> true end)
6350
:ok

apps/expert/test/expert_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule Expert.ExpertTest do
2+
use ExUnit.Case, async: true
3+
use Patch
4+
5+
describe "handle_request/2" do
6+
test "it replies with server_not_initialized on initialization error" do
7+
reason = :something_bad
8+
9+
patch(Expert.State, :initialize, fn _state, _request ->
10+
{:error, reason}
11+
end)
12+
13+
assigns = start_supervised!(GenLSP.Assigns, id: make_ref())
14+
GenLSP.Assigns.merge(assigns, %{state: %{}})
15+
16+
lsp = %GenLSP.LSP{
17+
mod: Elixir,
18+
assigns: assigns
19+
}
20+
21+
initialize_request = %GenLSP.Requests.Initialize{
22+
id: 1,
23+
jsonrpc: "2.0",
24+
method: "initialize"
25+
}
26+
27+
{:reply, response, _lsp} = Expert.handle_request(initialize_request, lsp)
28+
29+
message = to_string(reason)
30+
error_code = GenLSP.Enumerations.ErrorCodes.server_not_initialized()
31+
32+
assert %GenLSP.ErrorResponse{code: ^error_code, data: nil, message: ^message} = response
33+
end
34+
end
35+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Expert.Test.Protocol.TransportSupport do
2+
def with_patched_transport(_ \\ nil) do
3+
test = self()
4+
5+
Patch.patch(GenLSP, :notify_server, fn _, message ->
6+
send(test, {:transport, message})
7+
end)
8+
9+
Patch.patch(GenLSP, :notify, fn _, message ->
10+
send(test, {:transport, message})
11+
end)
12+
13+
Patch.patch(GenLSP, :request, fn _, message ->
14+
send(test, {:transport, message})
15+
end)
16+
17+
:ok
18+
end
19+
end

0 commit comments

Comments
 (0)