From 9b03dacf2b7829b584d26a999f80c315ae8ce897 Mon Sep 17 00:00:00 2001
From: Fred Hebert <mononcqc@ferd.ca>
Date: Mon, 22 Oct 2018 19:46:09 -0400
Subject: Allow Breakpoints during task runs

This is mostly useful for tests, where a test suite of any kind can be
interrupted halfway through so that the user can probe the running
system to see what is happening.

This is done as follows:

1. the user must call `r3:break()` in a test suite
2. the user runs the task as `r3:async_do(ct)`
3. the test holds up and the user can do whatever
4. the user calls `r3:resume()` and the test proceeds as normal

A safeguard is added so that breakpoints are only triggered in the shell
in async mode

Sample session:

    $ rebar3 shell
    ...
    1> rebar_agent:async_do(ct).
    ok
    ...
    Running Common Test suites...
    %%% rebar_alias_SUITE: .
    === BREAK ===

    2> % <do some checks>
    2> r3:resume().
    ok
    3> .....
    %%% rebar_as_SUITE: ...........
    %%% rebar_compile_SUITE: ......
    ...
---
 src/r3.erl          | 43 ++++++++++++++++++++++++++++++++++++++++++-
 src/rebar_agent.erl | 38 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 79 insertions(+), 2 deletions(-)

(limited to 'src')

diff --git a/src/r3.erl b/src/r3.erl
index bbf9eea..a79cc3a 100644
--- a/src/r3.erl
+++ b/src/r3.erl
@@ -1,8 +1,9 @@
 %%% @doc external alias for `rebar_agent' for more convenient
 %%% calls from a shell.
 -module(r3).
--export([do/1, do/2]).
+-export([do/1, do/2, async_do/1, async_do/2, break/0, resume/0]).
 -export(['$handle_undefined_function'/2]).
+-include("rebar.hrl").
 
 %% @doc alias for `rebar_agent:do/1'
 -spec do(atom()) -> ok | {error, term()}.
@@ -12,6 +13,46 @@ do(Command) -> rebar_agent:do(Command).
 -spec do(atom(), atom()) -> ok | {error, term()}.
 do(Namespace, Command) -> rebar_agent:do(Namespace, Command).
 
+%% @async_doc alias for `rebar_agent:async_do/1'
+-spec async_do(atom()) -> ok | {error, term()}.
+async_do(Command) -> rebar_agent:async_do(Command).
+
+%% @async_doc alias for `rebar_agent:async_do/2'
+-spec async_do(atom(), atom()) -> ok | {error, term()}.
+async_do(Namespace, Command) -> rebar_agent:async_do(Namespace, Command).
+
+break() ->
+    case whereis(rebar_agent) of % is the shell running
+        undefined ->
+            ok;
+        Pid ->
+            {dictionary, Dict} = process_info(Pid, dictionary),
+            case lists:keyfind(cmd_type, 1, Dict) of
+                {cmd_type, async} ->
+                    Self = self(),
+                    Ref = make_ref(),
+                    spawn_link(fun() ->
+                            register(r3_breakpoint_handler, self()),
+                            receive
+                                resume ->
+                                    Self ! Ref
+                            end
+                    end),
+                    io:format(user, "~n=== BREAK ===~n", []),
+                    receive
+                        Ref -> ok
+                    end;
+                _ ->
+                    ?DEBUG("ignoring breakpoint since command is not run "
+                           "in async mode", []),
+                    ok
+            end
+    end.
+
+resume() ->
+    r3_breakpoint_handler ! resume,
+    ok.
+
 %% @private defer to rebar_agent
 '$handle_undefined_function'(Cmd, Args) ->
     rebar_agent:'$handle_undefined_function'(Cmd, Args).
diff --git a/src/rebar_agent.erl b/src/rebar_agent.erl
index 445ae54..b4734f1 100644
--- a/src/rebar_agent.erl
+++ b/src/rebar_agent.erl
@@ -1,7 +1,7 @@
 %%% @doc Runs a process that holds a rebar3 state and can be used
 %%% to statefully maintain loaded project state into a running VM.
 -module(rebar_agent).
