Skip to content

Commit 7546fb1

Browse files
authored
Handle cases when one line belongs to many modules in debugger (#939)
* break in all protocol implementations * test unsetting implementation breakpoint * add test * implement fallback to runtime modules inspection * more tests and logs * cache module_info of interpreted modules call to module_info may deadlock when stopped on a breakpoint Fixes #940 * format * Handle errors in setting breakpoints Disallow setting breakpoints in Inspect protocol implementations Disallow setting breakpoints in builtin protocols and JasonV.Encoder protocol Fixes #900 Fixes #903 Fixes #942
1 parent 303dafb commit 7546fb1

File tree

6 files changed

+836
-64
lines changed

6 files changed

+836
-64
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule ElixirLS.Debugger.ModuleInfoCache do
2+
@moduledoc """
3+
Caches module_info of interpreted modules. There are cases when module_info call
4+
may deadlock (https://github.com/elixir-lsp/elixir-ls/issues/940)
5+
"""
6+
7+
use Agent
8+
9+
def start_link(args) do
10+
Agent.start_link(fn -> args end, name: __MODULE__)
11+
end
12+
13+
def get(module) do
14+
Agent.get(__MODULE__, & &1[module])
15+
end
16+
17+
def store(module) do
18+
Agent.update(__MODULE__, fn map ->
19+
if Map.has_key?(map, module) do
20+
map
21+
else
22+
Map.put(map, module, module.module_info())
23+
end
24+
end)
25+
end
26+
27+
def clear() do
28+
Agent.update(__MODULE__, fn _map -> %{} end)
29+
end
30+
end

apps/elixir_ls_debugger/lib/debugger/server.ex

Lines changed: 128 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ defmodule ElixirLS.Debugger.Server do
2323
Variables,
2424
Utils,
2525
BreakpointCondition,
26-
Binding
26+
Binding,
27+
ModuleInfoCache
2728
}
2829

2930
alias ElixirLS.Debugger.Stacktrace.Frame
@@ -84,6 +85,7 @@ defmodule ElixirLS.Debugger.Server do
8485
@impl GenServer
8586
def init(opts) do
8687
BreakpointCondition.start_link([])
88+
ModuleInfoCache.start_link(%{})
8789
state = if opts[:output], do: %__MODULE__{output: opts[:output]}, else: %__MODULE__{}
8890
{:ok, state}
8991
end
@@ -269,17 +271,17 @@ defmodule ElixirLS.Debugger.Server do
269271
for b <- breakpoints, do: {b["condition"], b["logMessage"], b["hitCondition"]}
270272

271273
existing_bps = state.breakpoints[path] || []
272-
existing_bp_lines = for {_module, line} <- existing_bps, do: line
274+
existing_bp_lines = for {_modules, line} <- existing_bps, do: line
273275
removed_lines = existing_bp_lines -- new_lines
274276
removed_bps = Enum.filter(existing_bps, fn {_, line} -> line in removed_lines end)
275277

276-
for {module, line} <- removed_bps do
278+
for {modules, line} <- removed_bps, module <- modules do
277279
:int.delete_break(module, line)
278280
BreakpointCondition.unregister_condition(module, [line])
279281
end
280282

281283
result = set_breakpoints(path, new_lines |> Enum.zip(new_conditions))
282-
new_bps = for {:ok, module, line} <- result, do: {module, line}
284+
new_bps = for {:ok, modules, line} <- result, do: {modules, line}
283285

284286
state =
285287
if new_bps == [] do
@@ -333,19 +335,30 @@ defmodule ElixirLS.Debugger.Server do
333335
result =
334336
case current[{m, f, a}] do
335337
nil ->
336-
case :int.ni(m) do
338+
case interpret(m, false) do
337339
{:module, _} ->
340+
ModuleInfoCache.store(m)
338341
breaks_before = :int.all_breaks(m)
339-
:ok = :int.break_in(m, f, a)
340-
breaks_after = :int.all_breaks(m)
341-
lines = for {{^m, line}, _} <- breaks_after -- breaks_before, do: line
342342

343-
# pass nil as log_message - not supported on function breakpoints as of DAP 1.51
344-
update_break_condition(m, lines, condition, nil, hit_count)
343+
Output.debugger_console(
344+
"Setting function breakpoint in #{inspect(m)}.#{f}/#{a}"
345+
)
345346

