Current section
Files
Jump to
Current section
Files
src/edb_server.erl
%% Copyright (c) Meta Platforms, Inc. and affiliates.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% % @format
-module(edb_server).
%% erlfmt:ignore
% @fb-only
-compile(warn_missing_spec_all).
-moduledoc false.
-behaviour(gen_server).
%% External exports
-export([start/0, stop/0, find/0]).
%% Reloading module support
-export([reapply_breakpoints/1, add_module_substitute/3, remove_module_substitute/1]).
%% gen_server call wrappers that will throw on invariant violations
-export([call/2, call/3]).
%% gen_server callbacks
-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2]).
%% Exceptions
-export([raise/2, raise/3, invariant_violation/1]).
-export_type([call_request/0]).
%%--------------------------------------------------------------------
%% Types
%%--------------------------------------------------------------------
-type set(A) :: edb_server_sets:set(A).
-type line() :: edb:line().
-type start_opts() :: [].
-export_type([start_error/0]).
-type start_error() :: unsupported | failed_to_register.
-export_type([add_substitute_error/0]).
-type add_substitute_error() :: already_has_breakpoints | already_substituted | is_already_a_substitute.
-export_type([remove_substitute_error/0]).
-type remove_substitute_error() :: not_a_substitute | has_dependent_substitute.
-record(state, {
debugger_session :: erl_debugger:session(),
breakpoints :: edb_server_break:breakpoints(),
suspended_procs :: set(pid()),
% Sets used to control what to suspend and what not to
% suspend when pausing.
% - pids in do_suspend_pid override everything else (so a
% pid in this set will be suspended even if it could be
% part of an app to be suspended, etc.)
do_suspend_pids :: set(pid()),
do_not_suspend_apps :: set(atom()),
do_not_suspend_regnames :: set(atom()),
do_not_suspend_pids :: set(pid()),
event_subscribers :: edb_events:subscribers()
}).
-type state() :: #state{}.
-type procs_spec() :: edb:procs_spec().
%%--------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------
-define(is_internal_pid(Pid), (node(Pid) =:= node())).
%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-spec start() -> ok | {error, start_error()}.
start() ->
case erl_debugger:supported() of
false ->
{error, unsupported};
true ->
StartResult = gen_server:start(
{local, ?MODULE},
?MODULE,
[],
[{spawn_opt, [{priority, high}]}]
),
case StartResult of
{ok, _Pid} -> ok;
Err = {error, Reason} when Reason =:= failed_to_register -> Err
end
end.
-spec stop() -> ok.
stop() ->
ok = gen_server:stop(?MODULE).
-spec find() -> pid() | undefined.
find() ->
case whereis(?MODULE) of
undefined -> undefined;
Pid when is_pid(Pid) -> Pid
end.
%%--------------------------------------------------------------------
%% Reapply breakpoints
%%--------------------------------------------------------------------
-doc """
Reapply all existing breakpoints for the specified module to the VM.
This function will attempt to set VM breakpoints for all lines that currently
have explicit breakpoints or step breakpoints registered for the given module.
The operation is atomic - either all breakpoints are successfully reapplied, or
none are applied at all. If any individual breakpoint fails to be set, all
previously applied breakpoints from this operation are rolled back and removed.
# Returns
- `ok` - All breakpoints were successfully reapplied
- `{error, Reason}` - Operation failed, no breakpoints were applied; `Reason`
describes the error that occurred when setting the first breakpoint that failed.
""".
-spec reapply_breakpoints(Module) -> 'ok' | {error, Reason} when
Module :: module(),
Reason :: edb:add_breakpoint_error().
reapply_breakpoints(Module) ->
call(node(), {reapply_breakpoints, Module}).
%%--------------------------------------------------------------------
%% Module conversions
%%--------------------------------------------------------------------
-spec to_vm_module(module(), state()) -> edb_server_break:vm_module().
to_vm_module(Module, State) ->
#state{breakpoints = Breakpoints} = State,
edb_server_break:to_vm_module(Module, Breakpoints).
-spec from_vm_module(edb_server_break:vm_module(), state()) -> module().
from_vm_module(VmModule, State) ->
#state{breakpoints = Breakpoints} = State,
edb_server_break:from_vm_module(VmModule, Breakpoints).
%%--------------------------------------------------------------------
%% Module substitutes
%%--------------------------------------------------------------------
-doc """
Add a module substitute from `Module` to `Substitute`.
When users try to set a breakpoint on `Module`, it will be set on `Substitute` instead.
This handles transitive substitutes as well, so if `Substitute` is substituted by another module,
then setting a breakpoint on `Module` will set it on that module.
If `Module` had breakpoints set, they will be transferred to `Substitute`.
`AddedFrames` is a list of frames that is expected in stack trace when functions in
`Substitute` are called. When transitivity occurs (A -> B -> C), the added frames
from each substitution are accumulated, so the final stack trace will include
frames from both A -> B and B -> C substitutions.
Returns {error, already_has_breakpoints} if `Substitute` already has breakpoints set.
Returns {error, already_substituted} if `Module` is already substituted by another module.
Returns {error, is_already_a_substitute} if `Substitute` is already a substitute of another module.
""".
-spec add_module_substitute(Module :: module(), Substitute :: module(), AddedFrames :: [mfa()]) ->
ok | {error, add_substitute_error()}.
add_module_substitute(Module, Substitute, AddedFrames) ->
call(node(), {add_module_substitute, Module, Substitute, AddedFrames}).
-doc """
Remove a module substitute.
All breakpoints on the substituted module will be transferred to the original module.
Returns {error, not_a_substitute} if the module has not been added as a substitute
using add_module_substitute/2.
Returns {error, has_dependent_substitute} if the module is part of a chain where
another substitute depends on it.
""".
-spec remove_module_substitute(SubstituteModule :: module()) -> ok | {error, remove_substitute_error()}.
remove_module_substitute(SubstituteModule) ->
call(node(), {remove_module_substitute, SubstituteModule}).
%%--------------------------------------------------------------------
%% Requests
%%--------------------------------------------------------------------
-type call_request() ::
{subscribe_to_events, pid()}
| {remove_event_subscription, edb:event_subscription()}
| {send_sync_event, edb:event_subscription()}
| {add_breakpoint, module(), line()}
| {clear_breakpoint, module(), line()}
| {clear_breakpoints, module()}
| get_breakpoints
| {get_breakpoints, module()}
| {set_breakpoints, module(), [line()]}
| {reapply_breakpoints, module()}
| get_breakpoints_hit
| {add_module_substitute, module(), module(), [mfa()]}
| {remove_module_substitute, module()}
| pause
| continue
| is_paused
| {process_info, pid(), [edb:process_info_field()]}
| {processes, [edb:process_info_field()]}
| {excluded_processes, [edb:process_info_field()]}
| {step_over, pid()}
| {step_in, pid()}
| {step_out, pid()}
| {exclude_processes, [procs_spec()]}
| {unexclude_processes, [procs_spec()]}
| {stack_frames, pid()}
| {stack_frame_vars, pid(), edb:frame_id(), Size :: non_neg_integer()}
| {eval, eval_opts(Result :: term())}.
-type cast_request() ::
term().
-type eval_opts(Result) ::
#{
pid := pid(),
frame_id := edb:frame_id(),
max_term_size := non_neg_integer(),
timeout := timeout(),
function := fun((Vars :: edb:stack_frame_vars()) -> Result),
dependencies := [module()]
}.
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
-spec init(start_opts()) -> {ok, state()} | {error, start_error()}.
init([]) ->
case erl_debugger:register(self()) of
{error, already_exists} ->
{error, failed_to_register};
{ok, DebuggerSession} ->
% We trap exit so that terminate/1 gets called in that case too
% and we get a chance to clean-up
erlang:process_flag(trap_exit, true),
{ok, #state{
debugger_session = DebuggerSession,
breakpoints = edb_server_break:create(),
suspended_procs = #{},
do_suspend_pids = #{},
do_not_suspend_apps = #{kernel => []},
do_not_suspend_regnames = #{},
do_not_suspend_pids = #{},
event_subscribers = edb_events:no_subscribers()
}}
end.
-spec terminate(Reason :: term(), State :: state()) -> ok.
terminate(Reason, State0) ->
erl_debugger:unregister(self(), State0#state.debugger_session),
BPs = get_breakpoints(State0),
State1 = maps:fold(
fun(VmModule, _ModuleBPS, StateN) ->
{reply, _, StateN_plus_1} = clear_breakpoints_impl(from_vm_module(VmModule, State0), StateN),
StateN_plus_1
end,
State0,
BPs
),
{ok, _ActuallyResumed, State2} = resume_processes(all, termination, State1),
ok = edb_events:broadcast({terminated, Reason}, State2#state.event_subscribers),
ok.
-spec handle_cast(Request, state()) -> Result when
Request :: cast_request(),
Result :: {noreply, state()} | {stop, shutdown, state()}.
handle_cast(_, _State) ->
error(not_implemented).
-spec call(Node :: node(), Request :: call_request()) -> dynamic().
call(Node, Request) ->
call(Node, Request, 5_000).
-spec call(Node :: node(), Request :: call_request(), Timeout :: pos_integer() | infinity) -> dynamic().
call(Node, Request, Timeout) ->
case gen_server:call({?MODULE, Node}, Request, Timeout) of
{finished, Reply} ->
Reply;
{raised, Class, {Reason, ST}} when Class =:= error; Class =:= throw; Class =:= exit ->
erlang:raise(Class, Reason, ST)
end.
-doc """
Abort the execution and raise an exception on the calling client side.
The gen_server state at the start of the request will be preserved, so
it is the caller's responsibility to ensure that things are left in a
consistent state.
""".
-spec raise(Class, Reason) -> no_return() when
Class :: error | exit | throw,
Reason :: term().
raise(Class, Reason) ->
throw({raised, Class, Reason}).
-doc """
Like `raise/2`, but specifying the stacktrace to be reported.
""".
-spec raise(Class, Reason, ST) -> no_return() when
Class :: error | exit | throw,
Reason :: term(),
ST :: erlang:stacktrace().
raise(Class, Reason, ST) ->
erlang:raise(throw, {raised, Class, Reason}, ST).
-doc """
Abort due to invariant violation. Will be raised as an error on the caller side.
""".
-spec invariant_violation(Reason :: term()) -> no_return().
invariant_violation(Reason) ->
raise(error, {invariant_violation, Reason}).
-spec handle_call(Request, From, state()) -> Result when
Request :: call_request(),
From :: gen_server:from(),
Result :: {reply, Reply :: term(), NewState :: state()} | {noreply, NewState :: state()}.
handle_call(Request, From, State) ->
try dispatch_call(Request, From, State) of
{reply, Reply, NewState} -> {reply, {finished, Reply}, NewState}
catch
throw:{raised, Class, Reason}:ST ->
{reply, {raised, Class, {Reason, ST}}, State}
end.
-spec dispatch_call(Request, From, state()) -> Result when
Request :: call_request(),
From :: gen_server:from(),
Result :: {reply, Reply :: term(), NewState :: state()} | {noreply, NewState :: state()}.
dispatch_call({subscribe_to_events, Pid}, _From, State0) ->
subscribe_to_events_impl(Pid, State0);
dispatch_call({remove_event_subscription, Subscription}, _From, State0) ->
remove_event_subscription_impl(Subscription, State0);
dispatch_call({send_sync_event, Subscription}, _From, State0) ->
send_sync_event_impl(Subscription, State0);
dispatch_call({add_breakpoint, Module, Line}, _From, State0) ->
add_breakpoint_impl(Module, Line, State0);
dispatch_call({clear_breakpoints, Module}, _From, State0) ->
clear_breakpoints_impl(Module, State0);
dispatch_call({clear_breakpoint, Module, Line}, _From, State0) ->
clear_breakpoint_impl(Module, Line, State0);
dispatch_call({set_breakpoints, Module, Lines}, _From, State0) ->
set_breakpoints_impl(Module, Lines, State0);
dispatch_call(get_breakpoints, _From, State0) ->
get_breakpoints_impl(State0);
dispatch_call({get_breakpoints, Module}, _From, State0) ->
get_breakpoints_impl(Module, State0);
dispatch_call(get_breakpoints_hit, _From, State) ->
get_breakpoints_hit_impl(State);
dispatch_call({reapply_breakpoints, Module}, _From, State) ->
reapply_breakpoints_impl(Module, State);
dispatch_call({add_module_substitute, Module, Substitute, AddedFrames}, _From, State0) ->
add_module_substitute_impl(Module, Substitute, AddedFrames, State0);
dispatch_call({remove_module_substitute, X}, _From, State0) ->
remove_module_substitute_impl(X, State0);
dispatch_call(pause, _From, State0) ->
pause_impl(State0);
dispatch_call(continue, _From, State0) ->
continue_impl(State0);
dispatch_call({process_info, Pid, RequestedFields}, _From, State0) ->
process_info_impl(Pid, RequestedFields, State0);
dispatch_call({processes, RequestedFields}, _From, State0) ->
processes_impl(RequestedFields, State0);
dispatch_call({excluded_processes, RequestedFields}, _From, State0) ->
excluded_processes_impl(RequestedFields, State0);
dispatch_call({exclude_processes, Specs}, _From, State0) ->
exclude_processes_impl(Specs, State0);
dispatch_call(is_paused, _From, State0) ->
is_paused_impl(State0);
dispatch_call({unexclude_processes, Specs}, _From, State0) ->
unexclude_processes_impl(Specs, State0);
dispatch_call({stack_frames, Pid}, _From, State0) ->
stack_frames_impl(Pid, State0);
dispatch_call({stack_frame_vars, Pid, FrameId, MaxTermSize}, _From, State0) ->
stack_frame_vars_impl(Pid, FrameId, MaxTermSize, State0);
dispatch_call({step_over, Pid}, _From, State0) ->
step_impl(fun(Breakpoints) -> edb_server_break:prepare_for_stepping(step_over, Pid, Breakpoints) end, State0);
dispatch_call({step_out, Pid}, _From, State0) ->
step_impl(fun(Breakpoints) -> edb_server_break:prepare_for_stepping(step_out, Pid, Breakpoints) end, State0);
dispatch_call({step_in, Pid}, _From, State0) ->
step_impl(fun(Breakpoints) -> edb_server_break:prepare_for_stepping_in(Pid, Breakpoints) end, State0);
dispatch_call({eval, Opts = #{}}, {CallerPid, _}, State0) ->
eval_impl(Opts, CallerPid, State0).
-spec handle_info(Info, State :: state()) -> {noreply, state()} when
Info :: erl_debugger:event_message() | {'DOWN', reference(), process, pid(), term()}.
handle_info({debugger_event, S, Event}, State0 = #state{debugger_session = S}) ->
{ok, State1} = handle_debugger_event(Event, State0),
{noreply, State1};
handle_info({'DOWN', MonitorRef, process, _Pid, _Info}, State0) ->
Subs0 = State0#state.event_subscribers,
Subs1 = edb_events:process_down(MonitorRef, Subs0),
State1 = State0#state{event_subscribers = Subs1},
{noreply, State1};
handle_info(_E, State0) ->
{noreply, State0}.
%%--------------------------------------------------------------------
%% Debugger events
%%--------------------------------------------------------------------
-spec handle_debugger_event(Event, state()) -> {ok, state()} when
Event :: erl_debugger:event().
handle_debugger_event({breakpoint, Pid, MFA, Line, Resume}, State0) ->
breakpoint_event_impl(Pid, MFA, Line, Resume, State0).
-spec breakpoint_event_impl(Pid, MFA, Line, Resume, State0) -> {ok, State1} when
Pid :: pid(),
MFA :: mfa(),
Line :: line(),
Resume :: fun(() -> ok),
State0 :: state(),
State1 :: state().
breakpoint_event_impl(Pid, MFA = {Module, _, _}, Line, Resume, State0) ->
Universe = erlang:processes(),
UnsuspendablePids = get_excluded_processes(Universe, State0),
State3 =
case edb_server_sets:is_element(Pid, UnsuspendablePids) of
true ->
ok = Resume(),
State0;
false ->
#state{breakpoints = BP0} = State0,
VmModule = to_vm_module(Module, State0),
case edb_server_break:register_breakpoint_event(VmModule, Line, Pid, Resume, BP0) of
resume ->
ok = Resume(),
State0;
{suspend, Reason, BP1} ->
State1 = State0#state{breakpoints = BP1},
{ok, State2} = suspend_all_processes(Universe, UnsuspendablePids, State1),
PausedEvent =
case Reason of
explicit ->
{breakpoint, Pid, MFA, {line, Line}};
step ->
% Pid was already suspended when processing the step breakpoint, so
% resume it here to balance the suspension count. Notice the process will
% remain suspended, resume_process here is cheap as it is just decreasing
% a counter.
edb_server_process:try_resume_process(Pid),
{step, Pid}
end,
ok = edb_events:broadcast(
{paused, PausedEvent},
State2#state.event_subscribers
),
State2
end
end,
{ok, State3}.
%%--------------------------------------------------------------------
%% handle_call implementations
%%--------------------------------------------------------------------
-spec subscribe_to_events_impl(Pid, State0) -> {reply, {ok, Subscription}, State1} when
Pid :: pid(),
State0 :: state(),
Subscription :: edb_events:subscription(),
State1 :: state().
subscribe_to_events_impl(Pid, State0 = #state{event_subscribers = Subs0}) ->
MonitorRef = erlang:monitor(process, Pid),
{ok, {Subscription, Subs1}} = edb_events:subscribe(Pid, MonitorRef, Subs0),
State1 = State0#state{event_subscribers = Subs1},
{reply, {ok, Subscription}, State1}.
-spec remove_event_subscription_impl(Subscription, State0) -> {reply, ok, State1} when
Subscription :: edb_events:subscription(),
State0 :: state(),
State1 :: state().
remove_event_subscription_impl(Subscription, State0 = #state{event_subscribers = Subs0}) ->
State1 =
case edb_events:send_to(Subscription, unsubscribed, Subs0) of
undefined ->
State0;
ok ->
case edb_events:unsubscribe(Subscription, Subs0) of
not_subscribed ->
State0;
{ok, {MonitorRef, Subs1}} ->
erlang:demonitor(MonitorRef, [flush]),
State0#state{event_subscribers = Subs1}
end
end,
{reply, ok, State1}.
-spec send_sync_event_impl(Subscription, State0) -> {reply, Reply, State1} when
Subscription :: edb_events:subscription(),
State0 :: state(),
Reply :: {ok, reference()} | {error, unknown_subscription},
State1 :: state().
send_sync_event_impl(Subscription, State) ->
SyncRef = erlang:make_ref(),
Reply =
case edb_events:send_to(Subscription, {sync, SyncRef}, State#state.event_subscribers) of
undefined ->
{error, unknown_subscription};
ok ->
{ok, SyncRef}
end,
{reply, Reply, State}.
-spec add_breakpoint_impl(Module, Line, State0) -> {reply, ok | {error, Reason}, State1} when
Module :: module(),
Line :: line(),
Reason :: edb:add_breakpoint_error(),
State0 :: state(),
State1 :: state().
add_breakpoint_impl(Module, Line, State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmModule = to_vm_module(Module, State0),
case edb_server_break:add_explicit(VmModule, Line, Breakpoints0) of
{ok, Breakpoints1} ->
State1 = State0#state{breakpoints = Breakpoints1},
{reply, ok, State1};
{error, Reason} ->
{reply, {error, Reason}, State0}
end.
-spec clear_breakpoints_impl(Module, State0) -> {reply, ok, State1} when
Module :: module(),
State0 :: state(),
State1 :: state().
clear_breakpoints_impl(Module, State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmModule = to_vm_module(Module, State0),
{ok, Breakpoints1} = edb_server_break:clear_explicits(VmModule, Breakpoints0),
State1 = State0#state{breakpoints = Breakpoints1},
{reply, ok, State1}.
-spec clear_breakpoint_impl(Module, Line, State0) -> {reply, ok | {error, not_found}, State1} when
Module :: module(),
Line :: line(),
State0 :: state(),
State1 :: state().
clear_breakpoint_impl(Module, Line, State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmModule = to_vm_module(Module, State0),
case edb_server_break:clear_explicit(VmModule, Line, Breakpoints0) of
{ok, _, Breakpoints1} ->
%% We don't do anything particular yet if the breakpoint vanished from the VM
State1 = State0#state{breakpoints = Breakpoints1},
{reply, ok, State1};
{error, Reason} ->
{reply, {error, Reason}, State0}
end.
-spec set_breakpoints_impl(Module, Lines, State0) -> {reply, Result, State1} when
Module :: module(),
Lines :: [line()],
State0 :: state(),
State1 :: state(),
Result :: edb:set_breakpoints_result().
set_breakpoints_impl(Module, Lines, State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmModule = to_vm_module(Module, State0),
{ok, Breakpoints1} = edb_server_break:clear_explicits(VmModule, Breakpoints0),
{LineResults, Breakpoints2} = edb_server_break:add_explicits(VmModule, Lines, Breakpoints1),
State2 = State0#state{breakpoints = Breakpoints2},
{reply, LineResults, State2}.
-spec get_breakpoints_impl(state()) -> {reply, #{module() => [edb:breakpoint_info()]}, state()}.
get_breakpoints_impl(State0) ->
BPSet = get_breakpoints(State0),
Result =
#{
Module => [#{module => Module, line => Line} || Line := [] <- Lines]
|| VmModule := Lines <- BPSet,
Module <- [from_vm_module(VmModule, State0)]
},
{reply, Result, State0}.
-spec get_breakpoints_impl(module(), state()) -> {reply, [edb:breakpoint_info()], state()}.
get_breakpoints_impl(Module, State0) ->
BPSet = get_breakpoints(State0),
Lines = maps:get(to_vm_module(Module, State0), BPSet, #{}),
BPInfoList = [#{module => Module, line => Line} || Line := [] <- Lines],
{reply, BPInfoList, State0}.
-spec get_breakpoints_hit_impl(State0 :: state()) -> {reply, BreakpointsHit, State1 :: state()} when
BreakpointsHit :: #{pid() => #{module := module(), line := line()}}.
get_breakpoints_hit_impl(State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmBreakpointsHit = edb_server_break:get_explicits_hit(Breakpoints0),
BreakpointsHit = maps:map(
fun(_Pid, #{line := Line, module := VmModule}) ->
#{line => Line, module => from_vm_module(VmModule, State0)}
end,
VmBreakpointsHit
),
{reply, BreakpointsHit, State0}.
-spec reapply_breakpoints_impl(module(), State0 :: state()) ->
{reply, ok | {error, Reason}, State1 :: state()}
when
Reason :: edb:add_breakpoint_error().
reapply_breakpoints_impl(Module, State0) ->
#state{breakpoints = Breakpoints0} = State0,
VmModule = to_vm_module(Module, State0),
Result = edb_server_break:reapply_breakpoints(VmModule, Breakpoints0),
{reply, Result, State0}.
-spec add_module_substitute_impl(Module :: module(), Substitute :: module(), AddedFrames :: [mfa()], State0 :: state()) ->
{reply, ok | {error, add_substitute_error()}, State1 :: state()}.
add_module_substitute_impl(Module, Substitute, AddedFrames, State0) ->
#state{breakpoints = Breakpoints0} = State0,
case edb_server_break:add_module_substitute(Module, Substitute, AddedFrames, Breakpoints0) of
{ok, Breakpoints1} ->
State1 = State0#state{breakpoints = Breakpoints1},
{reply, ok, State1};
Error ->
{reply, Error, State0}
end.
-spec remove_module_substitute_impl(SubstituteModule :: module(), State0 :: state()) ->
{reply, ok | {error, remove_substitute_error()}, State1 :: state()}.
remove_module_substitute_impl(SubstituteModule, State0) ->
#state{breakpoints = Breakpoints0} = State0,
case edb_server_break:remove_module_substitute(SubstituteModule, Breakpoints0) of
{ok, Breakpoints1} ->
State1 = State0#state{breakpoints = Breakpoints1},
{reply, ok, State1};
Error ->
{reply, Error, State0}
end.
-spec pause_impl(State0 :: state()) -> {reply, ok, State1 :: state()}.
pause_impl(State0) ->
State2 =
case is_paused(State0) of
true ->
State0;
false ->
Universe = erlang:processes(),
UnsuspendablePids = get_excluded_processes(Universe, State0),
{ok, State1} = suspend_all_processes(Universe, UnsuspendablePids, State0),
ok = edb_events:broadcast({paused, pause}, State0#state.event_subscribers),
State1
end,
{reply, ok, State2}.
-spec continue_impl(State0 :: state()) -> {reply, Result, State1 :: state()} when
Result :: {ok, resumed | not_paused}.
continue_impl(State0) ->
{ok, ActuallyResumed, State1} = resume_processes(all, continue, State0),
Result =
case maps:size(ActuallyResumed) > 0 of
true -> resumed;
false -> not_paused
end,
{reply, {ok, Result}, State1}.
-spec step_impl(PrepareStepBreakpoints, State0) -> {reply, ok | {error, Reason}, State1} when
PrepareStepBreakpoints :: fun((Breakpoints0) -> {ok, Breakpoints1} | {error, Reason}),
Breakpoints0 :: edb_server_break:breakpoints(),
Breakpoints1 :: edb_server_break:breakpoints(),
State0 :: state(),
State1 :: state().
step_impl(PrepareStepBreakpoints, State0) ->
#state{breakpoints = Breakpoints0} = State0,
case PrepareStepBreakpoints(Breakpoints0) of
{ok, Breakpoints1} ->
State1 = State0#state{breakpoints = Breakpoints1},
{ok, _, State2} = resume_processes(all, continue, State1),
{reply, ok, State2};
{error, _} = Error ->
{reply, Error, State0}
end.
-spec process_info_impl(Pid, RequestedFields, State0) -> {reply, Result, State1} when
Pid :: pid(),
RequestedFields :: [edb:process_info_field()],
State0 :: state(),
State1 :: state(),
Result :: {ok, edb:process_info()} | undefined.
process_info_impl(Pid, RequestedFields, State0) ->
Status = process_status(Pid, State0),
Result =
case edb_server_process:processes_info(#{Pid => Status}, RequestedFields) of
#{Pid := Info} -> {ok, Info};
_ -> undefined
end,
{reply, Result, State0}.
-spec processes_impl(RequestedFields, State0) -> {reply, Result, State1} when
RequestedFields :: [edb:process_info_field()],
State0 :: state(),
State1 :: state(),
Result :: #{pid() => edb:process_info()}.
processes_impl(RequestedFields, State0) ->
Universe = erlang:processes(),
Excluded = get_excluded_processes(Universe, State0),
Procs =
#{
Pid => process_status(Pid, State0)
|| Pid <- Universe, not maps:is_key(Pid, Excluded)
},
Result = edb_server_process:processes_info(Procs, RequestedFields),
{reply, Result, State0}.
-spec is_paused_impl(State0) -> {reply, Result, State1} when
State0 :: state(),
State1 :: state(),
Result :: boolean().
is_paused_impl(State0) ->
{reply, is_paused(State0), State0}.
-spec excluded_processes_impl(RequestedFields, State0) -> {reply, #{pid() => edb:process_info()}, State1} when
RequestedFields :: [edb:process_info_field()],
State0 :: state(),
State1 :: state().
excluded_processes_impl(RequestedFields, State0) ->
Universe = erlang:processes(),
Excluded = get_excluded_processes(Universe, State0),
Result = edb_server_process:excluded_processes_info(Excluded, RequestedFields),
{reply, Result, State0}.
-spec excluded_sys_processes(Universe :: [pid()], state()) -> set(pid()).
excluded_sys_processes(Universe, State0) ->
#state{do_suspend_pids = DoSuspendPids} = State0,
SysProcNames = [
application_controller,
erl_prim_loader,
erts_code_purger,
init,
logger
],
NamedSysProcs =
#{
Pid => []
|| N <- SysProcNames,
Pid <- [erlang:whereis(N)],
Pid /= undefined,
is_pid(Pid),
not edb_server_sets:is_element(Pid, DoSuspendPids)
},
InternalSysProcs = #{Pid => [] || Pid <- Universe, erts_internal:is_system_process(Pid)},
edb_server_sets:union(NamedSysProcs, InternalSysProcs).
-spec excluded_debugger_processes(state()) -> set(pid()).
excluded_debugger_processes(State) ->
#state{
do_suspend_pids = DoSuspendPids,
event_subscribers = Subscribers
} = State,
SubscribedPids =
#{
Pid => []
|| Pid <- edb_events:subscriber_pids(Subscribers),
not edb_server_sets:is_element(Pid, DoSuspendPids)
},
AllDebuggerPids = SubscribedPids#{self() => []},
AllDebuggerPids.
-spec excluded_processes_by_regname(state()) -> set(pid()).
excluded_processes_by_regname(State) ->
#state{do_not_suspend_regnames = DoNotSuspendRegNames} = State,
excluded_processes_by_regname(DoNotSuspendRegNames, State).
-spec excluded_processes_by_regname(RegNameSet, state()) -> set(pid()) when
RegNameSet :: set(atom()).
excluded_processes_by_regname(RegNameSet, State) ->
#state{do_suspend_pids = DoSuspendPids} = State,
#{
Pid => []
|| Name := [] <- RegNameSet,
Pid <- [erlang:whereis(Name)],
not edb_server_sets:is_element(Pid, DoSuspendPids),
is_pid(Pid),
Pid /= undefined
}.
-spec excluded_processes_by_app(Universe :: [pid()], state()) -> set(pid()).
excluded_processes_by_app(Universe, State) ->
#state{do_not_suspend_apps = DoNotSuspendApps} = State,
excluded_processes_by_app(DoNotSuspendApps, Universe, State).
-spec excluded_processes_by_app(AppSet, Universe, state()) -> set(pid()) when
AppSet :: set(atom()),
Universe :: [pid()].
excluded_processes_by_app(AppSet, Universe, State) ->
#state{do_suspend_pids = DoSuspendPids} = State,
case map_size(AppSet) == 0 of
true ->
#{};
false ->
ApplicationMasterPids =
#{
GlPid => []
|| App := _ <- AppSet,
{_App, GlPid} <- ets:lookup(ac_tab, {application_master, App})
},
#{
P => []
|| P <- Universe,
not edb_server_sets:is_element(P, DoSuspendPids),
{group_leader, GL} <- group_leader(P),
edb_server_sets:is_element(GL, ApplicationMasterPids)
}
end.
-spec group_leader(pid()) -> [{group_leader, pid()}].
group_leader(Pid) ->
case erlang:process_info(Pid, [group_leader]) of
undefined ->
[];
[{group_leader, GL}] ->
[{group_leader, GL}]
end.
-spec exclude_processes_impl(Specs, State0) -> {reply, ok, State1} when
State0 :: state(),
State1 :: state(),
Specs :: [procs_spec()].
exclude_processes_impl(Specs, State0) ->
ExcludedByPid = #{P => [] || {proc, P} <- Specs, is_pid(P), is_relevant_pid(P)},
ExcludedRegNames = #{Name => [] || {proc, Name} <- Specs, is_atom(Name)},
ExcludedApps = #{App => [] || {application, App} <- Specs},
ExceptedFromExclude = #{P => [] || {except, P} <- Specs, is_relevant_pid(P)},
#state{
do_not_suspend_apps = DoNotSuspendApps0,
do_not_suspend_pids = DoNotSuspendPids0,
do_not_suspend_regnames = DoNotSuspendRegNames0,
do_suspend_pids = DoSuspend0
} = State0,
State1 = State0#state{
do_not_suspend_apps = edb_server_sets:union(DoNotSuspendApps0, ExcludedApps),
do_not_suspend_pids = edb_server_sets:union(DoNotSuspendPids0, ExcludedByPid),
do_not_suspend_regnames = edb_server_sets:union(DoNotSuspendRegNames0, ExcludedRegNames),
do_suspend_pids = edb_server_sets:union(DoSuspend0, ExceptedFromExclude)
},
Universe = erlang:processes(),
State4 =
case State1#state.suspended_procs of
SuspendedProcs when map_size(SuspendedProcs) =:= 0 ->
State1;
_ ->
PidsExcludedByRegName = excluded_processes_by_regname(ExcludedRegNames, State1),
PidsExcludedByApp = excluded_processes_by_app(ExcludedApps, Universe, State1),
NewlyExcludedProcs = edb_server_sets:union([
ExcludedByPid, PidsExcludedByRegName, PidsExcludedByApp
]),
{ok, _ActuallyResumed, State2} = resume_processes(NewlyExcludedProcs, excluded, State1),
UnsuspendablePids = get_excluded_processes(Universe, State2),
{ok, State3} = suspend_all_processes(Universe, UnsuspendablePids, State2),
State3
end,
{reply, ok, State4}.
-spec unexclude_processes_impl(Specs, State0) -> {reply, ok, State1} when
State0 :: state(),
State1 :: state(),
Specs :: [procs_spec()].
unexclude_processes_impl(Specs, State0) ->
UnexcludedByPid = #{P => [] || {proc, P} <- Specs, is_pid(P), is_relevant_pid(P)},
UnexcludedRegNames = #{Name => [] || {proc, Name} <- Specs, is_atom(Name)},
UnexcludedApps = #{App => [] || {application, App} <- Specs},
UnexceptedFromExclude = #{P => [] || {except, P} <- Specs, is_relevant_pid(P)},
#state{
do_not_suspend_apps = DoNotSuspendApps0,
do_not_suspend_pids = DoNotSuspend0,
do_not_suspend_regnames = DoNotSuspendRegNames0,
do_suspend_pids = DoSuspend0
} = State0,
State1 = State0#state{
do_not_suspend_apps = edb_server_sets:subtract(DoNotSuspendApps0, UnexcludedApps),
do_not_suspend_pids = edb_server_sets:subtract(DoNotSuspend0, UnexcludedByPid),
do_not_suspend_regnames = edb_server_sets:subtract(DoNotSuspendRegNames0, UnexcludedRegNames),
do_suspend_pids = edb_server_sets:subtract(DoSuspend0, UnexceptedFromExclude)
},
Universe = erlang:processes(),
State4 =
case State1#state.suspended_procs of
SuspendedProcs when map_size(SuspendedProcs) =:= 0 ->
State1;
_ ->
UnsuspendablePids = get_excluded_processes(Universe, State1),
NewlyExcludedProcs = edb_server_sets:intersection(UnexceptedFromExclude, UnsuspendablePids),
{ok, _ActuallyResumed, State2} = resume_processes(NewlyExcludedProcs, excluded, State1),
{ok, State3} = suspend_all_processes(Universe, UnsuspendablePids, State2),
State3
end,
{reply, ok, State4}.
-spec stack_frames_impl(Pid, State0) -> {reply, Response, State1} when
Pid :: pid(),
State0 :: state(),
State1 :: state(),
Response :: not_paused | {ok, [edb:stack_frame()]}.
stack_frames_impl(Pid, State0) ->
case edb_server_stack_frames:raw_user_stack_frames(Pid) of
not_paused ->
{reply, not_paused, State0};
RawFrames ->
#state{breakpoints = Breakpoints0} = State0,
RelevantFrames = edb_server_stack_frames:without_bottom_terminator_frame(RawFrames),
Result = [format_frame(RawFrame, Breakpoints0) || RawFrame <- RelevantFrames],
{reply, {ok, Result}, State0}
end.
-spec format_frame(RawFrame :: erl_debugger:stack_frame(), edb_server_break:breakpoints()) -> edb:stack_frame().
format_frame({FrameNo, 'unknown function', _}, _) ->
#{
id => FrameNo,
mfa => unknown,
source => undefined,
line => undefined
};
format_frame({FrameNo, #{function := {M, F, A}, line := Line}, _}, Breakpoints) ->
VmModule = {vm_module, M},
OriginalModule = edb_server_break:from_vm_module(VmModule, Breakpoints),
#{
id => FrameNo,
mfa => {OriginalModule, F, A},
% TODO(T204197553) take md5 sum into account once it is available in the raw frame
source => edb_server_break:get_vm_module_source(VmModule, Breakpoints),
line => Line
}.
-spec stack_frame_vars_impl(Pid, FrameId, MaxTermSize, State0) -> {reply, Response, State1} when
Pid :: pid(),
FrameId :: edb:frame_id(),
MaxTermSize :: non_neg_integer(),
Result :: edb:stack_frame_vars(),
Response :: not_paused | undefined | {ok, Result},
State0 :: state(),
State1 :: state().
stack_frame_vars_impl(Pid, FrameId, MaxTermSize, State0) ->
Result = get_stack_frame_vars(Pid, FrameId, MaxTermSize, State0),
{reply, Result, State0}.
-spec get_stack_frame_vars(Pid, FrameId, MaxTermSize, State) -> not_paused | undefined | {ok, Result} when
Pid :: pid(),
FrameId :: edb:frame_id(),
MaxTermSize :: non_neg_integer(),
Result :: edb:stack_frame_vars(),
State :: state().
get_stack_frame_vars(Pid, FrameId, MaxTermSize, State) ->
case edb_server_stack_frames:raw_stack_frames(Pid) of
not_paused ->
not_paused;
RawFrames ->
ResolveLocalVars =
case edb_server_stack_frames:user_frames_only(RawFrames) of
[{TopFrameId, _, _} | _] when FrameId =:= TopFrameId ->
edb_server_break:is_process_trapped(Pid, State#state.breakpoints);
_ ->
false
end,
edb_server_stack_frames:stack_frame_vars(Pid, FrameId, MaxTermSize, RawFrames, #{
resolve_local_vars => ResolveLocalVars
})
end.
-spec eval_impl(Opts, CallerPid, State0) -> {reply, Response, State1} when
Opts :: eval_opts(Result),
CallerPid :: pid(),
Response :: not_paused | undefined | {ok, Result} | {eval_error, edb:eval_error()},
State0 :: state(),
State1 :: state().
eval_impl(Opts, CallerPid, State0) ->
#{pid := Pid, frame_id := FrameId, max_term_size := MaxTermSize, dependencies := Deps} = Opts,
Result =
case get_stack_frame_vars(Pid, FrameId, MaxTermSize, State0) of
{ok, StackFrameVars} ->
#{function := F, timeout := Timeout} = Opts,
case edb_server_eval:eval(F, StackFrameVars, node(CallerPid), Timeout, Deps) of
{failed_to_load_module, _, _} = LoadFailure ->
?MODULE:raise(error, LoadFailure);
EvalResult ->
EvalResult
end;
NotOk ->
NotOk
end,
{reply, Result, State0}.
%%--------------------------------------------------------------------
%% State helpers
%%--------------------------------------------------------------------
-spec is_paused(State) -> boolean() when State :: state().
is_paused(State) ->
#state{suspended_procs = Procs} = State,
HasAPausedProcess = maps:size(Procs) =/= 0,
HasAPausedProcess.
-spec get_breakpoints(State0) -> #{edb_server_break:vm_module() => #{line() => []}} when State0 :: state().
get_breakpoints(State0) ->
#state{breakpoints = Breakpoints} = State0,
edb_server_break:get_explicits(Breakpoints).
-spec resume_processes(Targets, Reason, State0) -> {ok, ActuallyResumed, State1} when
Targets :: set(pid()) | all,
Reason :: continue | excluded | termination,
ActuallyResumed :: set(pid()),
State0 :: state(),
State1 :: state().
resume_processes(Targets, Reason, State0) ->
#state{
suspended_procs = Suspended0,
breakpoints = BP0
} = State0,
{ToResume, Suspended1, BP1} =
case Targets of
all ->
{Suspended0, #{}, edb_server_break:resume_processes(all, BP0)};
Requested when is_map(Requested) ->
NeedToBeResumed = edb_server_sets:intersection(Requested, Suspended0),
RemainingSuspended = edb_server_sets:subtract(Suspended0, NeedToBeResumed),
RemainingBP = edb_server_break:resume_processes(NeedToBeResumed, BP0),
{NeedToBeResumed, RemainingSuspended, RemainingBP}
end,
ActuallyResumed = #{Pid => [] || Pid := [] <- ToResume, edb_server_process:try_resume_process(Pid)},
State1 = State0#state{
suspended_procs = Suspended1,
breakpoints = BP1
},
Subs = State1#state.event_subscribers,
case Targets of
_ when map_size(ActuallyResumed) =:= 0 ->
ok;
all when Reason =:= continue; Reason =:= termination ->
ok = edb_events:broadcast({resumed, {Reason, all}}, Subs);
_ when Reason =:= excluded ->
ok = edb_events:broadcast({resumed, {Reason, ActuallyResumed}}, Subs)
end,
{ok, ActuallyResumed, State1}.
-spec suspend_all_processes(Universe, Unsuspendable, State0) -> {ok, State1} when
Universe :: [pid()],
Unsuspendable :: set(pid()),
State0 :: state(),
State1 :: state().
suspend_all_processes(Universe, Unsuspendable, State0) ->
#state{suspended_procs = AlreadySuspended} = State0,
MustIgnore = fun(Pid) ->
edb_server_sets:is_element(Pid, AlreadySuspended) orelse edb_server_sets:is_element(Pid, Unsuspendable)
end,
JustSuspended =
#{
Pid => []
|| Pid <- Universe,
not MustIgnore(Pid),
edb_server_process:try_suspend_process(Pid)
},
AllSuspended = maps:merge(AlreadySuspended, JustSuspended),
State1 = State0#state{suspended_procs = AllSuspended},
{ok, State1}.
-spec get_excluded_processes(Universe, State) -> #{pid() => [edb:exclusion_reason()]} when
Universe :: [pid()],
State :: state().
get_excluded_processes(Universe, State) ->
#state{do_not_suspend_pids = DoNotSuspendPids} = State,
ExcludedSysProcs = edb_server_sets:to_map(excluded_sys_processes(Universe, State), [system_component]),
ExcludedByDebugger = edb_server_sets:to_map(excluded_debugger_processes(State), [debugger_component]),
ExcludedByPid = edb_server_sets:to_map(DoNotSuspendPids, [excluded_pid]),
ExcludedByName = edb_server_sets:to_map(excluded_processes_by_regname(State), [excluded_regname]),
ExcludedByApps = edb_server_sets:to_map(excluded_processes_by_app(Universe, State), [excluded_application]),
ExcludedCombined = lists:foldl(
fun(MapL, MapR) ->
maps:merge_with(fun(_, L, R) -> lists:merge(L, R) end, MapL, MapR)
end,
#{},
[ExcludedSysProcs, ExcludedByDebugger, ExcludedByPid, ExcludedByName, ExcludedByApps]
),
ExcludedCombined.
-spec process_status(Pid, State) -> running | paused | {breakpoint, edb:breakpoint_info()} when
Pid :: pid(),
State :: state().
process_status(Pid, State) ->
#state{suspended_procs = SuspendedProcs} = State,
case is_map_key(Pid, SuspendedProcs) of
false ->
running;
true ->
#state{breakpoints = BP} = State,
case edb_server_break:get_explicit_hit(Pid, BP) of
no_breakpoint_hit ->
paused;
{ok, #{module := VmModule, line := Line}} ->
{breakpoint, #{module => from_vm_module(VmModule, State), line => Line}}
end
end.
%%--------------------------------------------------------------------
%% Process helpers
%%--------------------------------------------------------------------
-spec is_relevant_pid(Pid :: pid()) -> boolean().
is_relevant_pid(Pid) ->
?is_internal_pid(Pid) andalso is_process_alive(Pid).