Skip to content

Commit d8642c7

Browse files
authored
Logger backend for language server and fixes for debugger (#746)
* console backend forked from elixir * rename * rename * remove colors * remove buffering * remove device * replace console logger fix errors * run formatter * remove tests * consistently use Logger for ordinary logging * crash instead of warning * use logger * attempt to show error message before crashing Fixes #741 * info->warn * exit server when config changes it will be restarted by the client * fix tests * use new function in debugger * use DAP compliant output category in debugger stdout and stderr is reserved for debuggee, console and important for debugger * set group leader to logger backend in tests * fix tests
1 parent 6fa8cb2 commit d8642c7

23 files changed

+428
-180
lines changed

apps/elixir_ls_debugger/lib/debugger.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ defmodule ElixirLS.Debugger do
44
"""
55

66
use Application
7+
alias ElixirLS.Debugger.Output
78

89
@impl Application
910
def start(_type, _args) do
1011
# We don't start this as a worker because if the debugger crashes, we want
1112
# this process to remain alive to print errors
12-
{:ok, _pid} = ElixirLS.Debugger.Output.start(ElixirLS.Debugger.Output)
13+
{:ok, _pid} = Output.start(Output)
1314

1415
children = [
1516
{ElixirLS.Debugger.Server, name: ElixirLS.Debugger.Server}
@@ -22,7 +23,7 @@ defmodule ElixirLS.Debugger do
2223
@impl Application
2324
def stop(_state) do
2425
if ElixirLS.Utils.WireProtocol.io_intercepted?() do
25-
IO.puts(:standard_error, "ElixirLS debugger has crashed")
26+
Output.debugger_important("ElixirLS debugger has crashed")
2627

2728
:init.stop(1)
2829
end

apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex

+11-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do
44
"""
55

66
use GenServer
7+
alias ElixirLS.Debugger.Output
78
@range 0..99
89

910
def start_link(args) do
@@ -162,7 +163,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do
162163
# Debug Adapter Protocol:
163164
# If this attribute exists and is non-empty, the backend must not 'break' (stop)
164165
# but log the message instead. Expressions within {} are interpolated.
165-
IO.puts(interpolate(log_message, elixir_binding))
166+
Output.debugger_console(interpolate(log_message, elixir_binding))
166167
false
167168
else
168169
result
@@ -178,7 +179,10 @@ defmodule ElixirLS.Debugger.BreakpointCondition do
178179
if term, do: true, else: false
179180
catch
180181
kind, error ->
181-
IO.warn("Error in conditional breakpoint: " <> Exception.format_banner(kind, error))
182+
Output.debugger_important(
183+
"Error in conditional breakpoint: " <> Exception.format_banner(kind, error)
184+
)
185+
182186
false
183187
end
184188
end
@@ -189,7 +193,10 @@ defmodule ElixirLS.Debugger.BreakpointCondition do
189193
to_string(term)
190194
catch
191195
kind, error ->
192-
IO.warn("Error in log message interpolation: " <> Exception.format_banner(kind, error))
196+
Output.debugger_important(
197+
"Error in log message interpolation: " <> Exception.format_banner(kind, error)
198+
)
199+
193200
""
194201
end
195202
end
@@ -220,7 +227,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do
220227
interpolate(expression_rest, [eval_result | acc], elixir_binding)
221228

222229
:error ->
223-
IO.warn("Log message has unpaired or nested `{}`")
230+
Output.debugger_important("Log message has unpaired or nested `{}`")
224231
acc
225232
end
226233
end

apps/elixir_ls_debugger/lib/debugger/cli.ex

+14-5
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,33 @@ defmodule ElixirLS.Debugger.CLI do
33
alias ElixirLS.Debugger.{Output, Server}
44

55
def main do
6-
WireProtocol.intercept_output(&Output.print/1, &Output.print_err/1)
6+
WireProtocol.intercept_output(&Output.debuggee_out/1, &Output.debuggee_err/1)
77
Launch.start_mix()
88
{:ok, _} = Application.ensure_all_started(:elixir_ls_debugger, :permanent)
99

10-
IO.puts("Started ElixirLS debugger v#{Launch.debugger_version()}")
11-
Launch.print_versions()
10+
Output.debugger_console("Started ElixirLS Debugger v#{Launch.debugger_version()}")
11+
versions = Launch.get_versions()
12+
13+
Output.debugger_console(
14+
"ElixirLS Debugger built with elixir #{versions.compile_elixir_version} on OTP #{versions.compile_otp_version}"
15+
)
16+
17+
Output.debugger_console(
18+
"Running on elixir #{versions.current_elixir_version} on OTP #{versions.current_otp_version}"
19+
)
20+
1221
Launch.limit_num_schedulers()
1322
warn_if_unsupported_version()
1423
WireProtocol.stream_packets(&Server.receive_packet/1)
1524
end
1625

1726
defp warn_if_unsupported_version do
1827
with {:error, message} <- ElixirLS.Utils.MinimumVersion.check_elixir_version() do
19-
Output.print_err("WARNING: " <> message)
28+
Output.debugger_important("WARNING: " <> message)
2029
end
2130

2231
with {:error, message} <- ElixirLS.Utils.MinimumVersion.check_otp_version() do
23-
Output.print_err("WARNING: " <> message)
32+
Output.debugger_important("WARNING: " <> message)
2433
end
2534
end
2635
end

apps/elixir_ls_debugger/lib/debugger/output.ex

+10-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,19 @@ defmodule ElixirLS.Debugger.Output do
2929
GenServer.call(server, {:send_event, event, body})
3030
end
3131

32-
def print(server \\ __MODULE__, str) when is_binary(str) do
32+
def debugger_console(server \\ __MODULE__, str) when is_binary(str) do
33+
send_event(server, "output", %{"category" => "console", "output" => str})
34+
end
35+
36+
def debugger_important(server \\ __MODULE__, str) when is_binary(str) do
37+
send_event(server, "output", %{"category" => "important", "output" => str})
38+
end
39+
40+
def debuggee_out(server \\ __MODULE__, str) when is_binary(str) do
3341
send_event(server, "output", %{"category" => "stdout", "output" => str})
3442
end
3543

36-
def print_err(server \\ __MODULE__, str) when is_binary(str) do
44+
def debuggee_err(server \\ __MODULE__, str) when is_binary(str) do
3745
send_event(server, "output", %{"category" => "stderr", "output" => str})
3846
end
3947

apps/elixir_ls_debugger/lib/debugger/server.ex

+25-22
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ defmodule ElixirLS.Debugger.Server do
166166
0
167167

168168
_ ->
169-
IO.puts(
170-
:standard_error,
169+
Output.debugger_important(
171170
"(Debugger) Task failed because " <> Exception.format_exit(reason)
172171
)
173172

@@ -181,8 +180,7 @@ defmodule ElixirLS.Debugger.Server do
181180
end
182181

183182
def handle_info({:DOWN, _ref, :process, pid, reason}, state = %__MODULE__{}) do
184-
IO.puts(
185-
:standard_error,
183+
Output.debugger_important(
186184
"debugged process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}"
187185
)
188186

@@ -222,7 +220,7 @@ defmodule ElixirLS.Debugger.Server do
222220
@impl GenServer
223221
def terminate(reason, _state = %__MODULE__{}) do
224222
if reason != :normal do
225-
IO.puts(:standard_error, "(Debugger) Terminating because #{Exception.format_exit(reason)}")
223+
Output.debugger_important("(Debugger) Terminating because #{Exception.format_exit(reason)}")
226224
end
227225
end
228226

@@ -231,17 +229,17 @@ defmodule ElixirLS.Debugger.Server do
231229
defp handle_request(initialize_req(_, client_info), %__MODULE__{client_info: nil} = state) do
232230
# linesStartAt1 is true by default and we only support 1-based indexing
233231
if client_info["linesStartAt1"] == false do
234-
IO.warn("0-based lines are not supported")
232+
Output.debugger_important("0-based lines are not supported")
235233
end
236234

237235
# columnsStartAt1 is true by default and we only support 1-based indexing
238236
if client_info["columnsStartAt1"] == false do
239-
IO.warn("0-based columns are not supported")
237+
Output.debugger_important("0-based columns are not supported")
240238
end
241239

242240
# pathFormat is `path` by default and we do not support other, e.g. `uri`
243241
if client_info["pathFormat"] not in [nil, "path"] do
244-
IO.warn("pathFormat #{client_info["pathFormat"]} not supported")
242+
Output.debugger_important("pathFormat #{client_info["pathFormat"]} not supported")
245243
end
246244

247245
{capabilities(), %{state | client_info: client_info}}
@@ -258,16 +256,15 @@ defmodule ElixirLS.Debugger.Server do
258256

259257
defp handle_request(launch_req(_, config) = args, state = %__MODULE__{}) do
260258
if args["arguments"]["noDebug"] == true do
261-
IO.warn("launch with no debug is not supported")
259+
Output.debugger_important("launch with no debug is not supported")
262260
end
263261

264262
{_, ref} = spawn_monitor(fn -> initialize(config) end)
265263

266264
receive do
267265
{:DOWN, ^ref, :process, _pid, reason} ->
268266
if reason != :normal do
269-
IO.puts(
270-
:standard_error,
267+
Output.debugger_important(
271268
"(Debugger) Initialization failed because " <> Exception.format_exit(reason)
272269
)
273270

@@ -337,7 +334,9 @@ defmodule ElixirLS.Debugger.Server do
337334
:ok
338335

339336
{:error, :function_not_found} ->
340-
IO.warn("Unable to delete function breakpoint on #{inspect({m, f, a})}")
337+
Output.debugger_important(
338+
"Unable to delete function breakpoint on #{inspect({m, f, a})}"
339+
)
341340
end
342341
end
343342

@@ -735,7 +734,9 @@ defmodule ElixirLS.Debugger.Server do
735734
catch
736735
kind, payload ->
737736
# when stepping out of interpreted code a MatchError is risen inside :int module (at least in OTP 23)
738-
IO.warn(":int.#{action}(#{inspect(pid)}) failed: #{Exception.format(kind, payload)}")
737+
Output.debugger_important(
738+
":int.#{action}(#{inspect(pid)}) failed: #{Exception.format(kind, payload)}"
739+
)
739740

740741
unless action == :continue do
741742
safe_int_action(pid, :continue)
@@ -972,7 +973,7 @@ defmodule ElixirLS.Debugger.Server do
972973
unless is_list(task_args) and "--no-compile" in task_args do
973974
case Mix.Task.run("compile", ["--ignore-module-conflict"]) do
974975
{:error, _} ->
975-
IO.puts(:standard_error, "Aborting debugger due to compile errors")
976+
Output.debugger_important("Aborting debugger due to compile errors")
976977
:init.stop(1)
977978

978979
_ ->
@@ -1020,7 +1021,7 @@ defmodule ElixirLS.Debugger.Server do
10201021
defp set_stack_trace_mode(nil), do: nil
10211022

10221023
defp set_stack_trace_mode(_) do
1023-
IO.warn(~S(stackTraceMode must be "all", "no_tail", or "false"))
1024+
Output.debugger_important(~S(stackTraceMode must be "all", "no_tail", or "false"))
10241025
end
10251026

10261027
defp capabilities do
@@ -1150,8 +1151,7 @@ defmodule ElixirLS.Debugger.Server do
11501151
[regex]
11511152

11521153
{:error, error} ->
1153-
IO.puts(
1154-
:standard_error,
1154+
Output.debugger_important(
11551155
"Unable to compile file pattern (#{inspect(pattern)}) into a regex. Received error: #{inspect(error)}"
11561156
)
11571157

@@ -1226,7 +1226,7 @@ defmodule ElixirLS.Debugger.Server do
12261226
{:module, _} = :int.ni(mod)
12271227
catch
12281228
_, _ ->
1229-
IO.warn(
1229+
Output.debugger_important(
12301230
"Module #{inspect(mod)} cannot be interpreted. Consider adding it to `excludeModules`."
12311231
)
12321232
end
@@ -1252,7 +1252,7 @@ defmodule ElixirLS.Debugger.Server do
12521252
end
12531253

12541254
{:error, reason} ->
1255-
IO.warn(
1255+
Output.debugger_important(
12561256
"Unable to set condition on a breakpoint in #{module}:#{inspect(lines)}: #{inspect(reason)}"
12571257
)
12581258
end
@@ -1266,7 +1266,7 @@ defmodule ElixirLS.Debugger.Server do
12661266
condition
12671267

12681268
{:error, reason} ->
1269-
IO.warn("Cannot parse breakpoint condition: #{inspect(reason)}")
1269+
Output.debugger_important("Cannot parse breakpoint condition: #{inspect(reason)}")
12701270
"true"
12711271
end
12721272
end
@@ -1280,12 +1280,15 @@ defmodule ElixirLS.Debugger.Server do
12801280
if is_integer(term) do
12811281
term
12821282
else
1283-
IO.warn("Hit condition must evaluate to integer")
1283+
Output.debugger_important("Hit condition must evaluate to integer")
12841284
0
12851285
end
12861286
catch
12871287
kind, error ->
1288-
IO.warn("Error while evaluating hit condition: " <> Exception.format_banner(kind, error))
1288+
Output.debugger_important(
1289+
"Error while evaluating hit condition: " <> Exception.format_banner(kind, error)
1290+
)
1291+
12891292
0
12901293
end
12911294
end

apps/elixir_ls_debugger/lib/debugger/stacktrace.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
22
@moduledoc """
33
Retrieves the stack trace for a process that's paused at a breakpoint
44
"""
5+
alias ElixirLS.Debugger.Output
56

67
defmodule Frame do
78
defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages]
@@ -56,7 +57,10 @@ defmodule ElixirLS.Debugger.Stacktrace do
5657
[first_frame | other_frames]
5758

5859
error ->
59-
IO.warn("Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}")
60+
Output.debugger_important(
61+
"Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}"
62+
)
63+
6064
[]
6165
end
6266
end

apps/elixir_ls_debugger/test/debugger_test.exs

+14-16
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ defmodule ElixirLS.Debugger.ServerTest do
402402
}),
403403
500
404404

405-
{log, stderr} =
405+
{log, _stderr} =
406406
capture_log_and_io(:standard_error, fn ->
407407
assert_receive event(_, "thread", %{
408408
"reason" => "exited",
@@ -412,7 +412,6 @@ defmodule ElixirLS.Debugger.ServerTest do
412412
end)
413413

414414
assert log =~ "Fixture MixProject expected error"
415-
assert stderr =~ "Fixture MixProject expected error"
416415
end)
417416
end
418417

@@ -461,7 +460,7 @@ defmodule ElixirLS.Debugger.ServerTest do
461460
}),
462461
5000
463462

464-
{log, io} =
463+
{log, _io} =
465464
capture_log_and_io(:stderr, fn ->
466465
assert_receive event(_, "thread", %{
467466
"reason" => "exited",
@@ -471,7 +470,6 @@ defmodule ElixirLS.Debugger.ServerTest do
471470
end)
472471

473472
assert log =~ "Fixture MixProject raise for exit_self/0"
474-
assert io =~ "Fixture MixProject raise for exit_self/0"
475473

476474
assert_receive event(_, "exited", %{
477475
"exitCode" => 1
@@ -560,20 +558,20 @@ defmodule ElixirLS.Debugger.ServerTest do
560558
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
561559
|> Enum.map(& &1["id"])
562560

563-
{_, stderr} =
564-
capture_log_and_io(:standard_error, fn ->
565-
Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
566-
assert_receive(response(_, 7, "pause", %{}), 500)
561+
Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
562+
assert_receive(response(_, 7, "pause", %{}), 500)
567563

568-
assert_receive event(_, "stopped", %{
569-
"allThreadsStopped" => false,
570-
"reason" => "pause",
571-
"threadId" => ^thread_id
572-
}),
573-
500
574-
end)
564+
assert_receive event(_, "stopped", %{
565+
"allThreadsStopped" => false,
566+
"reason" => "pause",
567+
"threadId" => ^thread_id
568+
}),
569+
500
575570

576-
assert stderr =~ "Failed to obtain meta for pid"
571+
assert_receive event(_, "output", %{
572+
"category" => "important",
573+
"output" => "Failed to obtain meta for pid" <> _
574+
})
577575
end)
578576
end
579577

0 commit comments

Comments
 (0)