From b63c960fd205df9f900d9e98fecb2e64eceafd35 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:12:51 +0200 Subject: [PATCH 01/19] add dbg backend --- apps/elixir_ls_debugger/lib/debugger.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/elixir_ls_debugger/lib/debugger.ex b/apps/elixir_ls_debugger/lib/debugger.ex index caf44f24f..453aa196d 100644 --- a/apps/elixir_ls_debugger/lib/debugger.ex +++ b/apps/elixir_ls_debugger/lib/debugger.ex @@ -5,6 +5,7 @@ defmodule ElixirLS.Debugger do use Application alias ElixirLS.Debugger.Output + alias ElixirLS.Debugger.Server @impl Application def start(_type, _args) do @@ -12,8 +13,12 @@ defmodule ElixirLS.Debugger do # this process to remain alive to print errors {:ok, _pid} = Output.start(Output) + if Version.match?(System.version(), ">= 1.14.0") do + Application.put_env(:elixir, :dbg_callback, {Server, :dbg, []}) + end + children = [ - {ElixirLS.Debugger.Server, name: ElixirLS.Debugger.Server} + {Server, name: Server} ] opts = [strategy: :one_for_one, name: ElixirLS.Debugger.Supervisor, max_restarts: 0] From c49242b368c53ce99530edf75c33ec2cc880048b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:13:36 +0200 Subject: [PATCH 02/19] break on dbg --- .../elixir_ls_debugger/lib/debugger/server.ex | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index ecdc92a3f..c3650e7d3 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -36,6 +36,7 @@ defmodule ElixirLS.Debugger.Server do defstruct client_info: nil, config: %{}, + dbg_session: nil, task_ref: nil, update_threads_ref: nil, thread_ids_to_pids: %{}, @@ -80,6 +81,15 @@ defmodule ElixirLS.Debugger.Server do GenServer.cast(server, {:paused, pid}) end + @spec dbg(Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t() + def dbg(code, options, %Macro.Env{} = caller) do + quote do + {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) + GenServer.call(unquote(__MODULE__), {:dbg, binding(), stacktrace, __ENV__}, :infinity) + unquote(Macro.dbg(code, options, caller)) + end + end + ## Server Callbacks @impl GenServer @@ -90,6 +100,33 @@ defmodule ElixirLS.Debugger.Server do {:ok, state} end + @impl GenServer + def handle_call({:dbg, binding, stacktrace, %Macro.Env{} = env}, from, state = %__MODULE__{}) do + {pid, _ref} = from + # :int.attach(pid, build_attach_mfa(:paused)) + ref = Process.monitor(pid) + + {state, thread_id, _new_ids} = ensure_thread_id(state, pid, []) + + # drop GenServer call to Debugger.Server from dbg callback + [_, first_frame | stacktrace] = Stacktrace.get(pid) + # overvrite debugger erlang debugger bindings with exact elixir ones + first_frame = %{first_frame | bindings: Map.new(binding), dbg_frame?: true} + + # Stacktrace.get(pid) |> tl |> IO.inspect(label: "debugger stack") + + paused_process = %PausedProcess{stack: [first_frame | stacktrace], ref: ref} + state = put_in(state.paused_processes[pid], paused_process) + + body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false} + Output.send_event("stopped", body) + # Output.debugger_console("#{inspect binding}\n") + # Output.debugger_console("#{inspect env}\n") + # Output.debugger_console("#{inspect stacktrace}\n") + + {:noreply, %{state | dbg_session: from}} + end + @impl GenServer def handle_cast({:receive_packet, request(_, "disconnect") = packet}, state = %__MODULE__{}) do Output.send_response(packet, %{}) From 6c4010434092b7ef60c21423f06aabcf566e4bb5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:14:27 +0200 Subject: [PATCH 03/19] continue --- .../elixir_ls_debugger/lib/debugger/server.ex | 70 +++++++++++++++++-- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index c3650e7d3..253568b61 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -660,23 +660,49 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request(continue_req(_, thread_id) = args, state = %__MODULE__{}) do - pid = get_pid_by_thread_id!(state, thread_id) + pid = get_pid_by_thread_id!(state, thread_id) + + state = case state.dbg_session do + {^pid, _ref} = from -> + GenServer.reply(from, :ok) + %{state | dbg_session: nil} + _ -> + safe_int_action(pid, :continue) + state + end - safe_int_action(pid, :continue) + # state = if match?({^pid, _ref}, state.dbg_session) do + # GenServer.reply(state.dbg_session, :ok) + # %{state | dbg_session: nil} + # else + # state + # end - paused_processes = remove_paused_process(state, pid) - paused_processes = maybe_continue_other_processes(args, paused_processes, pid) + safe_int_action(pid, :continue) + + paused_processes = remove_paused_process(state, pid) + paused_processes = maybe_continue_other_processes(args, paused_processes, pid) - processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) + processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) - {%{"allThreadsContinued" => not processes_paused?}, - %{state | paused_processes: paused_processes}} + {%{"allThreadsContinued" => not processes_paused?}, + %{state | paused_processes: paused_processes}} end defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) + validate_dbg_pid!(state, pid, "next") + + # state = if match?({^pid, _ref}, state.dbg_session) do + # GenServer.reply(state.dbg_session, :ok) + # %{state | dbg_session: nil} + # else + # state + # end + safe_int_action(pid, :next) + paused_processes = remove_paused_process(state, pid) {%{}, @@ -686,7 +712,17 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(step_in_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) + validate_dbg_pid!(state, pid, "stepIn") + + # state = if match?({^pid, _ref}, state.dbg_session) do + # GenServer.reply(state.dbg_session, :ok) + # %{state | dbg_session: nil} + # else + # state + # end + safe_int_action(pid, :step) + paused_processes = remove_paused_process(state, pid) {%{}, @@ -696,7 +732,17 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(step_out_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) + validate_dbg_pid!(state, pid, "stepOut") + + # state = if match?({^pid, _ref}, state.dbg_session) do + # GenServer.reply(state.dbg_session, :ok) + # %{state | dbg_session: nil} + # else + # state + # end + safe_int_action(pid, :finish) + paused_processes = remove_paused_process(state, pid) {%{}, @@ -1498,6 +1544,16 @@ defmodule ElixirLS.Debugger.Server do end else :error + end + + defp validate_dbg_pid!(state, pid, command) do + if match?({^pid, _ref}, state.dbg_session) do + raise ServerError, + message: "notSupported", + format: "Kernel.dbg breakpoints do not support {command} command", + variables: %{ + "command" => command + } end end end From 50063ab7f996e5e7e40f659198337fe9d418d1c9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:14:48 +0200 Subject: [PATCH 04/19] stop session --- apps/elixir_ls_debugger/lib/debugger/server.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 253568b61..dc0bde6a9 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -1463,7 +1463,11 @@ defmodule ElixirLS.Debugger.Server do }) end - state + if match?({^pid, _ref}, state.dbg_session) do + %{state | dbg_session: nil} + else + state + end end defp process_name(process_info) do From d92083c78646304e5b364a11f3f1601839b4053d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:15:31 +0200 Subject: [PATCH 05/19] properly get module code --- apps/elixir_ls_debugger/lib/debugger/stacktrace.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 1886d1fc0..46ce2d38c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -27,7 +27,8 @@ defmodule ElixirLS.Debugger.Stacktrace do function: function, args: args, file: get_file(module), - line: break_line(pid), + # vscode raises invalid request when line is nil + line: break_line(pid) || 1, bindings: get_bindings(meta_pid, level), messages: messages } @@ -48,7 +49,8 @@ defmodule ElixirLS.Debugger.Stacktrace do function: function, args: args, file: get_file(mod), - line: line, + # vscode raises invalid request when line is nil + line: line || 1, bindings: Enum.into(bindings, %{}), messages: messages } @@ -88,5 +90,9 @@ defmodule ElixirLS.Debugger.Stacktrace do defp get_file(module) do Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source])) +# case ElixirSense.Location.find_mod_file(module) do +# {module, file} -> file +# _ -> nil +# end end end From 2f44b193b18a57efeb4f1ca3e33a89d512b2ce09 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Mar 2023 00:16:29 +0200 Subject: [PATCH 06/19] overwrite bindings --- apps/elixir_ls_debugger/lib/debugger/server.ex | 8 ++++++-- apps/elixir_ls_debugger/lib/debugger/stacktrace.ex | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index dc0bde6a9..974011a5a 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -938,8 +938,12 @@ defmodule ElixirLS.Debugger.Server do defp all_variables(paused_processes, frame_id) do case find_frame(paused_processes, frame_id) do - {_pid, %Frame{bindings: bindings}} when is_map(bindings) -> - Binding.to_elixir_variable_names(bindings) + {_pid, %Frame{bindings: bindings, dbg_frame?: dbg_frame?}} when is_map(bindings) -> + if dbg_frame? do + bindings |> Enum.to_list() + else + Binding.to_elixir_variable_names(bindings) + end _ -> [] diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 46ce2d38c..8a7a8ab8a 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -6,7 +6,7 @@ defmodule ElixirLS.Debugger.Stacktrace do alias ElixirLS.Debugger.ModuleInfoCache defmodule Frame do - defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages] + defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages, {:dbg_frame?, false}] def name(%__MODULE__{} = frame) do "#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}" From 34618b8305d0abfb69aaa13926cc963f1356e9e1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 2 Apr 2023 09:35:32 +0200 Subject: [PATCH 07/19] step through pipes --- .../elixir_ls_debugger/lib/debugger/server.ex | 128 ++++++++++++++++-- .../lib/debugger/stacktrace.ex | 2 +- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 974011a5a..ef769e283 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -82,14 +82,66 @@ defmodule ElixirLS.Debugger.Server do end @spec dbg(Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t() + def dbg({:|>, _meta, _args} = ast, options, %Macro.Env{} = env) when is_list(options) do + [first_ast_chunk | asts_chunks] = ast |> Macro.unpipe() |> chunk_pipeline_asts_by_line(env) + + initial_acc = [ + quote do + env = __ENV__ + options = unquote(options) + + options = + if IO.ANSI.enabled?() do + Keyword.put_new(options, :syntax_colors, IO.ANSI.syntax_colors()) + else + options + end + + env = unquote(env_with_line_from_asts(first_ast_chunk)) + + next? = unquote(__MODULE__).pry_with_next(true, binding(), env) + value = unquote(pipe_chunk_of_asts(first_ast_chunk)) + + unquote(__MODULE__).__dbg_pipe_step__( + value, + unquote(asts_chunk_to_strings(first_ast_chunk)), + _start_with_pipe? = false, + options + ) + end + ] + + for asts_chunk <- asts_chunks, reduce: initial_acc do + ast_acc -> + piped_asts = pipe_chunk_of_asts([{quote(do: value), _index = 0}] ++ asts_chunk) + + quote do + unquote(ast_acc) + env = unquote(env_with_line_from_asts(asts_chunk)) + next? = unquote(__MODULE__).pry_with_next(next?, binding(), env) + value = unquote(piped_asts) + + unquote(__MODULE__).__dbg_pipe_step__( + value, + unquote(asts_chunk_to_strings(asts_chunk)), + _start_with_pipe? = true, + options + ) + end + end + end + def dbg(code, options, %Macro.Env{} = caller) do quote do - {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) - GenServer.call(unquote(__MODULE__), {:dbg, binding(), stacktrace, __ENV__}, :infinity) + GenServer.call(unquote(__MODULE__), {:dbg, binding(), __ENV__}, :infinity) unquote(Macro.dbg(code, options, caller)) end end + def pry_with_next(next?, binding, opts_or_env) when is_boolean(next?) do + next? and GenServer.call(__MODULE__, {:dbg, binding, opts_or_env}, :infinity) == {:ok, true} + end + ## Server Callbacks @impl GenServer @@ -101,7 +153,7 @@ defmodule ElixirLS.Debugger.Server do end @impl GenServer - def handle_call({:dbg, binding, stacktrace, %Macro.Env{} = env}, from, state = %__MODULE__{}) do + def handle_call({:dbg, binding, %Macro.Env{} = env}, from, state = %__MODULE__{}) do {pid, _ref} = from # :int.attach(pid, build_attach_mfa(:paused)) ref = Process.monitor(pid) @@ -111,7 +163,8 @@ defmodule ElixirLS.Debugger.Server do # drop GenServer call to Debugger.Server from dbg callback [_, first_frame | stacktrace] = Stacktrace.get(pid) # overvrite debugger erlang debugger bindings with exact elixir ones - first_frame = %{first_frame | bindings: Map.new(binding), dbg_frame?: true} + first_frame = %{first_frame | + bindings: Map.new(binding), dbg_frame?: true, dbg_env: env, file: env.file, line: env.line} # Stacktrace.get(pid) |> tl |> IO.inspect(label: "debugger stack") @@ -664,7 +717,7 @@ defmodule ElixirLS.Debugger.Server do state = case state.dbg_session do {^pid, _ref} = from -> - GenServer.reply(from, :ok) + GenServer.reply(from, {:ok, false}) %{state | dbg_session: nil} _ -> safe_int_action(pid, :continue) @@ -692,16 +745,15 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - validate_dbg_pid!(state, pid, "next") - - # state = if match?({^pid, _ref}, state.dbg_session) do - # GenServer.reply(state.dbg_session, :ok) - # %{state | dbg_session: nil} - # else - # state - # end + # validate_dbg_pid!(state, pid, "next") - safe_int_action(pid, :next) + state = if match?({^pid, _ref}, state.dbg_session) do + GenServer.reply(state.dbg_session, {:ok, true}) + %{state | dbg_session: nil} + else + safe_int_action(pid, :next) + state + end paused_processes = remove_paused_process(state, pid) @@ -1564,4 +1616,52 @@ defmodule ElixirLS.Debugger.Server do } end end + + # Made public to be called from dbg/3 to reduce the amount of generated code. + @doc false + def __dbg_pipe_step__(value, string_asts, start_with_pipe?, options) do + asts_string = Enum.intersperse(string_asts, [:faint, " |> ", :reset]) + + asts_string = + if start_with_pipe? do + IO.ANSI.format([:faint, "|> ", :reset, asts_string]) + else + asts_string + end + + [asts_string, :faint, " #=> ", :reset, inspect(value, options), "\n\n"] + |> IO.ANSI.format() + |> IO.write() + end + + defp chunk_pipeline_asts_by_line(asts, %Macro.Env{line: env_line}) do + Enum.chunk_by(asts, fn + {{_fun_or_var, meta, _args}, _pipe_index} -> meta[:line] || env_line + {_other_ast, _pipe_index} -> env_line + end) + end + + defp pipe_chunk_of_asts([{first_ast, _first_index} | asts] = _ast_chunk) do + Enum.reduce(asts, first_ast, fn {ast, index}, acc -> Macro.pipe(acc, ast, index) end) + end + + defp asts_chunk_to_strings(asts) do + Enum.map(asts, fn {ast, _pipe_index} -> Macro.to_string(ast) end) + end + + defp env_with_line_from_asts(asts) do + line = + Enum.find_value(asts, fn + {{_fun_or_var, meta, _args}, _pipe_index} -> meta[:line] + {_ast, _pipe_index} -> nil + end) + + if line do + quote do + %{env | line: unquote(line)} + end + else + quote do: env + end + end end diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 8a7a8ab8a..0875e5a3a 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -6,7 +6,7 @@ defmodule ElixirLS.Debugger.Stacktrace do alias ElixirLS.Debugger.ModuleInfoCache defmodule Frame do - defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages, {:dbg_frame?, false}] + defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages, {:dbg_frame?, false}, :dbg_env] def name(%__MODULE__{} = frame) do "#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}" From c7b0cbf2a5f36994e0c8ba02be86cc749e729928 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 3 Apr 2023 19:15:04 +0200 Subject: [PATCH 08/19] pass env to eval --- .../elixir_ls_debugger/lib/debugger/server.ex | 86 +++++++++---------- .../lib/debugger/stacktrace.ex | 2 + .../lib/language_server/dialyzer/utils.ex | 1 + 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index ef769e283..24ef36209 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -155,27 +155,21 @@ defmodule ElixirLS.Debugger.Server do @impl GenServer def handle_call({:dbg, binding, %Macro.Env{} = env}, from, state = %__MODULE__{}) do {pid, _ref} = from - # :int.attach(pid, build_attach_mfa(:paused)) ref = Process.monitor(pid) {state, thread_id, _new_ids} = ensure_thread_id(state, pid, []) # drop GenServer call to Debugger.Server from dbg callback [_, first_frame | stacktrace] = Stacktrace.get(pid) - # overvrite debugger erlang debugger bindings with exact elixir ones - first_frame = %{first_frame | - bindings: Map.new(binding), dbg_frame?: true, dbg_env: env, file: env.file, line: env.line} - - # Stacktrace.get(pid) |> tl |> IO.inspect(label: "debugger stack") + # overwrite erlang debugger bindings with exact elixir ones + first_frame = %{first_frame | + bindings: Map.new(binding), dbg_frame?: true, dbg_env: Code.env_for_eval(env), line: env.line} paused_process = %PausedProcess{stack: [first_frame | stacktrace], ref: ref} state = put_in(state.paused_processes[pid], paused_process) body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false} Output.send_event("stopped", body) - # Output.debugger_console("#{inspect binding}\n") - # Output.debugger_console("#{inspect env}\n") - # Output.debugger_console("#{inspect stacktrace}\n") {:noreply, %{state | dbg_session: from}} end @@ -675,9 +669,9 @@ defmodule ElixirLS.Debugger.Server do state = %__MODULE__{} ) do timeout = Map.get(state.config, "debugExpressionTimeoutMs", 10_000) - bindings = all_variables(state.paused_processes, args["frameId"]) + {binding, env_for_eval} = binding_and_env(state.paused_processes, args["frameId"]) - result = evaluate_code_expression(expr, bindings, timeout) + result = evaluate_code_expression(expr, binding, env_for_eval, timeout) case result do {:ok, value} -> @@ -724,14 +718,7 @@ defmodule ElixirLS.Debugger.Server do state end - # state = if match?({^pid, _ref}, state.dbg_session) do - # GenServer.reply(state.dbg_session, :ok) - # %{state | dbg_session: nil} - # else - # state - # end - - safe_int_action(pid, :continue) + # safe_int_action(pid, :continue) paused_processes = remove_paused_process(state, pid) paused_processes = maybe_continue_other_processes(args, paused_processes, pid) @@ -745,8 +732,6 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - # validate_dbg_pid!(state, pid, "next") - state = if match?({^pid, _ref}, state.dbg_session) do GenServer.reply(state.dbg_session, {:ok, true}) %{state | dbg_session: nil} @@ -766,13 +751,6 @@ defmodule ElixirLS.Debugger.Server do validate_dbg_pid!(state, pid, "stepIn") - # state = if match?({^pid, _ref}, state.dbg_session) do - # GenServer.reply(state.dbg_session, :ok) - # %{state | dbg_session: nil} - # else - # state - # end - safe_int_action(pid, :step) paused_processes = remove_paused_process(state, pid) @@ -786,13 +764,6 @@ defmodule ElixirLS.Debugger.Server do validate_dbg_pid!(state, pid, "stepOut") - # state = if match?({^pid, _ref}, state.dbg_session) do - # GenServer.reply(state.dbg_session, :ok) - # %{state | dbg_session: nil} - # else - # state - # end - safe_int_action(pid, :finish) paused_processes = remove_paused_process(state, pid) @@ -812,12 +783,15 @@ defmodule ElixirLS.Debugger.Server do |> String.split(["\r\n", "\n", "\r"]) |> Enum.at(line) - # it's not documented but VSCode uses utf16 positions + # It is measured in UTF-16 code units and the client capability + # `columnsStartAt1` determines whether it is 0- or 1-based. column = Utils.dap_character_to_elixir(line, column) prefix = String.slice(line, 0, column) - vars = - all_variables(state.paused_processes, args["arguments"]["frameId"]) + {binding, env_for_eval} = + binding_and_env(state.paused_processes, args["arguments"]["frameId"]) + + vars = binding |> Enum.map(fn {name, value} -> %ElixirSense.Core.State.VarInfo{ name: name, @@ -848,6 +822,7 @@ defmodule ElixirLS.Debugger.Server do resumed_pids = for {paused_pid, %PausedProcess{ref: ref}} when paused_pid != requested_pid <- paused_processes do + # TODO safe_int_action(paused_pid, :continue) true = Process.demonitor(ref, [:flush]) paused_pid @@ -945,7 +920,7 @@ defmodule ElixirLS.Debugger.Server do defp maybe_append_variable_type(map, _, _value), do: map - defp evaluate_code_expression(expr, bindings, timeout) do + defp evaluate_code_expression(expr, binding, env_or_opts, timeout) do task = Task.async(fn -> receive do @@ -953,7 +928,7 @@ defmodule ElixirLS.Debugger.Server do end try do - {term, _bindings} = Code.eval_string(expr, bindings) + {term, _bindings} = Code.eval_string(expr, binding, env_or_opts) term catch error -> error @@ -972,8 +947,11 @@ defmodule ElixirLS.Debugger.Server do end end - defp all_variables(paused_processes, nil) do - paused_processes + # for null frameId DAP spec suggest to return variables in the global scope + # there is no global scope in erlang/elixir so instead we flat map all variables + # from all paused processes and evaluator + defp binding_and_env(paused_processes, nil) do + binding = paused_processes |> Enum.flat_map(fn {:evaluator, _} -> # TODO setVariable? @@ -986,19 +964,31 @@ defmodule ElixirLS.Debugger.Server do |> Enum.flat_map(fn %Frame{bindings: bindings} -> Binding.to_elixir_variable_names(bindings) end) + + {binding, []} end - defp all_variables(paused_processes, frame_id) do + defp binding_and_env(paused_processes, frame_id) do case find_frame(paused_processes, frame_id) do - {_pid, %Frame{bindings: bindings, dbg_frame?: dbg_frame?}} when is_map(bindings) -> + {_pid, %Frame{bindings: bindings, dbg_frame?: dbg_frame?} = frame} when is_map(bindings) -> if dbg_frame? do - bindings |> Enum.to_list() + {bindings |> Enum.to_list(), frame.dbg_env} else - Binding.to_elixir_variable_names(bindings) + {Binding.to_elixir_variable_names(bindings), [ + file: frame.file, + line: frame.line + ]} end + {_pid, %Frame{} = frame} -> + {[], [ + file: frame.file, + line: frame.line + ]} + _ -> - [] + Output.debugger_console("Unable to find frame #{inspect(frame_id)}") + {[], []} end end @@ -1520,6 +1510,7 @@ defmodule ElixirLS.Debugger.Server do end if match?({^pid, _ref}, state.dbg_session) do + # no need to respond - the debugged process was waiting in GenServer.call but it exited %{state | dbg_session: nil} else state @@ -1604,6 +1595,7 @@ defmodule ElixirLS.Debugger.Server do end else :error + end end defp validate_dbg_pid!(state, pid, command) do diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 0875e5a3a..c680624ec 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -58,6 +58,7 @@ defmodule ElixirLS.Debugger.Stacktrace do end [first_frame | other_frames] + # TODO add process stack? error -> Output.debugger_important( @@ -90,6 +91,7 @@ defmodule ElixirLS.Debugger.Stacktrace do defp get_file(module) do Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source])) + # TODO why beam to source location hack needed here? # case ElixirSense.Location.find_mod_file(module) do # {module, file} -> file # _ -> nil diff --git a/apps/language_server/lib/language_server/dialyzer/utils.ex b/apps/language_server/lib/language_server/dialyzer/utils.ex index 95af5c274..2e8ec838f 100644 --- a/apps/language_server/lib/language_server/dialyzer/utils.ex +++ b/apps/language_server/lib/language_server/dialyzer/utils.ex @@ -8,6 +8,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Utils do is_list(file) and match?({:ok, _}, :dialyzer_utils.get_core_from_beam(file)) end + # TODO @spec get_beam_file(module()) :: charlist() | :preloaded | :non_existing | :cover_compiled def get_beam_file(module) do case :code.which(module) do From b8469e14c4d57bcfcf9dcdc3326b9cdd8d5a0171 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 26 Aug 2023 09:31:27 +0200 Subject: [PATCH 09/19] add tests --- .../elixir_ls_debugger/lib/debugger/server.ex | 110 ++--- .../lib/debugger/stacktrace.ex | 24 +- .../elixir_ls_debugger/test/debugger_test.exs | 386 ++++++++++++++++++ .../test/fixtures/mix_project/lib/dbg.ex | 19 + 4 files changed, 484 insertions(+), 55 deletions(-) create mode 100644 apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 24ef36209..546958137 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -161,9 +161,15 @@ defmodule ElixirLS.Debugger.Server do # drop GenServer call to Debugger.Server from dbg callback [_, first_frame | stacktrace] = Stacktrace.get(pid) + # overwrite erlang debugger bindings with exact elixir ones - first_frame = %{first_frame | - bindings: Map.new(binding), dbg_frame?: true, dbg_env: Code.env_for_eval(env), line: env.line} + first_frame = %{ + first_frame + | bindings: Map.new(binding), + dbg_frame?: true, + dbg_env: Code.env_for_eval(env), + line: env.line + } paused_process = %PausedProcess{stack: [first_frame | stacktrace], ref: ref} state = put_in(state.paused_processes[pid], paused_process) @@ -707,38 +713,41 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request(continue_req(_, thread_id) = args, state = %__MODULE__{}) do - pid = get_pid_by_thread_id!(state, thread_id) - - state = case state.dbg_session do - {^pid, _ref} = from -> - GenServer.reply(from, {:ok, false}) - %{state | dbg_session: nil} - _ -> - safe_int_action(pid, :continue) - state - end + pid = get_pid_by_thread_id!(state, thread_id) - # safe_int_action(pid, :continue) + state = + case state.dbg_session do + {^pid, _ref} = from -> + GenServer.reply(from, {:ok, false}) + %{state | dbg_session: nil} - paused_processes = remove_paused_process(state, pid) - paused_processes = maybe_continue_other_processes(args, paused_processes, pid) + _ -> + safe_int_action(pid, :continue) + state + end - processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) + # safe_int_action(pid, :continue) - {%{"allThreadsContinued" => not processes_paused?}, - %{state | paused_processes: paused_processes}} + paused_processes = remove_paused_process(state, pid) + paused_processes = maybe_continue_other_processes(args, paused_processes, pid) + + processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) + + {%{"allThreadsContinued" => not processes_paused?}, + %{state | paused_processes: paused_processes}} end defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - state = if match?({^pid, _ref}, state.dbg_session) do - GenServer.reply(state.dbg_session, {:ok, true}) - %{state | dbg_session: nil} - else - safe_int_action(pid, :next) - state - end + state = + if match?({^pid, _ref}, state.dbg_session) do + GenServer.reply(state.dbg_session, {:ok, true}) + %{state | dbg_session: nil} + else + safe_int_action(pid, :next) + state + end paused_processes = remove_paused_process(state, pid) @@ -788,10 +797,11 @@ defmodule ElixirLS.Debugger.Server do column = Utils.dap_character_to_elixir(line, column) prefix = String.slice(line, 0, column) - {binding, env_for_eval} = + {binding, _env_for_eval} = binding_and_env(state.paused_processes, args["arguments"]["frameId"]) - vars = binding + vars = + binding |> Enum.map(fn {name, value} -> %ElixirSense.Core.State.VarInfo{ name: name, @@ -832,7 +842,6 @@ defmodule ElixirLS.Debugger.Server do end defp maybe_continue_other_processes(_, paused_processes, _requested_pid), do: paused_processes - # TODO consider removing this workaround as the problem seems to no longer affect OTP 24 defp safe_int_action(pid, action) do apply(:int, action, [pid]) @@ -951,19 +960,20 @@ defmodule ElixirLS.Debugger.Server do # there is no global scope in erlang/elixir so instead we flat map all variables # from all paused processes and evaluator defp binding_and_env(paused_processes, nil) do - binding = paused_processes - |> Enum.flat_map(fn - {:evaluator, _} -> - # TODO setVariable? - [] - - {_pid, %PausedProcess{} = paused_process} -> - Map.values(paused_process.frame_ids_to_frames) - end) - |> Enum.filter(&match?(%Frame{bindings: bindings} when is_map(bindings), &1)) - |> Enum.flat_map(fn %Frame{bindings: bindings} -> - Binding.to_elixir_variable_names(bindings) - end) + binding = + paused_processes + |> Enum.flat_map(fn + {:evaluator, _} -> + # TODO setVariable? + [] + + {_pid, %PausedProcess{} = paused_process} -> + Map.values(paused_process.frame_ids_to_frames) + end) + |> Enum.filter(&match?(%Frame{bindings: bindings} when is_map(bindings), &1)) + |> Enum.flat_map(fn %Frame{bindings: bindings} -> + Binding.to_elixir_variable_names(bindings) + end) {binding, []} end @@ -974,17 +984,19 @@ defmodule ElixirLS.Debugger.Server do if dbg_frame? do {bindings |> Enum.to_list(), frame.dbg_env} else - {Binding.to_elixir_variable_names(bindings), [ - file: frame.file, - line: frame.line - ]} + {Binding.to_elixir_variable_names(bindings), + [ + file: frame.file, + line: frame.line + ]} end {_pid, %Frame{} = frame} -> - {[], [ - file: frame.file, - line: frame.line - ]} + {[], + [ + file: frame.file, + line: frame.line + ]} _ -> Output.debugger_console("Unable to find frame #{inspect(frame_id)}") diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index c680624ec..c2fecd17f 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -6,7 +6,18 @@ defmodule ElixirLS.Debugger.Stacktrace do alias ElixirLS.Debugger.ModuleInfoCache defmodule Frame do - defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages, {:dbg_frame?, false}, :dbg_env] + defstruct [ + :level, + :file, + :module, + :function, + :args, + :line, + :bindings, + :messages, + {:dbg_frame?, false}, + :dbg_env + ] def name(%__MODULE__{} = frame) do "#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}" @@ -58,7 +69,8 @@ defmodule ElixirLS.Debugger.Stacktrace do end [first_frame | other_frames] - # TODO add process stack? + + # TODO add process stack? error -> Output.debugger_important( @@ -92,9 +104,9 @@ defmodule ElixirLS.Debugger.Stacktrace do defp get_file(module) do Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source])) # TODO why beam to source location hack needed here? -# case ElixirSense.Location.find_mod_file(module) do -# {module, file} -> file -# _ -> nil -# end + # case ElixirSense.Location.find_mod_file(module) do + # {module, file} -> file + # _ -> nil + # end end end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 530db3eab..305cf1f18 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -13,10 +13,18 @@ defmodule ElixirLS.Debugger.ServerTest do setup do {:ok, packet_capture} = ElixirLS.Utils.PacketCapture.start_link(self()) Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), packet_capture) + original_server = Process.whereis(ElixirLS.Debugger.Server) {:ok, server} = Server.start_link() + Process.unregister(ElixirLS.Debugger.Server) + Process.register(server, ElixirLS.Debugger.Server) on_exit(fn -> + if is_pid(Process.whereis(ElixirLS.Debugger.Server)) do + Process.unregister(ElixirLS.Debugger.Server) + end + + Process.register(original_server, ElixirLS.Debugger.Server) for mod <- :int.interpreted(), do: :int.nn(mod) :int.auto_attach(false) :int.no_break() @@ -2293,6 +2301,384 @@ defmodule ElixirLS.Debugger.ServerTest do end end + if Version.match?(System.version(), ">= 1.14.0") do + describe "Kernel.dbg breakpoints" do + test "breaks on dbg", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + abs_path = Path.absname("lib/dbg.ex") + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Dbg.simple()"], + "projectDir" => File.cwd!() + # disable auto interpret + # "debugAutoInterpretAllModules" => false + }) + ) + + assert_receive(response(_, 2, "launch", _), 3000) + assert_receive(event(_, "initialized", %{}), 5000) + + # refute MixProject.Dbg in :int.interpreted() + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads})) + + # ensure thread ids are unique + thread_ids = Enum.map(threads, & &1["id"]) + assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids) + + # Process.unlink(server) + # Process.flag(:trap_exit, true) + assert_receive event(_, "output", %{ + "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" + }) + + # TODO why debugged process #PID<0.229.0> exited with reason normal + assert_receive event(_, "output", %{"output" => "debugged process" <> _}) + + assert_receive event(_, "output", %{ + "output" => "Running mix run -e MixProject.Dbg.simple()\n" + }) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(7, thread_id)) + + assert_receive response(_, 7, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 4, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + Server.receive_packet(server, scopes_req(8, frame_id)) + + assert_receive response(_, 8, "scopes", %{ + "scopes" => [ + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "variables", + "namedVariables" => 1, + "variablesReference" => vars_id + }, + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "process info", + "namedVariables" => _, + "variablesReference" => _ + } + ] + }) + + Server.receive_packet(server, vars_req(9, vars_id)) + + assert_receive response(_, 9, "variables", %{ + "variables" => [ + %{ + "name" => "a", + "value" => "5", + "variablesReference" => 0 + } + ] + }), + 1000 + + # stepIn is not supported + Server.receive_packet(server, step_in_req(12, thread_id)) + + assert_receive( + error_response( + _, + 12, + "stepIn", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepIn"} + ) + ) + + # stepOut is not supported + Server.receive_packet(server, step_out_req(13, thread_id)) + + assert_receive( + error_response( + _, + 13, + "stepOut", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepOut"} + ) + ) + + # next results in continue + Server.receive_packet(server, next_req(14, thread_id)) + assert_receive response(_, 14, "next", %{}) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(141, thread_id)) + + assert_receive response(_, 141, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 5, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + # continue + Server.receive_packet(server, continue_req(15, thread_id)) + assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true}) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(151, thread_id)) + + assert_receive response(_, 151, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 6, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + Server.receive_packet(server, continue_req(16, thread_id)) + assert_receive response(_, 16, "continue", %{"allThreadsContinued" => true}) + + refute_receive event(_, "thread", %{ + "reason" => "exited", + "threadId" => ^thread_id + }), + 1_000 + end) + end + + test "stepping through pipe", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + abs_path = Path.absname("lib/dbg.ex") + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Dbg.pipe()"], + "projectDir" => File.cwd!() + # disable auto interpret + # "debugAutoInterpretAllModules" => false + }) + ) + + assert_receive(response(_, 2, "launch", _), 3000) + assert_receive(event(_, "initialized", %{}), 5000) + + # refute MixProject.Dbg in :int.interpreted() + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads})) + + # ensure thread ids are unique + thread_ids = Enum.map(threads, & &1["id"]) + assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids) + + # Process.unlink(server) + # Process.flag(:trap_exit, true) + assert_receive event(_, "output", %{ + "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" + }) + + # TODO why debugged process #PID<0.229.0> exited with reason normal + assert_receive event(_, "output", %{"output" => "debugged process" <> _}) + + assert_receive event(_, "output", %{ + "output" => "Running mix run -e MixProject.Dbg.pipe()\n" + }) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(7, thread_id)) + + assert_receive response(_, 7, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 13, + "name" => "MixProject.Dbg.pipe/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + Server.receive_packet(server, scopes_req(8, frame_id)) + + assert_receive response(_, 8, "scopes", %{ + "scopes" => [ + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "variables", + "namedVariables" => 1, + "variablesReference" => vars_id + }, + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "process info", + "namedVariables" => _, + "variablesReference" => _ + } + ] + }) + + Server.receive_packet(server, vars_req(9, vars_id)) + + assert_receive response(_, 9, "variables", %{ + "variables" => [ + %{ + "name" => "a", + "value" => "5", + "variablesReference" => 0 + } + ] + }), + 1000 + + # stepIn is not supported + Server.receive_packet(server, step_in_req(12, thread_id)) + + assert_receive( + error_response( + _, + 12, + "stepIn", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepIn"} + ) + ) + + # stepOut is not supported + Server.receive_packet(server, step_out_req(13, thread_id)) + + assert_receive( + error_response( + _, + 13, + "stepOut", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepOut"} + ) + ) + + # next steps through pipe + Server.receive_packet(server, next_req(14, thread_id)) + assert_receive response(_, 14, "next", %{}) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(141, thread_id)) + + assert_receive response(_, 141, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 14, + "name" => "MixProject.Dbg.pipe/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + # continue skips pipe steps + Server.receive_packet(server, continue_req(15, thread_id)) + assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true}) + + refute_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 1_000 + + refute_receive event(_, "thread", %{ + "reason" => "exited", + "threadId" => ^thread_id + }) + end) + end + end + end + @tag :fixture test "server tracks running processes", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> diff --git a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex new file mode 100644 index 000000000..1297c1858 --- /dev/null +++ b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex @@ -0,0 +1,19 @@ +defmodule MixProject.Dbg do + def simple() do + a = 5 + b = __ENV__.file |> dbg() + c = String.split(b, "/", trim: true) |> dbg() + d = List.last(c) |> dbg() + File.exists?(d) + end + + def pipe() do + a = 5 + + __ENV__.file + |> String.split("/", trim: true) + |> List.last() + |> File.exists?() + |> dbg() + end +end From 97211f35e4d79dae80d737d47ba09d3f65534133 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 26 Aug 2023 12:15:53 +0200 Subject: [PATCH 10/19] continue dbg session --- .../elixir_ls_debugger/lib/debugger/server.ex | 74 +++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 546958137..e25bfb485 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -726,15 +726,14 @@ defmodule ElixirLS.Debugger.Server do state end - # safe_int_action(pid, :continue) - - paused_processes = remove_paused_process(state, pid) - paused_processes = maybe_continue_other_processes(args, paused_processes, pid) + state = + state + |> remove_paused_process(pid) + |> maybe_continue_other_processes(args) - processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) + processes_paused? = state.paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) - {%{"allThreadsContinued" => not processes_paused?}, - %{state | paused_processes: paused_processes}} + {%{"allThreadsContinued" => not processes_paused?}, state} end defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do @@ -749,10 +748,12 @@ defmodule ElixirLS.Debugger.Server do state end - paused_processes = remove_paused_process(state, pid) + state = + state + |> remove_paused_process(pid) + |> maybe_continue_other_processes(args) - {%{}, - %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} + {%{}, state} end defp handle_request(step_in_req(_, thread_id) = args, state = %__MODULE__{}) do @@ -762,10 +763,12 @@ defmodule ElixirLS.Debugger.Server do safe_int_action(pid, :step) - paused_processes = remove_paused_process(state, pid) + state = + state + |> remove_paused_process(pid) + |> maybe_continue_other_processes(args) - {%{}, - %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} + {%{}, state} end defp handle_request(step_out_req(_, thread_id) = args, state = %__MODULE__{}) do @@ -775,10 +778,12 @@ defmodule ElixirLS.Debugger.Server do safe_int_action(pid, :finish) - paused_processes = remove_paused_process(state, pid) + state = + state + |> remove_paused_process(pid) + |> maybe_continue_other_processes(args) - {%{}, - %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} + {%{}, state} end defp handle_request(completions_req(_, text) = args, state = %__MODULE__{}) do @@ -828,20 +833,32 @@ defmodule ElixirLS.Debugger.Server do } end - defp maybe_continue_other_processes(%{"singleThread" => true}, paused_processes, requested_pid) do - resumed_pids = - for {paused_pid, %PausedProcess{ref: ref}} when paused_pid != requested_pid <- - paused_processes do - # TODO - safe_int_action(paused_pid, :continue) - true = Process.demonitor(ref, [:flush]) - paused_pid + defp maybe_continue_other_processes(state, %{"singleThread" => true}) do + # continue dbg debug session + state = + case state.dbg_session do + {pid, _ref} = from -> + GenServer.reply(from, {:ok, false}) + {%PausedProcess{ref: ref}, paused_processes} = Map.pop!(state.paused_processes, pid) + true = Process.demonitor(ref, [:flush]) + %{state | dbg_session: nil, paused_processes: paused_processes} + + _ -> + state end - paused_processes |> Map.drop(resumed_pids) + # continue erlang debugger paused processes + for {paused_pid, %PausedProcess{ref: ref}} <- state.paused_processes do + safe_int_action(paused_pid, :continue) + true = Process.demonitor(ref, [:flush]) + paused_pid + end + + %{state | paused_processes: %{}} end - defp maybe_continue_other_processes(_, paused_processes, _requested_pid), do: paused_processes + defp maybe_continue_other_processes(state, _), do: state + # TODO consider removing this workaround as the problem seems to no longer affect OTP 24 defp safe_int_action(pid, action) do apply(:int, action, [pid]) @@ -882,7 +899,7 @@ defmodule ElixirLS.Debugger.Server do true = Process.demonitor(process.ref, [:flush]) end - paused_processes + %{state | paused_processes: paused_processes} end defp variables(state = %__MODULE__{}, pid, var, start, count, filter) do @@ -1505,12 +1522,11 @@ defmodule ElixirLS.Debugger.Server do defp handle_process_exit(state = %__MODULE__{}, pid) when is_pid(pid) do {thread_id, pids_to_thread_ids} = Map.pop(state.pids_to_thread_ids, pid) - paused_processes = remove_paused_process(state, pid) + state = remove_paused_process(state, pid) state = %__MODULE__{ state | thread_ids_to_pids: Map.delete(state.thread_ids_to_pids, thread_id), - paused_processes: paused_processes, pids_to_thread_ids: pids_to_thread_ids } From 8d0b49e0a0cc639fce00a80a9e19824cf02a311a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 26 Aug 2023 12:36:05 +0200 Subject: [PATCH 11/19] fix tests on < 1.14 --- .../elixir_ls_debugger/test/debugger_test.exs | 30 +++++++++++------ .../test/fixtures/mix_project/lib/dbg.ex | 32 ++++++++++--------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 305cf1f18..65b85f69e 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -2339,9 +2339,14 @@ defmodule ElixirLS.Debugger.ServerTest do # Process.unlink(server) # Process.flag(:trap_exit, true) - assert_receive event(_, "output", %{ - "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - }) + assert_receive event( + _, + "output", + %{ + "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" + }, + 3000 + ) # TODO why debugged process #PID<0.229.0> exited with reason normal assert_receive event(_, "output", %{"output" => "debugged process" <> _}) @@ -2454,7 +2459,7 @@ defmodule ElixirLS.Debugger.ServerTest do %{ "column" => 0, "id" => frame_id, - "line" => 5, + "line" => 6, "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } @@ -2481,7 +2486,7 @@ defmodule ElixirLS.Debugger.ServerTest do %{ "column" => 0, "id" => frame_id, - "line" => 6, + "line" => 7, "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } @@ -2536,9 +2541,14 @@ defmodule ElixirLS.Debugger.ServerTest do # Process.unlink(server) # Process.flag(:trap_exit, true) - assert_receive event(_, "output", %{ - "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - }) + assert_receive event( + _, + "output", + %{ + "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" + }, + 3000 + ) # TODO why debugged process #PID<0.229.0> exited with reason normal assert_receive event(_, "output", %{"output" => "debugged process" <> _}) @@ -2562,7 +2572,7 @@ defmodule ElixirLS.Debugger.ServerTest do %{ "column" => 0, "id" => frame_id, - "line" => 13, + "line" => 14, "name" => "MixProject.Dbg.pipe/0", "source" => %{"path" => ^abs_path} } @@ -2651,7 +2661,7 @@ defmodule ElixirLS.Debugger.ServerTest do %{ "column" => 0, "id" => frame_id, - "line" => 14, + "line" => 15, "name" => "MixProject.Dbg.pipe/0", "source" => %{"path" => ^abs_path} } diff --git a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex index 1297c1858..5d32f6635 100644 --- a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex +++ b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/dbg.ex @@ -1,19 +1,21 @@ -defmodule MixProject.Dbg do - def simple() do - a = 5 - b = __ENV__.file |> dbg() - c = String.split(b, "/", trim: true) |> dbg() - d = List.last(c) |> dbg() - File.exists?(d) - end +if Version.match?(System.version(), ">= 1.14.0") do + defmodule MixProject.Dbg do + def simple() do + a = 5 + b = __ENV__.file |> dbg() + c = String.split(b, "/", trim: true) |> dbg() + d = List.last(c) |> dbg() + File.exists?(d) + end - def pipe() do - a = 5 + def pipe() do + a = 5 - __ENV__.file - |> String.split("/", trim: true) - |> List.last() - |> File.exists?() - |> dbg() + __ENV__.file + |> String.split("/", trim: true) + |> List.last() + |> File.exists?() + |> dbg() + end end end From ced2406bbeffc2abd7e4a1e3fc9f71c1b668d6d0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 27 Aug 2023 07:47:57 +0200 Subject: [PATCH 12/19] fixing tests --- apps/elixir_ls_debugger/lib/debugger.ex | 11 +++++-- .../elixir_ls_debugger/test/debugger_test.exs | 30 +++++++------------ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger.ex b/apps/elixir_ls_debugger/lib/debugger.ex index 453aa196d..85e640155 100644 --- a/apps/elixir_ls_debugger/lib/debugger.ex +++ b/apps/elixir_ls_debugger/lib/debugger.ex @@ -17,9 +17,14 @@ defmodule ElixirLS.Debugger do Application.put_env(:elixir, :dbg_callback, {Server, :dbg, []}) end - children = [ - {Server, name: Server} - ] + children = + if Mix.env() != :test do + [ + {Server, name: Server} + ] + else + [] + end opts = [strategy: :one_for_one, name: ElixirLS.Debugger.Supervisor, max_restarts: 0] Supervisor.start_link(children, opts) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 65b85f69e..59d18b74d 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -4,33 +4,23 @@ defmodule ElixirLS.Debugger.ServerTest do # between the debugger's tests and the fixture project's tests. Expect to see output printed # from both. - alias ElixirLS.Debugger.{Server, Protocol, BreakpointCondition, ModuleInfoCache} + alias ElixirLS.Debugger.{Server, Protocol, BreakpointCondition} use ElixirLS.Utils.MixTest.Case, async: false use Protocol - doctest ElixirLS.Debugger.Server + doctest Server setup do {:ok, packet_capture} = ElixirLS.Utils.PacketCapture.start_link(self()) Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), packet_capture) - original_server = Process.whereis(ElixirLS.Debugger.Server) - {:ok, server} = Server.start_link() - Process.unregister(ElixirLS.Debugger.Server) - Process.register(server, ElixirLS.Debugger.Server) + {:ok, server} = Server.start_link(name: Server) on_exit(fn -> - if is_pid(Process.whereis(ElixirLS.Debugger.Server)) do - Process.unregister(ElixirLS.Debugger.Server) - end - - Process.register(original_server, ElixirLS.Debugger.Server) for mod <- :int.interpreted(), do: :int.nn(mod) :int.auto_attach(false) :int.no_break() :int.clear() - BreakpointCondition.clear() - ModuleInfoCache.clear() end) {:ok, %{server: server}} @@ -2344,9 +2334,9 @@ defmodule ElixirLS.Debugger.ServerTest do "output", %{ "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - }, - 3000 - ) + } + ), + 3000 # TODO why debugged process #PID<0.229.0> exited with reason normal assert_receive event(_, "output", %{"output" => "debugged process" <> _}) @@ -2370,7 +2360,7 @@ defmodule ElixirLS.Debugger.ServerTest do %{ "column" => 0, "id" => frame_id, - "line" => 4, + "line" => 5, "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } @@ -2546,9 +2536,9 @@ defmodule ElixirLS.Debugger.ServerTest do "output", %{ "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - }, - 3000 - ) + } + ), + 3000 # TODO why debugged process #PID<0.229.0> exited with reason normal assert_receive event(_, "output", %{"output" => "debugged process" <> _}) From 45d729fbefdbc12a8dee40b46895e117b1194248 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 27 Aug 2023 08:00:39 +0200 Subject: [PATCH 13/19] remove not needed asserts --- .../elixir_ls_debugger/test/debugger_test.exs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 59d18b74d..bf91dbc81 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -2327,24 +2327,6 @@ defmodule ElixirLS.Debugger.ServerTest do thread_ids = Enum.map(threads, & &1["id"]) assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids) - # Process.unlink(server) - # Process.flag(:trap_exit, true) - assert_receive event( - _, - "output", - %{ - "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - } - ), - 3000 - - # TODO why debugged process #PID<0.229.0> exited with reason normal - assert_receive event(_, "output", %{"output" => "debugged process" <> _}) - - assert_receive event(_, "output", %{ - "output" => "Running mix run -e MixProject.Dbg.simple()\n" - }) - assert_receive event(_, "stopped", %{ "allThreadsStopped" => false, "reason" => "breakpoint", @@ -2529,24 +2511,6 @@ defmodule ElixirLS.Debugger.ServerTest do thread_ids = Enum.map(threads, & &1["id"]) assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids) - # Process.unlink(server) - # Process.flag(:trap_exit, true) - assert_receive event( - _, - "output", - %{ - "output" => "Running with MIX_ENV: dev MIX_TARGET: host\n" - } - ), - 3000 - - # TODO why debugged process #PID<0.229.0> exited with reason normal - assert_receive event(_, "output", %{"output" => "debugged process" <> _}) - - assert_receive event(_, "output", %{ - "output" => "Running mix run -e MixProject.Dbg.pipe()\n" - }) - assert_receive event(_, "stopped", %{ "allThreadsStopped" => false, "reason" => "breakpoint", From 63fb46045ef35825af73c6e00fb5edfc1ae35e0d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 27 Aug 2023 08:39:37 +0200 Subject: [PATCH 14/19] make dbg breakpoints work when debugging in non interpreted mode --- .../elixir_ls_debugger/lib/debugger/server.ex | 51 +++-- .../lib/debugger/stacktrace.ex | 11 +- .../elixir_ls_debugger/test/debugger_test.exs | 192 +++++++++++++++++- 3 files changed, 233 insertions(+), 21 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index e25bfb485..247c726b4 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -159,19 +159,46 @@ defmodule ElixirLS.Debugger.Server do {state, thread_id, _new_ids} = ensure_thread_id(state, pid, []) - # drop GenServer call to Debugger.Server from dbg callback - [_, first_frame | stacktrace] = Stacktrace.get(pid) - - # overwrite erlang debugger bindings with exact elixir ones - first_frame = %{ - first_frame - | bindings: Map.new(binding), - dbg_frame?: true, - dbg_env: Code.env_for_eval(env), - line: env.line - } + stacktrace = + case Stacktrace.get(pid) do + [_gen_server_frame, first_frame | stacktrace] -> + IO.puts("level #{first_frame.level}") + # drop GenServer call to Debugger.Server from dbg callback + # overwrite erlang debugger bindings with exact elixir ones + first_frame = %{ + first_frame + | bindings: Map.new(binding), + dbg_frame?: true, + dbg_env: Code.env_for_eval(env), + module: env.module, + function: env.function, + file: env.file, + line: env.line + } + + [first_frame | stacktrace] + + [] -> + # no stacktrace if we are running in non interpreted mode + # build one frame stacktrace + # TODO look for ways of getting better stacktrace + first_frame = %Frame{ + level: 1, + module: env.module, + function: env.function, + file: env.file, + args: [], + messages: [], + bindings: Map.new(binding), + dbg_frame?: true, + dbg_env: Code.env_for_eval(env), + line: env.line + } + + [first_frame] + end - paused_process = %PausedProcess{stack: [first_frame | stacktrace], ref: ref} + paused_process = %PausedProcess{stack: stacktrace, ref: ref} state = put_in(state.paused_processes[pid], paused_process) body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false} diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index c2fecd17f..078b0065c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -19,8 +19,13 @@ defmodule ElixirLS.Debugger.Stacktrace do :dbg_env ] + def name(%__MODULE__{function: function} = frame) when not is_nil(function) do + {f, a} = frame.function + "#{inspect(frame.module)}.#{f}/#{a}" + end + def name(%__MODULE__{} = frame) do - "#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}" + "#{inspect(frame.module)}" end end @@ -35,7 +40,7 @@ defmodule ElixirLS.Debugger.Stacktrace do first_frame = %Frame{ level: level, module: module, - function: function, + function: {function, Enum.count(args)}, args: args, file: get_file(module), # vscode raises invalid request when line is nil @@ -57,7 +62,7 @@ defmodule ElixirLS.Debugger.Stacktrace do %Frame{ level: level, module: mod, - function: function, + function: {function, Enum.count(args)}, args: args, file: get_file(mod), # vscode raises invalid request when line is nil diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index bf91dbc81..c2d2e4413 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -2307,15 +2307,13 @@ defmodule ElixirLS.Debugger.ServerTest do "task" => "run", "taskArgs" => ["-e", "MixProject.Dbg.simple()"], "projectDir" => File.cwd!() - # disable auto interpret - # "debugAutoInterpretAllModules" => false }) ) assert_receive(response(_, 2, "launch", _), 3000) assert_receive(event(_, "initialized", %{}), 5000) - # refute MixProject.Dbg in :int.interpreted() + assert MixProject.Dbg in :int.interpreted() Server.receive_packet(server, request(5, "configurationDone", %{})) assert_receive(response(_, 5, "configurationDone", %{})) @@ -2491,15 +2489,13 @@ defmodule ElixirLS.Debugger.ServerTest do "task" => "run", "taskArgs" => ["-e", "MixProject.Dbg.pipe()"], "projectDir" => File.cwd!() - # disable auto interpret - # "debugAutoInterpretAllModules" => false }) ) assert_receive(response(_, 2, "launch", _), 3000) assert_receive(event(_, "initialized", %{}), 5000) - # refute MixProject.Dbg in :int.interpreted() + assert MixProject.Dbg in :int.interpreted() Server.receive_packet(server, request(5, "configurationDone", %{})) assert_receive(response(_, 5, "configurationDone", %{})) @@ -2640,6 +2636,190 @@ defmodule ElixirLS.Debugger.ServerTest do }) end) end + + test "breaks on dbg when module is not interpreted", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + abs_path = Path.absname("lib/dbg.ex") + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Dbg.simple()"], + "projectDir" => File.cwd!(), + # disable auto interpret + "debugAutoInterpretAllModules" => false + }) + ) + + assert_receive(response(_, 2, "launch", _), 3000) + assert_receive(event(_, "initialized", %{}), 5000) + + refute MixProject.Dbg in :int.interpreted() + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads})) + + # ensure thread ids are unique + thread_ids = Enum.map(threads, & &1["id"]) + assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(7, thread_id)) + + assert_receive response(_, 7, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 5, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + Server.receive_packet(server, scopes_req(8, frame_id)) + + assert_receive response(_, 8, "scopes", %{ + "scopes" => [ + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "variables", + "namedVariables" => 1, + "variablesReference" => vars_id + }, + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "process info", + "namedVariables" => _, + "variablesReference" => _ + } + ] + }) + + Server.receive_packet(server, vars_req(9, vars_id)) + + assert_receive response(_, 9, "variables", %{ + "variables" => [ + %{ + "name" => "a", + "value" => "5", + "variablesReference" => 0 + } + ] + }), + 1000 + + # stepIn is not supported + Server.receive_packet(server, step_in_req(12, thread_id)) + + assert_receive( + error_response( + _, + 12, + "stepIn", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepIn"} + ) + ) + + # stepOut is not supported + Server.receive_packet(server, step_out_req(13, thread_id)) + + assert_receive( + error_response( + _, + 13, + "stepOut", + "notSupported", + "Kernel.dbg breakpoints do not support {command} command", + %{"command" => "stepOut"} + ) + ) + + # next results in continue + Server.receive_packet(server, next_req(14, thread_id)) + assert_receive response(_, 14, "next", %{}) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(141, thread_id)) + + assert_receive response(_, 141, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 6, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + # continue + Server.receive_packet(server, continue_req(15, thread_id)) + assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true}) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "breakpoint", + "threadId" => ^thread_id + }), + 5_000 + + Server.receive_packet(server, stacktrace_req(151, thread_id)) + + assert_receive response(_, 151, "stackTrace", %{ + "totalFrames" => 1, + "stackFrames" => [ + %{ + "column" => 0, + "id" => frame_id, + "line" => 7, + "name" => "MixProject.Dbg.simple/0", + "source" => %{"path" => ^abs_path} + } + ] + }) + when is_integer(frame_id) + + Server.receive_packet(server, continue_req(16, thread_id)) + assert_receive response(_, 16, "continue", %{"allThreadsContinued" => true}) + + refute_receive event(_, "thread", %{ + "reason" => "exited", + "threadId" => ^thread_id + }), + 1_000 + end) + end end end From 9b26d3f15d017613ad00ef4118c51b73ee785412 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 29 Aug 2023 20:51:44 +0200 Subject: [PATCH 15/19] port bugfix from elixir --- apps/elixir_ls_debugger/lib/debugger/server.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 247c726b4..6bba441aa 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -1679,6 +1679,8 @@ defmodule ElixirLS.Debugger.Server do [asts_string, :faint, " #=> ", :reset, inspect(value, options), "\n\n"] |> IO.ANSI.format() |> IO.write() + + value end defp chunk_pipeline_asts_by_line(asts, %Macro.Env{line: env_line}) do From 29cb7408e194c356a8c9e6f1e3dbff87781e2529 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 29 Aug 2023 21:37:09 +0200 Subject: [PATCH 16/19] get stacktrace from Process.info if not in interpreted mode --- .../elixir_ls_debugger/lib/debugger/server.ex | 70 ++++++++++++++++--- .../lib/debugger/stacktrace.ex | 2 +- .../elixir_ls_debugger/test/debugger_test.exs | 9 ++- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 6bba441aa..ae2f2634d 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -133,13 +133,41 @@ defmodule ElixirLS.Debugger.Server do def dbg(code, options, %Macro.Env{} = caller) do quote do - GenServer.call(unquote(__MODULE__), {:dbg, binding(), __ENV__}, :infinity) + {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) + GenServer.call(unquote(__MODULE__), {:dbg, binding(), __ENV__, stacktrace}, :infinity) unquote(Macro.dbg(code, options, caller)) end end def pry_with_next(next?, binding, opts_or_env) when is_boolean(next?) do - next? and GenServer.call(__MODULE__, {:dbg, binding, opts_or_env}, :infinity) == {:ok, true} + if next? do + {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) + + GenServer.call(__MODULE__, {:dbg, binding, opts_or_env, stacktrace}, :infinity) == + {:ok, true} + else + false + end + end + + @elixir_internals [:elixir, :erl_eval] + @elixir_ls_internals [__MODULE__] + @debugger_internals @elixir_internals ++ @elixir_ls_internals + + defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @debugger_internals do + prune_stacktrace(t) + end + + defp prune_stacktrace([{Process, :info, 2, _} | t]) do + prune_stacktrace(t) + end + + defp prune_stacktrace([h | t]) do + [h | prune_stacktrace(t)] + end + + defp prune_stacktrace([]) do + [] end ## Server Callbacks @@ -153,7 +181,7 @@ defmodule ElixirLS.Debugger.Server do end @impl GenServer - def handle_call({:dbg, binding, %Macro.Env{} = env}, from, state = %__MODULE__{}) do + def handle_call({:dbg, binding, %Macro.Env{} = env, stacktrace}, from, state = %__MODULE__{}) do {pid, _ref} = from ref = Process.monitor(pid) @@ -162,7 +190,6 @@ defmodule ElixirLS.Debugger.Server do stacktrace = case Stacktrace.get(pid) do [_gen_server_frame, first_frame | stacktrace] -> - IO.puts("level #{first_frame.level}") # drop GenServer call to Debugger.Server from dbg callback # overwrite erlang debugger bindings with exact elixir ones first_frame = %{ @@ -180,10 +207,14 @@ defmodule ElixirLS.Debugger.Server do [] -> # no stacktrace if we are running in non interpreted mode - # build one frame stacktrace - # TODO look for ways of getting better stacktrace + # build frames from Process.info stacktrace + # drop first entry as we get it from env + [_ | stacktrace_rest] = prune_stacktrace(stacktrace) + + total_frames = length(stacktrace_rest) + 1 + first_frame = %Frame{ - level: 1, + level: total_frames, module: env.module, function: env.function, file: env.file, @@ -195,7 +226,30 @@ defmodule ElixirLS.Debugger.Server do line: env.line } - [first_frame] + frames_rest = + for {{m, f, a, keyword}, index} <- Enum.with_index(stacktrace_rest, 1) do + file = Stacktrace.get_file(m) + line = Keyword.get(keyword, :line, 0) + + %Frame{ + level: total_frames - index, + module: m, + function: {f, a}, + file: file, + args: [], + messages: [], + bindings: %{}, + dbg_frame?: true, + dbg_env: + Code.env_for_eval( + file: file, + line: line + ), + line: line + } + end + + [first_frame | frames_rest] end paused_process = %PausedProcess{stack: stacktrace, ref: ref} diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 078b0065c..22ed6719e 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -106,7 +106,7 @@ defmodule ElixirLS.Debugger.Stacktrace do Enum.into(:int.meta(meta_pid, :bindings, stack_level), %{}) end - defp get_file(module) do + def get_file(module) do Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source])) # TODO why beam to source location hack needed here? # case ElixirSense.Location.find_mod_file(module) do diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index c2d2e4413..8c346267f 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -2681,7 +2681,7 @@ defmodule ElixirLS.Debugger.ServerTest do Server.receive_packet(server, stacktrace_req(7, thread_id)) assert_receive response(_, 7, "stackTrace", %{ - "totalFrames" => 1, + "totalFrames" => 7, "stackFrames" => [ %{ "column" => 0, @@ -2690,6 +2690,7 @@ defmodule ElixirLS.Debugger.ServerTest do "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } + | _ ] }) when is_integer(frame_id) @@ -2770,7 +2771,7 @@ defmodule ElixirLS.Debugger.ServerTest do Server.receive_packet(server, stacktrace_req(141, thread_id)) assert_receive response(_, 141, "stackTrace", %{ - "totalFrames" => 1, + "totalFrames" => 7, "stackFrames" => [ %{ "column" => 0, @@ -2779,6 +2780,7 @@ defmodule ElixirLS.Debugger.ServerTest do "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } + | _ ] }) when is_integer(frame_id) @@ -2797,7 +2799,7 @@ defmodule ElixirLS.Debugger.ServerTest do Server.receive_packet(server, stacktrace_req(151, thread_id)) assert_receive response(_, 151, "stackTrace", %{ - "totalFrames" => 1, + "totalFrames" => 7, "stackFrames" => [ %{ "column" => 0, @@ -2806,6 +2808,7 @@ defmodule ElixirLS.Debugger.ServerTest do "name" => "MixProject.Dbg.simple/0", "source" => %{"path" => ^abs_path} } + | _ ] }) when is_integer(frame_id) From dc67921218269861b9d76d0e51ca57294ade3f32 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 29 Aug 2023 21:38:50 +0200 Subject: [PATCH 17/19] remove not relevant todos --- apps/elixir_ls_debugger/lib/debugger/stacktrace.ex | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 22ed6719e..754b26713 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -75,8 +75,6 @@ defmodule ElixirLS.Debugger.Stacktrace do [first_frame | other_frames] - # TODO add process stack? - error -> Output.debugger_important( "Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}" @@ -108,10 +106,5 @@ defmodule ElixirLS.Debugger.Stacktrace do def get_file(module) do Path.expand(to_string(ModuleInfoCache.get(module)[:compile][:source])) - # TODO why beam to source location hack needed here? - # case ElixirSense.Location.find_mod_file(module) do - # {module, file} -> file - # _ -> nil - # end end end From 6ef56809cd923e20a1ae9be24383b0a70eadca60 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 29 Aug 2023 22:12:25 +0200 Subject: [PATCH 18/19] fix breakpoint type detection --- apps/elixir_ls_debugger/lib/debugger/server.ex | 13 +++++++++---- .../lib/language_server/dialyzer/utils.ex | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index ae2f2634d..e9e439f6d 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -200,7 +200,7 @@ defmodule ElixirLS.Debugger.Server do module: env.module, function: env.function, file: env.file, - line: env.line + line: env.line || 1 } [first_frame | stacktrace] @@ -223,13 +223,13 @@ defmodule ElixirLS.Debugger.Server do bindings: Map.new(binding), dbg_frame?: true, dbg_env: Code.env_for_eval(env), - line: env.line + line: env.line || 1 } frames_rest = for {{m, f, a, keyword}, index} <- Enum.with_index(stacktrace_rest, 1) do file = Stacktrace.get_file(m) - line = Keyword.get(keyword, :line, 0) + line = Keyword.get(keyword, :line, 1) %Frame{ level: total_frames - index, @@ -1651,7 +1651,12 @@ defmodule ElixirLS.Debugger.Server do defp get_stop_reason(state = %__MODULE__{}, :breakpoint_reached, [first_frame = %Frame{} | _]) do file_breakpoints = Map.get(state.breakpoints, first_frame.file, []) - frame_mfa = {first_frame.module, first_frame.function, length(first_frame.args)} + frame_mfa = + case first_frame.function do + {f, a} -> {first_frame.module, f, a} + _ -> nil + end + function_breakpoints = Map.get(state.function_breakpoints, frame_mfa, []) cond do diff --git a/apps/language_server/lib/language_server/dialyzer/utils.ex b/apps/language_server/lib/language_server/dialyzer/utils.ex index 2e8ec838f..95af5c274 100644 --- a/apps/language_server/lib/language_server/dialyzer/utils.ex +++ b/apps/language_server/lib/language_server/dialyzer/utils.ex @@ -8,7 +8,6 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Utils do is_list(file) and match?({:ok, _}, :dialyzer_utils.get_core_from_beam(file)) end - # TODO @spec get_beam_file(module()) :: charlist() | :preloaded | :non_existing | :cover_compiled def get_beam_file(module) do case :code.which(module) do From fe3d1e0e9588abf0c27bbbfb6cb444cec4fa9a82 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 29 Aug 2023 22:22:28 +0200 Subject: [PATCH 19/19] make sure process is dead --- apps/elixir_ls_debugger/test/debugger_test.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 8c346267f..f8e53d597 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -21,6 +21,16 @@ defmodule ElixirLS.Debugger.ServerTest do :int.auto_attach(false) :int.no_break() :int.clear() + + if Process.alive?(server) do + Process.monitor(server) + Process.exit(server, :normal) + + receive do + {:DOWN, _, _, ^server, _} -> + :ok + end + end end) {:ok, %{server: server}}