Skip to content

Commit e77cebd

Browse files
zachallaunmhanberg
andauthored
fix: improve error handling for compiler diagnostics (#165)
Co-authored-by: Mitchell Hanberg <[email protected]>
1 parent 666e3cb commit e77cebd

File tree

5 files changed

+184
-64
lines changed

5 files changed

+184
-64
lines changed

lib/next_ls/extensions/elixir_extension.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule NextLS.ElixirExtension do
2424
end
2525

2626
@impl GenServer
27-
def handle_info({:compiler, diagnostics}, state) do
27+
def handle_info({:compiler, diagnostics}, state) when is_list(diagnostics) do
2828
DiagnosticCache.clear(state.cache, :elixir)
2929

3030
for d <- diagnostics do

lib/next_ls/runtime.ex

+43-19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule NextLS.Runtime do
33
use GenServer
44

55
@env Mix.env()
6+
defguardp is_ready(state) when is_map_key(state, :node)
67

78
def start_link(opts) do
89
GenServer.start_link(__MODULE__, opts)
@@ -125,10 +126,10 @@ defmodule NextLS.Runtime do
125126
{:DOWN, ^ref, :process, ^me, reason} ->
126127
case reason do
127128
:shutdown ->
128-
NextLS.Logger.log(logger, "The runtime for #{name} has successfully shutdown.")
129+
NextLS.Logger.log(logger, "The runtime for #{name} has successfully shut down.")
129130

130131
reason ->
131-
NextLS.Logger.error(logger, "The runtime for #{name} has crashed with reason: #{reason}.")
132+
NextLS.Logger.error(logger, "The runtime for #{name} has crashed with reason: #{inspect(reason)}")
132133
end
133134
end
134135
end)
@@ -144,8 +145,8 @@ defmodule NextLS.Runtime do
144145
|> Path.join("monkey/_next_ls_private_compiler.ex")
145146
|> then(&:rpc.call(node, Code, :compile_file, [&1]))
146147
|> tap(fn
147-
{:badrpc, :EXIT, {error, _}} ->
148-
NextLS.Logger.error(logger, error)
148+
{:badrpc, error} ->
149+
NextLS.Logger.error(logger, "Bad RPC call to node #{node}: #{inspect(error)}")
149150

150151
_ ->
151152
:ok
@@ -155,9 +156,8 @@ defmodule NextLS.Runtime do
155156

156157
send(me, {:node, node})
157158
else
158-
_ ->
159-
on_initialized.(:error)
160-
send(me, :cancel)
159+
error ->
160+
send(me, {:cancel, error})
161161
end
162162
end)
163163

@@ -180,35 +180,42 @@ defmodule NextLS.Runtime do
180180
end
181181

182182
@impl GenServer
183-
def handle_call(:ready?, _from, %{node: _node} = state) do
183+
def handle_call(:ready?, _from, state) when is_ready(state) do
184184
{:reply, true, state}
185185
end
186186

187187
def handle_call(:ready?, _from, state) do
188188
{:reply, false, state}
189189
end
190190

191+
def handle_call(_, _from, state) when not is_ready(state) do
192+
{:reply, {:error, :not_ready}, state}
193+
end
194+
191195
def handle_call({:call, {m, f, a}}, _from, %{node: node} = state) do
192196
reply = :rpc.call(node, m, f, a)
193197
{:reply, {:ok, reply}, state}
194198
end
195199

196-
def handle_call({:call, _}, _from, state) do
197-
{:reply, {:error, :not_ready}, state}
198-
end
199-
200200
def handle_call(:compile, from, %{node: node} = state) do
201201
task =
202202
Task.Supervisor.async_nolink(state.task_supervisor, fn ->
203-
{_, errors} = :rpc.call(node, :_next_ls_private_compiler, :compile, [])
203+
case :rpc.call(node, :_next_ls_private_compiler, :compile, []) do
204+
{:badrpc, error} ->
205+
NextLS.Logger.error(state.logger, "Bad RPC call to node #{node}: #{inspect(error)}")
206+
[]
204207

205-
Registry.dispatch(state.registry, :extensions, fn entries ->
206-
for {pid, _} <- entries do
207-
send(pid, {:compiler, errors})
208-
end
209-
end)
208+
{_, diagnostics} when is_list(diagnostics) ->
209+
Registry.dispatch(state.registry, :extensions, fn entries ->
210+
for {pid, _} <- entries, do: send(pid, {:compiler, diagnostics})
211+
end)
210212

211-
errors
213+
diagnostics
214+
215+
unknown ->
216+
NextLS.Logger.warning(state.logger, "Unexpected compiler response: #{inspect(unknown)}")
217+
[]
218+
end
212219
end)
213220

214221
{:noreply, %{state | compiler_ref: %{task.ref => from}}}
@@ -228,12 +235,29 @@ defmodule NextLS.Runtime do
228235
{:noreply, %{state | compiler_ref: nil}}
229236
end
230237