346-
{:ok, lines}
347+
case :int.break_in(m, f, a) do
348+
:ok ->
349+
breaks_after = :int.all_breaks(m)
350+
lines = for {{^m, line}, _} <- breaks_after -- breaks_before, do: line
347351

348-
_ ->
352+
# pass nil as log_message - not supported on function breakpoints as of DAP 1.51
353+
update_break_condition(m, lines, condition, nil, hit_count)
354+
355+
{:ok, lines}
356+
357+
{:error, :function_not_found} ->
358+
{:error, "Function #{inspect(m)}.#{f}/#{a} not found"}
359+
end
360+
361+
:error ->
349362
{:error, "Cannot interpret module #{inspect(m)}"}
350363
end
351364

@@ -1145,43 +1158,79 @@ defmodule ElixirLS.Debugger.Server do
11451158
defp save_and_reload(module, beam_bin) do
11461159
:ok = File.write(Path.join(@temp_beam_dir, to_string(module) <> ".beam"), beam_bin)
11471160
true = :code.delete(module)
1148-
{:module, _} = :int.ni(module)
1161+
{:module, _} = interpret(module)
1162+
ModuleInfoCache.store(module)
11491163
end
11501164

11511165
defp set_breakpoints(path, lines) do
11521166
if Path.extname(path) == ".erl" do
11531167
module = String.to_atom(Path.basename(path, ".erl"))
1154-
for line <- lines, do: set_breakpoint(module, line)
1168+
for line <- lines, do: set_breakpoint([module], path, line)
11551169
else
1156-
try do
1157-
metadata = ElixirSense.Core.Parser.parse_file(path, false, false, nil)
1158-
1159-
for line <- lines do
1160-
env = ElixirSense.Core.Metadata.get_env(metadata, {line |> elem(0), 1})
1161-
1162-
if env.module == nil do
1163-
{:error, "Could not determine module at line"}
1170+
loaded_elixir_modules =
1171+
:code.all_loaded()
1172+
|> Enum.map(&elem(&1, 0))
1173+
|> Enum.filter(fn module -> String.starts_with?(Atom.to_string(module), "Elixir.") end)
1174+
|> Enum.group_by(fn module ->
1175+
module_info = ModuleInfoCache.get(module) || module.module_info()
1176+
Path.expand(to_string(module_info[:compile][:source]))
1177+
end)
1178+
1179+
loaded_modules_from_path = Map.get(loaded_elixir_modules, path, [])
1180+
metadata = ElixirSense.Core.Parser.parse_file(path, false, false, nil)
1181+
1182+
for line <- lines do
1183+
env = ElixirSense.Core.Metadata.get_env(metadata, {line |> elem(0), 1})
1184+
metadata_modules = Enum.filter(env.module_variants, &(&1 != Elixir))
1185+
1186+
modules_to_break =
1187+
if metadata_modules != [] and
1188+
Enum.all?(metadata_modules, &(&1 in loaded_modules_from_path)) do
1189+
# prefer metadata modules if valid and loaded
1190+
metadata_modules
11641191
else
1165-
set_breakpoint(env.module, line)
1192+
# fall back to all loaded modules from file
1193+
# this may create breakpoints outside module line range
1194+
loaded_modules_from_path
11661195
end
1167-
end
1168-
rescue
1169-
error ->
1170-
for _line <- lines, do: {:error, Exception.format_exit(error)}
1196+
1197+
set_breakpoint(modules_to_break, path, line)
11711198
end
11721199
end
1200+
rescue
1201+
error ->
1202+
for _line <- lines, do: {:error, Exception.format_exit(error)}
11731203
end
11741204

1175-
defp set_breakpoint(module, {line, {condition, log_message, hit_count}}) do
1176-
case :int.ni(module) do
1177-
{:module, _} ->
1178-
:int.break(module, line)
1179-
update_break_condition(module, line, condition, log_message, hit_count)
1205+
defp set_breakpoint([], path, {line, _}) do
1206+
{:error, "Could not determine module at line #{line} in #{path}"}
1207+
end
11801208

