Skip to content

Commit

Permalink
monitor debugged processes
Browse files Browse the repository at this point in the history
add test for mix task exit
Fixes #454
  • Loading branch information
lukaszsamson committed Jan 9, 2021
1 parent ff9a842 commit 79bfaad
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 7 deletions.
47 changes: 40 additions & 7 deletions apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,23 @@ defmodule ElixirLS.Debugger.Server do

@impl GenServer
def handle_cast({:breakpoint_reached, pid}, state) do
{state, thread_id} = ensure_thread_id(state, pid)

paused_process = %PausedProcess{stack: Stacktrace.get(pid)}
state = put_in(state.paused_processes[pid], paused_process)

body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false}
Output.send_event("stopped", body)
# 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 =
if Process.alive?(pid) do
# monitor to clanup state if process dies
Process.monitor(pid)
{state, thread_id} = ensure_thread_id(state, pid)

paused_process = %PausedProcess{stack: Stacktrace.get(pid)}
state = put_in(state.paused_processes[pid], paused_process)

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

{:noreply, state}
end
Expand Down Expand Up @@ -134,6 +144,29 @@ defmodule ElixirLS.Debugger.Server do
{:noreply, %{state | task_ref: nil}}
end

def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
IO.puts(
:standard_error,
"debugged process #{inspect(pid)} exited with reason #{inspect(reason)}"
)

thread_id = state.threads_inverse[pid]
state = remove_paused_process(state, pid)

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

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

{:noreply, state}
end

# If we get the disconnect request from the client, we continue with :disconnect so the server will
# die right after responding to the request
@impl GenServer
Expand Down
116 changes: 116 additions & 0 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,122 @@ defmodule ElixirLS.Debugger.ServerTest do
end)
end

test "notifies about process exit", %{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.exit()"],
"projectDir" => File.cwd!()
})
)

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

Server.receive_packet(
server,
set_breakpoints_req(3, %{"path" => "lib/mix_project.ex"}, [%{"line" => 17}])
)

assert_receive(
response(_, 3, "setBreakpoints", %{"breakpoints" => [%{"verified" => true}]}), 1000
)

Server.receive_packet(server, request(4, "setExceptionBreakpoints", %{"filters" => []}))
assert_receive(response(_, 4, "setExceptionBreakpoints", %{}))

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
})

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

test "notifies about mix task exit", %{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.exit_self()"],
"projectDir" => File.cwd!()
})
)

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

Server.receive_packet(
server,
set_breakpoints_req(3, %{"path" => "lib/mix_project.ex"}, [%{"line" => 29}])
)

assert_receive(
response(_, 3, "setBreakpoints", %{"breakpoints" => [%{"verified" => true}]})
)

Server.receive_packet(server, request(4, "setExceptionBreakpoints", %{"filters" => []}))
assert_receive(response(_, 4, "setExceptionBreakpoints", %{}))

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
})

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

assert_receive event(_, "exited", %{
"exitCode" => 1
})

assert_receive event(_, "terminated", %{
"restart" => false
})
end)
end

test "sets breakpoints in erlang modules", %{server: server} do
in_fixture(__DIR__, "mix_project", fn ->
Server.receive_packet(server, initialize_req(1, %{}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,26 @@ defmodule MixProject do
def double(y) do
2 * y
end

def exit do
Task.start(fn ->
Task.start_link(fn ->
Process.sleep(1000)
raise ArgumentError
end)

Process.sleep(:infinity)
end)

Process.sleep(:infinity)
end

def exit_self do
Task.start_link(fn ->
Process.sleep(1000)
raise ArgumentError
end)

Process.sleep(:infinity)
end
end

0 comments on commit 79bfaad

Please sign in to comment.