Skip to content

Commit 58f07d8

Browse files
authored
Add support for pause and terminateThread requests in debugger (#675)
* add support for terminateThreads request * add support for pause * format
1 parent c5fd13f commit 58f07d8

File tree

5 files changed

+224
-15
lines changed

5 files changed

+224
-15
lines changed

apps/elixir_ls_debugger/lib/debugger/protocol.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ defmodule ElixirLS.Debugger.Protocol do
5555
end
5656
end
5757

58+
defmacro terminate_threads_req(seq, thread_ids) do
59+
quote do
60+
request(unquote(seq), "terminateThreads", %{"threadIds" => unquote(thread_ids)})
61+
end
62+
end
63+
64+
defmacro pause_req(seq, thread_id) do
65+
quote do
66+
request(unquote(seq), "pause", %{"threadId" => unquote(thread_id)})
67+
end
68+
end
69+
5870
defmacro stacktrace_req(seq, thread_id) do
5971
quote do
6072
request(unquote(seq), "stackTrace", %{"threadId" => unquote(thread_id)})

apps/elixir_ls_debugger/lib/debugger/server.ex

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ defmodule ElixirLS.Debugger.Server do
7171
GenServer.cast(server, {:breakpoint_reached, pid})
7272
end
7373

74+
def paused(pid, server) do
75+
GenServer.cast(server, {:paused, pid})
76+
end
77+
7478
## Server Callbacks
7579

7680
@impl GenServer
@@ -116,7 +120,8 @@ defmodule ElixirLS.Debugger.Server do
116120
end
117121

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

131-
# Debugger Adapter Protocol requires us to return 'function breakpoint' reason
132-
# but we can't tell what kind of a breakpoint was hit
133-
body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false}
136+
reason =
137+
case event do
138+
:breakpoint_reached ->
139+
# Debugger Adapter Protocol requires us to return 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' | 'goto'
140+
# | 'function breakpoint' | 'data breakpoint' | 'instruction breakpoint'
141+
# but we can't tell what kind of a breakpoint was hit
142+
"breakpoint"
143+
144+
:paused ->
145+
"pause"
146+
end
147+
148+
body = %{"reason" => reason, "threadId" => thread_id, "allThreadsStopped" => false}
134149
Output.send_event("stopped", body)
135150
state
136151
else
152+
Process.monitor(pid)
137153
state
138154
end
139155

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

172-
thread_id = state.threads_inverse[pid]
188+
{thread_id, threads_inverse} = state.threads_inverse |> Map.pop(pid)
173189
state = remove_paused_process(state, pid)
174190

175191
state = %{
176192
state
177193
| threads: state.threads |> Map.delete(thread_id),
178-
threads_inverse: state.threads_inverse |> Map.delete(pid)
194+
threads_inverse: threads_inverse
179195
}
180196

181-
Output.send_event("thread", %{
182-
"reason" => "exited",
183-
"threadId" => thread_id
184-
})
197+
if thread_id do
198+
Output.send_event("thread", %{
199+
"reason" => "exited",
200+
"threadId" => thread_id
201+
})
202+
end
185203

186204
{:noreply, state}
187205
end
@@ -361,8 +379,7 @@ defmodule ElixirLS.Debugger.Server do
361379
end
362380

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

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

416+
defp handle_request(terminate_threads_req(_, thread_ids), state = %__MODULE__{}) do
417+
for {id, pid} <- state.threads,
418+
id in thread_ids do
419+
# :kill is untrappable
420+
# do not need to cleanup here, :DOWN message handler will do it
421+
Process.monitor(pid)
422+
Process.exit(pid, :kill)
423+
end
424+
425+
{%{}, state}
426+
end
427+
428+
defp handle_request(pause_req(_, thread_id), state = %__MODULE__{}) do
429+
pid = state.threads[thread_id]
430+
431+
if pid do
432+
:int.attach(pid, build_attach_mfa(:paused))
433+
end
434+
435+
{%{}, state}
436+
end
437+
399438
defp handle_request(
400439
request(_, "stackTrace", %{"threadId" => thread_id} = args),
401440
state = %__MODULE__{}
@@ -623,8 +662,12 @@ defmodule ElixirLS.Debugger.Server do
623662
end
624663

625664
defp remove_paused_process(state = %__MODULE__{}, pid) do
626-
{process = %PausedProcess{}, paused_processes} = Map.pop(state.paused_processes, pid)
627-
true = Process.demonitor(process.ref, [:flush])
665+
{process, paused_processes} = Map.pop(state.paused_processes, pid)
666+
667+
if process do
668+
true = Process.demonitor(process.ref, [:flush])
669+
end
670+
628671
%__MODULE__{state | paused_processes: paused_processes}
629672
end
630673

@@ -904,6 +947,7 @@ defmodule ElixirLS.Debugger.Server do
904947
"supportsExceptionOptions" => false,
905948
"supportsValueFormattingOptions" => false,
906949
"supportsExceptionInfoRequest" => false,
950+
"supportsTerminateThreadsRequest" => true,
907951
"supportTerminateDebuggee" => false
908952
}
909953
end
@@ -1144,4 +1188,9 @@ defmodule ElixirLS.Debugger.Server do
11441188
0
11451189
end
11461190
end
1191+
1192+
defp build_attach_mfa(reason) do
1193+
server = Process.info(self())[:registered_name] || self()
1194+
{__MODULE__, reason, [server]}
1195+
end
11471196
end

