Skip to content

Commit

Permalink
Add support for pause and terminateThread requests in debugger (#675)
Browse files Browse the repository at this point in the history
* add support for terminateThreads request

* add support for pause

* format
  • Loading branch information
lukaszsamson authored Feb 13, 2022
1 parent c5fd13f commit 58f07d8
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 15 deletions.
12 changes: 12 additions & 0 deletions apps/elixir_ls_debugger/lib/debugger/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ defmodule ElixirLS.Debugger.Protocol do
end
end

defmacro terminate_threads_req(seq, thread_ids) do
quote do
request(unquote(seq), "terminateThreads", %{"threadIds" => unquote(thread_ids)})
end
end

defmacro pause_req(seq, thread_id) do
quote do
request(unquote(seq), "pause", %{"threadId" => unquote(thread_id)})
end
end

defmacro stacktrace_req(seq, thread_id) do
quote do
request(unquote(seq), "stackTrace", %{"threadId" => unquote(thread_id)})
Expand Down
77 changes: 63 additions & 14 deletions apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ defmodule ElixirLS.Debugger.Server do
GenServer.cast(server, {:breakpoint_reached, pid})
end

def paused(pid, server) do
GenServer.cast(server, {:paused, pid})
end

## Server Callbacks

@impl GenServer
Expand Down Expand Up @@ -116,7 +120,8 @@ defmodule ElixirLS.Debugger.Server do
end

@impl GenServer
def handle_cast({:breakpoint_reached, pid}, state = %__MODULE__{}) do
def handle_cast({event, pid}, state = %__MODULE__{})
when event in [:breakpoint_reached, :paused] do
# when debugged pid exits we get another breakpoint reached message (at least on OTP 23)
# check if process is alive to not debug dead ones
state =
Expand All @@ -128,12 +133,23 @@ defmodule ElixirLS.Debugger.Server do
paused_process = %PausedProcess{stack: Stacktrace.get(pid), ref: ref}
state = put_in(state.paused_processes[pid], paused_process)

# Debugger Adapter Protocol requires us to return 'function breakpoint' reason
# but we can't tell what kind of a breakpoint was hit
body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false}
reason =
case event do
:breakpoint_reached ->
# Debugger Adapter Protocol requires us to return 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' | 'goto'
# | 'function breakpoint' | 'data breakpoint' | 'instruction breakpoint'
# but we can't tell what kind of a breakpoint was hit
"breakpoint"

:paused ->
"pause"
end

body = %{"reason" => reason, "threadId" => thread_id, "allThreadsStopped" => false}
Output.send_event("stopped", body)
state
else
Process.monitor(pid)
state
end

Expand Down Expand Up @@ -169,19 +185,21 @@ defmodule ElixirLS.Debugger.Server do
"debugged process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}"
)

thread_id = state.threads_inverse[pid]
{thread_id, threads_inverse} = state.threads_inverse |> Map.pop(pid)
state = remove_paused_process(state, pid)

state = %{
state
| threads: state.threads |> Map.delete(thread_id),
threads_inverse: state.threads_inverse |> Map.delete(pid)
threads_inverse: threads_inverse
}

Output.send_event("thread", %{
"reason" => "exited",
"threadId" => thread_id
})
if thread_id do
Output.send_event("thread", %{
"reason" => "exited",
"threadId" => thread_id
})
end

{:noreply, state}
end
Expand Down Expand Up @@ -361,8 +379,7 @@ defmodule ElixirLS.Debugger.Server do
end

defp handle_request(configuration_done_req(_), state = %__MODULE__{}) do
server = :erlang.process_info(self())[:registered_name] || self()
:int.auto_attach([:break], {__MODULE__, :breakpoint_reached, [server]})
:int.auto_attach([:break], build_attach_mfa(:breakpoint_reached))

task = state.config["task"] || Mix.Project.config()[:default_task]
args = state.config["taskArgs"] || []
Expand Down Expand Up @@ -396,6 +413,28 @@ defmodule ElixirLS.Debugger.Server do
{%{"threads" => threads}, state}
end

defp handle_request(terminate_threads_req(_, thread_ids), state = %__MODULE__{}) do
for {id, pid} <- state.threads,
id in thread_ids do
# :kill is untrappable
# do not need to cleanup here, :DOWN message handler will do it
Process.monitor(pid)
Process.exit(pid, :kill)
end

{%{}, state}
end

defp handle_request(pause_req(_, thread_id), state = %__MODULE__{}) do
pid = state.threads[thread_id]

if pid do
:int.attach(pid, build_attach_mfa(:paused))
end

{%{}, state}
end