238+
def handle_info({:DOWN, _, :port, port, _}, %{port: port} = state) do
239+
unless is_ready(state) do
240+
state.on_initialized.({:error, :portdown})
241+
end
242+
243+
{:stop, {:shutdown, :portdown}, state}
244+
end
245+
246+
def handle_info({:cancel, error}, state) do
247+
state.on_initialized.({:error, error})
248+
{:stop, error, state}
249+
end
250+
231251
def handle_info({:node, node}, state) do
232252
Node.monitor(node, true)
233253
state.on_initialized.(:ready)
234254
{:noreply, Map.put(state, :node, node)}
235255
end
236256

257+
def handle_info({:nodedown, node}, %{node: node} = state) do
258+
{:stop, {:shutdown, :nodedown}, state}
259+
end
260+
237261
def handle_info({port, {:data, data}}, %{port: port} = state) do
238262
NextLS.Logger.log(state.logger, data)
239263
{:noreply, state}

test/next_ls/runtime_test.exs

+133-41
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
defmodule NextLs.RuntimeTest do
22
use ExUnit.Case, async: true
33

4-
import ExUnit.CaptureLog
54
import NextLS.Support.Utils
65

76
alias NextLS.Runtime
@@ -23,11 +22,13 @@ defmodule NextLs.RuntimeTest do
2322
end
2423
""")
2524

25+
me = self()
26+
2627
{:ok, logger} =
2728
Task.start_link(fn ->
2829
recv = fn recv ->
2930
receive do
30-
msg -> Logger.debug(inspect(msg))
31+
{:"$gen_cast", msg} -> send(me, msg)
3132
end
3233

3334
recv.(recv)
@@ -36,68 +37,136 @@ defmodule NextLs.RuntimeTest do
3637
recv.(recv)
3738
end)
3839

39-
me = self()
40+
on_init = fn msg -> send(me, msg) end
4041

41-
on_init = fn _ -> send(me, :ready) end
42+
on_exit(&flush_messages/0)
4243

4344
[logger: logger, cwd: Path.absname(tmp_dir), on_init: on_init]
4445
end
4546

46-
test "returns the response in an ok tuple", %{logger: logger, cwd: cwd, on_init: on_init} do
47-
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
48-
tvisor = start_supervised!(Task.Supervisor)
47+
describe "errors" do
48+
test "emitted on crash during initialization",
49+
%{tmp_dir: tmp_dir, logger: logger, cwd: cwd, on_init: on_init} do
50+
# obvious syntax error
51+
bad_mix_exs = String.replace(mix_exs(), "defmodule", "")
52+
File.write!(Path.join(tmp_dir, "mix.exs"), bad_mix_exs)
53+
54+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
55+
56+
tvisor = start_supervised!(Task.Supervisor)
4957

50-
pid =
5158
start_supervised!(
5259
{Runtime,
60+
task_supervisor: tvisor,
5361
name: "my_proj",
5462
on_initialized: on_init,
55-
task_supervisor: tvisor,
5663
working_dir: cwd,
5764
uri: "file://#{cwd}",
5865
parent: self(),
5966
logger: logger,
6067
db: :some_db,
61-
registry: RuntimeTest.Registry}
68+
registry: RuntimeTest.Registry},
69+
restart: :temporary
6270
)
6371

64-
Process.link(pid)
72+
assert_receive {:error, :portdown}
73+
74+
assert_receive {:log, :log, log_msg}
75+
assert log_msg =~ "syntax error"
76+
77+
assert_receive {:log, :error, error_msg}
78+
assert error_msg =~ "{:shutdown, :portdown}"
79+
end
80+
81+
test "emitted on crash after initialization",
82+
%{logger: logger, cwd: cwd, on_init: on_init} do
83+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
84+
85+
tvisor = start_supervised!(Task.Supervisor)
86+
87+
pid =
88+
start_supervised!(
89+
{Runtime,
90+
task_supervisor: tvisor,
91+
name: "my_proj",
92+
on_initialized: on_init,
93+
working_dir: cwd,
94+
uri: "file://#{cwd}",
95+
parent: self(),
96+
logger: logger,
97+
db: :some_db,
98+
registry: RuntimeTest.Registry},
99+
restart: :temporary
100+
)
101+
102+
assert_receive :ready
65103

66-
assert wait_for_ready()
104+
assert {:ok, {:badrpc, :nodedown}} = Runtime.call(pid, {System, :halt, [1]})
67105

68-
assert {:ok, "\"hi\""} = Runtime.call(pid, {Kernel, :inspect, ["hi"]})
106+
assert_receive {:log, :error, error_msg}
107+
assert error_msg =~ "{:shutdown, :nodedown}"
108+
end
69109
end
70110

71-
test "call returns an error when the runtime is node ready", %{logger: logger, cwd: cwd, on_init: on_init} do
72-
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
111+
describe "call/2" do
112+
test "responds with an ok tuple if the runtime has initialized",
113+
%{logger: logger, cwd: cwd, on_init: on_init} do
114+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
115+
tvisor = start_supervised!(Task.Supervisor)
73116

74-
tvisor = start_supervised!(Task.Supervisor)
117+
pid =
118+
start_supervised!(
119+
{Runtime,
120+
name: "my_proj",
121+
on_initialized: on_init,
122+
task_supervisor: tvisor,
123+
working_dir: cwd,
124+
uri: "file://#{cwd}",
125+
parent: self(),
126+
logger: logger,
127+
db: :some_db,
128+
registry: RuntimeTest.Registry}
129+
)
75130

76-
pid =
77-
start_supervised!(
78-
{Runtime,
79-
task_supervisor: tvisor,
80-
name: "my_proj",
81-
on_initialized: on_init,
82-
working_dir: cwd,
83-
uri: "file://#{cwd}",
84-
parent: self(),
85-
logger: logger,
86-
db: :some_db,
87-
registry: RuntimeTest.Registry}
88-
)
131+
Process.link(pid)
132+
133+
assert_receive :ready
89134

90-
Process.link(pid)
135+
assert {:ok, "\"hi\""} = Runtime.call(pid, {Kernel, :inspect, ["hi"]})
136+
end
137+
138+
test "responds with an error when the runtime hasn't initialized", %{logger: logger, cwd: cwd, on_init: on_init} do
139+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
140+
141+
tvisor = start_supervised!(Task.Supervisor)
142+
143+
pid =
144+
start_supervised!(
145+
{Runtime,
146+
task_supervisor: tvisor,
147+
name: "my_proj",
148+
on_initialized: on_init,
149+
working_dir: cwd,
150+
uri: "file://#{cwd}",
151+
parent: self(),
152+
logger: logger,
153+
db: :some_db,
154+
registry: RuntimeTest.Registry}
155+
)
91156

92-
assert {:error, :not_ready} = Runtime.call(pid, {IO, :puts, ["hi"]})
157+
Process.link(pid)
158+
159+
assert {:error, :not_ready} = Runtime.call(pid, {IO, :puts, ["hi"]})
160+
end
93161
end
94162

95-
test "compiles the code and returns diagnostics", %{logger: logger, cwd: cwd, on_init: on_init} do
96-
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
163+
describe "compile/1" do
164+
test "compiles the project and returns diagnostics",
165+
%{logger: logger, cwd: cwd, on_init: on_init} do
166+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
97167

98-
tvisor = start_supervised!(Task.Supervisor)
168+
tvisor = start_supervised!(Task.Supervisor)
99169

100-
capture_log(fn ->
101170
pid =
102171
start_link_supervised!(
103172
{Runtime,
@@ -112,7 +181,7 @@ defmodule NextLs.RuntimeTest do
112181
registry: RuntimeTest.Registry}
113182
)
114183

115-
assert wait_for_ready()
184+
assert_receive :ready
116185

117186
file = Path.join(cwd, "lib/bar.ex")
118187

@@ -143,15 +212,38 @@ defmodule NextLs.RuntimeTest do
143212
""")
144213

