From 9bf544abc776b72f803d408081885dcbf5c6cdd0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 9 Jan 2021 13:45:32 +0100 Subject: [PATCH] monitor debugged processes add test for mix task exit Fixes #454 --- .../elixir_ls_debugger/lib/debugger/server.ex | 47 +++++-- .../elixir_ls_debugger/test/debugger_test.exs | 116 ++++++++++++++++++ .../fixtures/mix_project/lib/mix_project.ex | 22 ++++ 3 files changed, 178 insertions(+), 7 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index b08307490..46bcb213a 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -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 @@ -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 diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index f05b2a708..43910aa3b 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -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, %{})) diff --git a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex index 8022a0c0e..4d82bd0a9 100644 --- a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex +++ b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex @@ -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