defp handle_request(
request(_, "stackTrace", %{"threadId" => thread_id} = args),
state = %__MODULE__{}
Expand Down Expand Up @@ -623,8 +662,12 @@ defmodule ElixirLS.Debugger.Server do
end

defp remove_paused_process(state = %__MODULE__{}, pid) do
{process = %PausedProcess{}, paused_processes} = Map.pop(state.paused_processes, pid)
true = Process.demonitor(process.ref, [:flush])
{process, paused_processes} = Map.pop(state.paused_processes, pid)

if process do
true = Process.demonitor(process.ref, [:flush])
end

%__MODULE__{state | paused_processes: paused_processes}
end

Expand Down Expand Up @@ -904,6 +947,7 @@ defmodule ElixirLS.Debugger.Server do
"supportsExceptionOptions" => false,
"supportsValueFormattingOptions" => false,
"supportsExceptionInfoRequest" => false,
"supportsTerminateThreadsRequest" => true,
"supportTerminateDebuggee" => false
}
end
Expand Down Expand Up @@ -1144,4 +1188,9 @@ defmodule ElixirLS.Debugger.Server do
0
end
end

defp build_attach_mfa(reason) do
server = Process.info(self())[:registered_name] || self()
{__MODULE__, reason, [server]}
end
end
2 changes: 1 addition & 1 deletion apps/elixir_ls_debugger/lib/debugger/stacktrace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
[first_frame | other_frames]

error ->
IO.warn("Failed to obtain meta pid for #{inspect(pid)}: #{inspect(error)}")
IO.warn("Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}")
[]
end
end
Expand Down
143 changes: 143 additions & 0 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,149 @@ defmodule ElixirLS.Debugger.ServerTest do
end)
end

@tag :fixture
test "terminate threads", %{server: server} do
in_fixture(__DIR__, "mix_project", fn ->
Server.receive_packet(server, initialize_req(1, %{}))
assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}))

Server.receive_packet(
server,
launch_req(2, %{
"request" => "launch",
"type" => "mix_task",
"task" => "run",
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
"projectDir" => File.cwd!()
})
)

assert_receive(response(_, 2, "launch", %{}), 5000)
assert_receive(event(_, "initialized", %{}))

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))
Process.sleep(1000)
Server.receive_packet(server, request(6, "threads", %{}))
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)

assert [thread_id] =
threads
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
|> Enum.map(& &1["id"])

Server.receive_packet(server, request(7, "terminateThreads", %{"threadIds" => [thread_id]}))
assert_receive(response(_, 7, "terminateThreads", %{}), 500)

assert_receive event(_, "thread", %{
"reason" => "exited",
"threadId" => ^thread_id
}),
5000
end)
end

describe "pause" do
@tag :fixture
test "alive", %{server: server} do
in_fixture(__DIR__, "mix_project", fn ->
Server.receive_packet(server, initialize_req(1, %{}))

assert_receive(
response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})
)

Server.receive_packet(
server,
launch_req(2, %{
"request" => "launch",
"type" => "mix_task",
"task" => "run",
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
"projectDir" => File.cwd!()
})
)

assert_receive(response(_, 2, "launch", %{}), 5000)
assert_receive(event(_, "initialized", %{}))

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))
Process.sleep(1000)
Server.receive_packet(server, request(6, "threads", %{}))
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)

assert [thread_id] =
threads
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
|> Enum.map(& &1["id"])

{_, stderr} =
capture_log_and_io(:standard_error, fn ->
Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
assert_receive(response(_, 7, "pause", %{}), 500)

assert_receive event(_, "stopped", %{
"allThreadsStopped" => false,
"reason" => "pause",
"threadId" => ^thread_id
}),
500
end)

assert stderr =~ "Failed to obtain meta for pid"
end)
end

@tag :fixture
test "dead", %{server: server} do
in_fixture(__DIR__, "mix_project", fn ->
Server.receive_packet(server, initialize_req(1, %{}))

assert_receive(
response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})
)

Server.receive_packet(
server,
launch_req(2, %{
"request" => "launch",
"type" => "mix_task",
"task" => "run",
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
"projectDir" => File.cwd!()
})
)

assert_receive(response(_, 2, "launch", %{}), 5000)
assert_receive(event(_, "initialized", %{}))

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))
Process.sleep(1000)
Server.receive_packet(server, request(6, "threads", %{}))
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)

assert [thread_id] =
threads
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
|> Enum.map(& &1["id"])

Process.whereis(MixProject.Some) |> Process.exit(:kill)
Process.sleep(1000)

Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
assert_receive(response(_, 7, "pause", %{}), 500)

assert_receive event(_, "thread", %{
"reason" => "exited",
"threadId" => ^thread_id
}),
5000
end)
end
end

describe "breakpoints" do
@tag :fixture
test "sets and unsets breakpoints in erlang modules", %{server: server} do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ defmodule MixProject.Some do
def quadruple(x) do
double(double(x))
end

def sleep do
Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__)
Process.sleep(:infinity)
end
end

0 comments on commit 58f07d8

Please sign in to comment.