145214
assert [] == Runtime.compile(pid)
146-
end) =~ "Connected to node"
215+
end
216+
217+
test "responds with an error when the runtime isn't ready", %{logger: logger, cwd: cwd, on_init: on_init} do
218+
start_supervised!({Registry, keys: :duplicate, name: RuntimeTest.Registry})
219+
220+
tvisor = start_supervised!(Task.Supervisor)
221+
222+
pid =
223+
start_supervised!(
224+
{Runtime,
225+
task_supervisor: tvisor,
226+
name: "my_proj",
227+
on_initialized: on_init,
228+
working_dir: cwd,
229+
uri: "file://#{cwd}",
230+
parent: self(),
231+
logger: logger,
232+
db: :some_db,
233+
registry: RuntimeTest.Registry}
234+
)
235+
236+
Process.link(pid)
237+
238+
assert {:error, :not_ready} = Runtime.compile(pid)
239+
end
147240
end
148241

149-
defp wait_for_ready do
242+
defp flush_messages do
150243
receive do
151-
:ready -> true
244+
_ -> flush_messages()
152245
after
153-
30_000 ->
154-
false
246+
0 -> :ok
155247
end
156248
end
157249
end

test/next_ls/workspaces_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ defmodule NextLS.WorkspacesTest do
8989
}
9090
})
9191

92-
message = "[NextLS] The runtime for #{context.module}-proj_two has successfully shutdown."
92+
message = "[NextLS] The runtime for #{context.module}-proj_two has successfully shut down."
9393

9494
assert_notification "window/logMessage", %{
9595
"message" => ^message

0 commit comments

Comments
 (0)