apps/elixir_ls_debugger/lib/debugger/stacktrace.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
5252
[first_frame | other_frames]
5353

5454
error ->
55-
IO.warn("Failed to obtain meta pid for #{inspect(pid)}: #{inspect(error)}")
55+
IO.warn("Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}")
5656
[]
5757
end
5858
end

apps/elixir_ls_debugger/test/debugger_test.exs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,149 @@ defmodule ElixirLS.Debugger.ServerTest do
476476
end)
477477
end
478478

479+
@tag :fixture
480+
test "terminate threads", %{server: server} do
481+
in_fixture(__DIR__, "mix_project", fn ->
482+
Server.receive_packet(server, initialize_req(1, %{}))
483+
assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}))
484+
485+
Server.receive_packet(
486+
server,
487+
launch_req(2, %{
488+
"request" => "launch",
489+
"type" => "mix_task",
490+
"task" => "run",
491+
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
492+
"projectDir" => File.cwd!()
493+
})
494+
)
495+
496+
assert_receive(response(_, 2, "launch", %{}), 5000)
497+
assert_receive(event(_, "initialized", %{}))
498+
499+
Server.receive_packet(server, request(5, "configurationDone", %{}))
500+
assert_receive(response(_, 5, "configurationDone", %{}))
501+
Process.sleep(1000)
502+
Server.receive_packet(server, request(6, "threads", %{}))
503+
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)
504+
505+
assert [thread_id] =
506+
threads
507+
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
508+
|> Enum.map(& &1["id"])
509+
510+
Server.receive_packet(server, request(7, "terminateThreads", %{"threadIds" => [thread_id]}))
511+
assert_receive(response(_, 7, "terminateThreads", %{}), 500)
512+
513+
assert_receive event(_, "thread", %{
514+
"reason" => "exited",
515+
"threadId" => ^thread_id
516+
}),
517+
5000
518+
end)
519+
end
520+
521+
describe "pause" do
522+
@tag :fixture
523+
test "alive", %{server: server} do
524+
in_fixture(__DIR__, "mix_project", fn ->
525+
Server.receive_packet(server, initialize_req(1, %{}))
526+
527+
assert_receive(
528+
response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})
529+
)
530+
531+
Server.receive_packet(
532+
server,
533+
launch_req(2, %{
534+
"request" => "launch",
535+
"type" => "mix_task",
536+
"task" => "run",
537+
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
538+
"projectDir" => File.cwd!()
539+
})
540+
)
541+
542+
assert_receive(response(_, 2, "launch", %{}), 5000)
543+
assert_receive(event(_, "initialized", %{}))
544+
545+
Server.receive_packet(server, request(5, "configurationDone", %{}))
546+
assert_receive(response(_, 5, "configurationDone", %{}))
547+
Process.sleep(1000)
548+
Server.receive_packet(server, request(6, "threads", %{}))
549+
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)
550+
551+
assert [thread_id] =
552+
threads
553+
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
554+
|> Enum.map(& &1["id"])
555+
556+
{_, stderr} =
557+
capture_log_and_io(:standard_error, fn ->
558+
Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
559+
assert_receive(response(_, 7, "pause", %{}), 500)
560+
561+
assert_receive event(_, "stopped", %{
562+
"allThreadsStopped" => false,
563+
"reason" => "pause",
564+
"threadId" => ^thread_id
565+
}),
566+
500
567+
end)
568+
569+
assert stderr =~ "Failed to obtain meta for pid"
570+
end)
571+
end
572+
573+
@tag :fixture
574+
test "dead", %{server: server} do
575+
in_fixture(__DIR__, "mix_project", fn ->
576+
Server.receive_packet(server, initialize_req(1, %{}))
577+
578+
assert_receive(
579+
response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})
580+
)
581+
582+
Server.receive_packet(
583+
server,
584+
launch_req(2, %{
585+
"request" => "launch",
586+
"type" => "mix_task",
587+
"task" => "run",
588+
"taskArgs" => ["-e", "MixProject.Some.sleep()"],
589+
"projectDir" => File.cwd!()
590+
})
591+
)
592+
593+
assert_receive(response(_, 2, "launch", %{}), 5000)
594+
assert_receive(event(_, "initialized", %{}))
595+
596+
Server.receive_packet(server, request(5, "configurationDone", %{}))
597+
assert_receive(response(_, 5, "configurationDone", %{}))
598+
Process.sleep(1000)
599+
Server.receive_packet(server, request(6, "threads", %{}))
600+
assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000)
601+
602+
assert [thread_id] =
603+
threads
604+
|> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some")))
605+
|> Enum.map(& &1["id"])
606+
607+
Process.whereis(MixProject.Some) |> Process.exit(:kill)
608+
Process.sleep(1000)
609+
610+
Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id}))
611+
assert_receive(response(_, 7, "pause", %{}), 500)
612+
613+
assert_receive event(_, "thread", %{
614+
"reason" => "exited",
615+
"threadId" => ^thread_id
616+
}),
617+
5000
618+
end)
619+
end
620+
end
621+
479622
describe "breakpoints" do
480623
@tag :fixture
481624
test "sets and unsets breakpoints in erlang modules", %{server: server} do

apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ defmodule MixProject.Some do
3838
def quadruple(x) do
3939
double(double(x))
4040
end
41+
42+
def sleep do
43+
Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__)
44+
Process.sleep(:infinity)
45+
end
4146
end

0 commit comments

Comments
 (0)