Current section

Files

Jump to
edb_core src edb_server_break.erl
Raw

src/edb_server_break.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_break).
%% erlfmt:ignore
% @fb-only
-compile(warn_missing_spec_all).
-moduledoc false.
% Creation
-export([create/0]).
% Explicit brekapoints manipulation
-export([add_explicit/3, add_explicits/3]).
-export([get_explicits/1, get_explicits/2]).
-export([clear_explicit/3, clear_explicits/2]).
-export([get_explicits_hit/1, get_explicit_hit/2]).
-export([reapply_breakpoints/2]).
% Stepping
-export([prepare_for_stepping/3]).
-export([prepare_for_stepping_in/2]).
% Execution control
-export([is_process_trapped/2]).
-export([register_breakpoint_event/5]).
-export([resume_processes/2]).
% Module substitutes
-export([add_module_substitute/4, remove_module_substitute/2]).
-export([to_vm_module/2, from_vm_module/2]).
-export([get_vm_module_source/2]).
%% --------------------------------------------------------------------
%% Types
%% --------------------------------------------------------------------
-type line() :: edb:line().
-type vm_module() :: {vm_module, module()}.
-type substitute_info() :: #{
substitute => vm_module(),
added_frames => [mfa()],
original_sources => file:filename() | undefined
}.
-type further_substitute_info() :: #{
substitute => vm_module(),
added_frames => [mfa()]
}.
-export_type([breakpoints/0, vm_module/0]).
-record(breakpoints, {
%% TODO(T198738599): we should somehow keep track of the identity
%% of the module instance on which the breakpoint
%% was set? The general problem is that right now when
%% a module is reloaded, breakpoints remain on the old
%% version of the code (until purged) and will not exist
%% on the new version (lines could have changed, so seems
%% right). We need some way to be aware/handle this.
%% Explicit breakpoints requested by the client, grouped by module
explicits :: #{vm_module() => #{line() => []}},
%% Internal breakpoints set by the server to step through, grouped by pid.
%% For each such breakpoint, there are only certain call-stacks under which
%% we want to suspend the process.
steps :: #{pid() => #{vm_module() => #{line() => #{call_stack_pattern() => []}}}},
%% Explicit breakpoints that have been hit
explicits_hit :: #{pid() => {vm_module(), line()}},
%% Callbacks to resume processes that hit a VM breakpoint
resume_actions :: #{pid() => fun(() -> ok)},
%% Sets of reasons why VM breakpoints were set on each locations
vm_breakpoints :: #{vm_module() => #{line() => #{vm_breakpoint_reason() => []}}},
substituted_modules :: #{module() => substitute_info()},
further_substituted_modules :: #{vm_module() => further_substitute_info()},
% The inverse of `substituted_modules` and `further_substituted_modules`
substituted_modules_reverse :: #{vm_module() => module() | vm_module()}
}).
-type vm_breakpoint_reason() ::
explicit
| {step, pid()}.
-opaque breakpoints() :: #breakpoints{}.
%% --------------------------------------------------------------------
%% Creation
%% --------------------------------------------------------------------
-spec create() -> breakpoints().
create() ->
#breakpoints{
explicits = #{},
steps = #{},
explicits_hit = #{},
resume_actions = #{},
vm_breakpoints = #{},
substituted_modules = #{},
further_substituted_modules = #{},
substituted_modules_reverse = #{}
}.
%% --------------------------------------------------------------------
%% Explicit breakpoints manipulation
%% --------------------------------------------------------------------
-spec add_explicit(vm_module(), line(), breakpoints()) -> {ok, breakpoints()} | {error, edb:add_breakpoint_error()}.
add_explicit({vm_module, Module} = VmModule, Line, Breakpoints0) ->
case code:ensure_loaded(Module) of
{module, Module} ->
case add_vm_breakpoint(VmModule, Line, explicit, Breakpoints0) of
{ok, Breakpoints1} ->
#breakpoints{explicits = Explicits1} = Breakpoints1,
Explicits2 = edb_server_maps:add(VmModule, Line, [], Explicits1),
Breakpoints2 = Breakpoints1#breakpoints{explicits = Explicits2},
{ok, Breakpoints2};
{error, Reason} ->
{error, Reason}
end;
{error, _} ->
{error, {badkey, Module}}
end.
-spec add_explicits(vm_module(), [line()], breakpoints()) -> {LineResults, breakpoints()} when
LineResults :: [{line(), Result}],
Result :: ok | {error, edb:add_breakpoint_error()}.
add_explicits(Module, Lines, Breakpoints0) ->
lists:mapfoldl(
fun(Line, AccBreakpointsIn) ->
case add_explicit(Module, Line, AccBreakpointsIn) of
{ok, AccBreakpointsOut} -> {{Line, ok}, AccBreakpointsOut};
{error, Error} -> {{Line, {error, Error}}, AccBreakpointsIn}
end
end,
Breakpoints0,
Lines
).
-spec get_explicits(breakpoints()) -> #{vm_module() => #{line() => []}}.
get_explicits(#breakpoints{explicits = Explicits}) ->
Explicits.
-spec get_explicits(vm_module(), breakpoints()) -> #{line() => []}.
get_explicits(Module, #breakpoints{explicits = Explicits}) ->
maps:get(Module, Explicits, #{}).
-spec clear_explicit(vm_module(), line(), breakpoints()) ->
{ok, removed | vanished, breakpoints()} | {error, not_found}.
clear_explicit(Module, Line, Breakpoints0) ->
case unregister_explicit(Module, Line, Breakpoints0) of
{found, Breakpoints1} ->
%% We knew about this breakpoint. Now remove it from the VM.
case remove_vm_breakpoint(Module, Line, explicit, Breakpoints1) of
{ok, DeletionResult, Breakpoints2} ->
{ok, DeletionResult, Breakpoints2};
{error, unknown_vm_breakpoint} ->
% Breakpoint was registered as explicit but not as a reason?
% This should never happen, if it does we have a bug.
edb_server:invariant_violation(inconsistent_explicit_breakpoint)
end;
not_found ->
%% We didn't know about this breakpoint, trying to clear it is a user error.
{error, not_found}
end.
-spec clear_explicits(vm_module(), breakpoints()) -> {ok, breakpoints()}.
clear_explicits(Module, Breakpoints0) ->
Lines = maps:keys(get_explicits(Module, Breakpoints0)),
Breakpoints1 = lists:foldl(
fun(Line, AccBreakpointsIn) ->
case clear_explicit(Module, Line, AccBreakpointsIn) of
{ok, _RemovedOrVanished, AccBreakpointsOut} -> AccBreakpointsOut;
% A breakpoint line taken from the list cannot be not_found
{error, not_found} -> edb_server:invariant_violation(unexpected_bp_not_found)
end
end,
Breakpoints0,
Lines
),
{ok, Breakpoints1}.
-spec unregister_explicit(vm_module(), line(), breakpoints()) -> {found, breakpoints()} | not_found.
unregister_explicit(Module, Line, Breakpoints0) ->
#breakpoints{explicits = Explicits0} = Breakpoints0,
BreakpointInfos0 = maps:get(Module, Explicits0, #{}),
case edb_server_sets:take_element(Line, BreakpointInfos0) of
not_found ->
not_found;
{found, BreakpointInfos1} ->
Explicits1 =
case edb_server_sets:is_empty(BreakpointInfos1) of
true -> maps:remove(Module, Explicits0);
false -> Explicits0#{Module := BreakpointInfos1}
end,
Breakpoints1 = Breakpoints0#breakpoints{explicits = Explicits1},
{found, Breakpoints1}
end.
-spec get_explicits_hit(breakpoints()) -> #{pid() => #{module := vm_module(), line := line()}}.
get_explicits_hit(#breakpoints{explicits_hit = ExplicitsHit}) ->
maps:map(
fun(_Pid, {Module, Line}) -> #{module => Module, line => Line} end,
ExplicitsHit
).
-spec get_explicit_hit(pid(), breakpoints()) ->
{ok, #{module := vm_module(), line := line()}} | no_breakpoint_hit.
get_explicit_hit(Pid, #breakpoints{explicits_hit = ExplicitsHit}) ->
case ExplicitsHit of
#{Pid := {Module, Line}} ->
{ok, #{module => Module, line => Line}};
#{} ->
no_breakpoint_hit
end.
%% --------------------------------------------------------------------
%% Stepping
%% --------------------------------------------------------------------
-spec prepare_for_stepping(StepType, Pid, breakpoints()) -> {ok, breakpoints()} | {error, Error} when
StepType :: step_over | step_out,
Pid :: pid(),
Error :: edb:step_error().
prepare_for_stepping(StepType, Pid, Breakpoints0) ->
case edb_server_stack_frames:raw_user_stack_frames(Pid) of
not_paused ->
{error, not_paused};
StackFrames when is_list(StackFrames) ->
case StepType of
step_over ->
RelevantFrames = StackFrames,
Types = [on_any_required, on_any | [on_exc_handler || _ <- tl(tl(RelevantFrames))]],
Addrs = call_stack_addrs(RelevantFrames),
add_steps_on_stack_frames(Pid, RelevantFrames, Addrs, Types, Breakpoints0);
step_out ->
RelevantFrames = tl(StackFrames),
Types = [on_any_required | [on_exc_handler || _ <- tl(RelevantFrames)]],
Addrs = call_stack_addrs(RelevantFrames),
add_steps_on_stack_frames(Pid, RelevantFrames, Addrs, Types, Breakpoints0)
end
end.
-spec prepare_for_stepping_in(Pid, breakpoints()) -> {ok, breakpoints()} | {error, Error} when
Pid :: pid(),
Error :: edb:step_in_error().
prepare_for_stepping_in(Pid, Breakpoints0) ->
case edb_server_stack_frames:raw_user_stack_frames(Pid) of
not_paused ->
{error, not_paused};
[TopFrame | _] = StackFrames ->
case get_targets_for_step_in(TopFrame) of
{error, _} = Error ->
Error;
{ok, TargetMFAs} ->
{_, #{function := CurrentMFA}, _} = TopFrame,
Addrs = call_stack_addrs(StackFrames),
AddStepsOnStepInTargetsResult = lists:foldl(
fun
(_TargetMFA, {{error, _}, _BreakpointsN} = Error) ->
Error;
(TargetMFA, {ok, BreakpointsN}) ->
case add_steps_on_step_in_target(Pid, CurrentMFA, TargetMFA, Addrs, BreakpointsN) of
{error, {cannot_breakpoint, erlang} = Error} ->
case lists:search(fun({M, _, _}) -> M /= erlang end, TargetMFAs) of
{value, _} ->
% Corner-case: the compiler may inject implicit calls to functions in the erlang
% module, like `erlang:'!'/2` or `erlang:error/1` and it may be that OTP is not
% built with +beam_debug_info. Let's just ignore those failures
{ok, BreakpointsN};
false ->
Error
end;
Error = {error, _} ->
{Error, BreakpointsN};
OkResult ->
OkResult
end
end,
{ok, Breakpoints0},
TargetMFAs
),
case AddStepsOnStepInTargetsResult of
{Error = {error, _}, Breakpoints1} ->
_Breakpoints2 = clear_steps(Pid, Breakpoints1),
Error;
{ok, Breakpoints1} ->
RelevantFrames = StackFrames,
Types = [on_exc_handler || _ <- RelevantFrames],
add_steps_on_stack_frames(Pid, RelevantFrames, Addrs, Types, Breakpoints1)
end
end
end.
-spec add_steps_on_step_in_target(Pid, CurrentMFA, TargetMFA, Addrs, breakpoints()) ->
{ok, breakpoints()} | {error, edb:step_in_error()}
when
Pid :: pid(),
CurrentMFA :: mfa(),
TargetMFA :: mfa(),
Addrs :: call_stack_addrs().
add_steps_on_step_in_target(Pid, CurrentMFA, TargetMFA, Addrs, Breakpoints0) ->
{Mod, Fun, Arity} = TargetMFA,
case code:ensure_loaded(Mod) of
{error, _} ->
{error, {call_target, {module_not_found, Mod}}};
{module, Mod} ->
case erl_debugger:breakpoints(Mod, Fun, Arity) of
{error, {badkey, Mod}} ->
% Mod deleted concurrently?
{error, {cannot_breakpoint, Mod}};
{error, {badkey, {Fun, Arity}}} ->
{error, {call_target, {function_not_found, TargetMFA}}};
{ok, _} ->
ExtraFrames = maybe_substituted_module_extra_frames(Mod, Breakpoints0),
BasePatterns = [
% Base pattern for non-tail-calls
ExtraFrames ++ [CurrentMFA | tl(Addrs)],
% Base pattern for tail-calls
ExtraFrames ++ tl(Addrs)
],
case add_steps_on_function(Pid, TargetMFA, BasePatterns, Breakpoints0) of
no_breakpoint_set ->
{error, {cannot_breakpoint, Mod}};
Result ->
Result
end
end
end.
-spec maybe_substituted_module_extra_frames(module(), breakpoints()) -> [mfa()].
maybe_substituted_module_extra_frames(Mod, Breakpoints) ->
#breakpoints{substituted_modules = SubstitutedModules} = Breakpoints,
case SubstitutedModules of
#{Mod := #{added_frames := Frames, substitute := SubstituteMod}} ->
get_further_frames_to_add(SubstituteMod, Breakpoints, [Frames]);
#{} ->
get_further_frames_to_add({vm_module, Mod}, Breakpoints, [])
end.
-spec get_further_frames_to_add(vm_module(), breakpoints(), [[mfa()]]) -> [mfa()].
get_further_frames_to_add(SubstituteMod, Breakpoints, Acc) ->
#breakpoints{further_substituted_modules = FurtherSubstitutes} = Breakpoints,
case FurtherSubstitutes of
#{SubstituteMod := #{added_frames := Frames, substitute := FurtherSubstituteMod}} ->
get_further_frames_to_add(FurtherSubstituteMod, Breakpoints, [Frames | Acc]);
#{} ->
lists:flatten(Acc)
end.
-spec add_steps_on_stack_frames(Pid, Frames, FrameAddrs, Types, breakpoints()) ->
{ok, breakpoints()} | {error, Error}
when
Pid :: pid(),
FrameAddrs :: call_stack_addrs(),
Frames :: [erl_debugger:stack_frame()],
Types :: [on_any_required | on_any | on_exc_handler],
Error :: {cannot_breakpoint, module()}.
add_steps_on_stack_frames(
Pid, [TopFrame | MoreFrames], FrameAddrs = [_ | MoreFrameAddrs], [Type | MoreTypes], Breakpoints0
) ->
case add_steps_on_stack_frame(Pid, TopFrame, FrameAddrs, Type, Breakpoints0) of
{ok, Breakpoints1} ->
add_steps_on_stack_frames(Pid, MoreFrames, MoreFrameAddrs, MoreTypes, Breakpoints1);
no_breakpoint_set when Type =:= on_any_required ->
%% We cannot claim success if no breakpoints were added
{_, #{function := {Module, _, _}}, _} = TopFrame,
{error, {cannot_breakpoint, Module}};
skipped when Type =:= on_any_required ->
edb_server:invariant_violation(stepping_from_unbreakable_frame);
Failure when Failure =:= skipped; Failure =:= no_breakpoint_set ->
add_steps_on_stack_frames(Pid, MoreFrames, MoreFrameAddrs, MoreTypes, Breakpoints0)
end;
add_steps_on_stack_frames(_Pid, [], [], [], Breakpoints) ->
{ok, Breakpoints}.
-spec add_steps_on_stack_frame(Pid, TopFrame, FrameAddrs, Type, breakpoints()) ->
{ok, breakpoints()} | no_breakpoint_set | skipped
when
Pid :: pid(),
TopFrame :: erl_debugger:stack_frame(),
FrameAddrs :: call_stack_addrs(),
Type :: on_any_required | on_any | on_exc_handler.
add_steps_on_stack_frame(Pid, Frame = {_, #{function := MFA, line := Line}, _}, FrameAddrs, Type, Breakpoints) when
is_tuple(MFA), is_integer(Line)
->
ShouldSkip = Type =:= on_exc_handler andalso not edb_server_stack_frames:has_exception_handler(Frame),
case ShouldSkip of
true ->
skipped;
false ->
BasePatterns = [tl(FrameAddrs)],
add_steps_on_function(Pid, MFA, BasePatterns, Breakpoints)
end;
add_steps_on_stack_frame(_Pid, _Addrs, _TopFrame, _Type, _Breakpoints) ->
% no line-number information or not a user function
skipped.
-spec add_steps_on_function(Pid, MFA, BasePatterns, breakpoints()) ->
{ok, breakpoints()} | no_breakpoint_set
when
Pid :: pid(),
MFA :: mfa(),
BasePatterns :: [call_stack_pattern()].
add_steps_on_function(Pid, MFA, BasePatterns, Breakpoints0) ->
{Module, Fun, Arity} = MFA,
{vm_module, VmModule} = to_vm_module(Module, Breakpoints0),
case erl_debugger:breakpoints(VmModule, Fun, Arity) of
{error, {badkey, _}} ->
no_breakpoint_set;
{ok, BreakableLines} ->
Patterns = [[{VmModule, Fun, Arity} | BasePattern] || BasePattern <- BasePatterns],
{Breakpoints1, SomeBreakpointSet} = maps:fold(
fun(Line1, _, Acc0 = {AccBreakpoints0, _}) ->
case add_step(Pid, Patterns, {vm_module, VmModule}, Line1, AccBreakpoints0) of
no_breakpoint_set ->
Acc0;
{ok, AccBreakpoints1} ->
{AccBreakpoints1, true}
end
end,
{Breakpoints0, false},
BreakableLines
),
case SomeBreakpointSet of
true -> {ok, Breakpoints1};
false -> no_breakpoint_set
end
end.
-spec add_step(Pid, Patterns, Module, Line, breakpoints()) -> {ok, breakpoints()} | no_breakpoint_set when
Pid :: pid(),
Patterns :: [call_stack_pattern()],
Module :: vm_module(),
Line :: line().
add_step(Pid, Patterns, Module, Line, Breakpoints0) ->
%% TODO(T233850146): Corner case - Sometimes we want to add a stepping breakpoint to the old (non-substitute)
%% version of a module. In this case, the semantics of Erlang will call fun() in the old version of
%% the module, and the breakpoint should be set in the old version, so we need to add a way to
%% do this in the VM.
case add_vm_breakpoint(Module, Line, {step, Pid}, Breakpoints0) of
{ok, Breakpoints1} ->
#breakpoints{steps = Steps1} = Breakpoints1,
Steps2 = lists:foldl(
fun(Pattern, StepsN) ->
edb_server_maps:add(Pid, Module, Line, Pattern, [], StepsN)
end,
Steps1,
Patterns
),
Breakpoints2 = Breakpoints1#breakpoints{steps = Steps2},
{ok, Breakpoints2};
{error, _} ->
% This is not a line where we can set a breakpoint.
no_breakpoint_set
end.
-spec get_targets_for_step_in(TopFrame) -> {ok, nonempty_list(mfa())} | {error, edb:step_in_error()} when
TopFrame :: erl_debugger:stack_frame().
get_targets_for_step_in({_, #{function := {M, _, _}, line := Line}, _}) when is_integer(Line) ->
case edb_server_code:get_debug_info(M, Line) of
{error, not_found} ->
{error, {call_target, {module_not_found, M}}};
{error, line_not_found} ->
{error, {call_target, not_found}};
{error, no_debug_info} ->
{error, {cannot_breakpoint, M}};
{ok, #{calls := Calls0}} ->
Calls1 = lists:filtermap(fun(Target) -> try_resolve_mfa({vm_module, M}, Target) end, Calls0),
case Calls1 of
[] -> {error, {call_target, not_found}};
_ -> {ok, Calls1}
end
end;
get_targets_for_step_in(_TopFrame) ->
edb_server:invariant_violation(stepping_from_unbreakable_frame).
-spec try_resolve_mfa(Mod, Target) -> false | {true, mfa()} when
Mod :: vm_module(),
Target :: edb_server_code:call_target().
try_resolve_mfa(_, MFA = {M, F, _}) when is_atom(M), is_atom(F) ->
{true, MFA};
try_resolve_mfa({vm_module, M}, {F, A}) when is_atom(F) ->
{true, {M, F, A}};
try_resolve_mfa(_, _) ->
false.
%% --------------------------------------------------------------------
%% Execution control
%% --------------------------------------------------------------------
-doc """
Returns true if the given process is either on an explicit breakpoint or a step breakpoint.
Equivalently, returns true if the given process is on a VM breakpoint.
""".
-spec is_process_trapped(Pid, Breakpoints) -> boolean() when
Pid :: pid(),
Breakpoints :: breakpoints().
is_process_trapped(Pid, Breakpoints) ->
#breakpoints{resume_actions = ResumeActions} = Breakpoints,
maps:is_key(Pid, ResumeActions).
-spec register_breakpoint_event(Module, Line, Pid, Resume, Breakpoints) ->
{suspend, explicit | step, breakpoints()} | resume
when
Breakpoints :: breakpoints(),
Module :: vm_module(),
Line :: integer(),
Pid :: pid(),
Resume :: fun(() -> ok).
register_breakpoint_event(Module, Line, Pid, Resume, Breakpoints0) ->
case should_be_suspended(Module, Line, Pid, Breakpoints0) of
{true, Reason} ->
%% Relevant breakpoint hit. Register it, clear steps in both cases and suspend.
Breakpoints1 = register_resume_action(Pid, Resume, Breakpoints0),
Breakpoints2 =
case Reason of
step ->
Breakpoints1;
explicit ->
register_explicit_hit(Module, Line, Pid, Breakpoints1)
end,
{ok, Breakpoints3} = clear_steps(Pid, Breakpoints2),
{suspend, Reason, Breakpoints3};
false ->
resume
end.
-spec should_be_suspended(vm_module(), line(), pid(), breakpoints()) -> {true, explicit | step} | false.
should_be_suspended(Module, Line, Pid, Breakpoints) ->
#breakpoints{explicits = Explicits, steps = Steps} = Breakpoints,
case Explicits of
#{Module := #{Line := []}} ->
%% This is an explicit breakpoint
{true, explicit};
_ ->
case Steps of
#{Pid := #{Module := #{Line := Patterns}}} ->
% We need stack-frames, and these require the process to be suspended
case edb_server_process:try_suspend_process(Pid) of
true ->
case edb_server_stack_frames:raw_user_stack_frames(Pid) of
not_paused ->
edb_server:invariant_violation(not_paused_right_after_suspending);
StackFrames when is_list(StackFrames) ->
ShouldSuspend = lists:any(
fun(Pattern) -> call_stack_matches(Pattern, StackFrames) end,
maps:keys(Patterns)
),
case ShouldSuspend of
true ->
%% This is a step breakpoint for the current process.
%% Caller expects this process to be suspended, in this case
{true, step};
_ ->
edb_server_process:try_resume_process(Pid),
false
end
end;
false ->
% Process was concurrently killed, etc
false
end;
_ ->
false
end
end.
-spec register_resume_action(Pid, Resume, Breakpoints) -> Breakpoints when
Breakpoints :: breakpoints(),
Pid :: pid(),
Resume :: fun(() -> ok).
register_resume_action(Pid, Resume, Breakpoints) ->
#breakpoints{resume_actions = ResumeActions} = Breakpoints,
ResumeActions1 = ResumeActions#{Pid => Resume},
Breakpoints#breakpoints{resume_actions = ResumeActions1}.
-spec register_explicit_hit(Module, Line, Pid, Breakpoints) -> breakpoints() when
Breakpoints :: breakpoints(),
Module :: vm_module(),
Line :: integer(),
Pid :: pid().
register_explicit_hit(Module, Line, Pid, Breakpoints) ->
#breakpoints{explicits_hit = ExplicitsHit} = Breakpoints,
NewBPHit = {Module, Line},
ExplicitsHit1 = ExplicitsHit#{Pid => NewBPHit},
Breakpoints#breakpoints{explicits_hit = ExplicitsHit1}.
-spec clear_steps(pid(), breakpoints()) -> {ok, breakpoints()}.
clear_steps(Pid, Breakpoints) ->
#breakpoints{steps = Steps} = Breakpoints,
%% Implementation note. At the time of first writing this, maps:take introduces spurious
%% dynamic() types that are worked around by the sequence of maps:get and maps:remove.
PidSteps = maps:get(Pid, Steps, #{}),
ModuleLines = [{Module, Line} || Module := LinePatterns <- PidSteps, Line := _ <- LinePatterns],
Steps1 = maps:remove(Pid, Steps),
Breakpoints1 = Breakpoints#breakpoints{steps = Steps1},
Breakpoints2 = lists:foldl(
fun({Module, Line}, Accu) ->
try_clear_step_in_vm(Pid, Module, Line, Accu)
end,
Breakpoints1,
ModuleLines
),
{ok, Breakpoints2}.
%% Try to clear one step breakpoint in the VM. If error happens, do nothing.
-spec try_clear_step_in_vm(pid(), vm_module(), line(), breakpoints()) -> breakpoints().
try_clear_step_in_vm(Pid, Module, Line, Breakpoints0) ->
case remove_vm_breakpoint(Module, Line, {step, Pid}, Breakpoints0) of
{ok, _, Breakpoints1} -> Breakpoints1;
{error, unknown_vm_breakpoint} -> Breakpoints0
end.
-spec resume_processes(all | edb_server_sets:set(pid()), breakpoints()) -> breakpoints().
resume_processes(ToResume, Breakpoints) ->
#breakpoints{explicits_hit = ExplicitsHit, resume_actions = ResumeActions} = Breakpoints,
{ExplicitsHit1, ResumeActions1} =
case ToResume of
all ->
[
ResumeProcThatHitBP()
|| _ := ResumeProcThatHitBP <- ResumeActions
],
{#{}, #{}};
_ ->
[
ResumeProcThatHitBP()
|| ProcThatHitBP := ResumeProcThatHitBP <- ResumeActions,
edb_server_sets:is_element(ProcThatHitBP, ToResume)
],
{
edb_server_sets:map_subtract_keys(ExplicitsHit, ToResume),
edb_server_sets:map_subtract_keys(ResumeActions, ToResume)
}
end,
Breakpoints#breakpoints{explicits_hit = ExplicitsHit1, resume_actions = ResumeActions1}.
%% --------------------------------------------------------------------
%% Helpers -- VM breakpoints
%% --------------------------------------------------------------------
% erlfmt:ignore-begin
-define(unbreakpointable_modules,
% When stepping on processes that have these modules on their call-stack,
% the overhead of having breakpoints on these is too high as too many processes
% use them. Once we add native VM support for conditional breakpoints and the overhead
% becomes low, we can remove these (T220510085)
#{
artillery_tracer => true, % fb-only
gen_factory => true, % fb-only
wa_request_context => true, % fb-only
gen_server => true,
gen_statem => true
}
).
% erlfmt:ignore-end
-spec add_vm_breakpoint(Module, Line, Reason, Breakpoints0) ->
{ok, Breakpoints1} | {error, edb:add_breakpoint_error()}
when
Module :: vm_module(),
Line :: line(),
Reason :: vm_breakpoint_reason(),
Breakpoints0 :: breakpoints(),
Breakpoints1 :: breakpoints().
add_vm_breakpoint({vm_module, Module}, _, _, _) when map_get(Module, ?unbreakpointable_modules) ->
{error, {unsupported, Module}};
add_vm_breakpoint(Module, Line, Reason, Breakpoints0) ->
%% Register the new breakpoint reason at this location
#breakpoints{vm_breakpoints = VmBreakpoints0} = Breakpoints0,
VmBreakpoints1 = edb_server_maps:add(Module, Line, Reason, [], VmBreakpoints0),
Breakpoints1 = Breakpoints0#breakpoints{vm_breakpoints = VmBreakpoints1},
%% Set the VM breakpoint.
%% We do this regardless of whether it was already set at this location
%% because the module could have been reloaded in the meantime.
case vm_set_breakpoint(Module, Line) of
ok ->
{ok, Breakpoints1};
{error, Error} ->
{error, Error}
end.
-spec remove_vm_breakpoint(Module, Line, Reason, Breakpoints0) ->
{ok, removed | vanished, Breakpoints1} | {error, unknown_vm_breakpoint}
when
Module :: vm_module(),
Line :: line(),
Reason :: vm_breakpoint_reason(),
Breakpoints0 :: breakpoints(),
Breakpoints1 :: breakpoints().
remove_vm_breakpoint(Module, Line, Reason, Breakpoints0) ->
#breakpoints{vm_breakpoints = VmBreakpoints0} = Breakpoints0,
case VmBreakpoints0 of
#{Module := #{Line := #{Reason := []} = Reasons0}} when map_size(Reasons0) =:= 1 ->
%% We have exactly one breakpoint reason on this line (and it's the one we're trying to remove)
%% Unset the breakpoint and remove this location from the state
VmBreakpoints1 = maps:remove({Module, Line}, VmBreakpoints0),
Breakpoints1 = Breakpoints0#breakpoints{vm_breakpoints = VmBreakpoints1},
DeletionResult = vm_unset_breakpoint(Module, Line),
{ok, DeletionResult, Breakpoints1};
#{Module := #{Line := #{Reason := []} = Reasons0}} when map_size(Reasons0) > 1 ->
%% We have more than one VM breakpoint on this line (and one of them is the one we're trying to remove)
%% Remove the reason and leave the VM breakpoint in place
Reasons1 = maps:remove(Reason, Reasons0),
VmBreakpoints1 = VmBreakpoints0#{Module => #{Line => Reasons1}},
Breakpoints1 = Breakpoints0#breakpoints{vm_breakpoints = VmBreakpoints1},
{ok, removed, Breakpoints1};
_ ->
%% We don't have the VM breakpoint we're trying to remove on this line, this is a user bug.
%% Since the user must be another function of this module, this is likely to be an error in the caller code.
%% If the caller expected this to be a valid breakpoint (it was registered in the internal state),
%% then this is invariant violation.
{error, unknown_vm_breakpoint}
end.
%% Low-level VM breakpoint functions. Do not use directly (but through add/remove).
-spec vm_set_breakpoint(Module, Line) -> ok | {error, edb:add_breakpoint_error()} when
Module :: vm_module(),
Line :: line().
vm_set_breakpoint({vm_module, Module}, Line) ->
case erl_debugger:instrumentations() of
#{line_breakpoint := true} ->
case erl_debugger:breakpoint(Module, Line, true) of
ok ->
ok;
{error, Reason} ->
{error, Reason}
end;
_ ->
{error, unsupported}
end.
-spec vm_unset_breakpoint(Module, Line) -> removed | vanished when
Module :: vm_module(),
Line :: line().
vm_unset_breakpoint({vm_module, Module}, Line) ->
case erl_debugger:breakpoint(Module, Line, false) of
ok ->
%% Breakpoint has been successfully removed from the VM
removed;
{error, _} ->
% Module with the breakpoint was unloaded or something? We don't
% have enough info atm to understand what happened, so just acknowled
% that it isn't set anymore
vanished
end.
-spec reapply_breakpoints(vm_module(), breakpoints()) -> ok | {error, edb:add_breakpoint_error()}.
reapply_breakpoints(Module, Breakpoints0) ->
#breakpoints{vm_breakpoints = VmBreakpoints} = Breakpoints0,
AllLines = maps:keys(maps:get(Module, VmBreakpoints, #{})),
reapply_breakpoints_with_rollback(Module, AllLines, []).
-spec reapply_breakpoints(SourceModule, TargetModule, breakpoints()) ->
{ok, breakpoints()} | {error, edb:add_breakpoint_error()}
when
SourceModule :: vm_module(),
TargetModule :: vm_module().
reapply_breakpoints(SourceModule, TargetModule, Breakpoints0) ->
#breakpoints{vm_breakpoints = VmBreakpoints} = Breakpoints0,
AllLines = maps:keys(maps:get(SourceModule, VmBreakpoints, #{})),
case reapply_breakpoints_with_rollback(TargetModule, AllLines, []) of
ok ->
Breakpoints1 = transfer_module_references(SourceModule, TargetModule, Breakpoints0),
{ok, Breakpoints1};
Error ->
Error
end.
-spec has_breakpoints(vm_module(), breakpoints()) -> boolean().
has_breakpoints(Module, Breakpoints0) ->
#breakpoints{vm_breakpoints = VmBreakpoints} = Breakpoints0,
maps:is_key(Module, VmBreakpoints).
-spec reapply_breakpoints_with_rollback(vm_module(), [line()], [line()]) ->
ok | {error, edb:add_breakpoint_error()}.
reapply_breakpoints_with_rollback(_Module, [], _Applied) ->
ok;
reapply_breakpoints_with_rollback(Module, [Line | Rest], Applied) ->
case vm_set_breakpoint(Module, Line) of
ok ->
reapply_breakpoints_with_rollback(Module, Rest, [Line | Applied]);
{error, _} = Error ->
% Unset all previously applied breakpoints
lists:foreach(
fun(AppliedLine) ->
vm_unset_breakpoint(Module, AppliedLine)
end,
Applied
),
Error
end.
-spec transfer_module_references(SourceModule, TargetModule, Breakpoints0) -> Breakpoints1 when
SourceModule :: vm_module(),
TargetModule :: vm_module(),
Breakpoints0 :: breakpoints(),
Breakpoints1 :: breakpoints().
transfer_module_references(SourceModule, TargetModule, Breakpoints0) ->
#breakpoints{
explicits = Explicits0,
steps = Steps0,
vm_breakpoints = VmBreakpoints0
} = Breakpoints0,
Explicits1 =
case Explicits0 of
#{SourceModule := Explicits} ->
Explicits0#{TargetModule => Explicits};
#{} ->
Explicits0
end,
Steps1 =
#{
Pid =>
case PidSteps of
#{SourceModule := LinePatterns} ->
PidSteps#{TargetModule => LinePatterns};
#{} ->
PidSteps
end
|| Pid := PidSteps <- Steps0
},
VmBreakpoints1 =
case VmBreakpoints0 of
#{SourceModule := LineReasons} ->
VmBreakpoints0#{TargetModule => LineReasons};
#{} ->
VmBreakpoints0
end,
Breakpoints0#breakpoints{
explicits = Explicits1,
steps = Steps1,
vm_breakpoints = VmBreakpoints1
}.
%%--------------------------------------------------------------------
%% Module substitutes
%%--------------------------------------------------------------------
-spec to_vm_module(Module, Breakpoints) -> vm_module() when
Module :: module(),
Breakpoints :: breakpoints().
to_vm_module(Module, Breakpoints) ->
resolve_module_substitute(Module, Breakpoints).
-spec from_vm_module(VmModule, Breakpoints) -> module() when
VmModule :: vm_module(),
Breakpoints :: breakpoints().
from_vm_module(VmModule, Breakpoints) ->
resolve_module_substitute_reverse(VmModule, Breakpoints).
-spec add_module_substitute(Module, Substitute, AddedFrames, Breakpoints0) ->
{ok, Breakpoints1} | {error, edb_server:add_substitute_error()}
when
Module :: module(),
Substitute :: module(),
AddedFrames :: [mfa()],
Breakpoints0 :: breakpoints(),
Breakpoints1 :: breakpoints().
add_module_substitute(Module, Substitute, AddedFrames, Breakpoints0) ->
#breakpoints{
substituted_modules = SubstitutedModules0,
further_substituted_modules = FurtherSubstitutes0,
substituted_modules_reverse = SubstitutedModulesReverse0
} = Breakpoints0,
case has_breakpoints(to_vm_module(Substitute, Breakpoints0), Breakpoints0) of
true ->
{error, already_has_breakpoints};
false ->
case maps:is_key(Module, SubstitutedModules0) of
true ->
{error, already_substituted};
false ->
case maps:is_key({vm_module, Substitute}, SubstitutedModulesReverse0) of
true ->
{error, is_already_a_substitute};
false ->
case FurtherSubstitutes0 of
#{{vm_module, Module} := _} ->
{error, already_substituted};
#{} ->
{ok, Breakpoints1} = reapply_breakpoints(
{vm_module, Module},
{vm_module, Substitute},
Breakpoints0
),
{ok,
case SubstitutedModulesReverse0 of
#{{vm_module, Module} := _} ->
FurtherSubstitutes1 = FurtherSubstitutes0#{
{vm_module, Module} => #{
substitute => {vm_module, Substitute},
added_frames => AddedFrames
}
},
SubstitutedModulesReverse1 = SubstitutedModulesReverse0#{
{vm_module, Substitute} => {vm_module, Module}
},
Breakpoints1#breakpoints{
further_substituted_modules = FurtherSubstitutes1,
substituted_modules_reverse = SubstitutedModulesReverse1
};
#{} ->
SubstitutedModules1 = SubstitutedModules0#{
Module => #{
substitute => {vm_module, Substitute},
added_frames => AddedFrames,
original_sources => edb_server_code:module_source(Module)
}
},
SubstitutedModulesReverse1 = SubstitutedModulesReverse0#{
{vm_module, Substitute} => Module
},
Breakpoints1#breakpoints{
substituted_modules = SubstitutedModules1,
substituted_modules_reverse = SubstitutedModulesReverse1
}
end}
end
end
end
end.
-spec resolve_module_substitute(Module, Breakpoints) -> vm_module() when
Module :: module() | vm_module(),
Breakpoints :: breakpoints().
resolve_module_substitute(Module, #breakpoints{substituted_modules = SubstitutedModules} = Breakpoints) when
is_atom(Module)
->
case SubstitutedModules of
#{Module := #{substitute := Substitute}} ->
resolve_module_substitute(Substitute, Breakpoints);
_ ->
{vm_module, Module}
end;
resolve_module_substitute(
Subst = {vm_module, _}, #breakpoints{further_substituted_modules = FurtherSubstitutedModules} = Breakpoints
) ->
case FurtherSubstitutedModules of
#{Subst := #{substitute := FurtherSubstitute}} ->
resolve_module_substitute(FurtherSubstitute, Breakpoints);
_ ->
Subst
end.
-spec resolve_module_substitute_reverse(Substitute, Breakpoints) -> module() when
Substitute :: vm_module(),
Breakpoints :: breakpoints().
resolve_module_substitute_reverse({vm_module, SubMod} = Substitute, Breakpoints) ->
#breakpoints{substituted_modules_reverse = SubstitutedModulesReverse} = Breakpoints,
case SubstitutedModulesReverse of
#{Substitute := {vm_module, _} = PrevSubstitute} ->
resolve_module_substitute_reverse(PrevSubstitute, Breakpoints);
#{Substitute := OrigModule} when is_atom(OrigModule) ->
OrigModule;
#{} ->
SubMod
end.
-spec remove_module_substitute(SubstituteModule, Breakpoints0) ->
{ok, Breakpoints1} | {error, edb_server:remove_substitute_error()}
when
SubstituteModule :: module(),
Breakpoints0 :: breakpoints(),
Breakpoints1 :: breakpoints().
remove_module_substitute(SubstituteModule, Breakpoints0) ->
#breakpoints{
substituted_modules = SubstitutedModules0,
further_substituted_modules = FurtherSubstitutes0,
substituted_modules_reverse = SubstituteModulesReverse0
} = Breakpoints0,
VmSubstitute = {vm_module, SubstituteModule},
case SubstituteModulesReverse0 of
#{VmSubstitute := OriginalModule} ->
{SubstitutedModules1, FurtherSubstitutes1, SubstitutedModulesReverse1} = remove_substitute(
OriginalModule, VmSubstitute, SubstitutedModules0, FurtherSubstitutes0, SubstituteModulesReverse0
),
case FurtherSubstitutes0 of
#{VmSubstitute := _SubstituteModuleNext} ->
{error, has_dependent_substitute};
#{} ->
Breakpoints1 = Breakpoints0#breakpoints{
substituted_modules = SubstitutedModules1,
further_substituted_modules = FurtherSubstitutes1,
substituted_modules_reverse = SubstitutedModulesReverse1
},
OriginalModuleAtom =
case OriginalModule of
{vm_module, Mod} -> Mod;
Mod -> Mod
end,
{module, OriginalModuleAtom} = code:ensure_loaded(OriginalModuleAtom),
{ok, Breakpoints2} = reapply_breakpoints(
VmSubstitute,
{vm_module, OriginalModuleAtom},
Breakpoints1
),
{ok, Breakpoints2}
end;
#{} ->
{error, not_a_substitute}
end.
-spec remove_substitute(
OriginalModule :: module() | vm_module(),
SubstituteModule :: vm_module(),
SubstitutedModules :: #{module() => substitute_info()},
FurtherSubstitutes :: #{vm_module() => further_substitute_info()},
SubstitutedModulesReverse :: #{vm_module() => module() | vm_module()}
) ->
{#{module() => substitute_info()}, #{vm_module() => further_substitute_info()}, #{
vm_module() => module() | vm_module()
}}.
remove_substitute(
{vm_module, _} = OriginalModule,
SubstituteModule,
SubstitutedModules,
FurtherSubstitutes,
SubstitutedModulesReverse
) ->
{
SubstitutedModules,
maps:remove(OriginalModule, FurtherSubstitutes),
maps:remove(SubstituteModule, SubstitutedModulesReverse)
};
remove_substitute(
OriginalModule, SubstituteModule, SubstitutedModules, FurtherSubstitutes, SubstitutedModulesReverse
) ->
{
maps:remove(OriginalModule, SubstitutedModules),
FurtherSubstitutes,
maps:remove(SubstituteModule, SubstitutedModulesReverse)
}.
%% --------------------------------------------------------------------
%% Module source
%% --------------------------------------------------------------------
-spec get_vm_module_source(VmModule, Breakpoints) -> file:filename() | undefined when
VmModule :: vm_module(),
Breakpoints :: breakpoints().
get_vm_module_source(VmModule, Breakpoints) ->
#breakpoints{substituted_modules = SubstitutedModules} = Breakpoints,
OriginalModule = from_vm_module(VmModule, Breakpoints),
case SubstitutedModules of
#{OriginalModule := #{original_sources := Source}} ->
Source;
#{} ->
edb_server_code:module_source(OriginalModule)
end.
%% --------------------------------------------------------------------
%% Helpers -- call-stacks and call-stack patterns
%% --------------------------------------------------------------------
-type call_stack_addrs() :: [CodeAddr :: pos_integer()].
-type call_stack_pattern() :: [mfa() | CodeAddr :: pos_integer()].
-spec call_stack_addrs(RawFrames) -> call_stack_addrs() when
RawFrames :: [erl_debugger:stack_frame()].
call_stack_addrs(RawFrames) ->
[CodeAddr || {_, _, #{code := CodeAddr}} <- RawFrames].
-spec call_stack_matches(Pattern, RawFrames) -> boolean() when
Pattern :: call_stack_pattern(),
RawFrames :: [erl_debugger:stack_frame()].
call_stack_matches([], []) ->
true;
call_stack_matches([MFA | MorePattern], [{_, #{function := MFA}, _} | MoreFrames]) when is_tuple(MFA) ->
call_stack_matches(MorePattern, MoreFrames);
call_stack_matches([CodeAddr | MorePattern], [{_, _, #{code := CodeAddr}} | MoreFrames]) when is_integer(CodeAddr) ->
call_stack_matches(MorePattern, MoreFrames);
call_stack_matches(_, _) ->
false.