diff --git a/apps/edb/src/edb_app.erl b/apps/edb/src/edb_app.erl index b76729f..fbcc8cb 100644 --- a/apps/edb/src/edb_app.erl +++ b/apps/edb/src/edb_app.erl @@ -27,10 +27,15 @@ edb public API -spec start(application:start_type(), term()) -> {ok, pid()}. start(_StartType, _StartArgs) -> - {ok, _Sup} = edb_sup:start_link(). + Options = + case application:get_env(edb, dap_language) of + {ok, DapLanguage} -> + #{dap_language => DapLanguage}; + undefined -> + #{} + end, + {ok, _Sup} = edb_sup:start_link(Options). -spec stop(term()) -> ok. stop(_State) -> ok. - -%% internal functions diff --git a/apps/edb/src/edb_dap_internal_events.erl b/apps/edb/src/edb_dap_internal_events.erl index 68b4f38..be5e209 100644 --- a/apps/edb/src/edb_dap_internal_events.erl +++ b/apps/edb/src/edb_dap_internal_events.erl @@ -128,10 +128,11 @@ paused_impl(#{state := S}, Event) -> Result :: edb:reverse_attachment_event(), State :: edb_dap_server:state(), Reaction :: reaction(). -reverse_attach_impl({attached, Node}, State0 = #{state := launching}) -> +reverse_attach_impl({attached, Node}, State0 = #{state := launching, dap_language := DapLanguage}) -> State1 = maps:without([shell_process_id], State0), ProcessId = list_to_integer(erpc:call(Node, os, getpid, [])), + DapLanguageState = DapLanguage:init(), AttachType0 = maps:with([shell_process_id], State0), AttachType1 = AttachType0#{request => launch, process_id => ProcessId}, @@ -140,7 +141,8 @@ reverse_attach_impl({attached, Node}, State0 = #{state := launching}) -> new_state => State1#{ state => configuring, type => AttachType1, - node => Node + node => Node, + dap_language_state => DapLanguageState } }; reverse_attach_impl({error, Node, {bootstrap_failed, BootstrapFailure}}, #{state := launching}) -> diff --git a/apps/edb/src/edb_dap_language.erl b/apps/edb/src/edb_dap_language.erl new file mode 100644 index 0000000..b08765c --- /dev/null +++ b/apps/edb/src/edb_dap_language.erl @@ -0,0 +1,34 @@ +%% 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_dap_language). + +-oncall("whatsapp_server_devx"). +-moduledoc """ +Language-specific hooks for the DAP adapter. +""". +-compile(warn_missing_spec_all). + +-export_type([state/0]). + +-type state() :: dynamic(). + +-callback init() -> dynamic(). +-callback source_to_modules(Path, Lines, State) -> {Modules, State} when + Path :: binary(), + Lines :: [edb:line()], + Modules :: [module()], + State :: dynamic(). diff --git a/apps/edb/src/edb_dap_language_erlang.erl b/apps/edb/src/edb_dap_language_erlang.erl new file mode 100644 index 0000000..ca59948 --- /dev/null +++ b/apps/edb/src/edb_dap_language_erlang.erl @@ -0,0 +1,40 @@ +%% 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_dap_language_erlang). + +-oncall("whatsapp_server_devx"). +-moduledoc """ +Default Erlang language hooks. +""". +-compile(warn_missing_spec_all). + +-behaviour(edb_dap_language). + +-export([init/0, source_to_modules/3]). + +-spec init() -> #{}. +init() -> + #{}. + +-spec source_to_modules(Path, Lines, State) -> {[module()], State} when + Path :: binary(), + Lines :: [edb:line()], + State :: #{}. +source_to_modules(Path, _Lines, State) -> + Extension = filename:extension(Path), + ModuleName = filename:basename(Path, Extension), + {[binary_to_atom(unicode:characters_to_binary(ModuleName))], State}. diff --git a/apps/edb/src/edb_dap_request_attach.erl b/apps/edb/src/edb_dap_request_attach.erl index 20d52c1..f53353c 100644 --- a/apps/edb/src/edb_dap_request_attach.erl +++ b/apps/edb/src/edb_dap_request_attach.erl @@ -72,7 +72,7 @@ parse_arguments(Args) -> -spec handle(State, Args) -> edb_dap_request:reaction() when State :: edb_dap_server:state(), Args :: config(). -handle(State0 = #{state := initialized}, Args) -> +handle(State0 = #{state := initialized, dap_language := DapLanguage}, Args) -> #{config := Config} = Args, AttachArgs = maps:without([cwd, stripSourcePrefix], Config), case edb:attach(AttachArgs) of @@ -83,6 +83,7 @@ handle(State0 = #{state := initialized}, Args) -> Node = maps:get(node, Config), ProcessId = list_to_integer(erpc:call(Node, os, getpid, [])), {ok, Subscription} = edb:subscribe(), + DapLanguageState = DapLanguage:init(), State1 = State0#{ state => configuring, type => #{ @@ -93,6 +94,7 @@ handle(State0 = #{state := initialized}, Args) -> node => Node, reverse_attach_ref => undefined, cwd => edb_dap_utils:strip_suffix(Cwd, StripSourcePrefix), + dap_language_state => DapLanguageState, subscription => Subscription }, #{ diff --git a/apps/edb/src/edb_dap_request_initialize.erl b/apps/edb/src/edb_dap_request_initialize.erl index fdadb8d..2e47266 100644 --- a/apps/edb/src/edb_dap_request_initialize.erl +++ b/apps/edb/src/edb_dap_request_initialize.erl @@ -183,10 +183,10 @@ parse_arguments(Args) -> -spec handle(State, Args) -> edb_dap_request:reaction(capabilities()) when State :: edb_dap_server:state(), Args :: arguments(). -handle(#{state := started}, ClientInfo) -> +handle(State0 = #{state := started}, ClientInfo) -> #{ response => edb_dap_request:success(capabilities()), - new_state => #{state => initialized, client_info => ClientInfo} + new_state => State0#{state => initialized, client_info => ClientInfo} }; handle(_InvalidState, _Args) -> edb_dap_request:unexpected_request(). diff --git a/apps/edb/src/edb_dap_request_set_breakpoints.erl b/apps/edb/src/edb_dap_request_set_breakpoints.erl index 90e8f0b..c1d8a2b 100644 --- a/apps/edb/src/edb_dap_request_set_breakpoints.erl +++ b/apps/edb/src/edb_dap_request_set_breakpoints.erl @@ -200,25 +200,27 @@ parse_arguments(Args) -> -spec handle(State, Args) -> edb_dap_request:reaction(response()) when State :: edb_dap_server:state(), Args :: arguments(). -handle(#{state := configuring}, Args) -> - set_breakpoints(Args); -handle(#{state := attached}, Args) -> - set_breakpoints(Args); +handle(State = #{state := configuring}, Args) -> + set_breakpoints(State, Args); +handle(State = #{state := attached}, Args) -> + set_breakpoints(State, Args); handle(_UnexpectedState, _) -> edb_dap_request:unexpected_request(). %% ------------------------------------------------------------------ %% Helpers %% ------------------------------------------------------------------ --spec set_breakpoints(Args) -> edb_dap_request:reaction(response()) when +-spec set_breakpoints(State, Args) -> edb_dap_request:reaction(response()) when + State :: edb_dap_server:state(), Args :: arguments(). -set_breakpoints(Args = #{source := #{path := Path}}) -> - Module = binary_to_atom(filename:basename(Path, ".erl")), - +set_breakpoints(State0, Args = #{source := #{path := Path}}) -> SourceBreakpoints = maps:get(breakpoints, Args, []), SourceBreakpointLines = [Line || #{line := Line} <- SourceBreakpoints], + #{dap_language := DapLanguage, dap_language_state := DapLanguageState0} = State0, + {Modules, DapLanguageState1} = DapLanguage:source_to_modules(Path, SourceBreakpointLines, DapLanguageState0), + State1 = State0#{dap_language_state => DapLanguageState1}, - LineResults = edb:set_breakpoints(Module, SourceBreakpointLines), + LineResults = set_breakpoints_in_modules(Modules, SourceBreakpointLines), Breakpoints = [ case Result of @@ -230,7 +232,39 @@ set_breakpoints(Args = #{source := #{path := Path}}) -> end || {Line, Result} <:- LineResults ], - #{response => edb_dap_request:success(#{breakpoints => Breakpoints})}. + #{ + response => edb_dap_request:success(#{breakpoints => Breakpoints}), + new_state => State1 + }. + +-spec set_breakpoints_in_modules(Modules, Lines) -> edb:set_breakpoints_result() when + Modules :: [module()], + Lines :: [edb:line()]. +set_breakpoints_in_modules([Module], Lines) -> + edb:set_breakpoints(Module, Lines); +set_breakpoints_in_modules(Modules, Lines) -> + ResultsByModule = [edb:set_breakpoints(Module, Lines) || Module <- Modules], + [{Line, merge_line_results(Line, ResultsByModule)} || Line <- Lines]. + +-spec merge_line_results(Line, ResultsByModule) -> ok | {error, edb:add_breakpoint_error()} when + Line :: edb:line(), + ResultsByModule :: [edb:set_breakpoints_result()]. +merge_line_results(Line, ResultsByModule) -> + LineResults = [ + Result + || ModuleResults <- ResultsByModule, + {ResultLine, Result} <- ModuleResults, + ResultLine =:= Line + ], + case lists:member(ok, LineResults) of + true -> + ok; + false -> + case LineResults of + [{error, Reason} | _] -> {error, Reason}; + [] -> {error, {badkey, Line}} + end + end. -spec format_breakpoint_error(Error) -> binary() when Error :: edb:add_breakpoint_error(). diff --git a/apps/edb/src/edb_dap_server.erl b/apps/edb/src/edb_dap_server.erl index f556169..9d05969 100644 --- a/apps/edb/src/edb_dap_server.erl +++ b/apps/edb/src/edb_dap_server.erl @@ -30,7 +30,7 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification -include_lib("edb/include/edb_dap.hrl"). % Public API --export([start_link/0]). +-export([start_link/0, start_link/1]). -export([handle_message/1]). %% gen_server callbacks @@ -47,6 +47,9 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification %%% Types %%%--------------------------------------------------------------------------------- -type client_info() :: edb_dap_request_initialize:arguments(). +-type options() :: #{ + dap_language => module() +}. -type attach_type() :: #{ @@ -62,17 +65,20 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification -type state() :: #{ % Server is up, waiting for an `initialize` request from the client - state := started + state := started, + dap_language := module() } | #{ % Server received an `initialize` request and is waiting for `attach`/`launch` requests state := initialized, - client_info := client_info() + client_info := client_info(), + dap_language := module() } | #{ % A `launch` request was received and we are waiting for the debuggee node to be up state := launching, client_info := client_info(), + dap_language := module(), port := port() | none, shell_process_id => number(), reverse_attach_ref := reference(), @@ -87,10 +93,12 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification state := configuring, type := attach_type(), client_info := client_info(), + dap_language := module(), port := port() | none, node := node(), reverse_attach_ref := reference() | undefined, cwd := binary(), + dap_language_state := edb_dap_language:state(), subscription := edb:event_subscription() } | #{ @@ -98,10 +106,12 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification state := attached, type := attach_type(), client_info := client_info(), + dap_language := module(), port := port() | none, node := node(), reverse_attach_ref := reference() | undefined, cwd := binary(), + dap_language_state := edb_dap_language:state(), subscription := edb:event_subscription() } | #{ @@ -154,7 +164,11 @@ For details see https://microsoft.github.io/debug-adapter-protocol/specification -spec start_link() -> {ok, pid()}. start_link() -> - {ok, _Pid} = gen_server:start_link({local, ?SERVER}, ?MODULE, noargs, []). + start_link(#{}). + +-spec start_link(options()) -> {ok, pid()}. +start_link(Options) -> + {ok, _Pid} = gen_server:start_link({local, ?SERVER}, ?MODULE, Options, []). -spec handle_message(Message) -> ok when Message :: edb_dap:request() | edb_dap:response(). @@ -165,9 +179,10 @@ handle_message(Message) -> %%% Callbacks %%%--------------------------------------------------------------------------------- --spec init(noargs) -> {ok, state()}. -init(noargs) -> - {ok, #{state => started}}. +-spec init(options()) -> {ok, state()}. +init(Options) -> + DapLanguage = maps:get(dap_language, Options, edb_dap_language_erlang), + {ok, #{state => started, dap_language => DapLanguage}}. -spec terminate(Reason :: term(), state()) -> ok. terminate(_Reason, _State) -> diff --git a/apps/edb/src/edb_sup.erl b/apps/edb/src/edb_sup.erl index 30b1084..f6cc7af 100644 --- a/apps/edb/src/edb_sup.erl +++ b/apps/edb/src/edb_sup.erl @@ -24,15 +24,23 @@ edb top level supervisor. -behaviour(supervisor). --export([start_link/0]). +-export([start_link/0, start_link/1]). -export([init/1]). -define(SERVER, ?MODULE). +-type options() :: #{ + dap_language => module() +}. + -spec start_link() -> supervisor:startlink_ret(). start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). + start_link(#{}). + +-spec start_link(options()) -> supervisor:startlink_ret(). +start_link(Options) -> + supervisor:start_link({local, ?SERVER}, ?MODULE, [Options]). %% sup_flags() = #{strategy => strategy(), % optional %% intensity => non_neg_integer(), % optional @@ -43,8 +51,8 @@ start_link() -> %% shutdown => shutdown(), % optional %% type => worker(), % optional %% modules => modules()} % optional --spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -init([]) -> +-spec init([options()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([Options]) -> SupFlags = #{ strategy => rest_for_one, intensity => 5, @@ -66,7 +74,7 @@ init([]) -> }, #{ id => edb_dap_server, - start => {edb_dap_server, start_link, []}, + start => {edb_dap_server, start_link, [maps:with([dap_language], Options)]}, restart => transient }, #{ diff --git a/apps/edb/test/edb_dap_language_SUITE.erl b/apps/edb/test/edb_dap_language_SUITE.erl new file mode 100644 index 0000000..9218b14 --- /dev/null +++ b/apps/edb/test/edb_dap_language_SUITE.erl @@ -0,0 +1,105 @@ +%% 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_dap_language_SUITE). + +-oncall("whatsapp_server_devx"). + +-include_lib("assert/include/assert.hrl"). + +%% CT callbacks +-export([ + all/0, + init_per_testcase/2, + end_per_testcase/2 +]). + +%% Test cases +-export([test_set_breakpoint_with_custom_dap_language/1]). + +%% edb_dap_language callbacks +-export([init/0, source_to_modules/3]). + +all() -> + [ + test_set_breakpoint_with_custom_dap_language + ]. + +init_per_testcase(_TestCase, Config) -> + {ok, _} = application:ensure_all_started(edb_core), + Config. + +end_per_testcase(_TestCase, _Config) -> + _ = application:stop(edb_core), + edb_test_support:stop_all_peers(), + ok. + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- +test_set_breakpoint_with_custom_dap_language(Config) -> + Module = edb_custom_language_target, + {ok, #{node := Node, cookie := Cookie}} = edb_test_support:start_peer_node(Config, #{ + modules => [ + {source, [ + ~"-module(edb_custom_language_target).\n", + ~"-export([go/0]).\n", + ~"go() ->\n", + ~" ok.\n" + ]} + ] + }), + ok = edb:attach(#{node => Node, cookie => Cookie}), + + SourcePath = ~"custom://source.edbtest", + BreakpointLines = [4], + DapLanguageState0 = #{ + modules_by_source => #{SourcePath => [Module]}, + source_lookups => [] + }, + Reaction = edb_dap_request_set_breakpoints:handle( + #{state => attached, dap_language => ?MODULE, dap_language_state => DapLanguageState0}, + #{ + source => #{path => SourcePath}, + breakpoints => [#{line => Line} || Line <- BreakpointLines] + } + ), + + ?assertMatch( + #{ + response := + #{ + success := true, + body := #{breakpoints := [#{line := 4, verified := true}]} + }, + new_state := #{ + dap_language_state := #{source_lookups := [{SourcePath, BreakpointLines}]} + } + }, + Reaction + ), + ok. + +%%-------------------------------------------------------------------- +%% edb_dap_language callbacks +%%-------------------------------------------------------------------- +init() -> + #{source_lookups => []}. + +source_to_modules(Path, Lines, State0 = #{modules_by_source := ModulesBySource}) -> + Modules = maps:get(Path, ModulesBySource), + SourceLookups = maps:get(source_lookups, State0), + State1 = State0#{source_lookups => SourceLookups ++ [{Path, Lines}]}, + {Modules, State1}.