--export([start_link/1, do/1, do/2]).
+-export([start_link/1, do/1, do/2, async_do/1, async_do/2]).
 -export(['$handle_undefined_function'/2]).
 -export([init/1,
          handle_call/3, handle_cast/2, handle_info/2,
@@ -35,6 +35,18 @@ do(Namespace, Command) when is_atom(Namespace), is_atom(Command) ->
 do(Namespace, Args) when is_atom(Namespace), is_list(Args) ->
     gen_server:call(?MODULE, {cmd, Namespace, do, Args}, infinity).
 
+-spec async_do(atom()) -> ok | {error, term()}.
+async_do(Command) when is_atom(Command) ->
+    gen_server:cast(?MODULE, {cmd, Command});
+async_do(Args) when is_list(Args) ->
+    gen_server:cast(?MODULE, {cmd, default, do, Args}).
+
+-spec async_do(atom(), atom()) -> ok.
+async_do(Namespace, Command) when is_atom(Namespace), is_atom(Command) ->
+    gen_server:cast(?MODULE, {cmd, Namespace, Command});
+async_do(Namespace, Args) when is_atom(Namespace), is_list(Args) ->
+    gen_server:cast(?MODULE, {cmd, Namespace, do, Args}).
+
 '$handle_undefined_function'(Cmd, [Namespace, Args]) ->
     gen_server:call(?MODULE, {cmd, Namespace, Cmd, Args}, infinity);
 '$handle_undefined_function'(Cmd, [Args]) ->
@@ -54,20 +66,44 @@ init(State) ->
 %% @private
 handle_call({cmd, Command}, _From, State=#state{state=RState, cwd=Cwd}) ->
     MidState = maybe_show_warning(State),
+    put(cmd_type, sync),
     {Res, NewRState} = run(default, Command, "", RState, Cwd),
+    put(cmd_type, undefined),
     {reply, Res, MidState#state{state=NewRState}, hibernate};
 handle_call({cmd, Namespace, Command}, _From, State = #state{state=RState, cwd=Cwd}) ->
     MidState = maybe_show_warning(State),
+    put(cmd_type, sync),
     {Res, NewRState} = run(Namespace, Command, "", RState, Cwd),
+    put(cmd_type, undefined),
     {reply, Res, MidState#state{state=NewRState}, hibernate};
 handle_call({cmd, Namespace, Command, Args}, _From, State = #state{state=RState, cwd=Cwd}) ->
     MidState = maybe_show_warning(State),
+    put(cmd_type, sync),
     {Res, NewRState} = run(Namespace, Command, Args, RState, Cwd),
+    put(cmd_type, undefined),
     {reply, Res, MidState#state{state=NewRState}, hibernate};
 handle_call(_Call, _From, State) ->
     {noreply, State}.
 
 %% @private
+handle_cast({cmd, Command}, State=#state{state=RState, cwd=Cwd}) ->
+    MidState = maybe_show_warning(State),
+    put(cmd_type, async),
+    {_, NewRState} = run(default, Command, "", RState, Cwd),
+    put(cmd_type, undefined),
+    {noreply, MidState#state{state=NewRState}, hibernate};
+handle_cast({cmd, Namespace, Command}, State = #state{state=RState, cwd=Cwd}) ->
+    MidState = maybe_show_warning(State),
+    put(cmd_type, async),
+    {_, NewRState} = run(Namespace, Command, "", RState, Cwd),
+    put(cmd_type, undefined),
+    {noreply, MidState#state{state=NewRState}, hibernate};
+handle_cast({cmd, Namespace, Command, Args}, State = #state{state=RState, cwd=Cwd}) ->
+    MidState = maybe_show_warning(State),
+    put(cmd_type, async),
+    {_, NewRState} = run(Namespace, Command, Args, RState, Cwd),
+    put(cmd_type, undefined),
+    {noreply, MidState#state{state=NewRState}, hibernate};
 handle_cast(_Cast, State) ->
     {noreply, State}.
 
-- 
cgit v1.1