1181-
{:ok, module, line}
1209+
defp set_breakpoint(modules, path, {line, {condition, log_message, hit_count}}) do
1210+
modules_with_breakpoints =
1211+
Enum.reduce(modules, [], fn module, added ->
1212+
case interpret(module, false) do
1213+
{:module, _} ->
1214+
ModuleInfoCache.store(module)
1215+
Output.debugger_console("Setting breakpoint in #{inspect(module)} #{path}:#{line}")
1216+
# no need to handle errors here, it can fail only with {:error, :break_exists}
1217+
:int.break(module, line)
1218+
update_break_condition(module, line, condition, log_message, hit_count)
11821219

1183-
_ ->
1184-
{:error, "Cannot interpret module #{inspect(module)}"}
1220+
[module | added]
1221+
1222+
:error ->
1223+
Output.debugger_console("Could not interpret module #{inspect(module)} in #{path}")
1224+
added
1225+
end
1226+
end)
1227+
1228+
if modules_with_breakpoints == [] do
1229+
{:error,
1230+
"Could not interpret any of the modules #{Enum.map_join(modules, ", ", &inspect/1)} in #{path}"}
1231+
else
1232+
# return :ok if at least one breakpoint was set
1233+
{:ok, modules_with_breakpoints, line}
11851234
end
11861235
end
11871236

@@ -1196,7 +1245,8 @@ defmodule ElixirLS.Debugger.Server do
11961245

11971246
defp interpret_module(mod) do
11981247
try do
1199-
{:module, _} = :int.ni(mod)
1248+
{:module, _} = interpret(mod)
1249+
ModuleInfoCache.store(mod)
12001250
catch
12011251
_, _ ->
12021252
Output.debugger_important(
@@ -1342,7 +1392,9 @@ defmodule ElixirLS.Debugger.Server do
13421392
function_breakpoints = Map.get(state.function_breakpoints, frame_mfa, [])
13431393

13441394
cond do
1345-
{first_frame.module, first_frame.line} in file_breakpoints ->
1395+
Enum.any?(file_breakpoints, fn {modules, line} ->
1396+
line == first_frame.line and first_frame.module in modules
1397+
end) ->
13461398
"breakpoint"
13471399

13481400
first_frame.line in function_breakpoints ->
@@ -1352,4 +1404,43 @@ defmodule ElixirLS.Debugger.Server do
13521404
"step"
13531405
end
13541406
end
1407+
1408+
@exclude_protocols_from_interpreting [
1409+
Enumerable,
1410+
Collectable,
1411+
List.Chars,
1412+
String.Chars,
1413+
Inspect,
1414+
IEx.Info,
1415+
JasonV.Encoder
1416+
]
1417+
1418+
@exclude_implementations_from_interpreting [Inspect]
1419+
1420+
defp interpret(module, print_message? \\ true)
1421+
1422+
defp interpret(module, _print_message?) when module in @exclude_protocols_from_interpreting do
1423+
:error
1424+
end
1425+
1426+
defp interpret(module, print_message?) do
1427+
if Code.ensure_loaded?(module) do
1428+
module_behaviours =
1429+
module.module_info(:attributes) |> Keyword.get_values(:behaviour) |> Enum.concat()
1430+
1431+
if Enum.any?(@exclude_implementations_from_interpreting, &(&1 in module_behaviours)) do
1432+
# debugger uses Inspect protocol and setting breakpoints in implementations leads to deadlocks
1433+
# https://github.com/elixir-lsp/elixir-ls/issues/903
1434+
:error
1435+
else
1436+
if print_message? do
1437+
Output.debugger_console("Interpreting module #{inspect(module)}")
1438+
end
1439+
1440+
:int.ni(module)
1441+
end
1442+
else
1443+
:error
1444+
end
1445+
end
13551446
end

apps/elixir_ls_debugger/lib/debugger/stacktrace.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
33
Retrieves the stack trace for a process that's paused at a breakpoint
44
"""
55
alias ElixirLS.Debugger.Output
6+
alias ElixirLS.Debugger.ModuleInfoCache
67

78
defmodule Frame do
89
defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages]
@@ -86,8 +87,6 @@ defmodule ElixirLS.Debugger.Stacktrace do
8687
end
8788

8889
defp get_file(module) do
89-
if Code.ensure_loaded?(module) do
90-
to_string(module.module_info[:compile][:source])
91-
end
90+
Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source]))
9291
end
9392
end

0 commit comments

Comments
 (0)