From 755c7f574da2bf4f606d15c32bb26a5d1d516dcf Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Thu, 14 Nov 2024 14:02:46 +0100
Subject: [PATCH 01/12] Refactor the JSON-RPC connection
---
CHANGELOG.md | 18 +
config/dev.config | 10 +-
config/local.config | 13 +-
config/test.config | 13 +
src/grisp_connect.app.src | 30 +-
src/grisp_connect_api.erl | 162 ++--
src/grisp_connect_client.erl | 313 ++++++--
src/grisp_connect_connection.erl | 741 ++++++++++++++++++
src/grisp_connect_internal.hrl | 25 +
src/grisp_connect_jsonrpc.erl | 6 +-
src/grisp_connect_logger_bin.erl | 23 -
src/grisp_connect_sup.erl | 1 -
src/grisp_connect_updater.erl | 7 +
src/grisp_connect_utils.erl | 45 ++
src/grisp_connect_ws.erl | 147 ----
test/grisp_connect_connection_SUITE.erl | 326 ++++++++
.../jsonrpc_examples.txt | 96 +++
test/grisp_connect_jsonrpc_SUITE.erl | 8 +-
test/grisp_connect_log_SUITE.erl | 28 +-
test/grisp_connect_reconnect_SUITE.erl | 21 +-
test/grisp_connect_test.hrl | 46 +-
test/grisp_connect_test_client.erl | 2 -
test/grisp_connect_test_server.erl | 88 ++-
23 files changed, 1735 insertions(+), 434 deletions(-)
create mode 100644 src/grisp_connect_connection.erl
create mode 100644 src/grisp_connect_internal.hrl
create mode 100644 src/grisp_connect_utils.erl
delete mode 100644 src/grisp_connect_ws.erl
create mode 100644 test/grisp_connect_connection_SUITE.erl
create mode 100644 test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d6081b..b36a110 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,24 @@ and this project adheres to
## [Unreleased]
+### Changed
+
+- The name of the grisp_connect configuration key to control the timout of
+individual JSON-RPC requests changed from ws_requests_timeout ot
+ws_request_timeout.
+- Le default log filter changed to trying to filter out only some messages to
+filtering out all progress messages, as it wasn't working reliably.
+- The connection is not a persistent process anymore, it is now a transiant
+process handling a connection and dying when the connection is closed.
+- Internally, the JSON-RPC is parsed into a list of atom or binaries to pave the
+road for namespaces. foo.bar.Buz is parsed into [foo, bar, <<"Buz">>] (if foo
+and bar are already existing atoms, but 'Buz' is not).
+
+## Fixed
+
+- The client is now waiting 1 second before trying to reconnect when it gets
+disconnected fomr the server.
+
## [1.1.0] - 2024-10-12
### Added
diff --git a/config/dev.config b/config/dev.config
index e22e04c..a626e7f 100644
--- a/config/dev.config
+++ b/config/dev.config
@@ -10,18 +10,16 @@
]},
{kernel, [
- {logger_level, info},
+ {logger_level, debug},
{logger, [
{handler, default, logger_std_h, #{
config => #{type => standard_io},
+ filter_default => log,
filters => [
- % Filter out supervisor progress reports so
- % TLS certificates are not swamping the console...
- {filter_out_progress, {
- fun grisp_connect_logger_bin:filter_out/2,
- {supervisor, report_progress}}}
+ {disable_progress, {fun logger_filters:progress/2, stop}}
]
}}
]}
]}
+
].
diff --git a/config/local.config b/config/local.config
index 7394944..f663e70 100644
--- a/config/local.config
+++ b/config/local.config
@@ -26,5 +26,16 @@
{port, 8443}
]},
- {kernel, [{logger_level, debug}]}
+ {kernel, [
+ {logger_level, debug},
+ {logger, [
+ {handler, default, logger_std_h, #{
+ config => #{type => standard_io},
+ filter_default => log,
+ filters => [
+ {disable_progress, {fun logger_filters:progress/2, stop}}
+ ]
+ }}
+ ]}
+ ]}
].
diff --git a/config/test.config b/config/test.config
index fe5cd15..d84962f 100644
--- a/config/test.config
+++ b/config/test.config
@@ -14,5 +14,18 @@
{grisp_connect, [
{domain, localhost},
{port, 3030}
+ ]},
+
+ {kernel, [
+ {logger_level, debug},
+ {logger, [
+ {handler, default, logger_std_h, #{
+ config => #{type => standard_io},
+ filter_default => log,
+ filters => [
+ {disable_progress, {fun logger_filters:progress/2, stop}}
+ ]
+ }}
+ ]}
]}
].
diff --git a/src/grisp_connect.app.src b/src/grisp_connect.app.src
index 110e439..ce7076c 100644
--- a/src/grisp_connect.app.src
+++ b/src/grisp_connect.app.src
@@ -21,26 +21,24 @@
{port, 443},
{connect, true}, % keeps a constant connection with grisp.io
{ntp, false}, % if set to true, starts the NTP client
- {ws_requests_timeout, 5_000},
+ {ws_transport, tls},
+ {ws_path, "/grisp-connect/ws"},
+ {ws_request_timeout, 5_000},
{ws_ping_timeout, 60_000},
{logs_interval, 2_000},
{logs_batch_size, 100},
{logger, [
- % Enable our own default handler,
- % which will receive all events from boot
- {handler,
- grisp_connect_log_handler,
- grisp_connect_logger_bin,
- #{formatter => {grisp_connect_logger_bin, #{}},
- % Filter out supervisor progress reports to prevent the ones
- % from tls_dyn_connection_sup that logs all the certificates
- % to crash the connection...
- filters => [
- {filter_out_progress,
- {fun grisp_connect_logger_bin:filter_out/2,
- {supervisor, report_progress}}}
- ]
- }}
+ % Enable our own default handler,
+ % which will receive all events from boot
+ {handler,
+ grisp_connect_log_handler,
+ grisp_connect_logger_bin,
+ #{formatter => {grisp_connect_logger_bin, #{}},
+ filter_default => log,
+ filters => [
+ {disable_progress, {fun logger_filters:progress/2, stop}}
+ ]
+ }}
]}
]},
{modules, []},
diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl
index f9bb96c..46bcb7d 100644
--- a/src/grisp_connect_api.erl
+++ b/src/grisp_connect_api.erl
@@ -1,152 +1,80 @@
%% @doc Library module containing the jsonrpc API logic
-module(grisp_connect_api).
--export([request/3]).
--export([notify/3]).
-export([handle_msg/1]).
-include_lib("kernel/include/logger.hrl").
%--- Macros --------------------------------------------------------------------
--define(method_get, <<"get">>).
--define(method_post, <<"post">>).
--define(method_patch, <<"patch">>).
--define(method_delete, <<"delete">>).
+-define(method_get, get).
+-define(method_post, post).
-%--- API -----------------------------------------------------------------------
-% #doc Assembles a jsonrpc request and its uuid
--spec request(Method :: atom() | binary(),
- Type :: atom() | binary(),
- Params :: map()) -> {ID :: binary(), Encoded :: binary()}.
-request(Method, Type, Params) ->
- ID = id(),
- Rpc = {request, Method, maps:put(type, Type, Params), ID},
- Encoded = grisp_connect_jsonrpc:encode(Rpc),
- {ID, Encoded}.
+%--- API -----------------------------------------------------------------------
-% #doc Assembles a jsonrpc notification
--spec notify(Method :: atom() | binary(),
- Type :: atom() | binary(),
- Params :: map()) -> Encoded :: binary().
-notify(Method, Type, Params) ->
- Rpc = {notification, Method, maps:put(type, Type, Params)},
- grisp_connect_jsonrpc:encode(Rpc).
+% @doc Handles requests, notifications and errors from grisp.io.
+-spec handle_msg(Msg) ->
+ ok | {reply, Result :: term(), ReqRef :: binary() | integer()}
+ when Msg :: {request, Method :: grisp_connect_connection:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
+ | {notification, grisp_connect_connection:method(), Params :: map() | list()}
+ | {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term()}.
+handle_msg({notification, M, Params}) ->
+ ?LOG_ERROR("Received unexpected notification ~p: ~p", [M, Params]),
+ ok;
+handle_msg({remote_error, Code, Message, _Data}) ->
+ ?LOG_ERROR("Received JSON-RPC error ~p: ~s", [Code, Message]),
+ ok;
+handle_msg({request, M, Params, ID})
+ when M == [?method_post]; M == [?method_get] ->
+ handle_request(M, Params, ID).
-% @doc Indentifies if the message is a request or a reply to a previous request.
-% In case it was a request, returns the reply to be sent to the peer.
-% In case it was a response, returns the parsed ID and content to be handled by
-% the caller.
--spec handle_msg(JSON :: binary()) ->
- {send_response, Response :: binary()} |
- {handle_response, ID :: binary(), {ok, Result :: map()} | {error, atom()}}.
-handle_msg(JSON) ->
- JSON_RPC = grisp_connect_jsonrpc:decode(JSON),
- handle_jsonrpc(JSON_RPC).
%--- Internal Funcitons --------------------------------------------------------
-format_error({internal_error, parse_error, ID}) ->
- {error, -32700, <<"Parse error">>, undefined, ID};
-format_error({internal_error, invalid_request, ID}) ->
- {error, -32600, <<"Invalid request">>, undefined, ID};
-format_error({internal_error, method_not_found, ID}) ->
- {error, -32601, <<"Method not found">>, undefined, ID};
-format_error({internal_error, invalid_params, ID}) ->
- {error, -32602, <<"Invalid params">>, undefined, ID};
-format_error({internal_error, Reason, ID}) ->
- {error, -32603, <<"Internal error">>, Reason, ID}.
-
-%FIXME: Batch are not supported yet. When receiving a batch of messages, as per
-% the JSON-RPC standard, all the responses should goes in a single batch
-% of responses.
-handle_jsonrpc(Messages) when is_list(Messages) ->
- handle_rpc_messages(Messages, []);
-handle_jsonrpc(Message) ->
- handle_rpc_messages([Message], []).
-
-handle_rpc_messages([], Replies) -> lists:reverse(Replies);
-handle_rpc_messages([{request, M, Params, ID} | Batch], Replies)
- when M == ?method_post;
- M == ?method_get ->
- handle_rpc_messages(Batch, [handle_request(M, Params, ID) | Replies]);
-handle_rpc_messages([{result, _, _} = Res| Batch], Replies) ->
- handle_rpc_messages(Batch, [handle_response(Res)| Replies]);
-handle_rpc_messages([{error, _Code, _Msg, _Data, _ID} = E | Batch], Replies) ->
- ?LOG_INFO("Received JsonRPC error: ~p",[E]),
- handle_rpc_messages(Batch, [handle_response(E)| Replies]);
-handle_rpc_messages([{decoding_error, _, _, _, _} = E | Batch], Replies) ->
- ?LOG_ERROR("JsonRPC decoding error: ~p",[E]),
- handle_rpc_messages(Batch, Replies).
-
-handle_request(?method_get, #{type := <<"system_info">>} = _Params, ID) ->
+handle_request([?method_get], #{type := <<"system_info">>} = _Params, ID) ->
Info = grisp_connect_updater:system_info(),
- {send_response, grisp_connect_jsonrpc:encode({result, Info, ID})};
-handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) ->
+ {reply, Info, ID};
+handle_request([?method_post], #{type := <<"start_update">>} = Params, ID) ->
try
URL = maps:get(url, Params),
- Reply = case grisp_connect_updater:start_update(URL) of
+ case grisp_connect_updater:start_update(URL) of
{error, grisp_updater_unavailable} ->
- {error, -10, grisp_updater_unavailable, undefined, ID};
+ {error, grisp_updater_unavailable, undefined, undefined, ID};
{error, already_updating} ->
- {error, -11, already_updating, undefined, ID};
+ {error, already_updating, undefined, undefined, ID};
{error, boot_system_not_validated} ->
- {error, -12, boot_system_not_validated, undefined, ID};
+ {error, boot_system_not_validated, undefined, undefined, ID};
{error, Reason} ->
ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])),
- format_error({internal_error, ReasonBinary, ID});
+ {error, internal_error, ReasonBinary, undefined, ID};
ok ->
- {result, ok, ID}
- end,
- {send_response, grisp_connect_jsonrpc:encode(Reply)}
+ {reply, ok, ID}
+ end
catch
throw:bad_key ->
- {send_response,
- format_error({internal_error, invalid_params, ID})}
- end;
-handle_request(?method_post, #{type := <<"validate">>}, ID) ->
- Reply = case grisp_connect_updater:validate() of
+ {error, internal_error, <<"Invalid params">>, ID}
+ end;
+handle_request([?method_post], #{type := <<"validate">>}, ID) ->
+ case grisp_connect_updater:validate() of
{error, grisp_updater_unavailable} ->
- {error, -10, grisp_updater_unavailable, undefined, ID};
+ {error, grisp_updater_unavailable, undefined, undefined, ID};
{error, {validate_from_unbooted, PartitionIndex}} ->
- {error, -13, validate_from_unbooted, PartitionIndex, ID};
+ {error, validate_from_unbooted, undefined, PartitionIndex, ID};
{error, Reason} ->
ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])),
- format_error({internal_error, ReasonBinary, ID});
+ {error, internal_error, ReasonBinary, undefined, ID};
ok ->
- {result, ok, ID}
- end,
- {send_response, grisp_connect_jsonrpc:encode(Reply)};
-handle_request(?method_post, #{type := <<"reboot">>}, ID) ->
+ {reply, ok, ID}
+ end;
+handle_request([?method_post], #{type := <<"reboot">>}, ID) ->
grisp_connect_client:reboot(),
- {send_response, grisp_connect_jsonrpc:encode({result, ok, ID})};
-handle_request(?method_post, #{type := <<"cancel">>}, ID) ->
- Reply = case grisp_connect_updater:cancel() of
+ {reply, ok, ID};
+handle_request([?method_post], #{type := <<"cancel">>}, ID) ->
+ case grisp_connect_updater:cancel() of
{error, grisp_updater_unavailable} ->
- {error, -10, grisp_updater_unavailable, undefined, ID};
+ {error, grisp_updater_unavailable, undefined, undefined, ID};
ok ->
- {result, ok, ID}
- end,
- {send_response, grisp_connect_jsonrpc:encode(Reply)};
+ {reply, ok, ID}
+ end;
handle_request(_T, _P, ID) ->
- Error = {internal_error, method_not_found, ID},
- FormattedError = format_error(Error),
- {send_response, grisp_connect_jsonrpc:encode(FormattedError)}.
-
-handle_response(Response) ->
- {ID, Reply} = case Response of
- {result, Result, ID0} ->
- {ID0, {ok, Result}};
- {error, Code, _Message, _Data, ID0} ->
- {ID0, {error, error_atom(Code)}}
- end,
- {handle_response, ID, Reply}.
-
-error_atom(-1) -> device_not_linked;
-error_atom(-2) -> token_expired;
-error_atom(-3) -> device_already_linked;
-error_atom(-4) -> invalid_token;
-error_atom(_) -> jsonrpc_error.
-
-id() ->
- list_to_binary(integer_to_list(erlang:unique_integer())).
+ {error, method_not_found, undefined, undefined, ID}.
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 7e53457..f9f9181 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -5,6 +5,12 @@
%% @end
-module(grisp_connect_client).
+-behaviour(gen_statem).
+
+-include("grisp_connect_internal.hrl").
+
+-import(grisp_connect_utils, [as_bin/1]).
+
% External API
-export([start_link/0]).
-export([connect/0]).
@@ -13,12 +19,9 @@
-export([notify/3]).
% Internal API
--export([connected/0]).
--export([disconnected/0]).
--export([handle_message/1]).
-export([reboot/0]).
--behaviour(gen_statem).
+% Behaviour gen_statem callback functions
-export([init/1, terminate/3, code_change/4, callback_mode/0]).
% State Functions
@@ -27,18 +30,50 @@
-export([connecting/3]).
-export([connected/3]).
--include_lib("kernel/include/logger.hrl").
+
+%--- Types ---------------------------------------------------------------------
+
+-record(data, {
+ domain :: binary(),
+ port :: inet:port_number(),
+ ws_path :: binary(),
+ ws_transport :: tcp | tls,
+ conn :: undefined | pid(),
+ retry_count = 0 :: non_neg_integer()
+}).
+
+-type data() :: #data{}.
+-type on_result_fun() :: fun((data(), Result :: term()) -> data()).
+-type on_error_fun() :: fun((data(), local | remote,
+ Code :: atom() | integer(),
+ Message :: undefined | binary(),
+ Data :: term()) -> data()).
+
+
+%--- Macros --------------------------------------------------------------------
-define(STD_TIMEOUT, 1000).
+-define(CONNECT_TIMEOUT, 2000).
+-define(ENV(KEY, GUARDS), fun() ->
+ case application:get_env(grisp_connect, KEY) of
+ {ok, V} when GUARDS -> V;
+ {ok, V} -> erlang:exit({invalid_env, KEY, V});
+ undefined -> erlang:exit({missing_env, KEY})
+ end
+end()).
+-define(ENV(KEY, GUARDS, CONV), fun() ->
+ case application:get_env(grisp_connect, KEY) of
+ {ok, V} when GUARDS -> CONV;
+ {ok, V} -> erlang:exit({invalid_env, KEY, V});
+ undefined -> erlang:exit({missing_env, KEY})
+ end
+end()).
-define(HANDLE_COMMON,
?FUNCTION_NAME(EventType, EventContent, Data) ->
handle_common(EventType, EventContent, ?FUNCTION_NAME, Data)).
--record(data, {
- requests = #{}
-}).
-% API
+%--- External API Functions ----------------------------------------------------
start_link() ->
gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -47,7 +82,9 @@ connect() ->
gen_statem:cast(?MODULE, ?FUNCTION_NAME).
is_connected() ->
- gen_statem:call(?MODULE, ?FUNCTION_NAME).
+ try gen_statem:call(?MODULE, ?FUNCTION_NAME)
+ catch exit:noproc -> false
+ end.
request(Method, Type, Params) ->
gen_statem:call(?MODULE, {?FUNCTION_NAME, Method, Type, Params}).
@@ -55,35 +92,47 @@ request(Method, Type, Params) ->
notify(Method, Type, Params) ->
gen_statem:cast(?MODULE, {?FUNCTION_NAME, Method, Type, Params}).
-connected() ->
- gen_statem:cast(?MODULE, ?FUNCTION_NAME).
-disconnected() ->
- gen_statem:cast(?MODULE, ?FUNCTION_NAME).
-
-handle_message(Payload) ->
- gen_statem:cast(?MODULE, {?FUNCTION_NAME, Payload}).
+%--- Internal API Functions ----------------------------------------------------
reboot() ->
erlang:send_after(1000, ?MODULE, reboot).
-% gen_statem CALLBACKS ---------------------------------------------------------
+
+%--- Behaviour gen_statem Callback Functions -----------------------------------
init([]) ->
- {ok, Connect} = application:get_env(grisp_connect, connect),
- NextState = case Connect of
+ process_flag(trap_exit, true),
+ AutoConnect = ?ENV(connect, is_boolean(V)),
+ Domain = ?ENV(domain, is_binary(V) orelse is_list(V) orelse is_atom(V), as_bin(V)),
+ Port = ?ENV(port, is_integer(V) andalso V >= 0 andalso V < 65536),
+ WsTransport = ?ENV(ws_transport, V =:= tls orelse V =:= tcp),
+ WsPath = ?ENV(ws_path, is_binary(V) orelse is_list(V), as_bin(V)),
+ Data = #data{
+ domain = Domain,
+ port = Port,
+ ws_transport = WsTransport,
+ ws_path = WsPath
+ },
+ % The error list is put in a persistent term to not add noise to the state.
+ persistent_term:put({?MODULE, self()}, generic_errors()),
+ NextState = case AutoConnect of
true -> waiting_ip;
false -> idle
end,
- {ok, NextState, #data{}}.
+ {ok, NextState, Data}.
-terminate(_Reason, _State, _Data) -> ok.
+terminate(Reason, _State, Data) ->
+ conn_close(Data, Reason),
+ persistent_term:erase({?MODULE, self()}),
+ ok.
code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}.
callback_mode() -> [state_functions, state_enter].
-%%% STATE CALLBACKS ------------------------------------------------------------
+
+%--- Behaviour gen_statem State Callback Functions -----------------------------
idle(enter, _OldState, _Data) ->
keep_state_and_data;
@@ -91,30 +140,47 @@ idle(cast, connect, Data) ->
{next_state, waiting_ip, Data};
?HANDLE_COMMON.
-waiting_ip(enter, _OldState, _Data) ->
- {keep_state_and_data, [{state_timeout, 0, retry}]};
-waiting_ip(state_timeout, retry, Data) ->
+waiting_ip(enter, _OldState, Data) ->
+ Delay = case Data#data.retry_count > 0 of
+ true -> ?STD_TIMEOUT;
+ false -> 0
+ end,
+ {keep_state_and_data, [{state_timeout, Delay, retry}]};
+waiting_ip(state_timeout, retry, Data = #data{retry_count = RetryCount}) ->
case check_inet_ipv4() of
{ok, IP} ->
?LOG_INFO(#{event => checked_ip, ip => IP}),
{next_state, connecting, Data};
invalid ->
?LOG_DEBUG(#{event => waiting_ip}),
- {next_state, waiting_ip, Data, [{state_timeout, ?STD_TIMEOUT, retry}]}
+ {next_state, waiting_ip, Data#data{retry_count = RetryCount + 1},
+ [{state_timeout, ?STD_TIMEOUT, retry}]}
end;
?HANDLE_COMMON.
-connecting(enter, _OldState, _Data) ->
- {ok, Domain} = application:get_env(grisp_connect, domain),
- {ok, Port} = application:get_env(grisp_connect, port),
- ?LOG_NOTICE(#{event => connecting, domain => Domain, port => Port}),
- grisp_connect_ws:connect(Domain, Port),
- keep_state_and_data;
-connecting(cast, connected, Data) ->
- ?LOG_NOTICE(#{event => connected}),
- {next_state, connected, Data};
-connecting(cast, disconnected, _Data) ->
- repeat_state_and_data;
+connecting(enter, _OldState, Data) ->
+ {keep_state, Data, [{state_timeout, 0, connect}]};
+connecting(state_timeout, connect,
+ Data = #data{conn = undefined, retry_count = RetryCount}) ->
+ ?GRISP_INFO("Connecting to grisp.io", [], #{event => connecting}),
+ case conn_start(Data) of
+ {ok, Data2} ->
+ {keep_state, Data2, [{state_timeout, ?CONNECT_TIMEOUT, timeout}]};
+ {error, Reason} ->
+ ?LOG_WARNING("Failed to connect to grisp.io: ~p", [Reason],
+ #{event => connection_failed, reason => Reason}),
+ {next_state, waiting_ip, Data#data{retry_count = RetryCount + 1}}
+ end;
+connecting(state_timeout, timeout, Data = #data{retry_count = RetryCount}) ->
+ Reason = connect_timeout,
+ ?GRISP_WARN("Timeout while connecting to grisp.io", [],
+ #{event => connection_failed, reason => Reason}),
+ Data2 = conn_close(Data, Reason),
+ {next_state, waiting_ip, Data2#data{retry_count = RetryCount + 1}};
+connecting(info, {conn, Conn, connected}, Data = #data{conn = Conn}) ->
+ % Received from the connection process
+ ?GRISP_INFO("Connected to grisp.io", [], #{event => connected}),
+ {next_state, connected, Data#data{retry_count = 0}};
?HANDLE_COMMON.
connected(enter, _OldState, _Data) ->
@@ -122,34 +188,15 @@ connected(enter, _OldState, _Data) ->
keep_state_and_data;
connected({call, From}, is_connected, _) ->
{keep_state_and_data, [{reply, From, true}]};
-connected(cast, disconnected, Data) ->
- ?LOG_WARNING(#{event => disconnected}),
- grisp_connect_log_server:stop(),
- {next_state, waiting_ip, Data};
-connected(cast, {handle_message, Payload}, #data{requests = Requests} = Data) ->
- Responses = grisp_connect_api:handle_msg(Payload),
- % A reduce operation is needed to support jsonrpc batch comunications
- case Responses of
- [] ->
- keep_state_and_data;
- [{send_response, Response}] -> % Response for a GRiSP.io request
- grisp_connect_ws:send(Response),
- keep_state_and_data;
- [{handle_response, ID, Response}] -> % handle a GRiSP.io response
- {OtherRequests, Actions} = dispatch_response(ID, Response, Requests),
- {keep_state, Data#data{requests = OtherRequests}, Actions}
- end;
-connected({call, From}, {request, Method, Type, Params},
- #data{requests = Requests} = Data) ->
- {ID, Payload} = grisp_connect_api:request(Method, Type, Params),
- grisp_connect_ws:send(Payload),
- NewRequests = Requests#{ID => From},
- {keep_state,
- Data#data{requests = NewRequests},
- [{{timeout, ID}, request_timeout(), request}]};
-connected(cast, {notify, Method, Type, Params}, _Data) ->
- Payload = grisp_connect_api:notify(Method, Type, Params),
- grisp_connect_ws:send(Payload),
+connected(info, {conn, Conn, Msg}, Data = #data{conn = Conn}) ->
+ handle_connection_message(Data, Msg);
+connected({call, From}, {request, Method, Type, Params}, Data) ->
+ Data2 = conn_post(Data, Method, Type, Params,
+ fun(D, R) -> gen_statem:reply(From, {ok, R}), D end,
+ fun(D, _, C, _, _) -> gen_statem:reply(From, {error, C}), D end),
+ {keep_state, Data2};
+connected(cast, {notify, Method, Type, Params}, Data) ->
+ conn_notify(Data, Method, Type, Params),
keep_state_and_data;
?HANDLE_COMMON.
@@ -164,14 +211,19 @@ when State =/= connected ->
handle_common(cast, {notify, _Method, _Type, _Params}, _State, _Data) ->
% We ignore notifications sent while disconnected
keep_state_and_data;
-handle_common({timeout, ID}, request, _, #data{requests = Requests} = Data) ->
- Caller = maps:get(ID, Requests),
- {keep_state,
- Data#data{requests = maps:remove(ID, Requests)},
- [{reply, Caller, {error, timeout}}]};
handle_common(info, reboot, _, _) ->
init:stop(),
keep_state_and_data;
+handle_common(info, {'EXIT', Conn, Reason}, _State,
+ Data = #data{conn = Conn, retry_count = RetryCount}) ->
+ % The connection process died
+ ?GRISP_WARN("The connection to grisp.io died: ~p", [Reason],
+ #{event => connection_failed, reason => Reason}),
+ {next_state, waiting_ip, conn_died(Data#data{retry_count = RetryCount + 1})};
+handle_common(info, {conn, Conn, Msg}, State, _Data) ->
+ ?LOG_DEBUG("Received message from unknown connection ~p in state ~w: ~p",
+ [Conn, State, Msg]),
+ keep_state_and_data;
handle_common(cast, Cast, _, _) ->
error({unexpected_cast, Cast});
handle_common({call, _}, Call, _, _) ->
@@ -183,22 +235,113 @@ handle_common(info, Info, State, Data) ->
data => Data}),
keep_state_and_data.
-% INTERNALS --------------------------------------------------------------------
-
-dispatch_response(ID, Response, Requests) ->
- case maps:take(ID, Requests) of
- {Caller, OtherRequests} ->
- Actions = [{{timeout, ID}, cancel}, {reply, Caller, Response}],
- {OtherRequests, Actions};
- error ->
- ?LOG_DEBUG(#{event => ?FUNCTION_NAME, reason => {missing_id, ID},
- data => Response}),
- {Requests, []}
+
+%--- Internal Functions --------------------------------------------------------
+
+generic_errors() -> [
+ {device_not_linked, -1, <<"Device no linked">>},
+ {token_expired, -2, <<"Token expired">>},
+ {device_already_linked, -3, <<"Device already linked">>},
+ {invalid_token, -4, <<"Invalid token">>},
+ {grisp_updater_unavailable, -10, <<"Software update unavailable">>},
+ {boot_system_not_validated, -12, <<"Boot system not validated">>},
+ {validate_from_unbooted, -13, <<"Validate from unbooted">>}
+].
+
+handle_connection_message(_Data, {response, _Result, #{on_result := undefined}}) ->
+ keep_state_and_data;
+handle_connection_message(Data, {response, Result, #{on_result := OnResult}}) ->
+ {keep_state, OnResult(Data, Result)};
+handle_connection_message(_Data, {remote_error, Code, Msg, _ErrorData,
+ #{on_error := undefined}}) ->
+ ?LOG_WARNING("Unhandled remote request error ~w: ~s", [Code, Msg]),
+ keep_state_and_data;
+handle_connection_message(Data, {remote_error, Code, Msg, ErrorData,
+ #{on_error := OnError}}) ->
+ {keep_state, OnError(Data, remote, Code, Msg, ErrorData)};
+
+handle_connection_message(_Data, {local_error, Reason,
+ #{on_error := undefined}}) ->
+ ?LOG_WARNING("Unhandled local request error ~w", [Reason]),
+ keep_state_and_data;
+handle_connection_message(Data, {local_error, Reason,
+ #{on_error := OnError}}) ->
+ {keep_state, OnError(Data, local, Reason, undefined, undefined)};
+handle_connection_message(Data, Msg) ->
+ case grisp_connect_api:handle_msg(Msg) of
+ ok -> keep_state_and_data;
+ {reply, Result, ReqRef} ->
+ conn_reply(Data, Result, ReqRef),
+ keep_state_and_data
+ end.
+
+% Connection Functions
+
+conn_start(Data = #data{conn = undefined,
+ domain = Domain,
+ port = Port,
+ ws_path = WsPath,
+ ws_transport = WsTransport}) ->
+ WsPingTimeout = ?ENV(ws_ping_timeout, V =:= infinity orelse is_integer(V)),
+ WsReqTimeout = ?ENV(ws_request_timeout, V =:= infinity orelse is_integer(V)),
+ ConnTransport = case WsTransport of
+ tcp -> tcp;
+ tls -> {tls, grisp_cryptoauth_tls:options(Domain)}
+ end,
+ ErrorList = persistent_term:get({?MODULE, self()}),
+ ConnOpts = #{
+ domain => Domain,
+ port => Port,
+ transport => ConnTransport,
+ path => WsPath,
+ errors => ErrorList,
+ ping_timeout => WsPingTimeout,
+ request_timeout => WsReqTimeout
+ },
+ case grisp_connect_connection:start_link(self(), ConnOpts) of
+ {error, _Reason} = Error -> Error;
+ {ok, Conn} -> {ok, Data#data{conn = Conn}}
+ end.
+
+% Safe to call in any state
+conn_close(Data = #data{conn = undefined}, _Reason) ->
+ Data;
+conn_close(Data = #data{conn = Conn}, _Reason) ->
+ grisp_connect_log_server:stop(),
+ catch grisp_connect_connection:disconnect(Conn),
+ Data#data{conn = undefined}.
+
+% Safe to call in any state
+conn_died(Data) ->
+ grisp_connect_log_server:stop(),
+ Data#data{conn = undefined}.
+
+-spec conn_post(data(), grisp_connect_connection:method(), atom(), map(),
+ undefined | on_result_fun(), undefined | on_error_fun())
+ -> data().
+conn_post(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
+ when Conn =/= undefined ->
+ ReqCtx = #{on_result => OnResult, on_error => OnError},
+ Params2 = maps:put(type, Type, Params),
+ try grisp_connect_connection:post(Conn, Method, Params2, ReqCtx) of
+ _ -> Data
+ catch
+ _:Reason when OnError =/= undefined ->
+ OnError(Data, local, Reason, undefined, undefined);
+ _:_ ->
+ Data
end.
-request_timeout() ->
- {ok, V} = application:get_env(grisp_connect, ws_requests_timeout),
- V.
+conn_notify(#data{conn = Conn}, Method, Type, Params)
+ when Conn =/= undefined ->
+ Params2 = maps:put(type, Type, Params),
+ catch grisp_connect_connection:notify(Conn, Method, Params2),
+ ok.
+
+conn_reply(#data{conn = Conn}, Result, ReqRef)
+ when Conn =/= undefined ->
+ catch grisp_connect_connection:reply(Conn, Result, ReqRef),
+ ok.
% IP check functions
diff --git a/src/grisp_connect_connection.erl b/src/grisp_connect_connection.erl
new file mode 100644
index 0000000..14dec02
--- /dev/null
+++ b/src/grisp_connect_connection.erl
@@ -0,0 +1,741 @@
+%% @doc JSONRpc 2.0 Websocket connection
+-module(grisp_connect_connection).
+
+-behaviour(gen_server).
+
+-include("grisp_connect_internal.hrl").
+
+-import(grisp_connect_utils, [as_bin/1]).
+-import(grisp_connect_utils, [parse_method/1]).
+-import(grisp_connect_utils, [format_method/1]).
+
+% API Functions
+-export([start_link/2]).
+-export([request/3]).
+-export([post/4]).
+-export([notify/3]).
+-export([reply/3]).
+-export([error/5]).
+-export([disconnect/1]).
+
+% Behaviour gen_server Callbacks
+-export([init/1]).
+-export([handle_call/3]).
+-export([handle_cast/2]).
+-export([handle_info/2]).
+-export([terminate/2]).
+
+
+%--- Types ---------------------------------------------------------------------
+
+-record(batch, {
+ bref :: reference(),
+ refcount :: pos_integer(),
+ responses :: list()
+}).
+
+-record(inbound_req, {
+ method :: method(),
+ id :: binary() | integer(),
+ bref :: undefined | reference() % Set if part of a batch
+}).
+
+-record(outbound_req, {
+ method :: method(),
+ id :: binary() | integer(),
+ tref :: undefined | reference(),
+ from :: undefined | gen_server:from(),
+ ctx :: undefined | term()
+}).
+
+-record(state, {
+ handler :: pid(),
+ uri :: iodata(),
+ domain :: binary(),
+ port :: inet:port_number(),
+ path :: binary(),
+ ping_timeout :: infinity | pos_integer(),
+ request_timeout :: infinity | pos_integer(),
+ batches = #{} :: #{reference() => #batch{}},
+ inbound = #{} :: #{binary() | integer() => #inbound_req{}},
+ outbound = #{} :: #{binary() | integer() => #outbound_req{}},
+ gun_pid :: undefined | pid(),
+ gun_ref :: undefined | reference(),
+ ws_stream :: undefined | gun:stream_ref(),
+ connected = false :: boolean(),
+ ping_tref :: undefined | reference()
+}).
+
+-type error_mapping() :: {atom(), integer(), binary()}.
+-type method() :: atom() | binary() | [atom() | binary()].
+-type start_options() :: #{
+ domain := atom() | string() | binary(),
+ port := inet:port_number(),
+ %FIXME: dialyzer do no like ssl:tls_client_option(), maybe some erlang version issue
+ transport := tcp | tls | {tls, TlsOpts :: list()},
+ path := atom() | string() | binary(),
+ errors => [error_mapping()],
+ ping_timeout => infinity | pos_integer(),
+ request_timeout => infinity | pos_integer()
+}.
+
+% Type specfication of the messages that are sent to the handler:
+-type handler_messages() ::
+ {conn, pid(), connected}
+ | {conn, pid(), {request, method(), Params :: map() | list(), ReqRef :: binary() | integer()}}
+ | {conn, pid(), {notification, method(), Params :: map() | list()}}
+ | {conn, pid(), {response, Result :: term(), Ctx :: term()}}
+ | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term()}}
+ | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term(), Ctx :: term()}}
+ | {conn, pid(), {local_error, Reason:: atom(), Ctx :: term()}}.
+
+-export_type([error_mapping/0, method/0, handler_messages/0]).
+
+
+%--- Macros --------------------------------------------------------------------
+
+-define(ENABLE_TRACE, false).
+-if(?ENABLE_TRACE =:= true).
+-define(TRACE_OUTPUT(ARG), ?GRISP_DEBUG("<<<<<<<<<< ~s", [ARG])).
+-define(TRACE_INPUT(ARG), ?GRISP_DEBUG(">>>>>>>>>> ~s", [ARG])).
+-else.
+-define(TRACE_OUTPUT(ARG), ok).
+-define(TRACE_INPUT(ARG), ok).
+-endif.
+
+-define(DEFAULT_PING_TIMEOUT, 60_000).
+-define(DEFAULT_REQUEST_TIMEOUT, 5_000).
+-define(DEFAULT_TRANSPORT, tcp).
+
+-define(DEFAULT_JSONRPC_ERRORS, [
+ {invalid_json, -32700, <<"Parse error">>},
+ {invalid_request, -32600, <<"Invalid Request">>},
+ {method_not_found, -32601, <<"Method not found">>},
+ {invalid_params, -32602, <<"Invalid parameters">>},
+ {internal_error, -32603, <<"Internal error">>}
+]).
+
+
+%--- API Functions -------------------------------------------------------------
+
+-spec start_link(Handler :: pid(), Options :: start_options()) ->
+ {ok, Conn :: pid()} | {error, Reason :: term()}.
+start_link(Handler, Opts = #{domain := _, port := _, path := _}) ->
+ gen_server:start_link(?MODULE, [Handler, Opts], []).
+
+-spec request(Conn :: pid(), Method :: method(), Params :: map()) ->
+ {ok, Result :: term()} | {error, timeout} | {error, not_connected}
+ | {error, Code :: integer(),
+ Message :: undefined | binary(), Data :: term()}.
+request(Conn, Method, Params) ->
+ case gen_server:call(Conn, {request, parse_method(Method), Params}) of
+ {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack);
+ Other -> Other
+ end.
+
+-spec post(Conn :: pid(), Method :: method(), Params :: map(),
+ ReqCtx :: term()) -> ok | {error, not_connected}.
+post(Conn, Method, Params, ReqCtx) ->
+ case gen_server:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
+ {ok, CallResult} -> CallResult;
+ {error, _Reason} = Error -> Error;
+ {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ end.
+
+-spec notify(Conn :: pid(), Method :: method(), Params :: map()) ->
+ ok | {error, not_connected}.
+notify(Conn, Method, Params) ->
+ case gen_server:call(Conn, {notify, parse_method(Method), Params}) of
+ {ok, CallResult} -> CallResult;
+ {error, _Reason} = Error -> Error;
+ {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ end.
+
+-spec reply(Conn :: pid(), Result :: any(), ReqRef :: binary()) ->
+ ok | {error, not_connected}.
+reply(Conn, Result, ReqRef) ->
+ case gen_server:call(Conn, {reply, Result, ReqRef}) of
+ {ok, CallResult} -> CallResult;
+ {error, _Reason} = Error -> Error;
+ {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ end.
+
+-spec error(Conn :: pid(), Code :: atom() | integer(),
+ Message :: undefined | binary(), Data :: term(),
+ ReqRef :: undefined | binary()) ->
+ ok | {error, not_connected}.
+error(Conn, Code, Message, Data, ReqRef) ->
+ case gen_server:call(Conn, {error, Code, Message, Data, ReqRef}) of
+ {ok, CallResult} -> CallResult;
+ {error, _Reason} = Error -> Error;
+ {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ end.
+
+-spec disconnect(Conn :: pid()) -> ok.
+disconnect(Conn) ->
+ gen_server:call(Conn, disconnect).
+
+
+%--- Behaviour gen_server Callbacks --------------------------------------------
+
+init([Handler, Opts]) ->
+ process_flag(trap_exit, true), % To ensure terminate/2 is called
+ #{domain := Domain, port := Port, path := Path} = Opts,
+ PingTimeout = maps:get(ping_timeout, Opts, ?DEFAULT_PING_TIMEOUT),
+ ReqTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT),
+ Transport = maps:get(transport, Opts, ?DEFAULT_TRANSPORT),
+ State = #state{
+ handler = Handler,
+ uri = format_ws_uri(Transport, Domain, Port, Path),
+ domain = as_bin(Domain),
+ port = Port,
+ path = as_bin(Path),
+ ping_timeout = PingTimeout,
+ request_timeout = ReqTimeout
+ },
+ index_errors(?DEFAULT_JSONRPC_ERRORS),
+ index_errors(maps:get(errors, Opts, [])),
+ case connection_start(State, Transport) of
+ {ok, _State2} = Result -> Result;
+ {error, Reason} -> {stop, Reason}
+ end.
+
+handle_call({request, Method, _Params}, _From,
+ State = #state{connected = false}) ->
+ ?GRISP_INFO("Request ~s performed while disconnected",
+ [format_method(Method)],
+ #{event => rpc_request_error, method => Method,
+ reason => not_connected}),
+ {reply, {error, not_connected}, State};
+handle_call({request, Method, Params}, From, State) ->
+ try send_request(State, Method, Params, From, undefined) of
+ State2 -> {noreply, State2}
+ catch
+ C:badarg:S -> {reply, {exception, C, badarg, S}, State};
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call({post, Method, _Params, ReqCtx}, _From,
+ State = #state{handler = Handler, connected = false}) ->
+ ?GRISP_INFO("Request ~s posted while disconnected",
+ [format_method(Method)],
+ #{event => rpc_post_error, method => Method,
+ reason => not_connected}),
+ % Notify the handler anyway so it doesn't have to make a special case
+ Handler ! {conn, self(), {local_error, not_connected, ReqCtx}},
+ {reply, {error, not_connected}, State};
+handle_call({post, Method, Params, ReqCtx}, _From, State) ->
+ try send_request(State, Method, Params, undefined, ReqCtx) of
+ State2 -> {reply, {ok, ok}, State2}
+ catch
+ C:badarg:S -> {reply, {exception, C, badarg, S}, State};
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call({notify, Method, _Params}, _From,
+ State = #state{connected = false}) ->
+ ?GRISP_INFO("Notification ~s posted while disconnected",
+ [format_method(Method)],
+ #{event => rpc_notify_error, method => Method,
+ reason => not_connected}),
+ {noreply, State};
+handle_call({notify, Method, Params}, _From, State) ->
+ try send_notification(State, Method, Params) of
+ State2 -> {reply, {ok, ok}, State2}
+ catch
+ C:badarg:S -> {reply, {exception, C, badarg, S}, State};
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call({reply, _Result, ReqRef}, _From,
+ State = #state{connected = false}) ->
+ ?GRISP_INFO("Reply to ~s posted while disconnected",
+ [inbound_req_tag(State, ReqRef)],
+ #{event => rpc_reply_error, ref => ReqRef,
+ method => inbound_method(State, ReqRef),
+ reason => not_connected}),
+ {reply, {error, not_connected}, State};
+handle_call({reply, Result, ReqRef}, _From, State) ->
+ try send_response(State, Result, ReqRef) of
+ State2 -> {reply, {ok, ok}, State2}
+ catch
+ C:badarg:S -> {reply, {exception, C, badarg, S}, State};
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call({error, Code, _Message, _Data, undefined}, _From,
+ State = #state{connected = false}) ->
+ ?GRISP_INFO("Error ~w posted while disconnected", [Code],
+ #{event => rpc_error_error, code => Code,
+ reason => not_connected}),
+ {reply, {error, not_connected}, State};
+handle_call({error, Code, _Message, _Data, ReqRef}, _From,
+ State = #state{connected = false}) ->
+ ?GRISP_INFO("Error ~w to ~s posted while disconnected",
+ [Code, inbound_req_tag(State, ReqRef)],
+ #{event => rpc_error_error, code => Code, ref => ReqRef,
+ reason => not_connected}),
+ {reply, {error, not_connected}, State};
+handle_call({error, Code, Message, Data, ReqRef}, _From, State) ->
+ try send_error(State, Code, Message, Data, ReqRef) of
+ State2 -> {reply, {ok, ok}, State2}
+ catch
+ C:badarg:S -> {reply, {exception, C, badarg, S}, State};
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call(disconnect, _From, State) ->
+ try connection_close(State) of
+ State2 -> {reply, {ok, ok}, State2}
+ catch
+ C:R:S -> {stop, R, {exception, C, R, S}, State}
+ end;
+handle_call(Call, From, State) ->
+ ?GRISP_ERROR("Unexpected call from ~p to ~s: ~p", [From, ?MODULE, Call],
+ #{event => unexpected_call, from => From, message => Call}),
+ {stop, {unexpected_call, Call}, {error, unexpected_call}, State}.
+
+handle_cast(Cast, State) ->
+ Reason = {unexpected_cast, Cast},
+ ?GRISP_ERROR("Unexpected cast to ~s: ~p", [?MODULE, Cast],
+ #{event => unexpected_cast, message => Cast}),
+ {stop, Reason, State}.
+
+handle_info({gun_up, GunPid, _}, State = #state{gun_pid = GunPid}) ->
+ ?GRISP_DEBUG("Connection to ~s established",
+ [State#state.uri],
+ #{event => ws_connection_enstablished, uri => State#state.uri}),
+ {noreply, connection_upgrade(State)};
+handle_info({gun_up, Pid, http} = _Msg, State = #state{gun_pid = GunPid}) ->
+ ?GRISP_DEBUG("Ignoring unexpected gun_up message"
+ " from pid ~p, current pid is ~p", [Pid, GunPid],
+ #{event => unexpected_gun_message, message => _Msg}),
+ {noreply, State};
+handle_info({gun_upgrade, Pid, Stream, [<<"websocket">>], _},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_DEBUG("Connection to ~s upgraded to websocket", [State#state.uri],
+ #{event => ws_upgraded, uri => State#state.uri}),
+ {noreply, connection_established(State)};
+handle_info({gun_response, Pid, Stream, _, Status, _Headers},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_INFO("Connection to ~s failed to upgrade to websocket: ~p",
+ [State#state.uri, Status],
+ #{event => ws_upgrade_failed, uri => State#state.uri,
+ status => Status}),
+ {stop, ws_upgrade_failed, connection_close(State)};
+handle_info({gun_ws, Pid, Stream, ping},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ {noreply, schedule_ping_timeout(State)};
+handle_info({gun_ws, Pid, Stream, {text, Text}},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ {noreply, process_data(State, Text)};
+handle_info({gun_ws, Pid, Stream, close},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_INFO("Connection to ~s closed without code", [State#state.uri],
+ #{event => ws_stream_closed, uri => State#state.uri}),
+ {stop, normal, connection_closed(State, closed)};
+handle_info({gun_ws, Pid, Stream, {close, Code, Message}},
+ State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_INFO("Connection to ~s closed: ~s (~w)",
+ [State#state.uri, Message, Code],
+ #{event => ws_stream_closed, uri => State#state.uri,
+ code => Code, reason => Message}),
+ {stop, normal, connection_closed(State, closed)};
+handle_info({gun_error, Pid, _Stream, Reason},
+ State = #state{gun_pid = Pid}) ->
+ ?GRISP_INFO("Connection to ~s got an error: ~p",
+ [State#state.uri, Reason],
+ #{event => ws_error, uri => State#state.uri,
+ reason => Reason}),
+ {stop, Reason, connection_close(State)};
+handle_info({gun_down, Pid, ws, Reason, [Stream]},
+ State = #state{gun_pid = Pid, ws_stream = Stream})
+ when Reason =:= closed; Reason =:= {error, closed}; Reason =:= normal ->
+ ?GRISP_INFO("Connection to ~s was closed by the server", [State#state.uri],
+ #{event => ws_closed_by_peer, uri => State#state.uri,
+ reason => closed}),
+ {stop, normal, connection_close(State)};
+handle_info({'DOWN', _, process, Pid, Reason},
+ State = #state{gun_pid = Pid}) ->
+ ?GRISP_INFO("gun process of the connection to ~s crashed: ~p",
+ [State#state.uri, Reason],
+ #{event => ws_gun_crash, uri => State#state.uri,
+ reason => Reason}),
+ {stop, Reason, connection_closed(State, gun_crashed)};
+handle_info(ping_timeout, State) ->
+ ?GRISP_INFO("Connection to ~s timed out", [State#state.uri],
+ #{event => ws_ping_timeout, uri => State#state.uri}),
+ {stop, normal, connection_close(State)};
+handle_info({outbound_timeout, ReqRef}, State) ->
+ ?GRISP_INFO("Request ~s time to ~s timed out",
+ [outbound_req_tag(State, ReqRef), State#state.uri],
+ #{event => rpc_request_timeout_error, uri => State#state.uri,
+ ref => ReqRef, method => outbound_method(State, ReqRef)}),
+ {noreply, outbound_timeout(State, ReqRef)};
+handle_info(Msg, State) ->
+ ?GRISP_WARN("Unexpected info message to ~s: ~p",
+ [?MODULE, Msg],
+ #{event => unexpected_info, message => Msg}),
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ connection_close(State),
+ persistent_term:erase({?MODULE, self(), tags}),
+ persistent_term:erase({?MODULE, self(), codes}),
+ ok.
+
+
+%--- INTERNAL FUNCTION ---------------------------------------------------------
+
+format_ws_uri(Transport, Domain, Port, Path) ->
+ Proto = case Transport of
+ tcp -> <<"ws">>;
+ tls -> <<"wss">>;
+ {tls, _} -> <<"wss">>
+ end,
+ ?FORMAT("~s://~s:~w~s", [Proto, Domain, Port, Path]).
+
+make_reqref() ->
+ list_to_binary(integer_to_list(erlang:unique_integer())).
+
+send_after(infinity, _Message) -> undefined;
+send_after(Timeout, Message) ->
+ erlang:send_after(Timeout, self(), Message).
+
+cancel_timer(undefined) -> ok;
+cancel_timer(TRef) ->
+ erlang:cancel_timer(TRef).
+
+schedule_ping_timeout(State = #state{ping_timeout = Timeout}) ->
+ State2 = cancel_ping_timeout(State),
+ TRef = send_after(Timeout, ping_timeout),
+ State2#state{ping_tref = TRef}.
+
+cancel_ping_timeout(State = #state{ping_tref = undefined}) ->
+ State;
+cancel_ping_timeout(State = #state{ping_tref = TRef}) ->
+ cancel_timer(TRef),
+ State#state{ping_tref = undefined}.
+
+% Returns either the method of the outbound request or its reference
+% as a binary if not found. Only mean for logging.
+outbound_req_tag(#state{outbound = ReqMap}, ReqRef) ->
+ case maps:find(ReqRef, ReqMap) of
+ error -> as_bin(ReqRef);
+ {ok, #outbound_req{method = Method}} -> format_method(Method)
+ end.
+
+outbound_method(#state{outbound = ReqMap}, ReqRef) ->
+ case maps:find(ReqRef, ReqMap) of
+ error -> undefined;
+ {ok, #outbound_req{method = Method}} -> Method
+ end.
+
+% Returns either the method of the inbound request or its reference
+% as a binary if not found. Only mean for logging.
+inbound_req_tag(#state{inbound = ReqMap}, ReqRef) ->
+ case maps:find(ReqRef, ReqMap) of
+ error -> as_bin(ReqRef);
+ {ok, #inbound_req{method = Method}} -> format_method(Method)
+ end.
+
+inbound_method(#state{inbound = ReqMap}, ReqRef) ->
+ case maps:find(ReqRef, ReqMap) of
+ error -> undefined;
+ {ok, #inbound_req{method = Method}} -> Method
+ end.
+
+index_errors(ErrorSpecs) ->
+ ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
+ ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
+ {ErrorTags2, ErrorCodes2} =
+ lists:foldl(fun({Tag, Code, Msg}, {Tags, Codes}) ->
+ {Tags#{Tag => {Code, Msg}}, Codes#{Code => {Tag, Msg}}}
+ end, {ErrorTags, ErrorCodes}, ErrorSpecs),
+ % The error list is put in a persistent term to not add noise to the state.
+ persistent_term:put({?MODULE, self(), tags}, ErrorTags2),
+ persistent_term:put({?MODULE, self(), codes}, ErrorCodes2),
+ ok.
+
+decode_error(Code, Message)
+ when is_integer(Code), Message =:= undefined ->
+ ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
+ case maps:find(Code, ErrorCodes) of
+ error -> {Code, undefined};
+ {ok, {Tag, DefaultMessage}} -> {Tag, DefaultMessage}
+ end;
+decode_error(Code, Message)
+ when is_integer(Code) ->
+ ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
+ case maps:find(Code, ErrorCodes) of
+ error -> {Code, Message};
+ {ok, {Tag, _DefaultMessage}} -> {Tag, Message}
+ end.
+
+encode_error(Code, Message)
+ when is_integer(Code), Message =:= undefined ->
+ ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
+ case maps:find(Code, ErrorCodes) of
+ error -> {Code, null};
+ {ok, {_Tag, DefaultMessage}} -> {Code, DefaultMessage}
+ end;
+encode_error(Code, Message)
+ when is_integer(Code) ->
+ {Code, Message};
+encode_error(Tag, Message)
+ when is_atom(Tag), Message =:= undefined ->
+ ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
+ case maps:find(Tag, ErrorTags) of
+ error -> erlang:error(badarg);
+ {ok, {Code, DefaultMessage}} -> {Code, DefaultMessage}
+ end;
+encode_error(Tag, Message)
+ when is_atom(Tag) ->
+ ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
+ case maps:find(Tag, ErrorTags) of
+ error -> erlang:error(badarg);
+ {ok, {Code, _DefaultMessage}} -> {Code, Message}
+ end.
+
+connection_start(State = #state{uri = Uri, domain = Domain, port = Port},
+ TransportSpec) ->
+ BaseGunOpts = #{protocols => [http], retry => 0},
+ GunOpts = case TransportSpec of
+ tcp -> BaseGunOpts#{transport => tcp};
+ tls -> BaseGunOpts#{transport => tls};
+ {tls, Opts} -> BaseGunOpts#{transport => tls, tls_opts => Opts}
+ end,
+ ?GRISP_DEBUG("Connecting to ~s", [Uri],
+ #{event => connecting, uri => Uri, options => GunOpts}),
+ case gun:open(binary_to_list(Domain), Port, GunOpts) of
+ {ok, GunPid} ->
+ GunRef = monitor(process, GunPid),
+ {ok, State#state{gun_pid = GunPid, gun_ref = GunRef}};
+ {error, Reason} ->
+ ?GRISP_ERROR("Failed to open connection to ~s: ~p", [Uri, Reason],
+ #{event => connection_failure, uri => Uri,
+ reason => Reason}),
+ {error, Reason}
+ end.
+
+connection_upgrade(State = #state{path = Path, gun_pid = GunPid}) ->
+ WsStream = gun:ws_upgrade(GunPid, Path,[], #{silence_pings => false}),
+ State#state{ws_stream = WsStream}.
+
+connection_established(State = #state{handler = Handler}) ->
+ State2 = schedule_ping_timeout(State#state{connected = true}),
+ Handler ! {conn, self(), connected},
+ State2.
+
+connection_close(State = #state{gun_pid = GunPid, gun_ref = GunRef})
+ when GunPid =/= undefined, GunRef =/= undefined ->
+ demonitor(GunRef),
+ gun:shutdown(GunPid),
+ State2 = cancel_ping_timeout(State),
+ State3 = requests_error(State2, not_connected),
+ State3#state{gun_pid = undefined, gun_ref = undefined,
+ ws_stream = undefined, connected = false};
+connection_close(State) ->
+ State2 = requests_error(State, not_connected),
+ State2#state{connected = false}.
+
+connection_closed(State, Reason) ->
+ State2 = cancel_ping_timeout(State),
+ State3 = requests_error(State2, Reason),
+ State3#state{gun_pid = undefined, gun_ref = undefined,
+ ws_stream = undefined, connected = false}.
+
+send_request(State, Method, Params, From, Ctx) ->
+ {ReqRef, State2} = outbound_add(State, Method, From, Ctx),
+ Msg = {request, format_method(Method), Params, ReqRef},
+ send_packet(State2, Msg).
+
+send_notification(State, Method, Params) ->
+ Msg = {notification, format_method(Method), Params},
+ send_packet(State, Msg).
+
+send_response(State, Result, ReqRef) ->
+ Msg = {result, Result, ReqRef},
+ inbound_response(State, Msg, ReqRef).
+
+send_error(State, Code, Message, Data, ReqRef) ->
+ {Code2, Message2} = encode_error(Code, Message),
+ Msg = {error, Code2, Message2, Data, ReqRef},
+ case ReqRef of
+ undefined -> send_packet(State, Msg);
+ _ -> inbound_response(State, Msg, ReqRef)
+ end.
+
+send_packet(State = #state{gun_pid = GunPid, ws_stream = Stream}, Packet) ->
+ Payload = grisp_connect_jsonrpc:encode(Packet),
+ ?TRACE_OUTPUT(Payload),
+ gun:ws_send(GunPid, Stream, {text, Payload}),
+ State.
+
+process_data(State = #state{batches = BatchMap}, Data) ->
+ ?TRACE_INPUT(Data),
+ DecodedData = grisp_connect_jsonrpc:decode(Data),
+ case DecodedData of
+ Messages when is_list(Messages) ->
+ BatchRef = make_ref(),
+ case process_messages(State, BatchRef, 0, [], Messages) of
+ {ReqCount, Replies, State2} when ReqCount > 0 ->
+ Batch = #batch{bref = BatchRef, refcount = ReqCount,
+ responses = Replies},
+ State2#state{batches = BatchMap#{BatchRef => Batch}};
+ {_ReqCount, [], State2} ->
+ State2;
+ {_ReqCount, [_|_] = Replies, State2} ->
+ % All the requests got a reply right away
+ send_packet(State2, Replies)
+ end;
+ Message when is_tuple(Message) ->
+ case process_messages(State, undefined, 0, [], [Message]) of
+ {_, [Reply], State2} ->
+ send_packet(State2, Reply);
+ {_, [], State2} ->
+ State2
+ end
+ end.
+
+process_messages(State, _BatchRef, ReqCount, Replies, []) ->
+ {ReqCount, lists:reverse(Replies), State};
+process_messages(State, BatchRef, ReqCount, Replies,
+ [{decoding_error, _, _, _, _} = Error | Rest]) ->
+ process_messages(State, BatchRef, ReqCount, [Error | Replies], Rest);
+process_messages(State, BatchRef, ReqCount, Replies,
+ [{request, RawMethod, Params, ReqRef} | Rest]) ->
+ Method = parse_method(RawMethod),
+ State2 = process_request(State, BatchRef, Method, Params, ReqRef),
+ process_messages(State2, BatchRef, ReqCount + 1, Replies, Rest);
+process_messages(State, BatchRef, ReqCount, Replies,
+ [{notification, RawMethod, Params} | Rest]) ->
+ Method = parse_method(RawMethod),
+ State2 = process_notification(State, Method, Params),
+ process_messages(State2, BatchRef, ReqCount, Replies, Rest);
+process_messages(State, BatchRef, ReqCount, Replies,
+ [{result, Result, ReqRef} | Rest]) ->
+ {State2, Replies2} = process_response(State, Replies, Result, ReqRef),
+ process_messages(State2, BatchRef, ReqCount, Replies2, Rest);
+process_messages(State, BatchRef, ReqCount, Replies,
+ [{error, Code, Message, Data, ReqRef} | Rest]) ->
+ {State2, Replies2} = process_error(State, Replies, Code,
+ Message, Data, ReqRef),
+ process_messages(State2, BatchRef, ReqCount, Replies2, Rest).
+
+process_request(State = #state{handler = Handler},
+ BatchRef, Method, Params, ReqRef) ->
+ Handler ! {conn, self(), {request, Method, Params, ReqRef}},
+ inbound_add(State, BatchRef, Method, ReqRef).
+
+process_notification(State = #state{handler = Handler}, Method, Params) ->
+ Handler ! {conn, self(), {notification, Method, Params}},
+ State.
+
+process_response(State = #state{handler = Handler}, Replies, Result, ReqRef) ->
+ case outbound_del(State, ReqRef) of
+ {error, not_found} ->
+ %FIXME: Not sure what error should be returned...
+ Error = {invalid_request, -32600, <<"Result for unknown request">>},
+ {State, [Error | Replies]};
+ {ok, _Method, undefined, Ctx, State2} ->
+ Handler ! {conn, self(), {response, Result, Ctx}},
+ {State2, Replies};
+ {ok, _, From, _, State2} ->
+ gen_server:reply(From, {ok, Result}),
+ {State2, Replies}
+ end.
+
+process_error(State = #state{handler = Handler},
+ Replies, Code, Message, Data, undefined) ->
+ {Code2, Message2} = decode_error(Code, Message),
+ Handler ! {conn, self(), {remote_error, Code2, Message2, Data}},
+ {State, Replies};
+process_error(State = #state{handler = Handler},
+ Replies, Code, Message, Data, ReqRef) ->
+ case outbound_del(State, ReqRef) of
+ {error, not_found} ->
+ %FIXME: Not sure what error should be returned...
+ Error = {invalid_request, -32600, <<"Error for unknown request">>},
+ {State, [Error | Replies]};
+ {ok, _Method, undefined, Ctx, State2} ->
+ {Code2, Message2} = decode_error(Code, Message),
+ Handler ! {conn, self(), {remote_error, Code2, Message2, Data, Ctx}},
+ {State2, Replies};
+ {ok, _, From, _, State2} ->
+ {Code2, Message2} = decode_error(Code, Message),
+ gen_server:reply(From, {remote_error, Code2, Message2, Data}),
+ {State2, Replies}
+ end.
+
+inbound_response(State = #state{batches = BatchMap, inbound = ReqMap},
+ Message, ReqRef) ->
+ case maps:take(ReqRef, ReqMap) of
+ error ->
+ ?GRISP_ERROR("Ask to send a response to the unknown request ~p",
+ [ReqRef],
+ #{event => internal_error, ref => ReqRef,
+ reason => unknown_request}),
+ State;
+ {#inbound_req{bref = undefined}, ReqMap2} ->
+ % Not part of a batch response
+ send_packet(State#state{inbound = ReqMap2}, Message);
+ {#inbound_req{bref = BatchRef}, ReqMap2} ->
+ % The batch must exists
+ case maps:find(BatchRef, BatchMap) of
+ {ok, #batch{refcount = 1, responses = Responses}} ->
+ % This is the last message of the batch
+ BatchMap2 = maps:remove(BatchRef, BatchMap),
+ send_packet(State#state{batches = BatchMap2,
+ inbound = ReqMap2},
+ [Message | Responses]);
+ {ok, Batch = #batch{refcount = RefCount,
+ responses = Responses}} ->
+ Batch2 = Batch#batch{refcount = RefCount - 1,
+ responses = [Message | Responses]},
+ BatchMap2 = BatchMap#{BatchRef => Batch2},
+ State#state{batches = BatchMap2, inbound = ReqMap2}
+ end
+ end.
+
+inbound_add(State = #state{inbound = ReqMap}, BatchRef, Method, ReqRef) ->
+ %TODO: Should we add a timeout for inbound requests ?
+ Req = #inbound_req{method = Method, id = ReqRef, bref = BatchRef},
+ State#state{inbound = ReqMap#{ReqRef => Req}}.
+
+outbound_add(State = #state{request_timeout = Timeout, outbound = ReqMap},
+ Method, From, Ctx) ->
+ ReqRef = make_reqref(),
+ TRef = send_after(Timeout, {outbound_timeout, ReqRef}),
+ Req = #outbound_req{id = ReqRef, method = Method, tref = TRef,
+ from = From, ctx = Ctx},
+ {ReqRef, State#state{outbound = ReqMap#{ReqRef => Req}}}.
+
+outbound_del(State = #state{outbound = ReqMap}, ReqRef) ->
+ case maps:find(ReqRef, ReqMap) of
+ error -> {error, not_found};
+ {ok, #outbound_req{method = Method, tref = TRef,
+ from = From, ctx = Ctx}} ->
+ cancel_timer(TRef),
+ State2 = State#state{outbound = maps:remove(ReqRef, ReqMap)},
+ {ok, Method, From, Ctx, State2}
+ end.
+
+outbound_timeout(State = #state{handler = Handler}, ReqRef) ->
+ case outbound_del(State, ReqRef) of
+ {error, not_found} ->
+ ?GRISP_WARN("Timeout for unknown request ~s", [ReqRef],
+ #{event => internal_error, ref => ReqRef,
+ reason => unknown_request}),
+ State;
+ {ok, _Method, undefined, Ctx, State2} ->
+ Handler ! {conn, self(), {local_error, timeout, Ctx}},
+ State2;
+ {ok, _, From, _, State2} ->
+ gen_server:reply(From, {error, timeout}),
+ State2
+ end.
+
+requests_error(State = #state{handler = Handler, outbound = ReqMap}, Reason) ->
+ maps:foreach(fun
+ (_, #outbound_req{from = undefined, ctx = Ctx}) ->
+ Handler ! {conn, self(), {local_error, Reason, Ctx}};
+ (_, #outbound_req{from = From, ctx = undefined}) ->
+ gen_server:reply(From, {error, Reason})
+ end, ReqMap),
+ State#state{outbound = #{}}.
diff --git a/src/grisp_connect_internal.hrl b/src/grisp_connect_internal.hrl
new file mode 100644
index 0000000..489c9a7
--- /dev/null
+++ b/src/grisp_connect_internal.hrl
@@ -0,0 +1,25 @@
+-ifndef(GRISP_CONNECT_INTERNAL_HRL).
+-define(GRISP_CONNECT_INTERNAL_HRL, true).
+
+-include_lib("kernel/include/logger.hrl").
+
+-define(FORMAT(FMT, ARGS), iolist_to_binary(io_lib:format(FMT, ARGS))).
+
+-define(GRISP_DEBUG(FMT, ARGS),
+ ?LOG_DEBUG(FMT, ARGS)).
+-define(GRISP_DEBUG(FMT, ARGS, REPORT),
+ ?LOG_DEBUG(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+-define(GRISP_INFO(FMT, ARGS),
+ ?LOG_INFO(FMT, ARGS)).
+-define(GRISP_INFO(FMT, ARGS, REPORT),
+ ?LOG_INFO(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+-define(GRISP_WARN(FMT, ARGS),
+ ?LOG_WARNING(FMT, ARGS)).
+-define(GRISP_WARN(FMT, ARGS, REPORT),
+ ?LOG_WARNING(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+-define(GRISP_ERROR(FMT, ARGS),
+ ?LOG_ERROR(FMT, ARGS)).
+-define(GRISP_ERROR(FMT, ARGS, REPORT),
+ ?LOG_ERROR(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+
+-endif. % GRISP_CONNECT_INTERNAL_HRL
diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl
index f37d1f8..5029387 100644
--- a/src/grisp_connect_jsonrpc.erl
+++ b/src/grisp_connect_jsonrpc.erl
@@ -66,7 +66,7 @@
decode(Data) ->
case json_to_term(iolist_to_binary(Data)) of
[] ->
- [{decoding_error, -32600, <<"Invalid Request">>, undefined, undefined}];
+ {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined};
Messages when is_list(Messages) ->
[unpack(M) || M <- Messages];
Message when is_map(Message) ->
@@ -120,9 +120,9 @@ unpack(#{error := #{code := Code, message := Message}, id := ID} = M)
when ?is_valid(M), is_integer(Code) ->
{error, Code, as_bin(Message), undefined, as_id(ID)};
unpack(#{id := ID}) ->
- {decoding_error, -32600, <<"Invalid request">>, undefined, as_id(ID)};
+ {decoding_error, -32600, <<"Invalid Request">>, undefined, as_id(ID)};
unpack(_M) ->
- {decoding_error, -32600, <<"Invalid request">>, undefined, undefined}.
+ {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined}.
pack({request, Method, undefined, ID})
when is_binary(Method) orelse is_atom(Method), ?is_id(ID) ->
diff --git a/src/grisp_connect_logger_bin.erl b/src/grisp_connect_logger_bin.erl
index a523588..95316b7 100644
--- a/src/grisp_connect_logger_bin.erl
+++ b/src/grisp_connect_logger_bin.erl
@@ -34,9 +34,6 @@
-export([handle_info/3]).
-export([terminate/3]).
-% % Log filtering functions
--export([filter_out/2]).
-
% Internal Callbacks
-export([queue_ctrl_init/1]).
-export([queue_ctrl_loop/1]).
@@ -150,26 +147,6 @@ handle_info(_, _, State) ->
terminate(_Name, _Reason, _State) ->
ok.
-% %--- Log Filtering Functions ---------------------------------------------------
-
-filter_out(LogEvent = #{meta := Meta}, Mfa = {M, F, A})
- when is_atom(M), is_atom(F), is_integer(A) ->
- case Meta of
- #{mfa := Mfa} -> stop;
- _ -> LogEvent
- end;
-filter_out(LogEvent = #{meta := Meta}, {M, F})
- when is_atom(M), is_atom(F) ->
- case Meta of
- #{mfa := {M, F, _}} -> stop;
- _ -> LogEvent
- end;
-filter_out(LogEvent = #{meta := Meta}, M)
- when is_atom(M) ->
- case Meta of
- #{mfa := {M, _, _}} -> stop;
- _ -> LogEvent
- end.
%--- Internal ------------------------------------------------------------------
diff --git a/src/grisp_connect_sup.erl b/src/grisp_connect_sup.erl
index ff8887c..f583f29 100644
--- a/src/grisp_connect_sup.erl
+++ b/src/grisp_connect_sup.erl
@@ -44,7 +44,6 @@ init([]) ->
%% Hence grisp_connect_log_server should be started before grisp_connect_client
%% and a crash in grisp_connect_log_server should crash grisp_connect_client as well.
ChildSpecs = NTP ++ [
- worker(grisp_connect_ws, []),
worker(grisp_connect_log_server, []),
worker(grisp_connect_client, [])
],
diff --git a/src/grisp_connect_updater.erl b/src/grisp_connect_updater.erl
index d8b34fa..123f7da 100644
--- a/src/grisp_connect_updater.erl
+++ b/src/grisp_connect_updater.erl
@@ -15,6 +15,13 @@
-export([validate/0]).
-export([cancel/0]).
+% Disable dialyzer warnings because of unknown dependencies
+-dialyzer({nowarn_function, system_info/0}).
+-dialyzer({nowarn_function, start_update/1}).
+-dialyzer({nowarn_function, validate/0}).
+-dialyzer({nowarn_function, cancel/0}).
+-dialyzer({nowarn_function, update_info/0}).
+
%--- API -----------------------------------------------------------------------
diff --git a/src/grisp_connect_utils.erl b/src/grisp_connect_utils.erl
new file mode 100644
index 0000000..9a73457
--- /dev/null
+++ b/src/grisp_connect_utils.erl
@@ -0,0 +1,45 @@
+-module(grisp_connect_utils).
+
+% API Functions
+-export([as_bin/1]).
+-export([maybe_atom/1]).
+-export([parse_method/1]).
+-export([format_method/1]).
+
+
+%--- API Functions -------------------------------------------------------------
+
+as_bin(Binary) when is_binary(Binary) -> Binary;
+as_bin(List) when is_list(List) -> list_to_binary(List);
+as_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
+
+maybe_atom(Bin) when is_binary(Bin) ->
+ try binary_to_existing_atom(Bin)
+ catch error:badarg -> Bin
+ end.
+
+parse_method(List) when is_list(List) -> check_method(List);
+parse_method(Atom) when is_atom(Atom) -> [Atom];
+parse_method(Binary) when is_binary(Binary) ->
+ [maybe_atom(B) || B <- binary:split(Binary, <<".">>, [global])].
+
+format_method(Binary) when is_binary(Binary) -> Binary;
+format_method(Atom) when is_atom(Atom) -> atom_to_binary(Atom);
+format_method(List) when is_list(List) ->
+ iolist_to_binary(lists:join(<<".">>, [as_bin(E) || E <- List])).
+
+
+%--- Internal Functions --------------------------------------------------------
+
+check_method(Method) when is_list(Method) ->
+ check_method(Method, Method);
+check_method(_Method) ->
+ erlang:exit(badarg).
+
+check_method(Method, []) -> Method;
+check_method(Method, [Atom | Rest]) when is_atom(Atom) ->
+ check_method(Method, Rest);
+check_method(Method, [Bin | Rest]) when is_binary(Bin) ->
+ check_method(Method, Rest);
+check_method(_Method, _Rest) ->
+ erlang:error(badarg).
diff --git a/src/grisp_connect_ws.erl b/src/grisp_connect_ws.erl
deleted file mode 100644
index 074ed46..0000000
--- a/src/grisp_connect_ws.erl
+++ /dev/null
@@ -1,147 +0,0 @@
-%% @doc Websocket client to connect to grisp.io
--module(grisp_connect_ws).
-
--export([start_link/0]).
--export([connect/0]).
--export([connect/2]).
--export([send/1]).
-
--behaviour(gen_server).
-
--export([init/1]).
--export([handle_call/3]).
--export([handle_cast/2]).
--export([handle_info/2]).
-
--record(state, {
- gun_pid,
- gun_ref,
- ws_stream,
- ws_up = false,
- ping_timer
-}).
-
--include_lib("kernel/include/logger.hrl").
-
-%--- API Functions -------------------------------------------------------------
-
-start_link() ->
- gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-
-connect() ->
- {ok, Domain} = application:get_env(grisp_connect, domain),
- {ok, Port} = application:get_env(grisp_connect, port),
- connect(Domain, Port).
-
-connect(Server, Port) ->
- gen_server:cast(?MODULE, {?FUNCTION_NAME, Server, Port}).
-
-send(Payload) ->
- gen_server:cast(?MODULE, {?FUNCTION_NAME, Payload}).
-
-% gen_server callbacks ---------------------------------------------------------
-
-init([]) -> {ok, #state{}}.
-
-handle_call(Call, _, _) ->
- error({unexpected_call, Call}).
-
-handle_cast({connect, Server, Port}, #state{gun_pid = undefined} = S) ->
- GunOpts = #{
- protocols => [http],
- transport => tls,
- retry => 0,
- tls_opts => grisp_cryptoauth_tls:options(Server)
- },
- case gun:open(Server, Port, GunOpts) of
- {ok, GunPid} ->
- GunRef = monitor(process, GunPid),
- {noreply, #state{gun_pid = GunPid, gun_ref = GunRef}};
- Error ->
- ?LOG_ERROR(#{event => connection_failure, reason => Error}),
- {noreply, S}
- end;
-handle_cast({connect, _Server, _Port}, S) ->
- {noreply, S};
-handle_cast({send, _}, #state{ws_up = false} = S) ->
- ?LOG_ERROR(#{event => ws_send, reason => ws_disconnected}),
- {noreply, S};
-handle_cast({send, Payload}, #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- gun:ws_send(Pid, Stream, {text, Payload}),
- {noreply, S}.
-
-handle_info({gun_up, GunPid, _}, #state{gun_pid = GunPid} = S) ->
- ?LOG_INFO(#{event => connection_enstablished}),
- WsStream = gun:ws_upgrade(GunPid, "/grisp-connect/ws",[],
- #{silence_pings => false}),
- NewS = S#state{gun_pid = GunPid, ws_stream = WsStream},
- {noreply, NewS};
-handle_info({gun_up, Pid, http}, #state{gun_pid = GunPid} = S) ->
- ?LOG_WARNING("Ignoring unexpected gun_up http message"
- " from pid ~p, current pid is ~p", [Pid, GunPid]),
- {noreply, S};
-handle_info({gun_upgrade, Pid, Stream, [<<"websocket">>], _},
- #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- ?LOG_INFO(#{event => ws_upgrade}),
- grisp_connect_client:connected(),
- {noreply, S#state{ws_up = true, ping_timer = start_ping_timer()}};
-handle_info({gun_response, Pid, Stream, _, Status, _Headers},
- #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- ?LOG_ERROR(#{event => ws_upgrade_failure, status => Status}),
- {noreply, shutdown_gun(S)};
-handle_info({gun_ws, Pid, Stream, ping},
- #state{gun_pid = Pid, ws_stream = Stream,
- ping_timer = PingTimer} = S) ->
- timer:cancel(PingTimer),
- {noreply, S#state{ping_timer = start_ping_timer()}};
-handle_info({gun_ws, Pid, Stream, {text, Text}},
- #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- grisp_connect_client:handle_message(Text),
- {noreply, S};
-handle_info({gun_ws, Pid, Stream, {close, Code, Message}},
- #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- ?LOG_WARNING(#{event => stream_closed, code => Code, reason => Message}),
- {noreply, S};
-handle_info({gun_error, Pid, _Stream, Reason}, #state{gun_pid = Pid} = S) ->
- ?LOG_ERROR(#{event => ws_closed, reason => Reason}),
- grisp_connect_client:disconnected(),
- {noreply, shutdown_gun(S)};
-handle_info({gun_down, Pid, ws, closed, [Stream]}, #state{gun_pid = Pid, ws_stream = Stream} = S) ->
- ?LOG_WARNING(#{event => ws_closed}),
- grisp_connect_client:disconnected(),
- {noreply, shutdown_gun(S)};
-handle_info({gun_down, Pid, ws, normal, _}, #state{gun_pid = Pid} = S) ->
- ?LOG_INFO(#{event => ws_closed, reason => normal}),
- grisp_connect_client:disconnected(),
- {noreply, shutdown_gun(S)};
-handle_info({'DOWN', _, process, Pid, Reason}, #state{gun_pid = Pid,
- ping_timer = Tref} = S) ->
- ?LOG_WARNING(#{event => gun_crash, reason => Reason}),
- timer:cancel(Tref),
- grisp_connect_client:disconnected(),
- {noreply, disconnected_state(S)};
-handle_info(ping_timeout, S) ->
- ?LOG_WARNING(#{event => ping_timeout}),
- grisp_connect_client:disconnected(),
- {noreply, shutdown_gun(S)};
-handle_info(M, S) ->
- ?LOG_WARNING(#{event => unhandled_info, info => M, state => S}),
- {noreply, S}.
-
-% internal functions -----------------------------------------------------------
-
-shutdown_gun(#state{gun_pid = Pid, gun_ref = GunRef,
- ping_timer = PingTimer} = S) ->
- timer:cancel(PingTimer),
- demonitor(GunRef),
- gun:shutdown(Pid),
- disconnected_state(S).
-
-start_ping_timer() ->
- {ok, Timeout} = application:get_env(grisp_connect, ws_ping_timeout),
- {ok, Tref} = timer:send_after(Timeout, ping_timeout),
- Tref.
-
-disconnected_state(S) ->
- S#state{gun_pid = undefined, gun_ref = undefined,
- ws_up = false, ping_timer = undefined}.
diff --git a/test/grisp_connect_connection_SUITE.erl b/test/grisp_connect_connection_SUITE.erl
new file mode 100644
index 0000000..ee3a288
--- /dev/null
+++ b/test/grisp_connect_connection_SUITE.erl
@@ -0,0 +1,326 @@
+-module(grisp_connect_connection_SUITE).
+
+-behaviour(ct_suite).
+-include_lib("common_test/include/ct.hrl").
+-include_lib("stdlib/include/assert.hrl").
+-include("grisp_connect_test.hrl").
+
+-compile([export_all, nowarn_export_all]).
+
+-import(grisp_connect_test_async, [async_eval/1]).
+-import(grisp_connect_test_async, [async_get_result/1]).
+
+-import(grisp_connect_test_server, [flush/0]).
+-import(grisp_connect_test_server, [send_text/1]).
+-import(grisp_connect_test_server, [send_jsonrpc_request/3]).
+-import(grisp_connect_test_server, [send_jsonrpc_notification/2]).
+-import(grisp_connect_test_server, [send_jsonrpc_result/2]).
+-import(grisp_connect_test_server, [send_jsonrpc_error/3]).
+
+
+%--- MACROS --------------------------------------------------------------------
+
+-define(fmt(Fmt, Args), lists:flatten(io_lib:format(Fmt, Args))).
+-define(assertConnRequest(Conn, M, P, R), fun() ->
+ receive {conn, Conn, {request, M, P = Result, R}} -> Result
+ after 1000 ->
+ ?assert(false, ?fmt("The client connection did not receive request ~s ~s ~s",
+ [??M, ??P, ??P]))
+ end
+end()).
+-define(assertConnNotification(Conn, M, P), fun() ->
+ receive {conn, Conn, {notification, M, P = Result}} -> Result
+ after 1000 ->
+ ?assert(false, ?fmt("The client did not receive notification ~s ~s",
+ [??M, ??P]))
+ end
+end()).
+-define(assertConnResponse(Conn, V, X), fun() ->
+ receive {conn, Conn, {response, V = Result, X}} -> Result
+ after 1000 ->
+ ?assert(false, ?fmt("The client connection did not receive response ~s ~s",
+ [??V, ??X]))
+ end
+end()).
+-define(assertConnRemoteError(Conn, C, M, D, X), fun() ->
+ receive {conn, Conn, {remote_error, C, M, D, X}} -> ok
+ after 1000 ->
+ ?assert(false, ?fmt("The client connection did not receive remote error ~s ~s ~s ~s",
+ [??C, ??M, ??D, ??X]))
+ end
+end()).
+-define(assertConnRemoteError(Conn, C, M, D), fun() ->
+ receive {conn, Conn, {remote_error, C, M, D}} -> ok
+ after 1000 ->
+ ?assert(false, ?fmt("The client connection did not receive remote error ~s ~s ~s",
+ [??C, ??M, ??D]))
+ end
+end()).
+-define(assertConnLocalError(Conn, R, X), fun() ->
+ receive {conn, Conn, {local_error, R, X}} -> ok
+ after 1000 ->
+ ?assert(false, ?fmt("The client connection did not receive local error ~s ~s",
+ [??R, ??X]))
+ end
+end()).
+
+
+%--- API -----------------------------------------------------------------------
+
+all() ->
+ [
+ F
+ ||
+ {F, 1} <- ?MODULE:module_info(exports),
+ lists:suffix("_test", atom_to_list(F))
+ ].
+
+init_per_suite(Config) ->
+ {ok, _} = application:ensure_all_started(gun),
+ Apps = grisp_connect_test_server:start(),
+ [{apps, Apps} | Config].
+
+end_per_suite(Config) ->
+ [?assertEqual(ok, application:stop(App)) || App <- ?config(apps, Config)].
+
+init_per_testcase(_TestCase, Config) ->
+ {ok, _} = application:ensure_all_started(grisp_emulation),
+ Config.
+
+end_per_testcase(_, Config) ->
+ ?assertEqual([], flush()),
+ Config.
+
+
+%--- Tests ---------------------------------------------------------------------
+
+basic_server_notifications_test(_) ->
+ Conn = connect(),
+ send_jsonrpc_notification(<<"ping">>, #{foo => null}),
+ ?assertConnNotification(Conn, [ping], #{foo := undefined}),
+ send_jsonrpc_notification(<<"foo.bar.ping">>, #{}),
+ ?assertConnNotification(Conn, [foo, bar, ping], _),
+ send_jsonrpc_notification(<<"foo.bar.NotAnAtom">>, #{}),
+ ?assertConnNotification(Conn, [foo, bar, <<"NotAnAtom">>], _),
+ disconnect(Conn).
+
+basic_client_notifications_test(_) ->
+ Conn = connect(),
+ grisp_connect_connection:notify(Conn, ping, #{foo => undefined}),
+ ?receiveNotification(<<"ping">>, #{foo := null}),
+ grisp_connect_connection:notify(Conn, [foo, bar, ping], #{}),
+ ?receiveNotification(<<"foo.bar.ping">>, _),
+ grisp_connect_connection:notify(Conn, [foo, bar, <<"NotAnAtom">>], #{}),
+ ?receiveNotification(<<"foo.bar.NotAnAtom">>, _),
+ disconnect(Conn).
+
+basic_server_request_test(_) ->
+ Conn = connect(),
+ send_jsonrpc_request(<<"toto">>, #{}, 1),
+ ?assertConnRequest(Conn, [toto], _, 1),
+ grisp_connect_connection:reply(Conn, <<"spam">>, 1),
+ ?receiveResult(<<"spam">>, 1),
+ send_jsonrpc_request(<<"foo.bar.tata">>, #{}, 2),
+ ?assertConnRequest(Conn, [foo, bar, tata], _, 2),
+ grisp_connect_connection:error(Conn, error1, undefined, undefined, 2),
+ ?receiveError(-1, <<"Error Number 1">>, 2),
+ send_jsonrpc_request(<<"foo.bar.toto">>, #{}, 3),
+ ?assertConnRequest(Conn, [foo, bar, toto], _, 3),
+ grisp_connect_connection:error(Conn, error2, <<"Custom">>, undefined, 3),
+ ?receiveError(-2, <<"Custom">>, 3),
+ send_jsonrpc_request(<<"foo.bar.titi">>, #{}, 4),
+ ?assertConnRequest(Conn, [foo, bar, titi], _, 4),
+ grisp_connect_connection:error(Conn, -42, <<"Message">>, undefined, 4),
+ ?receiveError(-42, <<"Message">>, 4),
+ disconnect(Conn).
+
+basic_client_synchronous_request_test(_) ->
+ Conn = connect(),
+ Async1 = async_eval(fun() -> grisp_connect_connection:request(Conn, [toto], #{}) end),
+ Id1 = ?receiveRequest(<<"toto">>, _),
+ send_jsonrpc_result(<<"spam">>, Id1),
+ ?assertEqual({ok, <<"spam">>}, async_get_result(Async1)),
+ Async2 = async_eval(fun() -> grisp_connect_connection:request(Conn, tata, #{}) end),
+ Id2 = ?receiveRequest(<<"tata">>, _),
+ send_jsonrpc_error(-1, null, Id2),
+ ?assertEqual({remote_error, error1, <<"Error Number 1">>, undefined}, async_get_result(Async2)),
+ Async3 = async_eval(fun() -> grisp_connect_connection:request(Conn, titi, #{}) end),
+ Id3 = ?receiveRequest(<<"titi">>, _),
+ send_jsonrpc_error(-2, <<"Custom">>, Id3),
+ ?assertEqual({remote_error, error2, <<"Custom">>, undefined}, async_get_result(Async3)),
+ disconnect(Conn).
+
+basic_client_asynchronous_request_test(_) ->
+ Conn = connect(),
+ grisp_connect_connection:post(Conn, toto, #{}, ctx1),
+ Id1 = ?receiveRequest(<<"toto">>, _),
+ send_jsonrpc_result(<<"spam">>, Id1),
+ ?assertConnResponse(Conn, <<"spam">>, ctx1),
+ grisp_connect_connection:post(Conn, tata, #{}, ctx2),
+ Id2 = ?receiveRequest(<<"tata">>, _),
+ send_jsonrpc_error(-1, null, Id2),
+ ?assertConnRemoteError(Conn, error1, <<"Error Number 1">>, undefined, ctx2),
+ grisp_connect_connection:post(Conn, titi, #{}, ctx3),
+ Id3 = ?receiveRequest(<<"titi">>, _),
+ send_jsonrpc_error(-2, <<"Custom">>, Id3),
+ ?assertConnRemoteError(Conn, error2, <<"Custom">>, undefined, ctx3),
+ disconnect(Conn).
+
+basic_error_test(_) ->
+ Conn = connect(),
+ grisp_connect_connection:error(Conn, -1, undefined, undefined, undefined),
+ ?receiveError(-1, <<"Error Number 1">>, null),
+ send_jsonrpc_error(-2, null, null),
+ ?assertConnRemoteError(Conn, error2, <<"Error Number 2">>, undefined),
+ disconnect(Conn).
+
+request_timeout_test(_) ->
+ Conn = connect(),
+ grisp_connect_connection:post(Conn, toto, #{}, ctx1),
+ _Id1 = ?receiveRequest(<<"toto">>, _),
+ timer:sleep(500),
+ ?assertConnLocalError(Conn, timeout, ctx1),
+ disconnect(Conn).
+
+spec_example_test(Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ ExamplesFile = filename:join(DataDir, "jsonrpc_examples.txt"),
+
+ Conn = connect(),
+
+ {ok, ExData} = file:read_file(ExamplesFile),
+ Examples = parse_examples(ExData),
+ maps:foreach(fun(Desc, Actions) ->
+ try
+ lists:foreach(fun
+ ({send, Text}) ->
+ send_text(Text);
+ ({recv, Expected}) when is_list(Expected) ->
+ example_handler(Conn),
+ SortedExpected = lists:sort(Expected),
+ Received = grisp_connect_test_server:receive_jsonrpc(),
+ ?assert(is_list(Received),
+ ?fmt("Invalid response to a batch request during ~s: ~p",
+ [Desc, Received])),
+ SortedReceived = lists:sort(Received),
+ ?assertEqual(SortedExpected, SortedReceived,
+ ?fmt("Invalid response during ~s", [Desc]));
+ ({recv, Expected}) ->
+ example_handler(Conn),
+ Received = grisp_connect_test_server:receive_jsonrpc(),
+ ?assertEqual(Expected, Received,
+ ?fmt("Invalid response during ~s", [Desc]))
+ end, Actions),
+ example_handler(Conn),
+ RemMsgs = flush(),
+ ?assertEqual([], RemMsgs,
+ ?fmt("Unexpected message during example ~s: ~p",
+ [Desc, RemMsgs]))
+ catch
+ error:timeout ->
+ ?assert(false, ?fmt("Timeout while testing example ~s", [Desc]))
+ end
+ end, Examples),
+
+ disconnect(Conn).
+
+
+%--- Internal Functions --------------------------------------------------------
+
+connect() ->
+ connect(#{}).
+
+connect(Opts) ->
+ DefaultOpts = #{domain => localhost, port => 3030, transport => tcp,
+ path => <<"/grisp-connect/ws">>,
+ request_timeout => 300,
+ errors => [
+ {error1, -1, <<"Error Number 1">>},
+ {error2, -2, <<"Error Number 2">>}
+ ]},
+ ConnOpts = maps:merge(DefaultOpts, Opts),
+ {ok, Conn} = grisp_connect_connection:start_link(self(), ConnOpts),
+ receive
+ {conn, Conn, connected} ->
+ grisp_connect_test_server:listen(),
+ Conn
+ after
+ 1000 ->
+ ?assert(false, "Connection to test server failed")
+ end.
+
+disconnect(Conn) ->
+ unlink(Conn),
+ grisp_connect_connection:disconnect(Conn),
+ ok.
+
+example_handler(Conn) ->
+ receive
+ {conn, Conn, {request, [subtract], [A, B], ReqRef}} ->
+ grisp_connect_connection:reply(Conn, A - B, ReqRef),
+ example_handler(Conn);
+ {conn, Conn, {request, [subtract], #{minuend := A, subtrahend := B}, ReqRef}} ->
+ grisp_connect_connection:reply(Conn, A - B, ReqRef),
+ example_handler(Conn);
+ {conn, Conn, {request, [sum], Values, ReqRef}} ->
+ Result = lists:foldl(fun(V, Acc) -> V + Acc end, 0, Values),
+ grisp_connect_connection:reply(Conn, Result, ReqRef),
+ example_handler(Conn);
+ {conn, Conn, {request, [get_data], _, ReqRef}} ->
+ grisp_connect_connection:reply(Conn, [<<"hello">>, 5], ReqRef),
+ example_handler(Conn);
+ {conn, Conn, {request, _M, _P, ReqRef}} ->
+ grisp_connect_connection:error(Conn, method_not_found, undefined, undefined, ReqRef),
+ example_handler(Conn);
+ {conn, Conn, {notification, _M, _P}} ->
+ example_handler(Conn);
+ % {conn, Conn, {response, R, Ctx :: term()}}
+ {conn, Conn, {remote_error, _C, _M, _D}} ->
+ example_handler(Conn)
+ after
+ 100 -> ok
+ end.
+
+parse_examples(Data) when is_binary(Data) ->
+ Lines = binary:split(Data, <<"\n">>, [global, trim]),
+ parse_examples_lines(Lines, #{}, undefined, undefined).
+
+parse_examples_lines([], Acc, undefined, _Actions) ->
+ Acc;
+parse_examples_lines([], Acc, Desc, Actions) ->
+ Acc#{Desc => lists:reverse(Actions)};
+parse_examples_lines([<<"-->", RestBin/binary>> | RestLines], Acc, Desc, Actions)
+ when Desc =/= undefined ->
+ {Raw, RestLines2} = parse_examples_collect([RestBin | RestLines], <<>>),
+ parse_examples_lines(RestLines2, Acc, Desc, [{send, Raw} | Actions]);
+parse_examples_lines([<<"<--", RestBin/binary>> | RestLines], Acc, Desc, Actions)
+ when Desc =/= undefined ->
+ case parse_examples_collect([RestBin | RestLines], <<>>) of
+ {<<"">>, RestLines2} ->
+ parse_examples_lines(RestLines2, Acc, Desc, Actions);
+ {Raw, RestLines2} ->
+ Decoded = jsx:decode(Raw, [{labels, attempt_atom}, return_maps]),
+ parse_examples_lines(RestLines2, Acc, Desc, [{recv, Decoded} | Actions])
+ end;
+parse_examples_lines([Line | RestLines], Acc, Desc, Actions) ->
+ case re:replace(Line, "^\\s+|\\s+$", "", [{return, binary}, global]) of
+ <<"">> -> parse_examples_lines(RestLines, Acc, Desc, Actions);
+ <<"//", _/binary>> -> parse_examples_lines(RestLines, Acc, Desc, Actions);
+ NewDesc ->
+ NewAcc = case Desc =/= undefined of
+ true -> Acc#{Desc => lists:reverse(Actions)};
+ false -> Acc
+ end,
+ NewDesc2 = re:replace(NewDesc, ":+$", "", [{return, binary}, global]),
+ parse_examples_lines(RestLines, NewAcc, NewDesc2, [])
+ end.
+
+parse_examples_collect([], Acc) -> {Acc, []};
+parse_examples_collect([Line | RestLines], Acc) ->
+ case re:replace(Line, "^\\s+|\\s+$", "", [{return, binary}, global]) of
+ <<"">> -> {Acc, RestLines};
+ <<"-->", _/binary>> -> {Acc, [Line | RestLines]};
+ <<"<--", _/binary>> -> {Acc, [Line | RestLines]};
+ <<"//", _/binary>> -> parse_examples_collect(RestLines, Acc);
+ Line2 -> parse_examples_collect(RestLines, <>)
+ end.
diff --git a/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt b/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
new file mode 100644
index 0000000..f6c0fad
--- /dev/null
+++ b/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
@@ -0,0 +1,96 @@
+// JSON-RPC Examples from specification
+// See: https://www.jsonrpc.org/specification#examples
+
+rpc call with positional parameters:
+
+--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
+<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
+
+--> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
+<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
+
+--> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
+<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
+
+rpc call with named parameters:
+
+--> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
+<-- {"jsonrpc": "2.0", "result": 19, "id": 3}
+
+--> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
+<-- {"jsonrpc": "2.0", "result": 19, "id": 4}
+
+a Notification:
+
+--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
+--> {"jsonrpc": "2.0", "method": "foobar"}
+
+rpc call of non-existent method:
+
+--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
+<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
+
+rpc call with invalid JSON:
+
+--> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
+<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
+
+rpc call with invalid Request object:
+
+--> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
+<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
+
+rpc call Batch, invalid JSON:
+
+--> [
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
+ {"jsonrpc": "2.0", "method"
+]
+<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
+
+rpc call with an empty Array:
+
+--> []
+<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
+
+rpc call with an invalid Batch (but not empty):
+
+--> [1]
+<-- [
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
+]
+
+rpc call with invalid Batch:
+
+--> [1,2,3]
+<-- [
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
+]
+
+rpc call Batch:
+
+--> [
+ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
+ {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
+ {"foo": "boo"},
+ {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
+ {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
+ ]
+<-- [
+ {"jsonrpc": "2.0", "result": 7, "id": "1"},
+ {"jsonrpc": "2.0", "result": 19, "id": "2"},
+ {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
+ {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
+ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
+ ]
+
+rpc call Batch (all notifications):
+
+--> [
+ {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
+ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
+ ]
+<-- //Nothing is returned for all notification batches
diff --git a/test/grisp_connect_jsonrpc_SUITE.erl b/test/grisp_connect_jsonrpc_SUITE.erl
index 16853a1..abc1e14 100644
--- a/test/grisp_connect_jsonrpc_SUITE.erl
+++ b/test/grisp_connect_jsonrpc_SUITE.erl
@@ -72,18 +72,18 @@ invalid_json(_) ->
<<"\"id\":null">>], JsonError)).
invalid_request(_) ->
- Term = {decoding_error, -32600, <<"Invalid request">>, undefined, undefined},
+ Term = {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined},
Json = <<"{\"jsonrpc\":\"2.0\",\"method\":1,\"params\":\"bar\"}">>,
?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
JsonError = grisp_connect_jsonrpc:encode(Term),
?assert(jsonrpc_check([<<"\"error\":{">>,
<<"\"code\":-32600">>,
- <<"\"message\":\"Invalid request\"">>,
+ <<"\"message\":\"Invalid Request\"">>,
<<"\"id\":null">>], JsonError)).
batch(_) ->
Term1 = {request, <<"sum">>, [1,2,4], <<"1">>},
- Term2 = {decoding_error, -32600, <<"Invalid request">>, undefined, undefined},
+ Term2 = {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined},
Json = <<"[{\"jsonrpc\":\"2.0\",\"method\":\"sum\",\"params\":[1,2,4],\"id\":\"1\"},{\"foo\":\"boo\"}]">>,
?assertMatch([Term1, Term2], grisp_connect_jsonrpc:decode(Json)),
JsonError = grisp_connect_jsonrpc:encode([Term1, Term2]),
@@ -92,7 +92,7 @@ batch(_) ->
<<"\"params\":[1,2,4]">>,
<<"\"error\":{">>,
<<"\"code\":-32600">>,
- <<"\"message\":\"Invalid request\"">>,
+ <<"\"message\":\"Invalid Request\"">>,
<<"\"id\":null">>], JsonError)).
result(_) ->
diff --git a/test/grisp_connect_log_SUITE.erl b/test/grisp_connect_log_SUITE.erl
index 39cb204..32b745e 100644
--- a/test/grisp_connect_log_SUITE.erl
+++ b/test/grisp_connect_log_SUITE.erl
@@ -13,6 +13,7 @@
-import(grisp_connect_test_server, [flush/0]).
-import(grisp_connect_test_server, [receive_jsonrpc_request/0]).
+-import(grisp_connect_test_server, [receive_jsonrpc_request/1]).
-import(grisp_connect_test_server, [send_jsonrpc_result/2]).
%--- API -----------------------------------------------------------------------
@@ -55,11 +56,11 @@ end_per_testcase(_, Config) ->
%--- Tests ---------------------------------------------------------------------
string_logs_test(_) ->
- LastSeq = last_seq(),
S1 = "@#$%^&*()_ +{}|:\"<>?-[];'./,\\`~!\néäüßóçøáîùêñÄÖÜÉÁÍÓÚàèìòùÂÊÎÔÛ€",
S2 = <<"@#$%^&*()_ +{}|:\"<>?-[];'./,\\`~!\néäüßóçøáîùêñÄÖÜÉÁÍÓÚàèìòùÂÊÎÔÛ€"/utf8>>,
Strings = [S1, S2],
Texts = [<>, <>],
+ LastSeq = reset_log(last_seq()),
Seqs = [LastSeq + 1, LastSeq + 2],
Fun = fun({Seq, String, Text}) ->
grisp_connect:log(error, [String]),
@@ -73,12 +74,12 @@ formatted_logs_test(_) ->
["~p, ~tp", [<<"tést">>, <<"tést"/utf8>>]],
["~s, ~ts", ["tést", "tést"]],
["~s, ~ts", [<<"tést">>, <<"tést"/utf8>>]]],
- LastSeq = last_seq(),
Texts = [<<"ä, €"/utf8>>,
<<"tést, tést"/utf8>>,
<<"<<\"tést\">>, <<\"tést\"/utf8>>"/utf8>>,
<<"tést, tést"/utf8>>,
<<"tést, tést"/utf8>>],
+ LastSeq = reset_log(last_seq()),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(ArgsList)),
Fun = fun({Seq, Args, Text}) ->
grisp_connect:log(error, Args),
@@ -96,7 +97,6 @@ structured_logs_test(_) ->
#{event => 1234},
#{event => 0.1},
#{event => {'äh', 'bäh'}}],
- LastSeq = last_seq(),
Texts = [#{event => <<"tést"/utf8>>},
#{event => "tést"},
#{event => <<"tést"/utf8>>},
@@ -106,6 +106,7 @@ structured_logs_test(_) ->
#{event => 1234},
#{event => 0.1},
<<"[JSON incompatible term]\n#{event => {äh,bäh}}"/utf8>>],
+ LastSeq = reset_log(last_seq()),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(Events)),
Fun = fun({Seq, Event, Text}) ->
grisp_connect:log(error, [Event]),
@@ -122,7 +123,7 @@ log_level_test(_) ->
notice,
info,
debug],
- LastSeq = last_seq(),
+ LastSeq = reset_log(last_seq()),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(Levels)),
Fun = fun({Seq, Level}) ->
grisp_connect:log(Level, ["level test"]),
@@ -146,7 +147,7 @@ meta_data_test(_) ->
custom5 => #{boolean => true},
custom6 => 6,
custom7 => 7.0},
- LastSeq = last_seq(),
+ LastSeq = reset_log(last_seq()),
Seq = LastSeq + 1,
grisp_connect:log(error, ["Test meta", Meta]),
send_logs(),
@@ -187,6 +188,23 @@ last_seq() ->
send_jsonrpc_result(#{seq => Seq, dropped => 0}, Id),
Seq.
+reset_log() ->
+ reset_log(undefined).
+
+reset_log(LasSeq) ->
+ send_logs(),
+ try receive_jsonrpc_request(200) of
+ #{id := Id, params := #{type := <<"logs">>, events := Events}} ->
+ AllSeq = [S || [S, _] <- Events],
+ MaxSeq = lists:max(AllSeq),
+ send_jsonrpc_result(#{seq => MaxSeq, dropped => 0}, Id),
+ reset_log(MaxSeq);
+ _Other ->
+ reset_log(LasSeq)
+ catch
+ error:timeout -> LasSeq
+ end.
+
check_log(Seq, Level, Text) ->
send_logs(),
ct:pal("Expected seq:~n~p~nExpected level:~n~p~nExpected text:~n~p",
diff --git a/test/grisp_connect_reconnect_SUITE.erl b/test/grisp_connect_reconnect_SUITE.erl
index d339e0f..144c216 100644
--- a/test/grisp_connect_reconnect_SUITE.erl
+++ b/test/grisp_connect_reconnect_SUITE.erl
@@ -48,11 +48,12 @@ end_per_testcase(_, Config) ->
flush(),
Config.
+
%--- Tests ---------------------------------------------------------------------
reconnect_on_gun_crash_test(_) ->
?assertMatch(ok, wait_connection(100)),
- {state, GunPid, _, _, _, _} = sys:get_state(grisp_connect_ws),
+ GunPid = connection_gun_pid(),
proc_lib:stop(GunPid),
?assertMatch(ok, wait_disconnection()),
?assertMatch(ok, wait_connection()).
@@ -62,23 +63,31 @@ reconnect_on_disconnection_test(Config) ->
stop_cowboy(),
?assertMatch(ok, wait_disconnection()),
start_cowboy(cert_dir()),
- ?assertMatch(ok, wait_connection(100)),
+ ?assertMatch(ok, wait_connection(1200)),
Config.
reconnect_on_ping_timeout_test(_) ->
?assertMatch(ok, wait_connection()),
- {state, GunPid, _, _, _, _} = sys:get_state(grisp_connect_ws),
+ GunPid = connection_gun_pid(),
proc_lib:stop(GunPid),
% Now decrease ping timeout so that the WS closes after just 1 second
application:set_env(grisp_connect, ws_ping_timeout, 1500),
?assertMatch(ok, wait_disconnection()),
- ?assertMatch(ok, wait_connection(150)),
+ ?assertMatch(ok, wait_connection(1200)),
?assertMatch(ok, wait_disconnection()),
- ?assertMatch(ok, wait_connection(150)),
+ ?assertMatch(ok, wait_connection(1200)),
?assertMatch(ok, wait_disconnection()).
reconnect_on_closed_frame_test(_) ->
?assertMatch(ok, wait_connection()),
close_websocket(),
?assertMatch(ok, wait_disconnection()),
- ?assertMatch(ok, wait_connection(100)).
+ ?assertMatch(ok, wait_connection(1200)).
+
+
+%--- Internal Functions --------------------------------------------------------
+
+connection_gun_pid() ->
+ {_, {data, _, _, _, _, ConnPid, _}} = sys:get_state(grisp_connect_client),
+ {state, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _, _} = sys:get_state(ConnPid),
+ GunPid.
diff --git a/test/grisp_connect_test.hrl b/test/grisp_connect_test.hrl
index 0e542f7..2c1f54a 100644
--- a/test/grisp_connect_test.hrl
+++ b/test/grisp_connect_test.hrl
@@ -7,7 +7,45 @@
?assertMatch(#{method := Method, params := Params}, Send),
maps:get(id, Send)
end)()).
-% TODO when needed:
-%-define(receiveNotification(Method, Params)
-%-define(receiveResult(Pattern, Id)
-%-define(receiveError(Code, Message, Id)
+
+% Receive a JSON-RPC request with explicit timeout,
+% pattern match method and params, return the id
+-define(receiveRequest(Timeout, Method, Params),
+ (fun() ->
+ Send = grisp_connect_test_server:receive_jsonrpc_request(Timeout),
+ ?assertMatch(#{method := Method, params := Params}, Send),
+ maps:get(id, Send)
+ end)()).
+
+
+% Receive a JSON-RPC notification, pattern match method and params
+-define(receiveNotification(Method, Params),
+ (fun() ->
+ Send = grisp_connect_test_server:receive_jsonrpc_notification(),
+ ?assertMatch(#{method := Method, params := Params}, Send),
+ ok
+ end)()).
+
+% Receive a JSON-RPC result, pattern match value and id
+-define(receiveResult(Value, Id),
+ (fun() ->
+ Send = grisp_connect_test_server:receive_jsonrpc_result(),
+ ?assertMatch(#{result := Value, id := Id}, Send),
+ ok
+ end)()).
+
+% Receive a JSON-RPC request error, pattern match code, message and id
+-define(receiveError(Code, Message, Id),
+ (fun() ->
+ Send = grisp_connect_test_server:receive_jsonrpc_error(),
+ ?assertMatch(#{error := #{code := Code, message := Message}, id := Id}, Send),
+ ok
+ end)()).
+
+% Receive a JSON-RPC standalone error, pattern match code and message
+-define(receiveError(Code, Message),
+ (fun() ->
+ Send = grisp_connect_test_server:receive_jsonrpc_error(),
+ ?assertMatch(#{code := Code, message := Message}, Send),
+ ok
+ end)()).
diff --git a/test/grisp_connect_test_client.erl b/test/grisp_connect_test_client.erl
index 63b131f..7a01d47 100644
--- a/test/grisp_connect_test_client.erl
+++ b/test/grisp_connect_test_client.erl
@@ -20,7 +20,6 @@ wait_connection() ->
wait_connection(30_000).
wait_connection(0) ->
- ct:pal("grisp_connect_ws state:~n~p~n", [sys:get_state(grisp_connect_ws)]),
{error, timeout};
wait_connection(N) ->
case grisp_connect:is_connected() of
@@ -34,7 +33,6 @@ wait_disconnection() ->
wait_disconnection(30_000).
wait_disconnection(0) ->
- ct:pal("grisp_connect_ws state:~n~p~n", [sys:get_state(grisp_connect_ws)]),
{error, timeout};
wait_disconnection(N) ->
case grisp_connect:is_connected() of
diff --git a/test/grisp_connect_test_server.erl b/test/grisp_connect_test_server.erl
index c41aec7..6f7e56e 100644
--- a/test/grisp_connect_test_server.erl
+++ b/test/grisp_connect_test_server.erl
@@ -1,16 +1,21 @@
-module(grisp_connect_test_server).
% API
--export([start/1]).
--export([start_cowboy/1]).
+-export([start/0, start/1]).
+-export([start_cowboy/0, start_cowboy/1]).
-export([stop_cowboy/0]).
-export([close_websocket/0]).
-export([listen/0]).
-export([flush/0]).
--export([receive_text/0]).
--export([receive_jsonrpc/0]).
--export([receive_jsonrpc_request/0]).
+-export([receive_text/0, receive_text/1]).
+-export([receive_jsonrpc/0, receive_jsonrpc/1]).
+-export([receive_jsonrpc_request/0, receive_jsonrpc_request/1]).
+-export([receive_jsonrpc_notification/0]).
+-export([receive_jsonrpc_result/0]).
+-export([receive_jsonrpc_error/0]).
-export([send_text/1]).
+-export([send_jsonrpc_notification/2]).
+-export([send_jsonrpc_request/3]).
-export([send_jsonrpc_result/2]).
-export([send_jsonrpc_error/3]).
@@ -27,6 +32,11 @@
% Start the cowboy application and server,
% call this in `init_per_suite'.
% Returns the started apps
+start() ->
+ {ok, Apps} = application:ensure_all_started(cowboy),
+ start_cowboy(),
+ Apps.
+
start(CertDir) ->
{ok, Apps} = application:ensure_all_started(cowboy),
% TODO: Disable ssl for testing
@@ -35,6 +45,12 @@ start(CertDir) ->
% Start the cowboy listener.
% You have to make sure cowboy is running before.
+start_cowboy() ->
+ Dispatch = cowboy_router:compile(
+ [{'_', [{"/grisp-connect/ws", grisp_connect_test_server, []}]}]),
+ {ok, _} = cowboy:start_clear(server_listener, [{port, 3030}],
+ #{env => #{dispatch => Dispatch}}).
+
start_cowboy(CertDir) ->
SslOpts = [
{verify, verify_peer},
@@ -77,33 +93,77 @@ flush(Acc) ->
end.
receive_text() ->
+ receive_text(5000).
+
+receive_text(Timeout) ->
receive {received_text, Msg} -> Msg
- after 5000 -> {error, timeout}
+ after Timeout -> error(timeout)
end.
receive_jsonrpc() ->
- case receive_text() of
- {error, _} = Error -> Error;
- Msg -> check_jsonrpc(Msg)
- end.
+ check_jsonrpc(receive_text()).
+
+receive_jsonrpc(Timeout) ->
+ check_jsonrpc(receive_text(Timeout)).
receive_jsonrpc_request() ->
case receive_jsonrpc() of
- {error, _} = Error -> Error;
- {error, _, _} = Error -> Error;
#{id := _} = Decoded -> Decoded;
- Decoded -> {error, invalid_jsonrpc_request, Decoded}
+ Decoded -> error({invalid_jsonrpc_request, Decoded})
+ end.
+
+receive_jsonrpc_request(Timeout) ->
+ case receive_jsonrpc(Timeout) of
+ #{id := _} = Decoded -> Decoded;
+ Decoded -> error({invalid_jsonrpc_request, Decoded})
+ end.
+
+receive_jsonrpc_notification() ->
+ case receive_jsonrpc() of
+ #{method := _} = Decoded -> Decoded;
+ Decoded -> error({invalid_jsonrpc_notification, Decoded})
+ end.
+
+receive_jsonrpc_result() ->
+ case receive_jsonrpc() of
+ #{result := _} = Decoded -> Decoded;
+ Decoded -> error({invalid_jsonrpc_result, Decoded})
+ end.
+
+receive_jsonrpc_error() ->
+ case receive_jsonrpc() of
+ #{error := _} = Decoded -> Decoded;
+ Decoded -> error({invalid_jsonrpc_error, Decoded})
end.
check_jsonrpc(Msg) ->
case jsx:decode(Msg, [{labels, attempt_atom}, return_maps]) of
#{jsonrpc := <<"2.0">>} = Decoded -> Decoded;
- _ -> {error, invalid_jsonrpc, Msg}
+ Batch when is_list(Batch) ->
+ lists:foreach(fun
+ (#{jsonrpc := <<"2.0">>}) -> ok;
+ (_) -> error({invalid_jsonrpc, Msg})
+ end, Batch),
+ Batch;
+ _ -> error({invalid_jsonrpc, Msg})
end.
send_text(Msg) ->
?MODULE ! {?FUNCTION_NAME, Msg}.
+send_jsonrpc_notification(Method, Params) ->
+ Map = #{jsonrpc => <<"2.0">>,
+ method => Method,
+ params => Params},
+ send_text(jsx:encode(Map)).
+
+send_jsonrpc_request(Method, Params, Id) ->
+ Map = #{jsonrpc => <<"2.0">>,
+ method => Method,
+ params => Params,
+ id => Id},
+ send_text(jsx:encode(Map)).
+
send_jsonrpc_result(Result, Id) ->
Map = #{jsonrpc => <<"2.0">>,
result => Result,
From 2658e9f2dbde47adc0aa99908f23162acb899ed3 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Tue, 26 Nov 2024 17:27:11 +0100
Subject: [PATCH 02/12] Fix Erlang 27 warnings
---
src/grisp_connect_internal.hrl | 8 ++++----
test/grisp_connect_test_client.erl | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/grisp_connect_internal.hrl b/src/grisp_connect_internal.hrl
index 489c9a7..a037dfb 100644
--- a/src/grisp_connect_internal.hrl
+++ b/src/grisp_connect_internal.hrl
@@ -8,18 +8,18 @@
-define(GRISP_DEBUG(FMT, ARGS),
?LOG_DEBUG(FMT, ARGS)).
-define(GRISP_DEBUG(FMT, ARGS, REPORT),
- ?LOG_DEBUG(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+ ?LOG_DEBUG(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
-define(GRISP_INFO(FMT, ARGS),
?LOG_INFO(FMT, ARGS)).
-define(GRISP_INFO(FMT, ARGS, REPORT),
- ?LOG_INFO(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+ ?LOG_INFO(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
-define(GRISP_WARN(FMT, ARGS),
?LOG_WARNING(FMT, ARGS)).
-define(GRISP_WARN(FMT, ARGS, REPORT),
- ?LOG_WARNING(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+ ?LOG_WARNING(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
-define(GRISP_ERROR(FMT, ARGS),
?LOG_ERROR(FMT, ARGS)).
-define(GRISP_ERROR(FMT, ARGS, REPORT),
- ?LOG_ERROR(REPORT#{description => ?FORMAT(FMT, ARGS)})).
+ ?LOG_ERROR(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
-endif. % GRISP_CONNECT_INTERNAL_HRL
diff --git a/test/grisp_connect_test_client.erl b/test/grisp_connect_test_client.erl
index 7a01d47..93b4fc5 100644
--- a/test/grisp_connect_test_client.erl
+++ b/test/grisp_connect_test_client.erl
@@ -12,7 +12,7 @@
%--- API -----------------------------------------------------------------------
-cert_dir() -> filename:join(code:lib_dir(grisp_connect, test), "certs").
+cert_dir() -> filename:join([code:lib_dir(grisp_connect), "test", "certs"]).
serial_number() -> <<"0000">>.
From 48a204397b210261e4349e44c7b60823e293a028 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Tue, 26 Nov 2024 17:59:48 +0100
Subject: [PATCH 03/12] Try fixing CT error on CI
---
test/grisp_connect_api_SUITE.erl | 2 +-
test/grisp_connect_connection_SUITE.erl | 2 +-
test/grisp_connect_log_SUITE.erl | 2 +-
test/grisp_connect_test_server.erl | 21 +++++++++++++++++++++
4 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/test/grisp_connect_api_SUITE.erl b/test/grisp_connect_api_SUITE.erl
index b63c1ce..e4db4c4 100644
--- a/test/grisp_connect_api_SUITE.erl
+++ b/test/grisp_connect_api_SUITE.erl
@@ -37,7 +37,7 @@ init_per_suite(Config) ->
[{apps, Apps} | Config].
end_per_suite(Config) ->
- [?assertEqual(ok, application:stop(App)) || App <- ?config(apps, Config)].
+ grisp_connect_test_server:stop(?config(apps, Config)).
init_per_testcase(TestCase, Config) ->
{ok, _} = application:ensure_all_started(grisp_emulation),
diff --git a/test/grisp_connect_connection_SUITE.erl b/test/grisp_connect_connection_SUITE.erl
index ee3a288..5d7d336 100644
--- a/test/grisp_connect_connection_SUITE.erl
+++ b/test/grisp_connect_connection_SUITE.erl
@@ -81,7 +81,7 @@ init_per_suite(Config) ->
[{apps, Apps} | Config].
end_per_suite(Config) ->
- [?assertEqual(ok, application:stop(App)) || App <- ?config(apps, Config)].
+ grisp_connect_test_server:stop(?config(apps, Config)).
init_per_testcase(_TestCase, Config) ->
{ok, _} = application:ensure_all_started(grisp_emulation),
diff --git a/test/grisp_connect_log_SUITE.erl b/test/grisp_connect_log_SUITE.erl
index 32b745e..56a843c 100644
--- a/test/grisp_connect_log_SUITE.erl
+++ b/test/grisp_connect_log_SUITE.erl
@@ -32,7 +32,7 @@ init_per_suite(Config) ->
[{apps, Apps} | Config].
end_per_suite(Config) ->
- [?assertEqual(ok, application:stop(App)) || App <- ?config(apps, Config)].
+ grisp_connect_test_server:stop(?config(apps, Config)).
init_per_testcase(log_level_test, Config) ->
#{level := Level} = logger:get_primary_config(),
diff --git a/test/grisp_connect_test_server.erl b/test/grisp_connect_test_server.erl
index 6f7e56e..db46f64 100644
--- a/test/grisp_connect_test_server.erl
+++ b/test/grisp_connect_test_server.erl
@@ -1,7 +1,10 @@
-module(grisp_connect_test_server).
+-include_lib("stdlib/include/assert.hrl").
+
% API
-export([start/0, start/1]).
+-export([stop/1]).
-export([start_cowboy/0, start_cowboy/1]).
-export([stop_cowboy/0]).
-export([close_websocket/0]).
@@ -43,6 +46,13 @@ start(CertDir) ->
start_cowboy(CertDir),
Apps.
+stop(Apps) ->
+ ?assertEqual(ok, stop_cowboy()),
+ [?assertEqual(ok, application:stop(App)) || App <- Apps],
+ % Ensure the process is unregistered...
+ wait_no_websocket().
+
+
% Start the cowboy listener.
% You have to make sure cowboy is running before.
start_cowboy() ->
@@ -203,3 +213,14 @@ websocket_info(close_websocket, State) ->
websocket_info(Info, State) ->
ct:pal("Ignore websocket info:~n~p", [Info]),
{[], State}.
+
+
+%--- Internal Functions --------------------------------------------------------
+
+wait_no_websocket() ->
+ case whereis(?MODULE) of
+ undefined -> ok;
+ _Pid ->
+ timer:sleep(200),
+ wait_no_websocket()
+ end.
From 60b21af7b93e27acd2ad084f92c279846c2b4e63 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Wed, 27 Nov 2024 12:28:20 +0100
Subject: [PATCH 04/12] Try fixing CT error on CI 2
---
test/grisp_connect_api_SUITE.erl | 1 +
test/grisp_connect_connection_SUITE.erl | 1 +
test/grisp_connect_log_SUITE.erl | 1 +
test/grisp_connect_test_server.erl | 23 +++++++++++------------
4 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/test/grisp_connect_api_SUITE.erl b/test/grisp_connect_api_SUITE.erl
index e4db4c4..3c09b25 100644
--- a/test/grisp_connect_api_SUITE.erl
+++ b/test/grisp_connect_api_SUITE.erl
@@ -52,6 +52,7 @@ init_per_testcase(TestCase, Config) ->
end_per_testcase(_, Config) ->
ok = application:stop(grisp_connect),
+ grisp_connect_test_server:wait_disconnection(),
?assertEqual([], flush()),
Config.
diff --git a/test/grisp_connect_connection_SUITE.erl b/test/grisp_connect_connection_SUITE.erl
index 5d7d336..295ca02 100644
--- a/test/grisp_connect_connection_SUITE.erl
+++ b/test/grisp_connect_connection_SUITE.erl
@@ -88,6 +88,7 @@ init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_, Config) ->
+ grisp_connect_test_server:wait_disconnection(),
?assertEqual([], flush()),
Config.
diff --git a/test/grisp_connect_log_SUITE.erl b/test/grisp_connect_log_SUITE.erl
index 56a843c..583736d 100644
--- a/test/grisp_connect_log_SUITE.erl
+++ b/test/grisp_connect_log_SUITE.erl
@@ -50,6 +50,7 @@ end_per_testcase(log_level_test, Config) ->
end_per_testcase(other, Config);
end_per_testcase(_, Config) ->
ok = application:stop(grisp_connect),
+ grisp_connect_test_server:wait_disconnection(),
?assertEqual([], flush()),
Config.
diff --git a/test/grisp_connect_test_server.erl b/test/grisp_connect_test_server.erl
index db46f64..97cb832 100644
--- a/test/grisp_connect_test_server.erl
+++ b/test/grisp_connect_test_server.erl
@@ -21,6 +21,7 @@
-export([send_jsonrpc_request/3]).
-export([send_jsonrpc_result/2]).
-export([send_jsonrpc_error/3]).
+-export([wait_disconnection/0]).
% Websocket Callbacks
-export([init/2]).
@@ -50,7 +51,7 @@ stop(Apps) ->
?assertEqual(ok, stop_cowboy()),
[?assertEqual(ok, application:stop(App)) || App <- Apps],
% Ensure the process is unregistered...
- wait_no_websocket().
+ wait_disconnection().
% Start the cowboy listener.
@@ -186,6 +187,15 @@ send_jsonrpc_error(Code, Msg, Id) ->
id => Id},
send_text(jsx:encode(Map)).
+wait_disconnection() ->
+ case whereis(?MODULE) of
+ undefined -> ok;
+ _Pid ->
+ timer:sleep(200),
+ wait_disconnection()
+ end.
+
+
%--- Websocket Callbacks -------------------------------------------------------
init(Req, State) ->
@@ -213,14 +223,3 @@ websocket_info(close_websocket, State) ->
websocket_info(Info, State) ->
ct:pal("Ignore websocket info:~n~p", [Info]),
{[], State}.
-
-
-%--- Internal Functions --------------------------------------------------------
-
-wait_no_websocket() ->
- case whereis(?MODULE) of
- undefined -> ok;
- _Pid ->
- timer:sleep(200),
- wait_no_websocket()
- end.
From b847cea9847c55df49e82e59cb06a47301b64828 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Wed, 27 Nov 2024 12:44:02 +0100
Subject: [PATCH 05/12] Add some archtecture documentation
---
docs/grisp_connect_architecture.md | 65 ++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 docs/grisp_connect_architecture.md
diff --git a/docs/grisp_connect_architecture.md b/docs/grisp_connect_architecture.md
new file mode 100644
index 0000000..850aec2
--- /dev/null
+++ b/docs/grisp_connect_architecture.md
@@ -0,0 +1,65 @@
+# Architecture
+
+```mermaid
+graph TD
+ RootSup[VM Root Supervisor]
+
+ subgraph GrispConnectApp[Grisp Connect Application]
+ GrispConnectRootSup[Root Supervisor
grisp_connect_sup]
+ GrispConnectLogServer[Log Server
grisp_connect_log_server]
+ GrispConnectClient[Client
grisp_connect_client]
+ GrispConnectConnection[JSON-RPC Connection
grisp_connect_connection]
+ GrispConnectJsonRPC[JSON-RPC Codec
grisp_connect_jsonrpc]
+
+ GrispConnectRootSup --Supervise--> GrispConnectLogServer
+ GrispConnectRootSup --Supervise--> GrispConnectClient
+ GrispConnectClient --Spawn and Monitor--> GrispConnectConnection
+ GrispConnectConnection --Use--> GrispConnectJsonRPC
+ end
+
+ subgraph GunApp[Gun Application]
+ GunRootSup[Gun Root Supervisor
gun_sup]
+ GunConnsSup[Gun Connection Supervisor
gun_conns_sup]
+ Gun[Gun Connection
gun]
+ Gun[Gun HTTP Handler
gun_http]
+
+ GunRootSup --Supervise--> GunConnsSup
+ GunConnsSup --Supervise--> Gun
+ end
+
+ RootSup --Supervise--> GrispConnectRootSup
+ RootSup --Supervise--> GunRootSup
+ GrispConnectConnection -.Interact.-> Gun
+```
+
+
+## Client
+
+The client process is the main state machine. Its responsabilities are:
+
+ - Trigger connection/reconnection to the backend.
+ - Expose high-level protocol API to the application.
+ - Implement generic API endpoints.
+
+See the [client documentation](grisp_connect_client.md).
+
+
+## Connection
+
+`grisp_connect_connection` module encpasulate a JSON-RPC connection.
+
+It is not supervised, the process starting it must monitor it.
+
+It provides a high-level API to a JSON-RPC connection:
+
+ - Perform synchronous requests
+ - Start asynchronous requests
+ - Reply to a request
+ - Send and error result for a request
+ - Send asynchronous notifications
+ - Send generic errors
+
+When performing an asynchronous request, the caller can give an opaque context
+term, that will given back when receiving a response or an error for this
+request, allowing the caller to handle the asynchronous operation without
+having to store information locally.
From d1fbbe578f17a0c8d57438962ea6274c68250043 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Wed, 27 Nov 2024 16:02:29 +0100
Subject: [PATCH 06/12] Move noproc error handling to grisp_connect_connection
---
src/grisp_connect_client.erl | 19 +++++++------------
src/grisp_connect_connection.erl | 25 +++++++++++++++++++------
2 files changed, 26 insertions(+), 18 deletions(-)
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index f9f9181..20fb86d 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -308,7 +308,7 @@ conn_close(Data = #data{conn = undefined}, _Reason) ->
Data;
conn_close(Data = #data{conn = Conn}, _Reason) ->
grisp_connect_log_server:stop(),
- catch grisp_connect_connection:disconnect(Conn),
+ grisp_connect_connection:disconnect(Conn),
Data#data{conn = undefined}.
% Safe to call in any state
@@ -323,25 +323,20 @@ conn_post(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
when Conn =/= undefined ->
ReqCtx = #{on_result => OnResult, on_error => OnError},
Params2 = maps:put(type, Type, Params),
- try grisp_connect_connection:post(Conn, Method, Params2, ReqCtx) of
- _ -> Data
- catch
- _:Reason when OnError =/= undefined ->
- OnError(Data, local, Reason, undefined, undefined);
- _:_ ->
- Data
+ case grisp_connect_connection:post(Conn, Method, Params2, ReqCtx) of
+ ok -> Data;
+ {error, Reason} ->
+ OnError(Data, local, Reason, undefined, undefined)
end.
conn_notify(#data{conn = Conn}, Method, Type, Params)
when Conn =/= undefined ->
Params2 = maps:put(type, Type, Params),
- catch grisp_connect_connection:notify(Conn, Method, Params2),
- ok.
+ grisp_connect_connection:notify(Conn, Method, Params2).
conn_reply(#data{conn = Conn}, Result, ReqRef)
when Conn =/= undefined ->
- catch grisp_connect_connection:reply(Conn, Result, ReqRef),
- ok.
+ grisp_connect_connection:reply(Conn, Result, ReqRef).
% IP check functions
diff --git a/src/grisp_connect_connection.erl b/src/grisp_connect_connection.erl
index 14dec02..604b6e0 100644
--- a/src/grisp_connect_connection.erl
+++ b/src/grisp_connect_connection.erl
@@ -128,36 +128,44 @@ start_link(Handler, Opts = #{domain := _, port := _, path := _}) ->
| {error, Code :: integer(),
Message :: undefined | binary(), Data :: term()}.
request(Conn, Method, Params) ->
- case gen_server:call(Conn, {request, parse_method(Method), Params}) of
+ try gen_server:call(Conn, {request, parse_method(Method), Params}) of
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack);
Other -> Other
+ catch
+ exit:{noproc, _} -> {error, not_connected}
end.
-spec post(Conn :: pid(), Method :: method(), Params :: map(),
ReqCtx :: term()) -> ok | {error, not_connected}.
post(Conn, Method, Params, ReqCtx) ->
- case gen_server:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
+ try gen_server:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ catch
+ exit:{noproc, _} -> {error, not_connected}
end.
-spec notify(Conn :: pid(), Method :: method(), Params :: map()) ->
ok | {error, not_connected}.
notify(Conn, Method, Params) ->
- case gen_server:call(Conn, {notify, parse_method(Method), Params}) of
+ try gen_server:call(Conn, {notify, parse_method(Method), Params}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ catch
+ exit:{noproc, _} -> {error, not_connected}
end.
-spec reply(Conn :: pid(), Result :: any(), ReqRef :: binary()) ->
ok | {error, not_connected}.
reply(Conn, Result, ReqRef) ->
- case gen_server:call(Conn, {reply, Result, ReqRef}) of
+ try gen_server:call(Conn, {reply, Result, ReqRef}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ catch
+ exit:{noproc, _} -> {error, not_connected}
end.
-spec error(Conn :: pid(), Code :: atom() | integer(),
@@ -165,15 +173,20 @@ reply(Conn, Result, ReqRef) ->
ReqRef :: undefined | binary()) ->
ok | {error, not_connected}.
error(Conn, Code, Message, Data, ReqRef) ->
- case gen_server:call(Conn, {error, Code, Message, Data, ReqRef}) of
+ try gen_server:call(Conn, {error, Code, Message, Data, ReqRef}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
+ catch
+ exit:{noproc, _} -> {error, not_connected}
end.
-spec disconnect(Conn :: pid()) -> ok.
disconnect(Conn) ->
- gen_server:call(Conn, disconnect).
+ try gen_server:call(Conn, disconnect)
+ catch
+ exit:{noproc, _} -> ok
+ end.
%--- Behaviour gen_server Callbacks --------------------------------------------
From 0a0d7a6960385d79df5cb012217bbff52716994e Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Tue, 3 Dec 2024 20:32:39 +0100
Subject: [PATCH 07/12] Change grisp_connect_connection to gen_statem
- Small changes in tests from review feedback
---
src/grisp_connect_connection.erl | 637 ++++++++++++------------
test/grisp_connect_connection_SUITE.erl | 53 +-
test/grisp_connect_log_SUITE.erl | 15 +-
test/grisp_connect_reconnect_SUITE.erl | 2 +-
4 files changed, 367 insertions(+), 340 deletions(-)
diff --git a/src/grisp_connect_connection.erl b/src/grisp_connect_connection.erl
index 604b6e0..af81fb9 100644
--- a/src/grisp_connect_connection.erl
+++ b/src/grisp_connect_connection.erl
@@ -1,7 +1,7 @@
%% @doc JSONRpc 2.0 Websocket connection
-module(grisp_connect_connection).
--behaviour(gen_server).
+-behaviour(gen_statem).
-include("grisp_connect_internal.hrl").
@@ -18,12 +18,15 @@
-export([error/5]).
-export([disconnect/1]).
-% Behaviour gen_server Callbacks
+% Behaviour gen_statem callback functions
+-export([callback_mode/0]).
-export([init/1]).
--export([handle_call/3]).
--export([handle_cast/2]).
--export([handle_info/2]).
--export([terminate/2]).
+-export([code_change/4]).
+-export([terminate/3]).
+
+% Data Functions
+-export([connecting/3]).
+-export([connected/3]).
%--- Types ---------------------------------------------------------------------
@@ -44,11 +47,11 @@
method :: method(),
id :: binary() | integer(),
tref :: undefined | reference(),
- from :: undefined | gen_server:from(),
+ from :: undefined | gen_statem:from(),
ctx :: undefined | term()
}).
--record(state, {
+-record(data, {
handler :: pid(),
uri :: iodata(),
domain :: binary(),
@@ -62,7 +65,6 @@
gun_pid :: undefined | pid(),
gun_ref :: undefined | reference(),
ws_stream :: undefined | gun:stream_ref(),
- connected = false :: boolean(),
ping_tref :: undefined | reference()
}).
@@ -85,8 +87,8 @@
| {conn, pid(), {request, method(), Params :: map() | list(), ReqRef :: binary() | integer()}}
| {conn, pid(), {notification, method(), Params :: map() | list()}}
| {conn, pid(), {response, Result :: term(), Ctx :: term()}}
- | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term()}}
- | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term(), Ctx :: term()}}
+ | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), ErData :: term()}}
+ | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), ErData :: term(), Ctx :: term()}}
| {conn, pid(), {local_error, Reason:: atom(), Ctx :: term()}}.
-export_type([error_mapping/0, method/0, handler_messages/0]).
@@ -115,20 +117,24 @@
{internal_error, -32603, <<"Internal error">>}
]).
+-define(HANDLE_COMMON,
+ ?FUNCTION_NAME(EventType, EventContent, Data) ->
+ handle_common(EventType, EventContent, ?FUNCTION_NAME, Data)).
+
%--- API Functions -------------------------------------------------------------
-spec start_link(Handler :: pid(), Options :: start_options()) ->
{ok, Conn :: pid()} | {error, Reason :: term()}.
start_link(Handler, Opts = #{domain := _, port := _, path := _}) ->
- gen_server:start_link(?MODULE, [Handler, Opts], []).
+ gen_statem:start_link(?MODULE, [Handler, Opts], []).
-spec request(Conn :: pid(), Method :: method(), Params :: map()) ->
{ok, Result :: term()} | {error, timeout} | {error, not_connected}
| {error, Code :: integer(),
- Message :: undefined | binary(), Data :: term()}.
+ Message :: undefined | binary(), ErData :: term()}.
request(Conn, Method, Params) ->
- try gen_server:call(Conn, {request, parse_method(Method), Params}) of
+ try gen_statem:call(Conn, {request, parse_method(Method), Params}) of
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack);
Other -> Other
catch
@@ -138,7 +144,7 @@ request(Conn, Method, Params) ->
-spec post(Conn :: pid(), Method :: method(), Params :: map(),
ReqCtx :: term()) -> ok | {error, not_connected}.
post(Conn, Method, Params, ReqCtx) ->
- try gen_server:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
+ try gen_statem:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
@@ -149,7 +155,7 @@ post(Conn, Method, Params, ReqCtx) ->
-spec notify(Conn :: pid(), Method :: method(), Params :: map()) ->
ok | {error, not_connected}.
notify(Conn, Method, Params) ->
- try gen_server:call(Conn, {notify, parse_method(Method), Params}) of
+ try gen_statem:call(Conn, {notify, parse_method(Method), Params}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
@@ -160,7 +166,7 @@ notify(Conn, Method, Params) ->
-spec reply(Conn :: pid(), Result :: any(), ReqRef :: binary()) ->
ok | {error, not_connected}.
reply(Conn, Result, ReqRef) ->
- try gen_server:call(Conn, {reply, Result, ReqRef}) of
+ try gen_statem:call(Conn, {reply, Result, ReqRef}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
@@ -169,11 +175,11 @@ reply(Conn, Result, ReqRef) ->
end.
-spec error(Conn :: pid(), Code :: atom() | integer(),
- Message :: undefined | binary(), Data :: term(),
+ Message :: undefined | binary(), ErData :: term(),
ReqRef :: undefined | binary()) ->
ok | {error, not_connected}.
-error(Conn, Code, Message, Data, ReqRef) ->
- try gen_server:call(Conn, {error, Code, Message, Data, ReqRef}) of
+error(Conn, Code, Message, ErData, ReqRef) ->
+ try gen_statem:call(Conn, {error, Code, Message, ErData, ReqRef}) of
{ok, CallResult} -> CallResult;
{error, _Reason} = Error -> Error;
{exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
@@ -183,13 +189,15 @@ error(Conn, Code, Message, Data, ReqRef) ->
-spec disconnect(Conn :: pid()) -> ok.
disconnect(Conn) ->
- try gen_server:call(Conn, disconnect)
+ try gen_statem:call(Conn, disconnect)
catch
exit:{noproc, _} -> ok
end.
-%--- Behaviour gen_server Callbacks --------------------------------------------
+%--- Behaviour gen_statem Callbacks --------------------------------------------
+
+callback_mode() -> [state_functions].
init([Handler, Opts]) ->
process_flag(trap_exit, true), % To ensure terminate/2 is called
@@ -197,7 +205,7 @@ init([Handler, Opts]) ->
PingTimeout = maps:get(ping_timeout, Opts, ?DEFAULT_PING_TIMEOUT),
ReqTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT),
Transport = maps:get(transport, Opts, ?DEFAULT_TRANSPORT),
- State = #state{
+ Data = #data{
handler = Handler,
uri = format_ws_uri(Transport, Domain, Port, Path),
domain = as_bin(Domain),
@@ -208,189 +216,210 @@ init([Handler, Opts]) ->
},
index_errors(?DEFAULT_JSONRPC_ERRORS),
index_errors(maps:get(errors, Opts, [])),
- case connection_start(State, Transport) of
- {ok, _State2} = Result -> Result;
+ case connection_start(Data, Transport) of
+ {ok, Data2} -> {ok, connecting, Data2};
{error, Reason} -> {stop, Reason}
end.
-handle_call({request, Method, _Params}, _From,
- State = #state{connected = false}) ->
- ?GRISP_INFO("Request ~s performed while disconnected",
+code_change(_Vsn, State, Data, _Extra) ->
+ {ok, State, Data}.
+
+terminate(_Reason, _State, Data) ->
+ connection_close(Data),
+ persistent_term:erase({?MODULE, self(), tags}),
+ persistent_term:erase({?MODULE, self(), codes}),
+ ok.
+
+
+%--- Behaviour gen_statem State Callback Functions -----------------------------
+
+connecting({call, From}, {request, Method, _Params}, _Data) ->
+ ?GRISP_INFO("Request ~s performed while connecting",
[format_method(Method)],
#{event => rpc_request_error, method => Method,
reason => not_connected}),
- {reply, {error, not_connected}, State};
-handle_call({request, Method, Params}, From, State) ->
- try send_request(State, Method, Params, From, undefined) of
- State2 -> {noreply, State2}
- catch
- C:badarg:S -> {reply, {exception, C, badarg, S}, State};
- C:R:S -> {stop, R, {exception, C, R, S}, State}
- end;
-handle_call({post, Method, _Params, ReqCtx}, _From,
- State = #state{handler = Handler, connected = false}) ->
- ?GRISP_INFO("Request ~s posted while disconnected",
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting({call, From}, {post, Method, _Params, ReqCtx},
+ #data{handler = Handler}) ->
+ ?GRISP_INFO("Request ~s posted while connecting",
[format_method(Method)],
#{event => rpc_post_error, method => Method,
reason => not_connected}),
% Notify the handler anyway so it doesn't have to make a special case
Handler ! {conn, self(), {local_error, not_connected, ReqCtx}},
- {reply, {error, not_connected}, State};
-handle_call({post, Method, Params, ReqCtx}, _From, State) ->
- try send_request(State, Method, Params, undefined, ReqCtx) of
- State2 -> {reply, {ok, ok}, State2}
- catch
- C:badarg:S -> {reply, {exception, C, badarg, S}, State};
- C:R:S -> {stop, R, {exception, C, R, S}, State}
- end;
-handle_call({notify, Method, _Params}, _From,
- State = #state{connected = false}) ->
- ?GRISP_INFO("Notification ~s posted while disconnected",
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting({call, From}, {notify, Method, _Params}, _Data) ->
+ ?GRISP_INFO("Notification ~s posted while connecting",
[format_method(Method)],
#{event => rpc_notify_error, method => Method,
reason => not_connected}),
- {noreply, State};
-handle_call({notify, Method, Params}, _From, State) ->
- try send_notification(State, Method, Params) of
- State2 -> {reply, {ok, ok}, State2}
- catch
- C:badarg:S -> {reply, {exception, C, badarg, S}, State};
- C:R:S -> {stop, R, {exception, C, R, S}, State}
- end;
-handle_call({reply, _Result, ReqRef}, _From,
- State = #state{connected = false}) ->
- ?GRISP_INFO("Reply to ~s posted while disconnected",
- [inbound_req_tag(State, ReqRef)],
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting({call, From}, {reply, _Result, ReqRef}, Data) ->
+ ?GRISP_INFO("Reply to ~s posted while connecting",
+ [inbound_req_tag(Data, ReqRef)],
#{event => rpc_reply_error, ref => ReqRef,
- method => inbound_method(State, ReqRef),
+ method => inbound_method(Data, ReqRef),
reason => not_connected}),
- {reply, {error, not_connected}, State};
-handle_call({reply, Result, ReqRef}, _From, State) ->
- try send_response(State, Result, ReqRef) of
- State2 -> {reply, {ok, ok}, State2}
- catch
- C:badarg:S -> {reply, {exception, C, badarg, S}, State};
- C:R:S -> {stop, R, {exception, C, R, S}, State}
- end;
-handle_call({error, Code, _Message, _Data, undefined}, _From,
- State = #state{connected = false}) ->
- ?GRISP_INFO("Error ~w posted while disconnected", [Code],
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting({call, From}, {error, Code, _Message, _ErData, undefined}, _Data) ->
+ ?GRISP_INFO("Error ~w posted while connecting", [Code],
#{event => rpc_error_error, code => Code,
reason => not_connected}),
- {reply, {error, not_connected}, State};
-handle_call({error, Code, _Message, _Data, ReqRef}, _From,
- State = #state{connected = false}) ->
- ?GRISP_INFO("Error ~w to ~s posted while disconnected",
- [Code, inbound_req_tag(State, ReqRef)],
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting({call, From}, {error, Code, _Message, _ErData, ReqRef}, Data) ->
+ ?GRISP_INFO("Error ~w to ~s posted while connecting",
+ [Code, inbound_req_tag(Data, ReqRef)],
#{event => rpc_error_error, code => Code, ref => ReqRef,
reason => not_connected}),
- {reply, {error, not_connected}, State};
-handle_call({error, Code, Message, Data, ReqRef}, _From, State) ->
- try send_error(State, Code, Message, Data, ReqRef) of
- State2 -> {reply, {ok, ok}, State2}
+ {keep_state_and_data, [{reply, From, {error, not_connected}}]};
+connecting(info, {gun_up, GunPid, _}, Data = #data{gun_pid = GunPid}) ->
+ ?GRISP_DEBUG("Connection to ~s established",
+ [Data#data.uri],
+ #{event => ws_connection_enstablished, uri => Data#data.uri}),
+ {keep_state, connection_upgrade(Data)};
+connecting(info, {gun_upgrade, Pid, Stream, [<<"websocket">>], _},
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_DEBUG("Connection to ~s upgraded to websocket", [Data#data.uri],
+ #{event => ws_upgraded, uri => Data#data.uri}),
+ {next_state, connected, connection_established(Data)};
+
+connecting(info, {gun_response, Pid, Stream, _, Status, _Headers},
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_INFO("Connection to ~s failed to upgrade to websocket: ~p",
+ [Data#data.uri, Status],
+ #{event => ws_upgrade_failed, uri => Data#data.uri,
+ status => Status}),
+ {stop, ws_upgrade_failed, connection_close(Data)};
+?HANDLE_COMMON.
+
+connected({call, From}, {request, Method, Params}, Data) ->
+ try send_request(Data, Method, Params, From, undefined) of
+ Data2 -> {keep_state, Data2}
catch
- C:badarg:S -> {reply, {exception, C, badarg, S}, State};
- C:R:S -> {stop, R, {exception, C, R, S}, State}
+ C:badarg:S ->
+ {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
+ C:R:S ->
+ {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
end;
-handle_call(disconnect, _From, State) ->
- try connection_close(State) of
- State2 -> {reply, {ok, ok}, State2}
+connected({call, From}, {post, Method, Params, ReqCtx}, Data) ->
+ try send_request(Data, Method, Params, undefined, ReqCtx) of
+ Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
catch
- C:R:S -> {stop, R, {exception, C, R, S}, State}
+ C:badarg:S ->
+ {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
+ C:R:S ->
+ {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
end;
-handle_call(Call, From, State) ->
- ?GRISP_ERROR("Unexpected call from ~p to ~s: ~p", [From, ?MODULE, Call],
- #{event => unexpected_call, from => From, message => Call}),
- {stop, {unexpected_call, Call}, {error, unexpected_call}, State}.
-
-handle_cast(Cast, State) ->
- Reason = {unexpected_cast, Cast},
- ?GRISP_ERROR("Unexpected cast to ~s: ~p", [?MODULE, Cast],
- #{event => unexpected_cast, message => Cast}),
- {stop, Reason, State}.
-
-handle_info({gun_up, GunPid, _}, State = #state{gun_pid = GunPid}) ->
- ?GRISP_DEBUG("Connection to ~s established",
- [State#state.uri],
- #{event => ws_connection_enstablished, uri => State#state.uri}),
- {noreply, connection_upgrade(State)};
-handle_info({gun_up, Pid, http} = _Msg, State = #state{gun_pid = GunPid}) ->
- ?GRISP_DEBUG("Ignoring unexpected gun_up message"
- " from pid ~p, current pid is ~p", [Pid, GunPid],
- #{event => unexpected_gun_message, message => _Msg}),
- {noreply, State};
-handle_info({gun_upgrade, Pid, Stream, [<<"websocket">>], _},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_DEBUG("Connection to ~s upgraded to websocket", [State#state.uri],
- #{event => ws_upgraded, uri => State#state.uri}),
- {noreply, connection_established(State)};
-handle_info({gun_response, Pid, Stream, _, Status, _Headers},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_INFO("Connection to ~s failed to upgrade to websocket: ~p",
- [State#state.uri, Status],
- #{event => ws_upgrade_failed, uri => State#state.uri,
- status => Status}),
- {stop, ws_upgrade_failed, connection_close(State)};
-handle_info({gun_ws, Pid, Stream, ping},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
- {noreply, schedule_ping_timeout(State)};
-handle_info({gun_ws, Pid, Stream, {text, Text}},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
- {noreply, process_data(State, Text)};
-handle_info({gun_ws, Pid, Stream, close},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_INFO("Connection to ~s closed without code", [State#state.uri],
- #{event => ws_stream_closed, uri => State#state.uri}),
- {stop, normal, connection_closed(State, closed)};
-handle_info({gun_ws, Pid, Stream, {close, Code, Message}},
- State = #state{gun_pid = Pid, ws_stream = Stream}) ->
+connected({call, From}, {notify, Method, Params}, Data) ->
+ try send_notification(Data, Method, Params) of
+ Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
+ catch
+ C:badarg:S ->
+ {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
+ C:R:S ->
+ {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
+ end;
+connected({call, From}, {reply, Result, ReqRef}, Data) ->
+ try send_response(Data, Result, ReqRef) of
+ Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
+ catch
+ C:badarg:S ->
+ {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
+ C:R:S ->
+ {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
+ end;
+connected({call, From}, {error, Code, Message, ErData, ReqRef}, Data) ->
+ try send_error(Data, Code, Message, ErData, ReqRef) of
+ Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
+ catch
+ C:badarg:S ->
+ {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
+ C:R:S ->
+ {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
+ end;
+connected(info, {gun_ws, Pid, Stream, {text, Text}},
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
+ {keep_state, process_text(Data, Text)};
+connected(info, ping_timeout, Data) ->
+ ?GRISP_INFO("Connection to ~s timed out", [Data#data.uri],
+ #{event => ws_ping_timeout, uri => Data#data.uri}),
+ {stop, normal, connection_close(Data)};
+connected(info, {outbound_timeout, ReqRef}, Data) ->
+ ?GRISP_INFO("Request ~s time to ~s timed out",
+ [outbound_req_tag(Data, ReqRef), Data#data.uri],
+ #{event => rpc_request_timeout_error, uri => Data#data.uri,
+ ref => ReqRef, method => outbound_method(Data, ReqRef)}),
+ {keep_state, outbound_timeout(Data, ReqRef)};
+?HANDLE_COMMON.
+
+handle_common({call, From}, disconnect, _StateName, Data) ->
+ try connection_close(Data) of
+ Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
+ catch
+ C:R:S -> {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
+ end;
+handle_common(info, {gun_ws, Pid, Stream, ping}, _StateName,
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
+ {keep_state, schedule_ping_timeout(Data)};
+handle_common(info, {gun_ws, Pid, Stream, close}, StateName,
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
+ ?GRISP_INFO("Connection to ~s closed without code", [Data#data.uri],
+ #{event => ws_stream_closed, uri => Data#data.uri,
+ state => StateName}),
+ {stop, normal, connection_closed(Data, closed)};
+handle_common(info, {gun_ws, Pid, Stream, {close, Code, Message}}, StateName,
+ Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
?GRISP_INFO("Connection to ~s closed: ~s (~w)",
- [State#state.uri, Message, Code],
- #{event => ws_stream_closed, uri => State#state.uri,
- code => Code, reason => Message}),
- {stop, normal, connection_closed(State, closed)};
-handle_info({gun_error, Pid, _Stream, Reason},
- State = #state{gun_pid = Pid}) ->
+ [Data#data.uri, Message, Code],
+ #{event => ws_stream_closed, uri => Data#data.uri,
+ code => Code, reason => Message, state => StateName}),
+ {stop, normal, connection_closed(Data, closed)};
+handle_common(info, {gun_error, Pid, _Stream, Reason}, StateName,
+ Data = #data{gun_pid = Pid}) ->
?GRISP_INFO("Connection to ~s got an error: ~p",
- [State#state.uri, Reason],
- #{event => ws_error, uri => State#state.uri,
- reason => Reason}),
- {stop, Reason, connection_close(State)};
-handle_info({gun_down, Pid, ws, Reason, [Stream]},
- State = #state{gun_pid = Pid, ws_stream = Stream})
+ [Data#data.uri, Reason],
+ #{event => ws_error, uri => Data#data.uri,
+ reason => Reason, state => StateName}),
+ {stop, Reason, connection_close(Data)};
+handle_common(info, {gun_down, Pid, ws, Reason, [Stream]}, StateName,
+ Data = #data{gun_pid = Pid, ws_stream = Stream})
when Reason =:= closed; Reason =:= {error, closed}; Reason =:= normal ->
- ?GRISP_INFO("Connection to ~s was closed by the server", [State#state.uri],
- #{event => ws_closed_by_peer, uri => State#state.uri,
- reason => closed}),
- {stop, normal, connection_close(State)};
-handle_info({'DOWN', _, process, Pid, Reason},
- State = #state{gun_pid = Pid}) ->
+ ?GRISP_INFO("Connection to ~s was closed by the server", [Data#data.uri],
+ #{event => ws_closed_by_peer, uri => Data#data.uri,
+ reason => closed, state => StateName}),
+ {stop, normal, connection_close(Data)};
+handle_common(info, {'DOWN', _, process, Pid, Reason}, StateName,
+ Data = #data{gun_pid = Pid}) ->
?GRISP_INFO("gun process of the connection to ~s crashed: ~p",
- [State#state.uri, Reason],
- #{event => ws_gun_crash, uri => State#state.uri,
- reason => Reason}),
- {stop, Reason, connection_closed(State, gun_crashed)};
-handle_info(ping_timeout, State) ->
- ?GRISP_INFO("Connection to ~s timed out", [State#state.uri],
- #{event => ws_ping_timeout, uri => State#state.uri}),
- {stop, normal, connection_close(State)};
-handle_info({outbound_timeout, ReqRef}, State) ->
- ?GRISP_INFO("Request ~s time to ~s timed out",
- [outbound_req_tag(State, ReqRef), State#state.uri],
- #{event => rpc_request_timeout_error, uri => State#state.uri,
- ref => ReqRef, method => outbound_method(State, ReqRef)}),
- {noreply, outbound_timeout(State, ReqRef)};
-handle_info(Msg, State) ->
+ [Data#data.uri, Reason],
+ #{event => ws_gun_crash, uri => Data#data.uri,
+ reason => Reason, state => StateName}),
+ {stop, Reason, connection_closed(Data, gun_crashed)};
+handle_common(info, {gun_up, Pid, http} = Info, StateName,
+ #data{gun_pid = GunPid}) ->
+ ?GRISP_DEBUG("Ignoring unexpected gun_up message"
+ " from pid ~p, current pid is ~p", [Pid, GunPid],
+ #{event => unexpected_gun_message, message => Info,
+ state => StateName}),
+ keep_state_and_data;
+handle_common({call, From}, Call, StateName, _Data) ->
+ ?GRISP_ERROR("Unexpected call from ~p to ~s: ~p", [From, ?MODULE, Call],
+ #{event => unexpected_call, from => From, message => Call,
+ state => StateName}),
+ {stop_and_reply, {unexpected_call, Call},
+ [{reply, From, {error, unexpected_call}}]};
+handle_common(cast, Cast, StateName, _Data) ->
+ Reason = {unexpected_cast, Cast},
+ ?GRISP_ERROR("Unexpected cast to ~s: ~p", [?MODULE, Cast],
+ #{event => unexpected_cast, message => Cast,
+ state => StateName}),
+ {stop, Reason};
+handle_common(info, Info, StateName, _Data) ->
?GRISP_WARN("Unexpected info message to ~s: ~p",
- [?MODULE, Msg],
- #{event => unexpected_info, message => Msg}),
- {noreply, State}.
-
-terminate(_Reason, State) ->
- connection_close(State),
- persistent_term:erase({?MODULE, self(), tags}),
- persistent_term:erase({?MODULE, self(), codes}),
- ok.
+ [?MODULE, Info],
+ #{event => unexpected_info, message => Info,
+ state => StateName}),
+ keep_state_and_data.
%--- INTERNAL FUNCTION ---------------------------------------------------------
@@ -414,26 +443,26 @@ cancel_timer(undefined) -> ok;
cancel_timer(TRef) ->
erlang:cancel_timer(TRef).
-schedule_ping_timeout(State = #state{ping_timeout = Timeout}) ->
- State2 = cancel_ping_timeout(State),
+schedule_ping_timeout(Data = #data{ping_timeout = Timeout}) ->
+ Data2 = cancel_ping_timeout(Data),
TRef = send_after(Timeout, ping_timeout),
- State2#state{ping_tref = TRef}.
+ Data2#data{ping_tref = TRef}.
-cancel_ping_timeout(State = #state{ping_tref = undefined}) ->
- State;
-cancel_ping_timeout(State = #state{ping_tref = TRef}) ->
+cancel_ping_timeout(Data = #data{ping_tref = undefined}) ->
+ Data;
+cancel_ping_timeout(Data = #data{ping_tref = TRef}) ->
cancel_timer(TRef),
- State#state{ping_tref = undefined}.
+ Data#data{ping_tref = undefined}.
% Returns either the method of the outbound request or its reference
% as a binary if not found. Only mean for logging.
-outbound_req_tag(#state{outbound = ReqMap}, ReqRef) ->
+outbound_req_tag(#data{outbound = ReqMap}, ReqRef) ->
case maps:find(ReqRef, ReqMap) of
error -> as_bin(ReqRef);
{ok, #outbound_req{method = Method}} -> format_method(Method)
end.
-outbound_method(#state{outbound = ReqMap}, ReqRef) ->
+outbound_method(#data{outbound = ReqMap}, ReqRef) ->
case maps:find(ReqRef, ReqMap) of
error -> undefined;
{ok, #outbound_req{method = Method}} -> Method
@@ -441,13 +470,13 @@ outbound_method(#state{outbound = ReqMap}, ReqRef) ->
% Returns either the method of the inbound request or its reference
% as a binary if not found. Only mean for logging.
-inbound_req_tag(#state{inbound = ReqMap}, ReqRef) ->
+inbound_req_tag(#data{inbound = ReqMap}, ReqRef) ->
case maps:find(ReqRef, ReqMap) of
error -> as_bin(ReqRef);
{ok, #inbound_req{method = Method}} -> format_method(Method)
end.
-inbound_method(#state{inbound = ReqMap}, ReqRef) ->
+inbound_method(#data{inbound = ReqMap}, ReqRef) ->
case maps:find(ReqRef, ReqMap) of
error -> undefined;
{ok, #inbound_req{method = Method}} -> Method
@@ -460,7 +489,7 @@ index_errors(ErrorSpecs) ->
lists:foldl(fun({Tag, Code, Msg}, {Tags, Codes}) ->
{Tags#{Tag => {Code, Msg}}, Codes#{Code => {Tag, Msg}}}
end, {ErrorTags, ErrorCodes}, ErrorSpecs),
- % The error list is put in a persistent term to not add noise to the state.
+ % The error list is put in a persistent term to not add noise to the Data.
persistent_term:put({?MODULE, self(), tags}, ErrorTags2),
persistent_term:put({?MODULE, self(), codes}, ErrorCodes2),
ok.
@@ -505,7 +534,7 @@ encode_error(Tag, Message)
{ok, {Code, _DefaultMessage}} -> {Code, Message}
end.
-connection_start(State = #state{uri = Uri, domain = Domain, port = Port},
+connection_start(Data = #data{uri = Uri, domain = Domain, port = Port},
TransportSpec) ->
BaseGunOpts = #{protocols => [http], retry => 0},
GunOpts = case TransportSpec of
@@ -518,7 +547,7 @@ connection_start(State = #state{uri = Uri, domain = Domain, port = Port},
case gun:open(binary_to_list(Domain), Port, GunOpts) of
{ok, GunPid} ->
GunRef = monitor(process, GunPid),
- {ok, State#state{gun_pid = GunPid, gun_ref = GunRef}};
+ {ok, Data#data{gun_pid = GunPid, gun_ref = GunRef}};
{error, Reason} ->
?GRISP_ERROR("Failed to open connection to ~s: ~p", [Uri, Reason],
#{event => connection_failure, uri => Uri,
@@ -526,157 +555,153 @@ connection_start(State = #state{uri = Uri, domain = Domain, port = Port},
{error, Reason}
end.
-connection_upgrade(State = #state{path = Path, gun_pid = GunPid}) ->
+connection_upgrade(Data = #data{path = Path, gun_pid = GunPid}) ->
WsStream = gun:ws_upgrade(GunPid, Path,[], #{silence_pings => false}),
- State#state{ws_stream = WsStream}.
+ Data#data{ws_stream = WsStream}.
-connection_established(State = #state{handler = Handler}) ->
- State2 = schedule_ping_timeout(State#state{connected = true}),
+connection_established(Data = #data{handler = Handler}) ->
Handler ! {conn, self(), connected},
- State2.
+ schedule_ping_timeout(Data).
-connection_close(State = #state{gun_pid = GunPid, gun_ref = GunRef})
+connection_close(Data = #data{gun_pid = GunPid, gun_ref = GunRef})
when GunPid =/= undefined, GunRef =/= undefined ->
demonitor(GunRef),
gun:shutdown(GunPid),
- State2 = cancel_ping_timeout(State),
- State3 = requests_error(State2, not_connected),
- State3#state{gun_pid = undefined, gun_ref = undefined,
- ws_stream = undefined, connected = false};
-connection_close(State) ->
- State2 = requests_error(State, not_connected),
- State2#state{connected = false}.
-
-connection_closed(State, Reason) ->
- State2 = cancel_ping_timeout(State),
- State3 = requests_error(State2, Reason),
- State3#state{gun_pid = undefined, gun_ref = undefined,
- ws_stream = undefined, connected = false}.
-
-send_request(State, Method, Params, From, Ctx) ->
- {ReqRef, State2} = outbound_add(State, Method, From, Ctx),
+ Data2 = cancel_ping_timeout(Data),
+ Data3 = requests_error(Data2, not_connected),
+ Data3#data{gun_pid = undefined, gun_ref = undefined, ws_stream = undefined};
+connection_close(Data) ->
+ requests_error(Data, not_connected).
+
+connection_closed(Data, Reason) ->
+ Data2 = cancel_ping_timeout(Data),
+ Data3 = requests_error(Data2, Reason),
+ Data3#data{gun_pid = undefined, gun_ref = undefined, ws_stream = undefined}.
+
+send_request(Data, Method, Params, From, Ctx) ->
+ {ReqRef, Data2} = outbound_add(Data, Method, From, Ctx),
Msg = {request, format_method(Method), Params, ReqRef},
- send_packet(State2, Msg).
+ send_packet(Data2, Msg).
-send_notification(State, Method, Params) ->
+send_notification(Data, Method, Params) ->
Msg = {notification, format_method(Method), Params},
- send_packet(State, Msg).
+ send_packet(Data, Msg).
-send_response(State, Result, ReqRef) ->
+send_response(Data, Result, ReqRef) ->
Msg = {result, Result, ReqRef},
- inbound_response(State, Msg, ReqRef).
+ inbound_response(Data, Msg, ReqRef).
-send_error(State, Code, Message, Data, ReqRef) ->
+send_error(Data, Code, Message, ErData, ReqRef) ->
{Code2, Message2} = encode_error(Code, Message),
- Msg = {error, Code2, Message2, Data, ReqRef},
+ Msg = {error, Code2, Message2, ErData, ReqRef},
case ReqRef of
- undefined -> send_packet(State, Msg);
- _ -> inbound_response(State, Msg, ReqRef)
+ undefined -> send_packet(Data, Msg);
+ _ -> inbound_response(Data, Msg, ReqRef)
end.
-send_packet(State = #state{gun_pid = GunPid, ws_stream = Stream}, Packet) ->
+send_packet(Data = #data{gun_pid = GunPid, ws_stream = Stream}, Packet) ->
Payload = grisp_connect_jsonrpc:encode(Packet),
?TRACE_OUTPUT(Payload),
gun:ws_send(GunPid, Stream, {text, Payload}),
- State.
+ Data.
-process_data(State = #state{batches = BatchMap}, Data) ->
+process_text(Data = #data{batches = BatchMap}, Text) ->
?TRACE_INPUT(Data),
- DecodedData = grisp_connect_jsonrpc:decode(Data),
+ DecodedData = grisp_connect_jsonrpc:decode(Text),
case DecodedData of
Messages when is_list(Messages) ->
BatchRef = make_ref(),
- case process_messages(State, BatchRef, 0, [], Messages) of
- {ReqCount, Replies, State2} when ReqCount > 0 ->
+ case process_messages(Data, BatchRef, 0, [], Messages) of
+ {ReqCount, Replies, Data2} when ReqCount > 0 ->
Batch = #batch{bref = BatchRef, refcount = ReqCount,
responses = Replies},
- State2#state{batches = BatchMap#{BatchRef => Batch}};
- {_ReqCount, [], State2} ->
- State2;
- {_ReqCount, [_|_] = Replies, State2} ->
+ Data2#data{batches = BatchMap#{BatchRef => Batch}};
+ {_ReqCount, [], Data2} ->
+ Data2;
+ {_ReqCount, [_|_] = Replies, Data2} ->
% All the requests got a reply right away
- send_packet(State2, Replies)
+ send_packet(Data2, Replies)
end;
Message when is_tuple(Message) ->
- case process_messages(State, undefined, 0, [], [Message]) of
- {_, [Reply], State2} ->
- send_packet(State2, Reply);
- {_, [], State2} ->
- State2
+ case process_messages(Data, undefined, 0, [], [Message]) of
+ {_, [Reply], Data2} ->
+ send_packet(Data2, Reply);
+ {_, [], Data2} ->
+ Data2
end
end.
-process_messages(State, _BatchRef, ReqCount, Replies, []) ->
- {ReqCount, lists:reverse(Replies), State};
-process_messages(State, BatchRef, ReqCount, Replies,
+process_messages(Data, _BatchRef, ReqCount, Replies, []) ->
+ {ReqCount, lists:reverse(Replies), Data};
+process_messages(Data, BatchRef, ReqCount, Replies,
[{decoding_error, _, _, _, _} = Error | Rest]) ->
- process_messages(State, BatchRef, ReqCount, [Error | Replies], Rest);
-process_messages(State, BatchRef, ReqCount, Replies,
+ process_messages(Data, BatchRef, ReqCount, [Error | Replies], Rest);
+process_messages(Data, BatchRef, ReqCount, Replies,
[{request, RawMethod, Params, ReqRef} | Rest]) ->
Method = parse_method(RawMethod),
- State2 = process_request(State, BatchRef, Method, Params, ReqRef),
- process_messages(State2, BatchRef, ReqCount + 1, Replies, Rest);
-process_messages(State, BatchRef, ReqCount, Replies,
+ Data2 = process_request(Data, BatchRef, Method, Params, ReqRef),
+ process_messages(Data2, BatchRef, ReqCount + 1, Replies, Rest);
+process_messages(Data, BatchRef, ReqCount, Replies,
[{notification, RawMethod, Params} | Rest]) ->
Method = parse_method(RawMethod),
- State2 = process_notification(State, Method, Params),
- process_messages(State2, BatchRef, ReqCount, Replies, Rest);
-process_messages(State, BatchRef, ReqCount, Replies,
+ Data2 = process_notification(Data, Method, Params),
+ process_messages(Data2, BatchRef, ReqCount, Replies, Rest);
+process_messages(Data, BatchRef, ReqCount, Replies,
[{result, Result, ReqRef} | Rest]) ->
- {State2, Replies2} = process_response(State, Replies, Result, ReqRef),
- process_messages(State2, BatchRef, ReqCount, Replies2, Rest);
-process_messages(State, BatchRef, ReqCount, Replies,
- [{error, Code, Message, Data, ReqRef} | Rest]) ->
- {State2, Replies2} = process_error(State, Replies, Code,
- Message, Data, ReqRef),
- process_messages(State2, BatchRef, ReqCount, Replies2, Rest).
-
-process_request(State = #state{handler = Handler},
+ {Data2, Replies2} = process_response(Data, Replies, Result, ReqRef),
+ process_messages(Data2, BatchRef, ReqCount, Replies2, Rest);
+process_messages(Data, BatchRef, ReqCount, Replies,
+ [{error, Code, Message, ErData, ReqRef} | Rest]) ->
+ {Data2, Replies2} = process_error(Data, Replies, Code,
+ Message, ErData, ReqRef),
+ process_messages(Data2, BatchRef, ReqCount, Replies2, Rest).
+
+process_request(Data = #data{handler = Handler},
BatchRef, Method, Params, ReqRef) ->
Handler ! {conn, self(), {request, Method, Params, ReqRef}},
- inbound_add(State, BatchRef, Method, ReqRef).
+ inbound_add(Data, BatchRef, Method, ReqRef).
-process_notification(State = #state{handler = Handler}, Method, Params) ->
+process_notification(Data = #data{handler = Handler}, Method, Params) ->
Handler ! {conn, self(), {notification, Method, Params}},
- State.
+ Data.
-process_response(State = #state{handler = Handler}, Replies, Result, ReqRef) ->
- case outbound_del(State, ReqRef) of
+process_response(Data = #data{handler = Handler}, Replies, Result, ReqRef) ->
+ case outbound_del(Data, ReqRef) of
{error, not_found} ->
%FIXME: Not sure what error should be returned...
Error = {invalid_request, -32600, <<"Result for unknown request">>},
- {State, [Error | Replies]};
- {ok, _Method, undefined, Ctx, State2} ->
+ {Data, [Error | Replies]};
+ {ok, _Method, undefined, Ctx, Data2} ->
Handler ! {conn, self(), {response, Result, Ctx}},
- {State2, Replies};
- {ok, _, From, _, State2} ->
- gen_server:reply(From, {ok, Result}),
- {State2, Replies}
+ {Data2, Replies};
+ {ok, _, From, _, Data2} ->
+ gen_statem:reply(From, {ok, Result}),
+ {Data2, Replies}
end.
-process_error(State = #state{handler = Handler},
- Replies, Code, Message, Data, undefined) ->
+process_error(Data = #data{handler = Handler},
+ Replies, Code, Message, ErData, undefined) ->
{Code2, Message2} = decode_error(Code, Message),
- Handler ! {conn, self(), {remote_error, Code2, Message2, Data}},
- {State, Replies};
-process_error(State = #state{handler = Handler},
- Replies, Code, Message, Data, ReqRef) ->
- case outbound_del(State, ReqRef) of
+ Handler ! {conn, self(), {remote_error, Code2, Message2, ErData}},
+ {Data, Replies};
+process_error(Data = #data{handler = Handler},
+ Replies, Code, Message, ErData, ReqRef) ->
+ case outbound_del(Data, ReqRef) of
{error, not_found} ->
%FIXME: Not sure what error should be returned...
Error = {invalid_request, -32600, <<"Error for unknown request">>},
- {State, [Error | Replies]};
- {ok, _Method, undefined, Ctx, State2} ->
+ {Data, [Error | Replies]};
+ {ok, _Method, undefined, Ctx, Data2} ->
{Code2, Message2} = decode_error(Code, Message),
- Handler ! {conn, self(), {remote_error, Code2, Message2, Data, Ctx}},
- {State2, Replies};
- {ok, _, From, _, State2} ->
+ Handler ! {conn, self(), {remote_error, Code2, Message2, ErData, Ctx}},
+ {Data2, Replies};
+ {ok, _, From, _, Data2} ->
{Code2, Message2} = decode_error(Code, Message),
- gen_server:reply(From, {remote_error, Code2, Message2, Data}),
- {State2, Replies}
+ gen_statem:reply(From, {remote_error, Code2, Message2, ErData}),
+ {Data2, Replies}
end.
-inbound_response(State = #state{batches = BatchMap, inbound = ReqMap},
+inbound_response(Data = #data{batches = BatchMap, inbound = ReqMap},
Message, ReqRef) ->
case maps:take(ReqRef, ReqMap) of
error ->
@@ -684,17 +709,17 @@ inbound_response(State = #state{batches = BatchMap, inbound = ReqMap},
[ReqRef],
#{event => internal_error, ref => ReqRef,
reason => unknown_request}),
- State;
+ Data;
{#inbound_req{bref = undefined}, ReqMap2} ->
% Not part of a batch response
- send_packet(State#state{inbound = ReqMap2}, Message);
+ send_packet(Data#data{inbound = ReqMap2}, Message);
{#inbound_req{bref = BatchRef}, ReqMap2} ->
% The batch must exists
case maps:find(BatchRef, BatchMap) of
{ok, #batch{refcount = 1, responses = Responses}} ->
% This is the last message of the batch
BatchMap2 = maps:remove(BatchRef, BatchMap),
- send_packet(State#state{batches = BatchMap2,
+ send_packet(Data#data{batches = BatchMap2,
inbound = ReqMap2},
[Message | Responses]);
{ok, Batch = #batch{refcount = RefCount,
@@ -702,53 +727,53 @@ inbound_response(State = #state{batches = BatchMap, inbound = ReqMap},
Batch2 = Batch#batch{refcount = RefCount - 1,
responses = [Message | Responses]},
BatchMap2 = BatchMap#{BatchRef => Batch2},
- State#state{batches = BatchMap2, inbound = ReqMap2}
+ Data#data{batches = BatchMap2, inbound = ReqMap2}
end
end.
-inbound_add(State = #state{inbound = ReqMap}, BatchRef, Method, ReqRef) ->
+inbound_add(Data = #data{inbound = ReqMap}, BatchRef, Method, ReqRef) ->
%TODO: Should we add a timeout for inbound requests ?
Req = #inbound_req{method = Method, id = ReqRef, bref = BatchRef},
- State#state{inbound = ReqMap#{ReqRef => Req}}.
+ Data#data{inbound = ReqMap#{ReqRef => Req}}.
-outbound_add(State = #state{request_timeout = Timeout, outbound = ReqMap},
+outbound_add(Data = #data{request_timeout = Timeout, outbound = ReqMap},
Method, From, Ctx) ->
ReqRef = make_reqref(),
TRef = send_after(Timeout, {outbound_timeout, ReqRef}),
Req = #outbound_req{id = ReqRef, method = Method, tref = TRef,
from = From, ctx = Ctx},
- {ReqRef, State#state{outbound = ReqMap#{ReqRef => Req}}}.
+ {ReqRef, Data#data{outbound = ReqMap#{ReqRef => Req}}}.
-outbound_del(State = #state{outbound = ReqMap}, ReqRef) ->
+outbound_del(Data = #data{outbound = ReqMap}, ReqRef) ->
case maps:find(ReqRef, ReqMap) of
error -> {error, not_found};
{ok, #outbound_req{method = Method, tref = TRef,
from = From, ctx = Ctx}} ->
cancel_timer(TRef),
- State2 = State#state{outbound = maps:remove(ReqRef, ReqMap)},
- {ok, Method, From, Ctx, State2}
+ Data2 = Data#data{outbound = maps:remove(ReqRef, ReqMap)},
+ {ok, Method, From, Ctx, Data2}
end.
-outbound_timeout(State = #state{handler = Handler}, ReqRef) ->
- case outbound_del(State, ReqRef) of
+outbound_timeout(Data = #data{handler = Handler}, ReqRef) ->
+ case outbound_del(Data, ReqRef) of
{error, not_found} ->
?GRISP_WARN("Timeout for unknown request ~s", [ReqRef],
#{event => internal_error, ref => ReqRef,
reason => unknown_request}),
- State;
- {ok, _Method, undefined, Ctx, State2} ->
+ Data;
+ {ok, _Method, undefined, Ctx, Data2} ->
Handler ! {conn, self(), {local_error, timeout, Ctx}},
- State2;
- {ok, _, From, _, State2} ->
- gen_server:reply(From, {error, timeout}),
- State2
+ Data2;
+ {ok, _, From, _, Data2} ->
+ gen_statem:reply(From, {error, timeout}),
+ Data2
end.
-requests_error(State = #state{handler = Handler, outbound = ReqMap}, Reason) ->
+requests_error(Data = #data{handler = Handler, outbound = ReqMap}, Reason) ->
maps:foreach(fun
(_, #outbound_req{from = undefined, ctx = Ctx}) ->
Handler ! {conn, self(), {local_error, Reason, Ctx}};
(_, #outbound_req{from = From, ctx = undefined}) ->
- gen_server:reply(From, {error, Reason})
+ gen_statem:reply(From, {error, Reason})
end, ReqMap),
- State#state{outbound = #{}}.
+ Data#data{outbound = #{}}.
diff --git a/test/grisp_connect_connection_SUITE.erl b/test/grisp_connect_connection_SUITE.erl
index 295ca02..81bb8ed 100644
--- a/test/grisp_connect_connection_SUITE.erl
+++ b/test/grisp_connect_connection_SUITE.erl
@@ -85,9 +85,12 @@ end_per_suite(Config) ->
init_per_testcase(_TestCase, Config) ->
{ok, _} = application:ensure_all_started(grisp_emulation),
- Config.
+ Conn = connect(),
+ [{conn, Conn} | Config].
-end_per_testcase(_, Config) ->
+end_per_testcase(_TestCase, Config) ->
+ Conn = proplists:get_value(conn, Config),
+ disconnect(Conn),
grisp_connect_test_server:wait_disconnection(),
?assertEqual([], flush()),
Config.
@@ -95,28 +98,28 @@ end_per_testcase(_, Config) ->
%--- Tests ---------------------------------------------------------------------
-basic_server_notifications_test(_) ->
- Conn = connect(),
+basic_server_notifications_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
send_jsonrpc_notification(<<"ping">>, #{foo => null}),
?assertConnNotification(Conn, [ping], #{foo := undefined}),
send_jsonrpc_notification(<<"foo.bar.ping">>, #{}),
?assertConnNotification(Conn, [foo, bar, ping], _),
send_jsonrpc_notification(<<"foo.bar.NotAnAtom">>, #{}),
?assertConnNotification(Conn, [foo, bar, <<"NotAnAtom">>], _),
- disconnect(Conn).
+ ok.
-basic_client_notifications_test(_) ->
- Conn = connect(),
+basic_client_notifications_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
grisp_connect_connection:notify(Conn, ping, #{foo => undefined}),
?receiveNotification(<<"ping">>, #{foo := null}),
grisp_connect_connection:notify(Conn, [foo, bar, ping], #{}),
?receiveNotification(<<"foo.bar.ping">>, _),
grisp_connect_connection:notify(Conn, [foo, bar, <<"NotAnAtom">>], #{}),
?receiveNotification(<<"foo.bar.NotAnAtom">>, _),
- disconnect(Conn).
+ ok.
-basic_server_request_test(_) ->
- Conn = connect(),
+basic_server_request_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
send_jsonrpc_request(<<"toto">>, #{}, 1),
?assertConnRequest(Conn, [toto], _, 1),
grisp_connect_connection:reply(Conn, <<"spam">>, 1),
@@ -133,10 +136,10 @@ basic_server_request_test(_) ->
?assertConnRequest(Conn, [foo, bar, titi], _, 4),
grisp_connect_connection:error(Conn, -42, <<"Message">>, undefined, 4),
?receiveError(-42, <<"Message">>, 4),
- disconnect(Conn).
+ ok.
-basic_client_synchronous_request_test(_) ->
- Conn = connect(),
+basic_client_synchronous_request_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
Async1 = async_eval(fun() -> grisp_connect_connection:request(Conn, [toto], #{}) end),
Id1 = ?receiveRequest(<<"toto">>, _),
send_jsonrpc_result(<<"spam">>, Id1),
@@ -149,10 +152,10 @@ basic_client_synchronous_request_test(_) ->
Id3 = ?receiveRequest(<<"titi">>, _),
send_jsonrpc_error(-2, <<"Custom">>, Id3),
?assertEqual({remote_error, error2, <<"Custom">>, undefined}, async_get_result(Async3)),
- disconnect(Conn).
+ ok.
-basic_client_asynchronous_request_test(_) ->
- Conn = connect(),
+basic_client_asynchronous_request_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
grisp_connect_connection:post(Conn, toto, #{}, ctx1),
Id1 = ?receiveRequest(<<"toto">>, _),
send_jsonrpc_result(<<"spam">>, Id1),
@@ -165,29 +168,29 @@ basic_client_asynchronous_request_test(_) ->
Id3 = ?receiveRequest(<<"titi">>, _),
send_jsonrpc_error(-2, <<"Custom">>, Id3),
?assertConnRemoteError(Conn, error2, <<"Custom">>, undefined, ctx3),
- disconnect(Conn).
+ ok.
-basic_error_test(_) ->
- Conn = connect(),
+basic_error_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
grisp_connect_connection:error(Conn, -1, undefined, undefined, undefined),
?receiveError(-1, <<"Error Number 1">>, null),
send_jsonrpc_error(-2, null, null),
?assertConnRemoteError(Conn, error2, <<"Error Number 2">>, undefined),
- disconnect(Conn).
+ ok.
-request_timeout_test(_) ->
- Conn = connect(),
+request_timeout_test(Config) ->
+ Conn = proplists:get_value(conn, Config),
grisp_connect_connection:post(Conn, toto, #{}, ctx1),
_Id1 = ?receiveRequest(<<"toto">>, _),
timer:sleep(500),
?assertConnLocalError(Conn, timeout, ctx1),
- disconnect(Conn).
+ ok.
spec_example_test(Config) ->
DataDir = proplists:get_value(data_dir, Config),
ExamplesFile = filename:join(DataDir, "jsonrpc_examples.txt"),
- Conn = connect(),
+ Conn = proplists:get_value(conn, Config),
{ok, ExData} = file:read_file(ExamplesFile),
Examples = parse_examples(ExData),
@@ -223,7 +226,7 @@ spec_example_test(Config) ->
end
end, Examples),
- disconnect(Conn).
+ ok.
%--- Internal Functions --------------------------------------------------------
diff --git a/test/grisp_connect_log_SUITE.erl b/test/grisp_connect_log_SUITE.erl
index 583736d..8e6c249 100644
--- a/test/grisp_connect_log_SUITE.erl
+++ b/test/grisp_connect_log_SUITE.erl
@@ -61,7 +61,7 @@ string_logs_test(_) ->
S2 = <<"@#$%^&*()_ +{}|:\"<>?-[];'./,\\`~!\néäüßóçøáîùêñÄÖÜÉÁÍÓÚàèìòùÂÊÎÔÛ€"/utf8>>,
Strings = [S1, S2],
Texts = [<>, <>],
- LastSeq = reset_log(last_seq()),
+ LastSeq = last_seq(),
Seqs = [LastSeq + 1, LastSeq + 2],
Fun = fun({Seq, String, Text}) ->
grisp_connect:log(error, [String]),
@@ -80,7 +80,7 @@ formatted_logs_test(_) ->
<<"<<\"tést\">>, <<\"tést\"/utf8>>"/utf8>>,
<<"tést, tést"/utf8>>,
<<"tést, tést"/utf8>>],
- LastSeq = reset_log(last_seq()),
+ LastSeq = last_seq(),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(ArgsList)),
Fun = fun({Seq, Args, Text}) ->
grisp_connect:log(error, Args),
@@ -107,7 +107,7 @@ structured_logs_test(_) ->
#{event => 1234},
#{event => 0.1},
<<"[JSON incompatible term]\n#{event => {äh,bäh}}"/utf8>>],
- LastSeq = reset_log(last_seq()),
+ LastSeq = last_seq(),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(Events)),
Fun = fun({Seq, Event, Text}) ->
grisp_connect:log(error, [Event]),
@@ -124,7 +124,7 @@ log_level_test(_) ->
notice,
info,
debug],
- LastSeq = reset_log(last_seq()),
+ LastSeq = last_seq(),
Seqs = lists:seq(LastSeq + 1, LastSeq + length(Levels)),
Fun = fun({Seq, Level}) ->
grisp_connect:log(Level, ["level test"]),
@@ -148,7 +148,7 @@ meta_data_test(_) ->
custom5 => #{boolean => true},
custom6 => 6,
custom7 => 7.0},
- LastSeq = reset_log(last_seq()),
+ LastSeq = last_seq(),
Seq = LastSeq + 1,
grisp_connect:log(error, ["Test meta", Meta]),
send_logs(),
@@ -187,10 +187,9 @@ last_seq() ->
Events = maps:get(events, maps:get(params, Send)),
[Seq, _] = lists:last(Events),
send_jsonrpc_result(#{seq => Seq, dropped => 0}, Id),
- Seq.
+ % Consume all the log events, updating the sequence number
+ reset_log(Seq).
-reset_log() ->
- reset_log(undefined).
reset_log(LasSeq) ->
send_logs(),
diff --git a/test/grisp_connect_reconnect_SUITE.erl b/test/grisp_connect_reconnect_SUITE.erl
index 144c216..343c882 100644
--- a/test/grisp_connect_reconnect_SUITE.erl
+++ b/test/grisp_connect_reconnect_SUITE.erl
@@ -89,5 +89,5 @@ reconnect_on_closed_frame_test(_) ->
connection_gun_pid() ->
{_, {data, _, _, _, _, ConnPid, _}} = sys:get_state(grisp_connect_client),
- {state, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _, _} = sys:get_state(ConnPid),
+ {_, {data, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _}} = sys:get_state(ConnPid),
GunPid.
From 2e1df561892666a18b8aa3aa5194ab8a6100ce7f Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Fri, 10 Jan 2025 19:34:25 +0100
Subject: [PATCH 08/12] Move JSON-RPC connection out to its own library
---
CHANGELOG.md | 5 +-
docs/grisp_connect_architecture.md | 30 +-
rebar.config | 4 +-
src/grisp_connect.app.src | 4 +-
src/grisp_connect_api.erl | 4 +-
src/grisp_connect_client.erl | 36 +-
src/grisp_connect_connection.erl | 779 ------------------
src/grisp_connect_internal.hrl | 25 -
src/grisp_connect_jsonrpc.erl | 190 -----
src/grisp_connect_utils.erl | 45 -
test/grisp_connect_connection_SUITE.erl | 330 --------
.../jsonrpc_examples.txt | 96 ---
test/grisp_connect_jsonrpc_SUITE.erl | 118 ---
13 files changed, 46 insertions(+), 1620 deletions(-)
delete mode 100644 src/grisp_connect_connection.erl
delete mode 100644 src/grisp_connect_internal.hrl
delete mode 100644 src/grisp_connect_jsonrpc.erl
delete mode 100644 src/grisp_connect_utils.erl
delete mode 100644 test/grisp_connect_connection_SUITE.erl
delete mode 100644 test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
delete mode 100644 test/grisp_connect_jsonrpc_SUITE.erl
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b36a110..7ab1898 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,9 +15,8 @@ individual JSON-RPC requests changed from ws_requests_timeout ot
ws_request_timeout.
- Le default log filter changed to trying to filter out only some messages to
filtering out all progress messages, as it wasn't working reliably.
-- The connection is not a persistent process anymore, it is now a transiant
-process handling a connection and dying when the connection is closed.
-- Internally, the JSON-RPC is parsed into a list of atom or binaries to pave the
+- JSON-RPC logic was extracted into the jarl library.
+- Jarl parses the methods into a list of atom or binaries to pave the
road for namespaces. foo.bar.Buz is parsed into [foo, bar, <<"Buz">>] (if foo
and bar are already existing atoms, but 'Buz' is not).
diff --git a/docs/grisp_connect_architecture.md b/docs/grisp_connect_architecture.md
index 850aec2..05857c7 100644
--- a/docs/grisp_connect_architecture.md
+++ b/docs/grisp_connect_architecture.md
@@ -3,20 +3,24 @@
```mermaid
graph TD
RootSup[VM Root Supervisor]
-
+
subgraph GrispConnectApp[Grisp Connect Application]
GrispConnectRootSup[Root Supervisor
grisp_connect_sup]
GrispConnectLogServer[Log Server
grisp_connect_log_server]
GrispConnectClient[Client
grisp_connect_client]
- GrispConnectConnection[JSON-RPC Connection
grisp_connect_connection]
- GrispConnectJsonRPC[JSON-RPC Codec
grisp_connect_jsonrpc]
-
+
GrispConnectRootSup --Supervise--> GrispConnectLogServer
GrispConnectRootSup --Supervise--> GrispConnectClient
- GrispConnectClient --Spawn and Monitor--> GrispConnectConnection
- GrispConnectConnection --Use--> GrispConnectJsonRPC
+ GrispConnectClient --Spawn and Monitor--> JarlConnection
+ end
+
+ subgraph JarlApp[Jarl Application]
+ JarlConnection[JSON-RPC Connection
jarl_connection]
+ JarlJsonRpc[JSON-RPC Codec
jarl_jsonrpc]
+
+ JarlConnection --Use--> JarlJsonRpc
end
-
+
subgraph GunApp[Gun Application]
GunRootSup[Gun Root Supervisor
gun_sup]
GunConnsSup[Gun Connection Supervisor
gun_conns_sup]
@@ -26,10 +30,10 @@ graph TD
GunRootSup --Supervise--> GunConnsSup
GunConnsSup --Supervise--> Gun
end
-
+
RootSup --Supervise--> GrispConnectRootSup
RootSup --Supervise--> GunRootSup
- GrispConnectConnection -.Interact.-> Gun
+ JarlConnection -.Interact.-> Gun
```
@@ -40,13 +44,13 @@ The client process is the main state machine. Its responsabilities are:
- Trigger connection/reconnection to the backend.
- Expose high-level protocol API to the application.
- Implement generic API endpoints.
-
+
See the [client documentation](grisp_connect_client.md).
## Connection
-`grisp_connect_connection` module encpasulate a JSON-RPC connection.
+grisp_connect use the jarl library to handle the JSON-RPC connection.
It is not supervised, the process starting it must monitor it.
@@ -61,5 +65,5 @@ It provides a high-level API to a JSON-RPC connection:
When performing an asynchronous request, the caller can give an opaque context
term, that will given back when receiving a response or an error for this
-request, allowing the caller to handle the asynchronous operation without
-having to store information locally.
+request, allowing the caller to handle the asynchronous operation without having
+to store information locally.
diff --git a/rebar.config b/rebar.config
index 44cc1fd..407aba5 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,8 +1,8 @@
{erl_opts, [debug_info]}.
{deps, [
- {grisp, "~> 2.7"},
- {gun, "2.1.0"},
jsx,
+ {jarl, {git, "https://github.com/grisp/jarl.git"}},
+ {grisp, "~> 2.7"},
{grisp_cryptoauth, "~> 2.4"},
{certifi, "2.13.0"}
]}.
diff --git a/src/grisp_connect.app.src b/src/grisp_connect.app.src
index ce7076c..9c90847 100644
--- a/src/grisp_connect.app.src
+++ b/src/grisp_connect.app.src
@@ -7,10 +7,10 @@
kernel,
inets,
stdlib,
+ jsx,
+ jarl,
grisp,
grisp_cryptoauth,
- gun,
- jsx,
certifi
]},
{optional_applications, [
diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl
index 46bcb7d..eca79a2 100644
--- a/src/grisp_connect_api.erl
+++ b/src/grisp_connect_api.erl
@@ -15,8 +15,8 @@
% @doc Handles requests, notifications and errors from grisp.io.
-spec handle_msg(Msg) ->
ok | {reply, Result :: term(), ReqRef :: binary() | integer()}
- when Msg :: {request, Method :: grisp_connect_connection:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
- | {notification, grisp_connect_connection:method(), Params :: map() | list()}
+ when Msg :: {request, Method :: jarl:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
+ | {notification, jarl:method(), Params :: map() | list()}
| {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term()}.
handle_msg({notification, M, Params}) ->
?LOG_ERROR("Received unexpected notification ~p: ~p", [M, Params]),
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 20fb86d..6adec2e 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -7,9 +7,7 @@
-behaviour(gen_statem).
--include("grisp_connect_internal.hrl").
-
--import(grisp_connect_utils, [as_bin/1]).
+-include_lib("kernel/include/logger.hrl").
% External API
-export([start_link/0]).
@@ -52,6 +50,7 @@
%--- Macros --------------------------------------------------------------------
+-define(FORMAT(FMT, ARGS), iolist_to_binary(io_lib:format(FMT, ARGS))).
-define(STD_TIMEOUT, 1000).
-define(CONNECT_TIMEOUT, 2000).
-define(ENV(KEY, GUARDS), fun() ->
@@ -162,7 +161,8 @@ connecting(enter, _OldState, Data) ->
{keep_state, Data, [{state_timeout, 0, connect}]};
connecting(state_timeout, connect,
Data = #data{conn = undefined, retry_count = RetryCount}) ->
- ?GRISP_INFO("Connecting to grisp.io", [], #{event => connecting}),
+ ?LOG_INFO(#{description => <<"Connecting to grisp.io">>,
+ event => connecting}),
case conn_start(Data) of
{ok, Data2} ->
{keep_state, Data2, [{state_timeout, ?CONNECT_TIMEOUT, timeout}]};
@@ -173,13 +173,14 @@ connecting(state_timeout, connect,
end;
connecting(state_timeout, timeout, Data = #data{retry_count = RetryCount}) ->
Reason = connect_timeout,
- ?GRISP_WARN("Timeout while connecting to grisp.io", [],
- #{event => connection_failed, reason => Reason}),
+ ?LOG_WARNING(#{description => <<"Timeout while connecting to grisp.io">>,
+ event => connection_failed, reason => Reason}),
Data2 = conn_close(Data, Reason),
{next_state, waiting_ip, Data2#data{retry_count = RetryCount + 1}};
connecting(info, {conn, Conn, connected}, Data = #data{conn = Conn}) ->
% Received from the connection process
- ?GRISP_INFO("Connected to grisp.io", [], #{event => connected}),
+ ?LOG_INFO(#{description => <<"Connected to grisp.io">>,
+ event => connected}),
{next_state, connected, Data#data{retry_count = 0}};
?HANDLE_COMMON.
@@ -217,8 +218,9 @@ handle_common(info, reboot, _, _) ->
handle_common(info, {'EXIT', Conn, Reason}, _State,
Data = #data{conn = Conn, retry_count = RetryCount}) ->
% The connection process died
- ?GRISP_WARN("The connection to grisp.io died: ~p", [Reason],
- #{event => connection_failed, reason => Reason}),
+ ?LOG_WARNING(#{description =>
+ ?FORMAT("The connection to grisp.io died: ~p", [Reason]),
+ event => connection_failed, reason => Reason}),
{next_state, waiting_ip, conn_died(Data#data{retry_count = RetryCount + 1})};
handle_common(info, {conn, Conn, Msg}, State, _Data) ->
?LOG_DEBUG("Received message from unknown connection ~p in state ~w: ~p",
@@ -248,6 +250,10 @@ generic_errors() -> [
{validate_from_unbooted, -13, <<"Validate from unbooted">>}
].
+as_bin(Binary) when is_binary(Binary) -> Binary;
+as_bin(List) when is_list(List) -> list_to_binary(List);
+as_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
+
handle_connection_message(_Data, {response, _Result, #{on_result := undefined}}) ->
keep_state_and_data;
handle_connection_message(Data, {response, Result, #{on_result := OnResult}}) ->
@@ -298,7 +304,7 @@ conn_start(Data = #data{conn = undefined,
ping_timeout => WsPingTimeout,
request_timeout => WsReqTimeout
},
- case grisp_connect_connection:start_link(self(), ConnOpts) of
+ case jarl:start_link(self(), ConnOpts) of
{error, _Reason} = Error -> Error;
{ok, Conn} -> {ok, Data#data{conn = Conn}}
end.
@@ -308,7 +314,7 @@ conn_close(Data = #data{conn = undefined}, _Reason) ->
Data;
conn_close(Data = #data{conn = Conn}, _Reason) ->
grisp_connect_log_server:stop(),
- grisp_connect_connection:disconnect(Conn),
+ jarl:disconnect(Conn),
Data#data{conn = undefined}.
% Safe to call in any state
@@ -316,14 +322,14 @@ conn_died(Data) ->
grisp_connect_log_server:stop(),
Data#data{conn = undefined}.
--spec conn_post(data(), grisp_connect_connection:method(), atom(), map(),
+-spec conn_post(data(), jarl:method(), atom(), map(),
undefined | on_result_fun(), undefined | on_error_fun())
-> data().
conn_post(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
when Conn =/= undefined ->
ReqCtx = #{on_result => OnResult, on_error => OnError},
Params2 = maps:put(type, Type, Params),
- case grisp_connect_connection:post(Conn, Method, Params2, ReqCtx) of
+ case jarl:post(Conn, Method, Params2, ReqCtx) of
ok -> Data;
{error, Reason} ->
OnError(Data, local, Reason, undefined, undefined)
@@ -332,11 +338,11 @@ conn_post(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
conn_notify(#data{conn = Conn}, Method, Type, Params)
when Conn =/= undefined ->
Params2 = maps:put(type, Type, Params),
- grisp_connect_connection:notify(Conn, Method, Params2).
+ jarl:notify(Conn, Method, Params2).
conn_reply(#data{conn = Conn}, Result, ReqRef)
when Conn =/= undefined ->
- grisp_connect_connection:reply(Conn, Result, ReqRef).
+ jarl:reply(Conn, Result, ReqRef).
% IP check functions
diff --git a/src/grisp_connect_connection.erl b/src/grisp_connect_connection.erl
deleted file mode 100644
index af81fb9..0000000
--- a/src/grisp_connect_connection.erl
+++ /dev/null
@@ -1,779 +0,0 @@
-%% @doc JSONRpc 2.0 Websocket connection
--module(grisp_connect_connection).
-
--behaviour(gen_statem).
-
--include("grisp_connect_internal.hrl").
-
--import(grisp_connect_utils, [as_bin/1]).
--import(grisp_connect_utils, [parse_method/1]).
--import(grisp_connect_utils, [format_method/1]).
-
-% API Functions
--export([start_link/2]).
--export([request/3]).
--export([post/4]).
--export([notify/3]).
--export([reply/3]).
--export([error/5]).
--export([disconnect/1]).
-
-% Behaviour gen_statem callback functions
--export([callback_mode/0]).
--export([init/1]).
--export([code_change/4]).
--export([terminate/3]).
-
-% Data Functions
--export([connecting/3]).
--export([connected/3]).
-
-
-%--- Types ---------------------------------------------------------------------
-
--record(batch, {
- bref :: reference(),
- refcount :: pos_integer(),
- responses :: list()
-}).
-
--record(inbound_req, {
- method :: method(),
- id :: binary() | integer(),
- bref :: undefined | reference() % Set if part of a batch
-}).
-
--record(outbound_req, {
- method :: method(),
- id :: binary() | integer(),
- tref :: undefined | reference(),
- from :: undefined | gen_statem:from(),
- ctx :: undefined | term()
-}).
-
--record(data, {
- handler :: pid(),
- uri :: iodata(),
- domain :: binary(),
- port :: inet:port_number(),
- path :: binary(),
- ping_timeout :: infinity | pos_integer(),
- request_timeout :: infinity | pos_integer(),
- batches = #{} :: #{reference() => #batch{}},
- inbound = #{} :: #{binary() | integer() => #inbound_req{}},
- outbound = #{} :: #{binary() | integer() => #outbound_req{}},
- gun_pid :: undefined | pid(),
- gun_ref :: undefined | reference(),
- ws_stream :: undefined | gun:stream_ref(),
- ping_tref :: undefined | reference()
-}).
-
--type error_mapping() :: {atom(), integer(), binary()}.
--type method() :: atom() | binary() | [atom() | binary()].
--type start_options() :: #{
- domain := atom() | string() | binary(),
- port := inet:port_number(),
- %FIXME: dialyzer do no like ssl:tls_client_option(), maybe some erlang version issue
- transport := tcp | tls | {tls, TlsOpts :: list()},
- path := atom() | string() | binary(),
- errors => [error_mapping()],
- ping_timeout => infinity | pos_integer(),
- request_timeout => infinity | pos_integer()
-}.
-
-% Type specfication of the messages that are sent to the handler:
--type handler_messages() ::
- {conn, pid(), connected}
- | {conn, pid(), {request, method(), Params :: map() | list(), ReqRef :: binary() | integer()}}
- | {conn, pid(), {notification, method(), Params :: map() | list()}}
- | {conn, pid(), {response, Result :: term(), Ctx :: term()}}
- | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), ErData :: term()}}
- | {conn, pid(), {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), ErData :: term(), Ctx :: term()}}
- | {conn, pid(), {local_error, Reason:: atom(), Ctx :: term()}}.
-
--export_type([error_mapping/0, method/0, handler_messages/0]).
-
-
-%--- Macros --------------------------------------------------------------------
-
--define(ENABLE_TRACE, false).
--if(?ENABLE_TRACE =:= true).
--define(TRACE_OUTPUT(ARG), ?GRISP_DEBUG("<<<<<<<<<< ~s", [ARG])).
--define(TRACE_INPUT(ARG), ?GRISP_DEBUG(">>>>>>>>>> ~s", [ARG])).
--else.
--define(TRACE_OUTPUT(ARG), ok).
--define(TRACE_INPUT(ARG), ok).
--endif.
-
--define(DEFAULT_PING_TIMEOUT, 60_000).
--define(DEFAULT_REQUEST_TIMEOUT, 5_000).
--define(DEFAULT_TRANSPORT, tcp).
-
--define(DEFAULT_JSONRPC_ERRORS, [
- {invalid_json, -32700, <<"Parse error">>},
- {invalid_request, -32600, <<"Invalid Request">>},
- {method_not_found, -32601, <<"Method not found">>},
- {invalid_params, -32602, <<"Invalid parameters">>},
- {internal_error, -32603, <<"Internal error">>}
-]).
-
--define(HANDLE_COMMON,
- ?FUNCTION_NAME(EventType, EventContent, Data) ->
- handle_common(EventType, EventContent, ?FUNCTION_NAME, Data)).
-
-
-%--- API Functions -------------------------------------------------------------
-
--spec start_link(Handler :: pid(), Options :: start_options()) ->
- {ok, Conn :: pid()} | {error, Reason :: term()}.
-start_link(Handler, Opts = #{domain := _, port := _, path := _}) ->
- gen_statem:start_link(?MODULE, [Handler, Opts], []).
-
--spec request(Conn :: pid(), Method :: method(), Params :: map()) ->
- {ok, Result :: term()} | {error, timeout} | {error, not_connected}
- | {error, Code :: integer(),
- Message :: undefined | binary(), ErData :: term()}.
-request(Conn, Method, Params) ->
- try gen_statem:call(Conn, {request, parse_method(Method), Params}) of
- {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack);
- Other -> Other
- catch
- exit:{noproc, _} -> {error, not_connected}
- end.
-
--spec post(Conn :: pid(), Method :: method(), Params :: map(),
- ReqCtx :: term()) -> ok | {error, not_connected}.
-post(Conn, Method, Params, ReqCtx) ->
- try gen_statem:call(Conn, {post, parse_method(Method), Params, ReqCtx}) of
- {ok, CallResult} -> CallResult;
- {error, _Reason} = Error -> Error;
- {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
- catch
- exit:{noproc, _} -> {error, not_connected}
- end.
-
--spec notify(Conn :: pid(), Method :: method(), Params :: map()) ->
- ok | {error, not_connected}.
-notify(Conn, Method, Params) ->
- try gen_statem:call(Conn, {notify, parse_method(Method), Params}) of
- {ok, CallResult} -> CallResult;
- {error, _Reason} = Error -> Error;
- {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
- catch
- exit:{noproc, _} -> {error, not_connected}
- end.
-
--spec reply(Conn :: pid(), Result :: any(), ReqRef :: binary()) ->
- ok | {error, not_connected}.
-reply(Conn, Result, ReqRef) ->
- try gen_statem:call(Conn, {reply, Result, ReqRef}) of
- {ok, CallResult} -> CallResult;
- {error, _Reason} = Error -> Error;
- {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
- catch
- exit:{noproc, _} -> {error, not_connected}
- end.
-
--spec error(Conn :: pid(), Code :: atom() | integer(),
- Message :: undefined | binary(), ErData :: term(),
- ReqRef :: undefined | binary()) ->
- ok | {error, not_connected}.
-error(Conn, Code, Message, ErData, ReqRef) ->
- try gen_statem:call(Conn, {error, Code, Message, ErData, ReqRef}) of
- {ok, CallResult} -> CallResult;
- {error, _Reason} = Error -> Error;
- {exception, Class, Reason, Stack} -> erlang:raise(Class, Reason, Stack)
- catch
- exit:{noproc, _} -> {error, not_connected}
- end.
-
--spec disconnect(Conn :: pid()) -> ok.
-disconnect(Conn) ->
- try gen_statem:call(Conn, disconnect)
- catch
- exit:{noproc, _} -> ok
- end.
-
-
-%--- Behaviour gen_statem Callbacks --------------------------------------------
-
-callback_mode() -> [state_functions].
-
-init([Handler, Opts]) ->
- process_flag(trap_exit, true), % To ensure terminate/2 is called
- #{domain := Domain, port := Port, path := Path} = Opts,
- PingTimeout = maps:get(ping_timeout, Opts, ?DEFAULT_PING_TIMEOUT),
- ReqTimeout = maps:get(request_timeout, Opts, ?DEFAULT_REQUEST_TIMEOUT),
- Transport = maps:get(transport, Opts, ?DEFAULT_TRANSPORT),
- Data = #data{
- handler = Handler,
- uri = format_ws_uri(Transport, Domain, Port, Path),
- domain = as_bin(Domain),
- port = Port,
- path = as_bin(Path),
- ping_timeout = PingTimeout,
- request_timeout = ReqTimeout
- },
- index_errors(?DEFAULT_JSONRPC_ERRORS),
- index_errors(maps:get(errors, Opts, [])),
- case connection_start(Data, Transport) of
- {ok, Data2} -> {ok, connecting, Data2};
- {error, Reason} -> {stop, Reason}
- end.
-
-code_change(_Vsn, State, Data, _Extra) ->
- {ok, State, Data}.
-
-terminate(_Reason, _State, Data) ->
- connection_close(Data),
- persistent_term:erase({?MODULE, self(), tags}),
- persistent_term:erase({?MODULE, self(), codes}),
- ok.
-
-
-%--- Behaviour gen_statem State Callback Functions -----------------------------
-
-connecting({call, From}, {request, Method, _Params}, _Data) ->
- ?GRISP_INFO("Request ~s performed while connecting",
- [format_method(Method)],
- #{event => rpc_request_error, method => Method,
- reason => not_connected}),
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting({call, From}, {post, Method, _Params, ReqCtx},
- #data{handler = Handler}) ->
- ?GRISP_INFO("Request ~s posted while connecting",
- [format_method(Method)],
- #{event => rpc_post_error, method => Method,
- reason => not_connected}),
- % Notify the handler anyway so it doesn't have to make a special case
- Handler ! {conn, self(), {local_error, not_connected, ReqCtx}},
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting({call, From}, {notify, Method, _Params}, _Data) ->
- ?GRISP_INFO("Notification ~s posted while connecting",
- [format_method(Method)],
- #{event => rpc_notify_error, method => Method,
- reason => not_connected}),
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting({call, From}, {reply, _Result, ReqRef}, Data) ->
- ?GRISP_INFO("Reply to ~s posted while connecting",
- [inbound_req_tag(Data, ReqRef)],
- #{event => rpc_reply_error, ref => ReqRef,
- method => inbound_method(Data, ReqRef),
- reason => not_connected}),
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting({call, From}, {error, Code, _Message, _ErData, undefined}, _Data) ->
- ?GRISP_INFO("Error ~w posted while connecting", [Code],
- #{event => rpc_error_error, code => Code,
- reason => not_connected}),
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting({call, From}, {error, Code, _Message, _ErData, ReqRef}, Data) ->
- ?GRISP_INFO("Error ~w to ~s posted while connecting",
- [Code, inbound_req_tag(Data, ReqRef)],
- #{event => rpc_error_error, code => Code, ref => ReqRef,
- reason => not_connected}),
- {keep_state_and_data, [{reply, From, {error, not_connected}}]};
-connecting(info, {gun_up, GunPid, _}, Data = #data{gun_pid = GunPid}) ->
- ?GRISP_DEBUG("Connection to ~s established",
- [Data#data.uri],
- #{event => ws_connection_enstablished, uri => Data#data.uri}),
- {keep_state, connection_upgrade(Data)};
-connecting(info, {gun_upgrade, Pid, Stream, [<<"websocket">>], _},
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_DEBUG("Connection to ~s upgraded to websocket", [Data#data.uri],
- #{event => ws_upgraded, uri => Data#data.uri}),
- {next_state, connected, connection_established(Data)};
-
-connecting(info, {gun_response, Pid, Stream, _, Status, _Headers},
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_INFO("Connection to ~s failed to upgrade to websocket: ~p",
- [Data#data.uri, Status],
- #{event => ws_upgrade_failed, uri => Data#data.uri,
- status => Status}),
- {stop, ws_upgrade_failed, connection_close(Data)};
-?HANDLE_COMMON.
-
-connected({call, From}, {request, Method, Params}, Data) ->
- try send_request(Data, Method, Params, From, undefined) of
- Data2 -> {keep_state, Data2}
- catch
- C:badarg:S ->
- {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
- C:R:S ->
- {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-connected({call, From}, {post, Method, Params, ReqCtx}, Data) ->
- try send_request(Data, Method, Params, undefined, ReqCtx) of
- Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
- catch
- C:badarg:S ->
- {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
- C:R:S ->
- {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-connected({call, From}, {notify, Method, Params}, Data) ->
- try send_notification(Data, Method, Params) of
- Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
- catch
- C:badarg:S ->
- {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
- C:R:S ->
- {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-connected({call, From}, {reply, Result, ReqRef}, Data) ->
- try send_response(Data, Result, ReqRef) of
- Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
- catch
- C:badarg:S ->
- {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
- C:R:S ->
- {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-connected({call, From}, {error, Code, Message, ErData, ReqRef}, Data) ->
- try send_error(Data, Code, Message, ErData, ReqRef) of
- Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
- catch
- C:badarg:S ->
- {keep_state_and_data, [{reply, From, {exception, C, badarg, S}}]};
- C:R:S ->
- {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-connected(info, {gun_ws, Pid, Stream, {text, Text}},
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- {keep_state, process_text(Data, Text)};
-connected(info, ping_timeout, Data) ->
- ?GRISP_INFO("Connection to ~s timed out", [Data#data.uri],
- #{event => ws_ping_timeout, uri => Data#data.uri}),
- {stop, normal, connection_close(Data)};
-connected(info, {outbound_timeout, ReqRef}, Data) ->
- ?GRISP_INFO("Request ~s time to ~s timed out",
- [outbound_req_tag(Data, ReqRef), Data#data.uri],
- #{event => rpc_request_timeout_error, uri => Data#data.uri,
- ref => ReqRef, method => outbound_method(Data, ReqRef)}),
- {keep_state, outbound_timeout(Data, ReqRef)};
-?HANDLE_COMMON.
-
-handle_common({call, From}, disconnect, _StateName, Data) ->
- try connection_close(Data) of
- Data2 -> {keep_state, Data2, [{reply, From, {ok, ok}}]}
- catch
- C:R:S -> {stop_and_reply, R, [{reply, From, {exception, C, R, S}}]}
- end;
-handle_common(info, {gun_ws, Pid, Stream, ping}, _StateName,
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- {keep_state, schedule_ping_timeout(Data)};
-handle_common(info, {gun_ws, Pid, Stream, close}, StateName,
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_INFO("Connection to ~s closed without code", [Data#data.uri],
- #{event => ws_stream_closed, uri => Data#data.uri,
- state => StateName}),
- {stop, normal, connection_closed(Data, closed)};
-handle_common(info, {gun_ws, Pid, Stream, {close, Code, Message}}, StateName,
- Data = #data{gun_pid = Pid, ws_stream = Stream}) ->
- ?GRISP_INFO("Connection to ~s closed: ~s (~w)",
- [Data#data.uri, Message, Code],
- #{event => ws_stream_closed, uri => Data#data.uri,
- code => Code, reason => Message, state => StateName}),
- {stop, normal, connection_closed(Data, closed)};
-handle_common(info, {gun_error, Pid, _Stream, Reason}, StateName,
- Data = #data{gun_pid = Pid}) ->
- ?GRISP_INFO("Connection to ~s got an error: ~p",
- [Data#data.uri, Reason],
- #{event => ws_error, uri => Data#data.uri,
- reason => Reason, state => StateName}),
- {stop, Reason, connection_close(Data)};
-handle_common(info, {gun_down, Pid, ws, Reason, [Stream]}, StateName,
- Data = #data{gun_pid = Pid, ws_stream = Stream})
- when Reason =:= closed; Reason =:= {error, closed}; Reason =:= normal ->
- ?GRISP_INFO("Connection to ~s was closed by the server", [Data#data.uri],
- #{event => ws_closed_by_peer, uri => Data#data.uri,
- reason => closed, state => StateName}),
- {stop, normal, connection_close(Data)};
-handle_common(info, {'DOWN', _, process, Pid, Reason}, StateName,
- Data = #data{gun_pid = Pid}) ->
- ?GRISP_INFO("gun process of the connection to ~s crashed: ~p",
- [Data#data.uri, Reason],
- #{event => ws_gun_crash, uri => Data#data.uri,
- reason => Reason, state => StateName}),
- {stop, Reason, connection_closed(Data, gun_crashed)};
-handle_common(info, {gun_up, Pid, http} = Info, StateName,
- #data{gun_pid = GunPid}) ->
- ?GRISP_DEBUG("Ignoring unexpected gun_up message"
- " from pid ~p, current pid is ~p", [Pid, GunPid],
- #{event => unexpected_gun_message, message => Info,
- state => StateName}),
- keep_state_and_data;
-handle_common({call, From}, Call, StateName, _Data) ->
- ?GRISP_ERROR("Unexpected call from ~p to ~s: ~p", [From, ?MODULE, Call],
- #{event => unexpected_call, from => From, message => Call,
- state => StateName}),
- {stop_and_reply, {unexpected_call, Call},
- [{reply, From, {error, unexpected_call}}]};
-handle_common(cast, Cast, StateName, _Data) ->
- Reason = {unexpected_cast, Cast},
- ?GRISP_ERROR("Unexpected cast to ~s: ~p", [?MODULE, Cast],
- #{event => unexpected_cast, message => Cast,
- state => StateName}),
- {stop, Reason};
-handle_common(info, Info, StateName, _Data) ->
- ?GRISP_WARN("Unexpected info message to ~s: ~p",
- [?MODULE, Info],
- #{event => unexpected_info, message => Info,
- state => StateName}),
- keep_state_and_data.
-
-
-%--- INTERNAL FUNCTION ---------------------------------------------------------
-
-format_ws_uri(Transport, Domain, Port, Path) ->
- Proto = case Transport of
- tcp -> <<"ws">>;
- tls -> <<"wss">>;
- {tls, _} -> <<"wss">>
- end,
- ?FORMAT("~s://~s:~w~s", [Proto, Domain, Port, Path]).
-
-make_reqref() ->
- list_to_binary(integer_to_list(erlang:unique_integer())).
-
-send_after(infinity, _Message) -> undefined;
-send_after(Timeout, Message) ->
- erlang:send_after(Timeout, self(), Message).
-
-cancel_timer(undefined) -> ok;
-cancel_timer(TRef) ->
- erlang:cancel_timer(TRef).
-
-schedule_ping_timeout(Data = #data{ping_timeout = Timeout}) ->
- Data2 = cancel_ping_timeout(Data),
- TRef = send_after(Timeout, ping_timeout),
- Data2#data{ping_tref = TRef}.
-
-cancel_ping_timeout(Data = #data{ping_tref = undefined}) ->
- Data;
-cancel_ping_timeout(Data = #data{ping_tref = TRef}) ->
- cancel_timer(TRef),
- Data#data{ping_tref = undefined}.
-
-% Returns either the method of the outbound request or its reference
-% as a binary if not found. Only mean for logging.
-outbound_req_tag(#data{outbound = ReqMap}, ReqRef) ->
- case maps:find(ReqRef, ReqMap) of
- error -> as_bin(ReqRef);
- {ok, #outbound_req{method = Method}} -> format_method(Method)
- end.
-
-outbound_method(#data{outbound = ReqMap}, ReqRef) ->
- case maps:find(ReqRef, ReqMap) of
- error -> undefined;
- {ok, #outbound_req{method = Method}} -> Method
- end.
-
-% Returns either the method of the inbound request or its reference
-% as a binary if not found. Only mean for logging.
-inbound_req_tag(#data{inbound = ReqMap}, ReqRef) ->
- case maps:find(ReqRef, ReqMap) of
- error -> as_bin(ReqRef);
- {ok, #inbound_req{method = Method}} -> format_method(Method)
- end.
-
-inbound_method(#data{inbound = ReqMap}, ReqRef) ->
- case maps:find(ReqRef, ReqMap) of
- error -> undefined;
- {ok, #inbound_req{method = Method}} -> Method
- end.
-
-index_errors(ErrorSpecs) ->
- ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
- ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
- {ErrorTags2, ErrorCodes2} =
- lists:foldl(fun({Tag, Code, Msg}, {Tags, Codes}) ->
- {Tags#{Tag => {Code, Msg}}, Codes#{Code => {Tag, Msg}}}
- end, {ErrorTags, ErrorCodes}, ErrorSpecs),
- % The error list is put in a persistent term to not add noise to the Data.
- persistent_term:put({?MODULE, self(), tags}, ErrorTags2),
- persistent_term:put({?MODULE, self(), codes}, ErrorCodes2),
- ok.
-
-decode_error(Code, Message)
- when is_integer(Code), Message =:= undefined ->
- ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
- case maps:find(Code, ErrorCodes) of
- error -> {Code, undefined};
- {ok, {Tag, DefaultMessage}} -> {Tag, DefaultMessage}
- end;
-decode_error(Code, Message)
- when is_integer(Code) ->
- ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
- case maps:find(Code, ErrorCodes) of
- error -> {Code, Message};
- {ok, {Tag, _DefaultMessage}} -> {Tag, Message}
- end.
-
-encode_error(Code, Message)
- when is_integer(Code), Message =:= undefined ->
- ErrorCodes = persistent_term:get({?MODULE, self(), codes}, #{}),
- case maps:find(Code, ErrorCodes) of
- error -> {Code, null};
- {ok, {_Tag, DefaultMessage}} -> {Code, DefaultMessage}
- end;
-encode_error(Code, Message)
- when is_integer(Code) ->
- {Code, Message};
-encode_error(Tag, Message)
- when is_atom(Tag), Message =:= undefined ->
- ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
- case maps:find(Tag, ErrorTags) of
- error -> erlang:error(badarg);
- {ok, {Code, DefaultMessage}} -> {Code, DefaultMessage}
- end;
-encode_error(Tag, Message)
- when is_atom(Tag) ->
- ErrorTags = persistent_term:get({?MODULE, self(), tags}, #{}),
- case maps:find(Tag, ErrorTags) of
- error -> erlang:error(badarg);
- {ok, {Code, _DefaultMessage}} -> {Code, Message}
- end.
-
-connection_start(Data = #data{uri = Uri, domain = Domain, port = Port},
- TransportSpec) ->
- BaseGunOpts = #{protocols => [http], retry => 0},
- GunOpts = case TransportSpec of
- tcp -> BaseGunOpts#{transport => tcp};
- tls -> BaseGunOpts#{transport => tls};
- {tls, Opts} -> BaseGunOpts#{transport => tls, tls_opts => Opts}
- end,
- ?GRISP_DEBUG("Connecting to ~s", [Uri],
- #{event => connecting, uri => Uri, options => GunOpts}),
- case gun:open(binary_to_list(Domain), Port, GunOpts) of
- {ok, GunPid} ->
- GunRef = monitor(process, GunPid),
- {ok, Data#data{gun_pid = GunPid, gun_ref = GunRef}};
- {error, Reason} ->
- ?GRISP_ERROR("Failed to open connection to ~s: ~p", [Uri, Reason],
- #{event => connection_failure, uri => Uri,
- reason => Reason}),
- {error, Reason}
- end.
-
-connection_upgrade(Data = #data{path = Path, gun_pid = GunPid}) ->
- WsStream = gun:ws_upgrade(GunPid, Path,[], #{silence_pings => false}),
- Data#data{ws_stream = WsStream}.
-
-connection_established(Data = #data{handler = Handler}) ->
- Handler ! {conn, self(), connected},
- schedule_ping_timeout(Data).
-
-connection_close(Data = #data{gun_pid = GunPid, gun_ref = GunRef})
- when GunPid =/= undefined, GunRef =/= undefined ->
- demonitor(GunRef),
- gun:shutdown(GunPid),
- Data2 = cancel_ping_timeout(Data),
- Data3 = requests_error(Data2, not_connected),
- Data3#data{gun_pid = undefined, gun_ref = undefined, ws_stream = undefined};
-connection_close(Data) ->
- requests_error(Data, not_connected).
-
-connection_closed(Data, Reason) ->
- Data2 = cancel_ping_timeout(Data),
- Data3 = requests_error(Data2, Reason),
- Data3#data{gun_pid = undefined, gun_ref = undefined, ws_stream = undefined}.
-
-send_request(Data, Method, Params, From, Ctx) ->
- {ReqRef, Data2} = outbound_add(Data, Method, From, Ctx),
- Msg = {request, format_method(Method), Params, ReqRef},
- send_packet(Data2, Msg).
-
-send_notification(Data, Method, Params) ->
- Msg = {notification, format_method(Method), Params},
- send_packet(Data, Msg).
-
-send_response(Data, Result, ReqRef) ->
- Msg = {result, Result, ReqRef},
- inbound_response(Data, Msg, ReqRef).
-
-send_error(Data, Code, Message, ErData, ReqRef) ->
- {Code2, Message2} = encode_error(Code, Message),
- Msg = {error, Code2, Message2, ErData, ReqRef},
- case ReqRef of
- undefined -> send_packet(Data, Msg);
- _ -> inbound_response(Data, Msg, ReqRef)
- end.
-
-send_packet(Data = #data{gun_pid = GunPid, ws_stream = Stream}, Packet) ->
- Payload = grisp_connect_jsonrpc:encode(Packet),
- ?TRACE_OUTPUT(Payload),
- gun:ws_send(GunPid, Stream, {text, Payload}),
- Data.
-
-process_text(Data = #data{batches = BatchMap}, Text) ->
- ?TRACE_INPUT(Data),
- DecodedData = grisp_connect_jsonrpc:decode(Text),
- case DecodedData of
- Messages when is_list(Messages) ->
- BatchRef = make_ref(),
- case process_messages(Data, BatchRef, 0, [], Messages) of
- {ReqCount, Replies, Data2} when ReqCount > 0 ->
- Batch = #batch{bref = BatchRef, refcount = ReqCount,
- responses = Replies},
- Data2#data{batches = BatchMap#{BatchRef => Batch}};
- {_ReqCount, [], Data2} ->
- Data2;
- {_ReqCount, [_|_] = Replies, Data2} ->
- % All the requests got a reply right away
- send_packet(Data2, Replies)
- end;
- Message when is_tuple(Message) ->
- case process_messages(Data, undefined, 0, [], [Message]) of
- {_, [Reply], Data2} ->
- send_packet(Data2, Reply);
- {_, [], Data2} ->
- Data2
- end
- end.
-
-process_messages(Data, _BatchRef, ReqCount, Replies, []) ->
- {ReqCount, lists:reverse(Replies), Data};
-process_messages(Data, BatchRef, ReqCount, Replies,
- [{decoding_error, _, _, _, _} = Error | Rest]) ->
- process_messages(Data, BatchRef, ReqCount, [Error | Replies], Rest);
-process_messages(Data, BatchRef, ReqCount, Replies,
- [{request, RawMethod, Params, ReqRef} | Rest]) ->
- Method = parse_method(RawMethod),
- Data2 = process_request(Data, BatchRef, Method, Params, ReqRef),
- process_messages(Data2, BatchRef, ReqCount + 1, Replies, Rest);
-process_messages(Data, BatchRef, ReqCount, Replies,
- [{notification, RawMethod, Params} | Rest]) ->
- Method = parse_method(RawMethod),
- Data2 = process_notification(Data, Method, Params),
- process_messages(Data2, BatchRef, ReqCount, Replies, Rest);
-process_messages(Data, BatchRef, ReqCount, Replies,
- [{result, Result, ReqRef} | Rest]) ->
- {Data2, Replies2} = process_response(Data, Replies, Result, ReqRef),
- process_messages(Data2, BatchRef, ReqCount, Replies2, Rest);
-process_messages(Data, BatchRef, ReqCount, Replies,
- [{error, Code, Message, ErData, ReqRef} | Rest]) ->
- {Data2, Replies2} = process_error(Data, Replies, Code,
- Message, ErData, ReqRef),
- process_messages(Data2, BatchRef, ReqCount, Replies2, Rest).
-
-process_request(Data = #data{handler = Handler},
- BatchRef, Method, Params, ReqRef) ->
- Handler ! {conn, self(), {request, Method, Params, ReqRef}},
- inbound_add(Data, BatchRef, Method, ReqRef).
-
-process_notification(Data = #data{handler = Handler}, Method, Params) ->
- Handler ! {conn, self(), {notification, Method, Params}},
- Data.
-
-process_response(Data = #data{handler = Handler}, Replies, Result, ReqRef) ->
- case outbound_del(Data, ReqRef) of
- {error, not_found} ->
- %FIXME: Not sure what error should be returned...
- Error = {invalid_request, -32600, <<"Result for unknown request">>},
- {Data, [Error | Replies]};
- {ok, _Method, undefined, Ctx, Data2} ->
- Handler ! {conn, self(), {response, Result, Ctx}},
- {Data2, Replies};
- {ok, _, From, _, Data2} ->
- gen_statem:reply(From, {ok, Result}),
- {Data2, Replies}
- end.
-
-process_error(Data = #data{handler = Handler},
- Replies, Code, Message, ErData, undefined) ->
- {Code2, Message2} = decode_error(Code, Message),
- Handler ! {conn, self(), {remote_error, Code2, Message2, ErData}},
- {Data, Replies};
-process_error(Data = #data{handler = Handler},
- Replies, Code, Message, ErData, ReqRef) ->
- case outbound_del(Data, ReqRef) of
- {error, not_found} ->
- %FIXME: Not sure what error should be returned...
- Error = {invalid_request, -32600, <<"Error for unknown request">>},
- {Data, [Error | Replies]};
- {ok, _Method, undefined, Ctx, Data2} ->
- {Code2, Message2} = decode_error(Code, Message),
- Handler ! {conn, self(), {remote_error, Code2, Message2, ErData, Ctx}},
- {Data2, Replies};
- {ok, _, From, _, Data2} ->
- {Code2, Message2} = decode_error(Code, Message),
- gen_statem:reply(From, {remote_error, Code2, Message2, ErData}),
- {Data2, Replies}
- end.
-
-inbound_response(Data = #data{batches = BatchMap, inbound = ReqMap},
- Message, ReqRef) ->
- case maps:take(ReqRef, ReqMap) of
- error ->
- ?GRISP_ERROR("Ask to send a response to the unknown request ~p",
- [ReqRef],
- #{event => internal_error, ref => ReqRef,
- reason => unknown_request}),
- Data;
- {#inbound_req{bref = undefined}, ReqMap2} ->
- % Not part of a batch response
- send_packet(Data#data{inbound = ReqMap2}, Message);
- {#inbound_req{bref = BatchRef}, ReqMap2} ->
- % The batch must exists
- case maps:find(BatchRef, BatchMap) of
- {ok, #batch{refcount = 1, responses = Responses}} ->
- % This is the last message of the batch
- BatchMap2 = maps:remove(BatchRef, BatchMap),
- send_packet(Data#data{batches = BatchMap2,
- inbound = ReqMap2},
- [Message | Responses]);
- {ok, Batch = #batch{refcount = RefCount,
- responses = Responses}} ->
- Batch2 = Batch#batch{refcount = RefCount - 1,
- responses = [Message | Responses]},
- BatchMap2 = BatchMap#{BatchRef => Batch2},
- Data#data{batches = BatchMap2, inbound = ReqMap2}
- end
- end.
-
-inbound_add(Data = #data{inbound = ReqMap}, BatchRef, Method, ReqRef) ->
- %TODO: Should we add a timeout for inbound requests ?
- Req = #inbound_req{method = Method, id = ReqRef, bref = BatchRef},
- Data#data{inbound = ReqMap#{ReqRef => Req}}.
-
-outbound_add(Data = #data{request_timeout = Timeout, outbound = ReqMap},
- Method, From, Ctx) ->
- ReqRef = make_reqref(),
- TRef = send_after(Timeout, {outbound_timeout, ReqRef}),
- Req = #outbound_req{id = ReqRef, method = Method, tref = TRef,
- from = From, ctx = Ctx},
- {ReqRef, Data#data{outbound = ReqMap#{ReqRef => Req}}}.
-
-outbound_del(Data = #data{outbound = ReqMap}, ReqRef) ->
- case maps:find(ReqRef, ReqMap) of
- error -> {error, not_found};
- {ok, #outbound_req{method = Method, tref = TRef,
- from = From, ctx = Ctx}} ->
- cancel_timer(TRef),
- Data2 = Data#data{outbound = maps:remove(ReqRef, ReqMap)},
- {ok, Method, From, Ctx, Data2}
- end.
-
-outbound_timeout(Data = #data{handler = Handler}, ReqRef) ->
- case outbound_del(Data, ReqRef) of
- {error, not_found} ->
- ?GRISP_WARN("Timeout for unknown request ~s", [ReqRef],
- #{event => internal_error, ref => ReqRef,
- reason => unknown_request}),
- Data;
- {ok, _Method, undefined, Ctx, Data2} ->
- Handler ! {conn, self(), {local_error, timeout, Ctx}},
- Data2;
- {ok, _, From, _, Data2} ->
- gen_statem:reply(From, {error, timeout}),
- Data2
- end.
-
-requests_error(Data = #data{handler = Handler, outbound = ReqMap}, Reason) ->
- maps:foreach(fun
- (_, #outbound_req{from = undefined, ctx = Ctx}) ->
- Handler ! {conn, self(), {local_error, Reason, Ctx}};
- (_, #outbound_req{from = From, ctx = undefined}) ->
- gen_statem:reply(From, {error, Reason})
- end, ReqMap),
- Data#data{outbound = #{}}.
diff --git a/src/grisp_connect_internal.hrl b/src/grisp_connect_internal.hrl
deleted file mode 100644
index a037dfb..0000000
--- a/src/grisp_connect_internal.hrl
+++ /dev/null
@@ -1,25 +0,0 @@
--ifndef(GRISP_CONNECT_INTERNAL_HRL).
--define(GRISP_CONNECT_INTERNAL_HRL, true).
-
--include_lib("kernel/include/logger.hrl").
-
--define(FORMAT(FMT, ARGS), iolist_to_binary(io_lib:format(FMT, ARGS))).
-
--define(GRISP_DEBUG(FMT, ARGS),
- ?LOG_DEBUG(FMT, ARGS)).
--define(GRISP_DEBUG(FMT, ARGS, REPORT),
- ?LOG_DEBUG(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
--define(GRISP_INFO(FMT, ARGS),
- ?LOG_INFO(FMT, ARGS)).
--define(GRISP_INFO(FMT, ARGS, REPORT),
- ?LOG_INFO(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
--define(GRISP_WARN(FMT, ARGS),
- ?LOG_WARNING(FMT, ARGS)).
--define(GRISP_WARN(FMT, ARGS, REPORT),
- ?LOG_WARNING(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
--define(GRISP_ERROR(FMT, ARGS),
- ?LOG_ERROR(FMT, ARGS)).
--define(GRISP_ERROR(FMT, ARGS, REPORT),
- ?LOG_ERROR(maps:put(description, ?FORMAT(FMT, ARGS), REPORT))).
-
--endif. % GRISP_CONNECT_INTERNAL_HRL
diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl
deleted file mode 100644
index 5029387..0000000
--- a/src/grisp_connect_jsonrpc.erl
+++ /dev/null
@@ -1,190 +0,0 @@
--module(grisp_connect_jsonrpc).
-
-% API
--export([decode/1]).
--export([encode/1]).
-
-
-%--- Types ---------------------------------------------------------------------
-
--type json_rpc_message() ::
- {request, Method :: binary(), Params :: map() | list(),
- ReqRef :: binary() | integer()}
- | {result, Result :: term(), ReqRef :: binary()}
- | {notification, Method :: binary(), Params :: map() | list()}
- | {error, Code :: integer(), Message :: undefined | binary(),
- Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}
- | {decoding_error, Code :: integer(), Message :: undefined | binary(),
- Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}.
-
-
-
-%--- Macros --------------------------------------------------------------------
-
--define(V, jsonrpc => <<"2.0">>).
--define(is_valid(Message),
- (map_get(jsonrpc, Message) == <<"2.0">>)
-).
--define(is_method(Method),
- (is_atom(Method) orelse is_binary(Method))
-).
--define(is_params(Params),
- (is_map(Params) orelse is_list(Params))
-).
--define(is_id(ID),
- (is_binary(ID) orelse is_integer(ID))
-).
-
-
-%--- API -----------------------------------------------------------------------
-
-%% @doc Decode a JSONRpc text packet and decoded message or a list of decoded
-%% messages in the case of a batch.
-%%
-%% If it returns a list, the all the responses are supposed to be sent back in
-%% a batch too, as per the JSONRpc 2.0 specifications.
-%%
-%% If some decoding errors occure, a special error message with the tag
-%% `decoding_error' will be returned, this message can be encoded and sent back
-%% directly to the JSON-RPC peer.
-%%
-%% During JSON decoding, the `null' values are changed to `undefined' and when
-%% encoding, `undefined' values are changed back to `null'.
-%%
-%% The `method' will always be a binary, and `id' will always be either
-%% a binary or an integer.
-%%
-%% The possible decoded messages are:
-%%
-%% - `{request, Method :: binary(), Params :: map() | list(), ReqRef :: binary() | integer()}'
-%% - `{result, Result :: term(), ReqRef :: binary()}'
-%% - `{notification, Method :: binary(), Params :: map() | list()}'
-%% - `{error, Code :: integer(), Message :: undefined | binary(), Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}'
-%% - `{decoding_error, Code :: integer(), Message :: undefined | binary(), Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}'
-%%
--spec decode(Data :: iodata()) -> json_rpc_message() | [json_rpc_message()].
-decode(Data) ->
- case json_to_term(iolist_to_binary(Data)) of
- [] ->
- {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined};
- Messages when is_list(Messages) ->
- [unpack(M) || M <- Messages];
- Message when is_map(Message) ->
- unpack(Message);
- {error, _Reason} ->
- % Even though the data could have been a batch, we return a single
- % error, as per JSON-RPC specifications
- {decoding_error, -32700, <<"Parse error">>, undefined, undefined}
- end.
-
-%% @doc Encode a JSONRpc message or a list of JSONRpc messages to JSON text.
-%% For backward compatibility, the `method' can be an atom.
--spec encode(Messages :: json_rpc_message() | [json_rpc_message()]) -> iodata().
-encode(Messages) when is_list(Messages) ->
- term_to_json([pack(M) || M <- Messages]);
-encode(Message) ->
- term_to_json(pack(Message)).
-
-
-%--- Internal ------------------------------------------------------------------
-
-as_bin(undefined) -> undefined;
-as_bin(Binary) when is_binary(Binary) -> Binary;
-as_bin(List) when is_list(List) -> list_to_binary(List).
-
-as_id(undefined) -> undefined;
-as_id(Integer) when is_integer(Integer) -> Integer;
-as_id(Binary) when is_binary(Binary) -> Binary;
-as_id(List) when is_list(List) -> list_to_binary(List).
-
-unpack(#{method := Method, params := Params, id := ID} = M)
- when ?is_valid(M), ?is_method(Method), ?is_params(Params), ID =/= undefined ->
- {request, as_bin(Method), Params, as_id(ID)};
-unpack(#{method := Method, id := ID} = M)
- when ?is_valid(M), ?is_method(Method), ID =/= undefined ->
- {request, as_bin(Method), undefined, as_id(ID)};
-unpack(#{method := Method, params := Params} = M)
- when ?is_valid(M), ?is_method(Method), ?is_params(Params) ->
- {notification, as_bin(Method), Params};
-unpack(#{method := Method} = M)
- when ?is_valid(M), ?is_method(Method) ->
- {notification, as_bin(Method), undefined};
-unpack(#{result := Result, id := ID} = M)
- when ?is_valid(M) ->
- {result, Result, as_id(ID)};
-unpack(#{error := #{code := Code, message := Message, data := Data},
- id := ID} = M)
- when ?is_valid(M), is_integer(Code) ->
- {error, Code, as_bin(Message), Data, as_id(ID)};
-unpack(#{error := #{code := Code, message := Message}, id := ID} = M)
- when ?is_valid(M), is_integer(Code) ->
- {error, Code, as_bin(Message), undefined, as_id(ID)};
-unpack(#{id := ID}) ->
- {decoding_error, -32600, <<"Invalid Request">>, undefined, as_id(ID)};
-unpack(_M) ->
- {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined}.
-
-pack({request, Method, undefined, ID})
- when is_binary(Method) orelse is_atom(Method), ?is_id(ID) ->
- #{?V, method => Method, id => ID};
-pack({request, Method, Params, ID})
- when is_binary(Method) orelse is_atom(Method),
- Params =:= undefined orelse ?is_params(Params),
- ?is_id(ID) ->
- #{?V, method => Method, params => Params, id => ID};
-pack({notification, Method, undefined})
- when is_binary(Method) orelse is_atom(Method) ->
- #{?V, method => Method};
-pack({notification, Method, Params})
- when is_binary(Method), Params =:= undefined orelse ?is_params(Params) ->
- #{?V, method => Method, params => Params};
-pack({result, Result, ID})
- when ?is_id(ID) ->
- #{?V, result => Result, id => ID};
-pack({ErrorTag, Code, Message, undefined, undefined})
- when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code),
- Message =:= undefined orelse is_binary(Message) ->
- #{?V, error => #{code => Code, message => Message}, id => null};
-pack({ErrorTag, Code, Message, undefined, ID})
- when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code),
- Message =:= undefined orelse is_binary(Message), ?is_id(ID) ->
- #{?V, error => #{code => Code, message => Message}, id => ID};
-pack({ErrorTag, Code, Message, Data, undefined})
- when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code),
- Message =:= undefined orelse is_binary(Message) ->
- #{?V, error => #{code => Code, message => Message, data => Data, id => null}};
-pack({ErrorTag, Code, Message, Data, ID})
- when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code),
- Message =:= undefined orelse is_binary(Message), ?is_id(ID) ->
- #{?V, error => #{code => Code, message => Message, data => Data}, id => ID};
-pack(Message) ->
- erlang:error({badarg, Message}).
-
-json_to_term(Bin) ->
- try jsx:decode(Bin, [{labels, attempt_atom}, return_maps]) of
- Json -> postprocess(Json)
- catch
- error:E -> {error, E}
- end.
-
-term_to_json(Term) ->
- jsx:encode(preprocess(Term)).
-
-postprocess(null) -> undefined;
-postprocess(Integer) when is_integer(Integer) -> Integer;
-postprocess(Float) when is_float(Float) -> Float;
-postprocess(Binary) when is_binary(Binary) -> Binary;
-postprocess(List) when is_list(List) ->
- [postprocess(E) || E <- List];
-postprocess(Map) when is_map(Map) ->
- maps:map(fun(_K, V) -> postprocess(V) end, Map).
-
-preprocess(undefined) -> null;
-preprocess(Atom) when is_atom(Atom) -> Atom;
-preprocess(Integer) when is_integer(Integer) -> Integer;
-preprocess(Float) when is_float(Float) -> Float;
-preprocess(Binary) when is_binary(Binary) -> Binary;
-preprocess(List) when is_list(List) ->
- [preprocess(E) || E <- List];
-preprocess(Map) when is_map(Map) ->
- maps:map(fun(_K, V) -> preprocess(V) end, Map).
diff --git a/src/grisp_connect_utils.erl b/src/grisp_connect_utils.erl
deleted file mode 100644
index 9a73457..0000000
--- a/src/grisp_connect_utils.erl
+++ /dev/null
@@ -1,45 +0,0 @@
--module(grisp_connect_utils).
-
-% API Functions
--export([as_bin/1]).
--export([maybe_atom/1]).
--export([parse_method/1]).
--export([format_method/1]).
-
-
-%--- API Functions -------------------------------------------------------------
-
-as_bin(Binary) when is_binary(Binary) -> Binary;
-as_bin(List) when is_list(List) -> list_to_binary(List);
-as_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
-
-maybe_atom(Bin) when is_binary(Bin) ->
- try binary_to_existing_atom(Bin)
- catch error:badarg -> Bin
- end.
-
-parse_method(List) when is_list(List) -> check_method(List);
-parse_method(Atom) when is_atom(Atom) -> [Atom];
-parse_method(Binary) when is_binary(Binary) ->
- [maybe_atom(B) || B <- binary:split(Binary, <<".">>, [global])].
-
-format_method(Binary) when is_binary(Binary) -> Binary;
-format_method(Atom) when is_atom(Atom) -> atom_to_binary(Atom);
-format_method(List) when is_list(List) ->
- iolist_to_binary(lists:join(<<".">>, [as_bin(E) || E <- List])).
-
-
-%--- Internal Functions --------------------------------------------------------
-
-check_method(Method) when is_list(Method) ->
- check_method(Method, Method);
-check_method(_Method) ->
- erlang:exit(badarg).
-
-check_method(Method, []) -> Method;
-check_method(Method, [Atom | Rest]) when is_atom(Atom) ->
- check_method(Method, Rest);
-check_method(Method, [Bin | Rest]) when is_binary(Bin) ->
- check_method(Method, Rest);
-check_method(_Method, _Rest) ->
- erlang:error(badarg).
diff --git a/test/grisp_connect_connection_SUITE.erl b/test/grisp_connect_connection_SUITE.erl
deleted file mode 100644
index 81bb8ed..0000000
--- a/test/grisp_connect_connection_SUITE.erl
+++ /dev/null
@@ -1,330 +0,0 @@
--module(grisp_connect_connection_SUITE).
-
--behaviour(ct_suite).
--include_lib("common_test/include/ct.hrl").
--include_lib("stdlib/include/assert.hrl").
--include("grisp_connect_test.hrl").
-
--compile([export_all, nowarn_export_all]).
-
--import(grisp_connect_test_async, [async_eval/1]).
--import(grisp_connect_test_async, [async_get_result/1]).
-
--import(grisp_connect_test_server, [flush/0]).
--import(grisp_connect_test_server, [send_text/1]).
--import(grisp_connect_test_server, [send_jsonrpc_request/3]).
--import(grisp_connect_test_server, [send_jsonrpc_notification/2]).
--import(grisp_connect_test_server, [send_jsonrpc_result/2]).
--import(grisp_connect_test_server, [send_jsonrpc_error/3]).
-
-
-%--- MACROS --------------------------------------------------------------------
-
--define(fmt(Fmt, Args), lists:flatten(io_lib:format(Fmt, Args))).
--define(assertConnRequest(Conn, M, P, R), fun() ->
- receive {conn, Conn, {request, M, P = Result, R}} -> Result
- after 1000 ->
- ?assert(false, ?fmt("The client connection did not receive request ~s ~s ~s",
- [??M, ??P, ??P]))
- end
-end()).
--define(assertConnNotification(Conn, M, P), fun() ->
- receive {conn, Conn, {notification, M, P = Result}} -> Result
- after 1000 ->
- ?assert(false, ?fmt("The client did not receive notification ~s ~s",
- [??M, ??P]))
- end
-end()).
--define(assertConnResponse(Conn, V, X), fun() ->
- receive {conn, Conn, {response, V = Result, X}} -> Result
- after 1000 ->
- ?assert(false, ?fmt("The client connection did not receive response ~s ~s",
- [??V, ??X]))
- end
-end()).
--define(assertConnRemoteError(Conn, C, M, D, X), fun() ->
- receive {conn, Conn, {remote_error, C, M, D, X}} -> ok
- after 1000 ->
- ?assert(false, ?fmt("The client connection did not receive remote error ~s ~s ~s ~s",
- [??C, ??M, ??D, ??X]))
- end
-end()).
--define(assertConnRemoteError(Conn, C, M, D), fun() ->
- receive {conn, Conn, {remote_error, C, M, D}} -> ok
- after 1000 ->
- ?assert(false, ?fmt("The client connection did not receive remote error ~s ~s ~s",
- [??C, ??M, ??D]))
- end
-end()).
--define(assertConnLocalError(Conn, R, X), fun() ->
- receive {conn, Conn, {local_error, R, X}} -> ok
- after 1000 ->
- ?assert(false, ?fmt("The client connection did not receive local error ~s ~s",
- [??R, ??X]))
- end
-end()).
-
-
-%--- API -----------------------------------------------------------------------
-
-all() ->
- [
- F
- ||
- {F, 1} <- ?MODULE:module_info(exports),
- lists:suffix("_test", atom_to_list(F))
- ].
-
-init_per_suite(Config) ->
- {ok, _} = application:ensure_all_started(gun),
- Apps = grisp_connect_test_server:start(),
- [{apps, Apps} | Config].
-
-end_per_suite(Config) ->
- grisp_connect_test_server:stop(?config(apps, Config)).
-
-init_per_testcase(_TestCase, Config) ->
- {ok, _} = application:ensure_all_started(grisp_emulation),
- Conn = connect(),
- [{conn, Conn} | Config].
-
-end_per_testcase(_TestCase, Config) ->
- Conn = proplists:get_value(conn, Config),
- disconnect(Conn),
- grisp_connect_test_server:wait_disconnection(),
- ?assertEqual([], flush()),
- Config.
-
-
-%--- Tests ---------------------------------------------------------------------
-
-basic_server_notifications_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- send_jsonrpc_notification(<<"ping">>, #{foo => null}),
- ?assertConnNotification(Conn, [ping], #{foo := undefined}),
- send_jsonrpc_notification(<<"foo.bar.ping">>, #{}),
- ?assertConnNotification(Conn, [foo, bar, ping], _),
- send_jsonrpc_notification(<<"foo.bar.NotAnAtom">>, #{}),
- ?assertConnNotification(Conn, [foo, bar, <<"NotAnAtom">>], _),
- ok.
-
-basic_client_notifications_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- grisp_connect_connection:notify(Conn, ping, #{foo => undefined}),
- ?receiveNotification(<<"ping">>, #{foo := null}),
- grisp_connect_connection:notify(Conn, [foo, bar, ping], #{}),
- ?receiveNotification(<<"foo.bar.ping">>, _),
- grisp_connect_connection:notify(Conn, [foo, bar, <<"NotAnAtom">>], #{}),
- ?receiveNotification(<<"foo.bar.NotAnAtom">>, _),
- ok.
-
-basic_server_request_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- send_jsonrpc_request(<<"toto">>, #{}, 1),
- ?assertConnRequest(Conn, [toto], _, 1),
- grisp_connect_connection:reply(Conn, <<"spam">>, 1),
- ?receiveResult(<<"spam">>, 1),
- send_jsonrpc_request(<<"foo.bar.tata">>, #{}, 2),
- ?assertConnRequest(Conn, [foo, bar, tata], _, 2),
- grisp_connect_connection:error(Conn, error1, undefined, undefined, 2),
- ?receiveError(-1, <<"Error Number 1">>, 2),
- send_jsonrpc_request(<<"foo.bar.toto">>, #{}, 3),
- ?assertConnRequest(Conn, [foo, bar, toto], _, 3),
- grisp_connect_connection:error(Conn, error2, <<"Custom">>, undefined, 3),
- ?receiveError(-2, <<"Custom">>, 3),
- send_jsonrpc_request(<<"foo.bar.titi">>, #{}, 4),
- ?assertConnRequest(Conn, [foo, bar, titi], _, 4),
- grisp_connect_connection:error(Conn, -42, <<"Message">>, undefined, 4),
- ?receiveError(-42, <<"Message">>, 4),
- ok.
-
-basic_client_synchronous_request_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- Async1 = async_eval(fun() -> grisp_connect_connection:request(Conn, [toto], #{}) end),
- Id1 = ?receiveRequest(<<"toto">>, _),
- send_jsonrpc_result(<<"spam">>, Id1),
- ?assertEqual({ok, <<"spam">>}, async_get_result(Async1)),
- Async2 = async_eval(fun() -> grisp_connect_connection:request(Conn, tata, #{}) end),
- Id2 = ?receiveRequest(<<"tata">>, _),
- send_jsonrpc_error(-1, null, Id2),
- ?assertEqual({remote_error, error1, <<"Error Number 1">>, undefined}, async_get_result(Async2)),
- Async3 = async_eval(fun() -> grisp_connect_connection:request(Conn, titi, #{}) end),
- Id3 = ?receiveRequest(<<"titi">>, _),
- send_jsonrpc_error(-2, <<"Custom">>, Id3),
- ?assertEqual({remote_error, error2, <<"Custom">>, undefined}, async_get_result(Async3)),
- ok.
-
-basic_client_asynchronous_request_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- grisp_connect_connection:post(Conn, toto, #{}, ctx1),
- Id1 = ?receiveRequest(<<"toto">>, _),
- send_jsonrpc_result(<<"spam">>, Id1),
- ?assertConnResponse(Conn, <<"spam">>, ctx1),
- grisp_connect_connection:post(Conn, tata, #{}, ctx2),
- Id2 = ?receiveRequest(<<"tata">>, _),
- send_jsonrpc_error(-1, null, Id2),
- ?assertConnRemoteError(Conn, error1, <<"Error Number 1">>, undefined, ctx2),
- grisp_connect_connection:post(Conn, titi, #{}, ctx3),
- Id3 = ?receiveRequest(<<"titi">>, _),
- send_jsonrpc_error(-2, <<"Custom">>, Id3),
- ?assertConnRemoteError(Conn, error2, <<"Custom">>, undefined, ctx3),
- ok.
-
-basic_error_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- grisp_connect_connection:error(Conn, -1, undefined, undefined, undefined),
- ?receiveError(-1, <<"Error Number 1">>, null),
- send_jsonrpc_error(-2, null, null),
- ?assertConnRemoteError(Conn, error2, <<"Error Number 2">>, undefined),
- ok.
-
-request_timeout_test(Config) ->
- Conn = proplists:get_value(conn, Config),
- grisp_connect_connection:post(Conn, toto, #{}, ctx1),
- _Id1 = ?receiveRequest(<<"toto">>, _),
- timer:sleep(500),
- ?assertConnLocalError(Conn, timeout, ctx1),
- ok.
-
-spec_example_test(Config) ->
- DataDir = proplists:get_value(data_dir, Config),
- ExamplesFile = filename:join(DataDir, "jsonrpc_examples.txt"),
-
- Conn = proplists:get_value(conn, Config),
-
- {ok, ExData} = file:read_file(ExamplesFile),
- Examples = parse_examples(ExData),
- maps:foreach(fun(Desc, Actions) ->
- try
- lists:foreach(fun
- ({send, Text}) ->
- send_text(Text);
- ({recv, Expected}) when is_list(Expected) ->
- example_handler(Conn),
- SortedExpected = lists:sort(Expected),
- Received = grisp_connect_test_server:receive_jsonrpc(),
- ?assert(is_list(Received),
- ?fmt("Invalid response to a batch request during ~s: ~p",
- [Desc, Received])),
- SortedReceived = lists:sort(Received),
- ?assertEqual(SortedExpected, SortedReceived,
- ?fmt("Invalid response during ~s", [Desc]));
- ({recv, Expected}) ->
- example_handler(Conn),
- Received = grisp_connect_test_server:receive_jsonrpc(),
- ?assertEqual(Expected, Received,
- ?fmt("Invalid response during ~s", [Desc]))
- end, Actions),
- example_handler(Conn),
- RemMsgs = flush(),
- ?assertEqual([], RemMsgs,
- ?fmt("Unexpected message during example ~s: ~p",
- [Desc, RemMsgs]))
- catch
- error:timeout ->
- ?assert(false, ?fmt("Timeout while testing example ~s", [Desc]))
- end
- end, Examples),
-
- ok.
-
-
-%--- Internal Functions --------------------------------------------------------
-
-connect() ->
- connect(#{}).
-
-connect(Opts) ->
- DefaultOpts = #{domain => localhost, port => 3030, transport => tcp,
- path => <<"/grisp-connect/ws">>,
- request_timeout => 300,
- errors => [
- {error1, -1, <<"Error Number 1">>},
- {error2, -2, <<"Error Number 2">>}
- ]},
- ConnOpts = maps:merge(DefaultOpts, Opts),
- {ok, Conn} = grisp_connect_connection:start_link(self(), ConnOpts),
- receive
- {conn, Conn, connected} ->
- grisp_connect_test_server:listen(),
- Conn
- after
- 1000 ->
- ?assert(false, "Connection to test server failed")
- end.
-
-disconnect(Conn) ->
- unlink(Conn),
- grisp_connect_connection:disconnect(Conn),
- ok.
-
-example_handler(Conn) ->
- receive
- {conn, Conn, {request, [subtract], [A, B], ReqRef}} ->
- grisp_connect_connection:reply(Conn, A - B, ReqRef),
- example_handler(Conn);
- {conn, Conn, {request, [subtract], #{minuend := A, subtrahend := B}, ReqRef}} ->
- grisp_connect_connection:reply(Conn, A - B, ReqRef),
- example_handler(Conn);
- {conn, Conn, {request, [sum], Values, ReqRef}} ->
- Result = lists:foldl(fun(V, Acc) -> V + Acc end, 0, Values),
- grisp_connect_connection:reply(Conn, Result, ReqRef),
- example_handler(Conn);
- {conn, Conn, {request, [get_data], _, ReqRef}} ->
- grisp_connect_connection:reply(Conn, [<<"hello">>, 5], ReqRef),
- example_handler(Conn);
- {conn, Conn, {request, _M, _P, ReqRef}} ->
- grisp_connect_connection:error(Conn, method_not_found, undefined, undefined, ReqRef),
- example_handler(Conn);
- {conn, Conn, {notification, _M, _P}} ->
- example_handler(Conn);
- % {conn, Conn, {response, R, Ctx :: term()}}
- {conn, Conn, {remote_error, _C, _M, _D}} ->
- example_handler(Conn)
- after
- 100 -> ok
- end.
-
-parse_examples(Data) when is_binary(Data) ->
- Lines = binary:split(Data, <<"\n">>, [global, trim]),
- parse_examples_lines(Lines, #{}, undefined, undefined).
-
-parse_examples_lines([], Acc, undefined, _Actions) ->
- Acc;
-parse_examples_lines([], Acc, Desc, Actions) ->
- Acc#{Desc => lists:reverse(Actions)};
-parse_examples_lines([<<"-->", RestBin/binary>> | RestLines], Acc, Desc, Actions)
- when Desc =/= undefined ->
- {Raw, RestLines2} = parse_examples_collect([RestBin | RestLines], <<>>),
- parse_examples_lines(RestLines2, Acc, Desc, [{send, Raw} | Actions]);
-parse_examples_lines([<<"<--", RestBin/binary>> | RestLines], Acc, Desc, Actions)
- when Desc =/= undefined ->
- case parse_examples_collect([RestBin | RestLines], <<>>) of
- {<<"">>, RestLines2} ->
- parse_examples_lines(RestLines2, Acc, Desc, Actions);
- {Raw, RestLines2} ->
- Decoded = jsx:decode(Raw, [{labels, attempt_atom}, return_maps]),
- parse_examples_lines(RestLines2, Acc, Desc, [{recv, Decoded} | Actions])
- end;
-parse_examples_lines([Line | RestLines], Acc, Desc, Actions) ->
- case re:replace(Line, "^\\s+|\\s+$", "", [{return, binary}, global]) of
- <<"">> -> parse_examples_lines(RestLines, Acc, Desc, Actions);
- <<"//", _/binary>> -> parse_examples_lines(RestLines, Acc, Desc, Actions);
- NewDesc ->
- NewAcc = case Desc =/= undefined of
- true -> Acc#{Desc => lists:reverse(Actions)};
- false -> Acc
- end,
- NewDesc2 = re:replace(NewDesc, ":+$", "", [{return, binary}, global]),
- parse_examples_lines(RestLines, NewAcc, NewDesc2, [])
- end.
-
-parse_examples_collect([], Acc) -> {Acc, []};
-parse_examples_collect([Line | RestLines], Acc) ->
- case re:replace(Line, "^\\s+|\\s+$", "", [{return, binary}, global]) of
- <<"">> -> {Acc, RestLines};
- <<"-->", _/binary>> -> {Acc, [Line | RestLines]};
- <<"<--", _/binary>> -> {Acc, [Line | RestLines]};
- <<"//", _/binary>> -> parse_examples_collect(RestLines, Acc);
- Line2 -> parse_examples_collect(RestLines, <>)
- end.
diff --git a/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt b/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
deleted file mode 100644
index f6c0fad..0000000
--- a/test/grisp_connect_connection_SUITE_data/jsonrpc_examples.txt
+++ /dev/null
@@ -1,96 +0,0 @@
-// JSON-RPC Examples from specification
-// See: https://www.jsonrpc.org/specification#examples
-
-rpc call with positional parameters:
-
---> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
-<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
-
---> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
-<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
-
---> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
-<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
-
-rpc call with named parameters:
-
---> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
-<-- {"jsonrpc": "2.0", "result": 19, "id": 3}
-
---> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
-<-- {"jsonrpc": "2.0", "result": 19, "id": 4}
-
-a Notification:
-
---> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
---> {"jsonrpc": "2.0", "method": "foobar"}
-
-rpc call of non-existent method:
-
---> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
-<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
-
-rpc call with invalid JSON:
-
---> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
-<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
-
-rpc call with invalid Request object:
-
---> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
-<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
-
-rpc call Batch, invalid JSON:
-
---> [
- {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
- {"jsonrpc": "2.0", "method"
-]
-<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": null}
-
-rpc call with an empty Array:
-
---> []
-<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
-
-rpc call with an invalid Batch (but not empty):
-
---> [1]
-<-- [
- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
-]
-
-rpc call with invalid Batch:
-
---> [1,2,3]
-<-- [
- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null}
-]
-
-rpc call Batch:
-
---> [
- {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
- {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
- {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
- {"foo": "boo"},
- {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
- {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
- ]
-<-- [
- {"jsonrpc": "2.0", "result": 7, "id": "1"},
- {"jsonrpc": "2.0", "result": 19, "id": "2"},
- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": null},
- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
- {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
- ]
-
-rpc call Batch (all notifications):
-
---> [
- {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
- {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}
- ]
-<-- //Nothing is returned for all notification batches
diff --git a/test/grisp_connect_jsonrpc_SUITE.erl b/test/grisp_connect_jsonrpc_SUITE.erl
deleted file mode 100644
index abc1e14..0000000
--- a/test/grisp_connect_jsonrpc_SUITE.erl
+++ /dev/null
@@ -1,118 +0,0 @@
--module(grisp_connect_jsonrpc_SUITE).
-
--include_lib("stdlib/include/assert.hrl").
-
--export([all/0]).
-
--export([positional_parameters/1,
- named_parameters/1,
- using_existing_atoms/1,
- notification/1,
- invalid_json/1,
- invalid_request/1,
- batch/1,
- result/1,
- null_values/1]).
-
-all() -> [
- positional_parameters,
- named_parameters,
- using_existing_atoms,
- notification,
- invalid_json,
- invalid_request,
- batch,
- result,
- null_values
-].
-
-positional_parameters(_) ->
- Term = {request, <<"subtract">>, [42,23], 1},
- Json = <<"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"subtract\",\"params\":[42,23]}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"id\":1">>,
- <<"\"method\":\"subtract\"">>,
- <<"\"params\":[42,23]">>], Json2)).
-
-named_parameters(_) ->
- Term = {request, <<"divide">>, #{<<"dividend">> => 42, <<"divisor">> => 2}, 2},
- Json = <<"{\"id\":2,\"jsonrpc\":\"2.0\",\"method\":\"divide\",\"params\":{\"dividend\":42,\"divisor\":2}}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"id\":2">>,
- <<"\"method\":\"divide\"">>,
- <<"\"dividend\":42">>,
- <<"\"divisor\":2">>], Json2)).
-
-using_existing_atoms(_) ->
- % The ID and method are matching existing atoms, checks they are not atoms
- Term = {request, <<"notification">>, #{}, <<"request">>},
- Json = <<"{\"id\":\"request\",\"jsonrpc\":\"2.0\",\"method\":\"notification\",\"params\":{}}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"id\":\"request\"">>, <<"\"method\":\"notification\"">>], Json2)).
-
-notification(_) ->
- Term = {notification, <<"update">>, [1,2,3,4,5]},
- Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"update\",\"params\":[1,2,3,4,5]}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"method\":\"update\"">>,
- <<"\"params\":[1,2,3,4,5]">>], Json2)).
-
-invalid_json(_) ->
- Term = {decoding_error, -32700, <<"Parse error">>, undefined, undefined},
- Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"foobar,\"params\":\"bar\",\"baz]">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- JsonError = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"error\":{">>,
- <<"\"code\":-32700">>,
- <<"\"message\":\"Parse error\"">>,
- <<"\"id\":null">>], JsonError)).
-
-invalid_request(_) ->
- Term = {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined},
- Json = <<"{\"jsonrpc\":\"2.0\",\"method\":1,\"params\":\"bar\"}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- JsonError = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"error\":{">>,
- <<"\"code\":-32600">>,
- <<"\"message\":\"Invalid Request\"">>,
- <<"\"id\":null">>], JsonError)).
-
-batch(_) ->
- Term1 = {request, <<"sum">>, [1,2,4], <<"1">>},
- Term2 = {decoding_error, -32600, <<"Invalid Request">>, undefined, undefined},
- Json = <<"[{\"jsonrpc\":\"2.0\",\"method\":\"sum\",\"params\":[1,2,4],\"id\":\"1\"},{\"foo\":\"boo\"}]">>,
- ?assertMatch([Term1, Term2], grisp_connect_jsonrpc:decode(Json)),
- JsonError = grisp_connect_jsonrpc:encode([Term1, Term2]),
- ?assert(jsonrpc_check([<<"\"id\":\"1\"">>,
- <<"\"method\":\"sum\"">>,
- <<"\"params\":[1,2,4]">>,
- <<"\"error\":{">>,
- <<"\"code\":-32600">>,
- <<"\"message\":\"Invalid Request\"">>,
- <<"\"id\":null">>], JsonError)).
-
-result(_) ->
- Term = {result, 7, 45},
- Json = <<"{\"id\":45,\"jsonrpc\":\"2.0\",\"result\":7}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"id\":45">>,
- <<"\"result\":7">>], Json2)).
-
-null_values(_) ->
- Term = {notification, <<"test_null">>, #{array => [undefined], object => #{foo => undefined}, value => undefined}},
- Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"test_null\",\"params\":{\"array\":[null],\"object\":{\"foo\":null},\"value\":null}}">>,
- ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)),
- Json2 = grisp_connect_jsonrpc:encode(Term),
- ?assert(jsonrpc_check([<<"\"array\":[null]">>,
- <<"\"foo\":null">>,
- <<"\"value\":null">>],
- Json2)).
-
-jsonrpc_check(Elements, JsonString) ->
- Elements2 = [<<"\"jsonrpc\":\"2.0\"">>| Elements],
- lists:all(fun(E) -> binary:match(JsonString, E) =/= nomatch end, Elements2).
From b90ad1baf9074de3bf2aa16c22f2225d41686a77 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Tue, 21 Jan 2025 16:30:08 +0100
Subject: [PATCH 09/12] Fix typos
---
CHANGELOG.md | 6 +++---
src/grisp_connect_client.erl | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ab1898..cbc4bd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,8 @@ and this project adheres to
### Changed
-- The name of the grisp_connect configuration key to control the timout of
-individual JSON-RPC requests changed from ws_requests_timeout ot
+- The name of the grisp_connect configuration key to control the timeout of
+individual JSON-RPC requests changed from ws_requests_timeout to
ws_request_timeout.
- Le default log filter changed to trying to filter out only some messages to
filtering out all progress messages, as it wasn't working reliably.
@@ -23,7 +23,7 @@ and bar are already existing atoms, but 'Buz' is not).
## Fixed
- The client is now waiting 1 second before trying to reconnect when it gets
-disconnected fomr the server.
+disconnected from the server.
## [1.1.0] - 2024-10-12
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 6adec2e..8fb397e 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -241,7 +241,7 @@ handle_common(info, Info, State, Data) ->
%--- Internal Functions --------------------------------------------------------
generic_errors() -> [
- {device_not_linked, -1, <<"Device no linked">>},
+ {device_not_linked, -1, <<"Device not linked">>},
{token_expired, -2, <<"Token expired">>},
{device_already_linked, -3, <<"Device already linked">>},
{invalid_token, -4, <<"Invalid token">>},
From c8495b40aa379e0dcf88132311aeb9d31dd0e371 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Thu, 30 Jan 2025 14:30:51 +0100
Subject: [PATCH 10/12] Update to follow the changes in jarl API
---
src/grisp_connect_api.erl | 6 +-----
src/grisp_connect_client.erl | 24 ++++++++++++------------
2 files changed, 13 insertions(+), 17 deletions(-)
diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl
index eca79a2..89541a8 100644
--- a/src/grisp_connect_api.erl
+++ b/src/grisp_connect_api.erl
@@ -16,14 +16,10 @@
-spec handle_msg(Msg) ->
ok | {reply, Result :: term(), ReqRef :: binary() | integer()}
when Msg :: {request, Method :: jarl:method(), Params :: map() | list(), ReqRef :: binary() | integer()}
- | {notification, jarl:method(), Params :: map() | list()}
- | {remote_error, Code :: integer() | atom(), Message :: undefined | binary(), Data :: term()}.
+ | {notification, jarl:method(), Params :: map() | list()}.
handle_msg({notification, M, Params}) ->
?LOG_ERROR("Received unexpected notification ~p: ~p", [M, Params]),
ok;
-handle_msg({remote_error, Code, Message, _Data}) ->
- ?LOG_ERROR("Received JSON-RPC error ~p: ~s", [Code, Message]),
- ok;
handle_msg({request, M, Params, ID})
when M == [?method_post]; M == [?method_get] ->
handle_request(M, Params, ID).
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 8fb397e..840d998 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -177,7 +177,7 @@ connecting(state_timeout, timeout, Data = #data{retry_count = RetryCount}) ->
event => connection_failed, reason => Reason}),
Data2 = conn_close(Data, Reason),
{next_state, waiting_ip, Data2#data{retry_count = RetryCount + 1}};
-connecting(info, {conn, Conn, connected}, Data = #data{conn = Conn}) ->
+connecting(info, {jarl, Conn, connected}, Data = #data{conn = Conn}) ->
% Received from the connection process
?LOG_INFO(#{description => <<"Connected to grisp.io">>,
event => connected}),
@@ -189,10 +189,10 @@ connected(enter, _OldState, _Data) ->
keep_state_and_data;
connected({call, From}, is_connected, _) ->
{keep_state_and_data, [{reply, From, true}]};
-connected(info, {conn, Conn, Msg}, Data = #data{conn = Conn}) ->
+connected(info, {jarl, Conn, Msg}, Data = #data{conn = Conn}) ->
handle_connection_message(Data, Msg);
connected({call, From}, {request, Method, Type, Params}, Data) ->
- Data2 = conn_post(Data, Method, Type, Params,
+ Data2 = conn_request(Data, Method, Type, Params,
fun(D, R) -> gen_statem:reply(From, {ok, R}), D end,
fun(D, _, C, _, _) -> gen_statem:reply(From, {error, C}), D end),
{keep_state, Data2};
@@ -222,7 +222,7 @@ handle_common(info, {'EXIT', Conn, Reason}, _State,
?FORMAT("The connection to grisp.io died: ~p", [Reason]),
event => connection_failed, reason => Reason}),
{next_state, waiting_ip, conn_died(Data#data{retry_count = RetryCount + 1})};
-handle_common(info, {conn, Conn, Msg}, State, _Data) ->
+handle_common(info, {jarl, Conn, Msg}, State, _Data) ->
?LOG_DEBUG("Received message from unknown connection ~p in state ~w: ~p",
[Conn, State, Msg]),
keep_state_and_data;
@@ -258,19 +258,19 @@ handle_connection_message(_Data, {response, _Result, #{on_result := undefined}})
keep_state_and_data;
handle_connection_message(Data, {response, Result, #{on_result := OnResult}}) ->
{keep_state, OnResult(Data, Result)};
-handle_connection_message(_Data, {remote_error, Code, Msg, _ErrorData,
+handle_connection_message(_Data, {error, Code, Msg, _ErrorData,
#{on_error := undefined}}) ->
?LOG_WARNING("Unhandled remote request error ~w: ~s", [Code, Msg]),
keep_state_and_data;
-handle_connection_message(Data, {remote_error, Code, Msg, ErrorData,
+handle_connection_message(Data, {error, Code, Msg, ErrorData,
#{on_error := OnError}}) ->
{keep_state, OnError(Data, remote, Code, Msg, ErrorData)};
-handle_connection_message(_Data, {local_error, Reason,
+handle_connection_message(_Data, {jarl_error, Reason,
#{on_error := undefined}}) ->
?LOG_WARNING("Unhandled local request error ~w", [Reason]),
keep_state_and_data;
-handle_connection_message(Data, {local_error, Reason,
+handle_connection_message(Data, {jarl_error, Reason,
#{on_error := OnError}}) ->
{keep_state, OnError(Data, local, Reason, undefined, undefined)};
handle_connection_message(Data, Msg) ->
@@ -322,16 +322,16 @@ conn_died(Data) ->
grisp_connect_log_server:stop(),
Data#data{conn = undefined}.
--spec conn_post(data(), jarl:method(), atom(), map(),
+-spec conn_request(data(), jarl:method(), atom(), map(),
undefined | on_result_fun(), undefined | on_error_fun())
-> data().
-conn_post(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
+conn_request(Data = #data{conn = Conn}, Method, Type, Params, OnResult, OnError)
when Conn =/= undefined ->
ReqCtx = #{on_result => OnResult, on_error => OnError},
Params2 = maps:put(type, Type, Params),
- case jarl:post(Conn, Method, Params2, ReqCtx) of
+ case jarl:request(Conn, Method, Params2, ReqCtx) of
ok -> Data;
- {error, Reason} ->
+ {jarl_error, Reason} ->
OnError(Data, local, Reason, undefined, undefined)
end.
From 984e720b6140dd80933f3da90d24fcdb064eb328 Mon Sep 17 00:00:00 2001
From: GwendalLaurent
Date: Wed, 5 Feb 2025 13:48:38 +0100
Subject: [PATCH 11/12] increasing connection timeout
---
src/grisp_connect_client.erl | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 840d998..099ba24 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -52,7 +52,7 @@
-define(FORMAT(FMT, ARGS), iolist_to_binary(io_lib:format(FMT, ARGS))).
-define(STD_TIMEOUT, 1000).
--define(CONNECT_TIMEOUT, 2000).
+-define(CONNECT_TIMEOUT, 5000).
-define(ENV(KEY, GUARDS), fun() ->
case application:get_env(grisp_connect, KEY) of
{ok, V} when GUARDS -> V;
@@ -179,8 +179,8 @@ connecting(state_timeout, timeout, Data = #data{retry_count = RetryCount}) ->
{next_state, waiting_ip, Data2#data{retry_count = RetryCount + 1}};
connecting(info, {jarl, Conn, connected}, Data = #data{conn = Conn}) ->
% Received from the connection process
- ?LOG_INFO(#{description => <<"Connected to grisp.io">>,
- event => connected}),
+ ?LOG_NOTICE(#{description => <<"Connected to grisp.io">>,
+ event => connected}),
{next_state, connected, Data#data{retry_count = 0}};
?HANDLE_COMMON.
@@ -222,6 +222,9 @@ handle_common(info, {'EXIT', Conn, Reason}, _State,
?FORMAT("The connection to grisp.io died: ~p", [Reason]),
event => connection_failed, reason => Reason}),
{next_state, waiting_ip, conn_died(Data#data{retry_count = RetryCount + 1})};
+handle_common(info, {'EXIT', _Conn, _Reason}, _State, _Data) ->
+ % Ignore any EXIT from past jarl connections
+ keep_state_and_data;
handle_common(info, {jarl, Conn, Msg}, State, _Data) ->
?LOG_DEBUG("Received message from unknown connection ~p in state ~w: ~p",
[Conn, State, Msg]),
From da879a9350c55af7be59fbf58dc2ed2621931da0 Mon Sep 17 00:00:00 2001
From: Sebastien Merle
Date: Fri, 7 Feb 2025 17:34:59 +0100
Subject: [PATCH 12/12] Fixes from review feedback
---
CHANGELOG.md | 2 +-
config/dev.config | 3 ++-
config/local.config | 2 ++
config/test.config | 2 ++
docs/grisp_connect_architecture.md | 5 ++---
rebar.lock | 8 ++++++--
src/grisp_connect.app.src | 8 ++++----
src/grisp_connect_client.erl | 12 ++++++++++--
test/grisp_connect_reconnect_SUITE.erl | 3 ++-
9 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbc4bd4..b1082ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@ and this project adheres to
- The name of the grisp_connect configuration key to control the timeout of
individual JSON-RPC requests changed from ws_requests_timeout to
ws_request_timeout.
-- Le default log filter changed to trying to filter out only some messages to
+- The default log filter changed to trying to filter out only some messages to
filtering out all progress messages, as it wasn't working reliably.
- JSON-RPC logic was extracted into the jarl library.
- Jarl parses the methods into a list of atom or binaries to pave the
diff --git a/config/dev.config b/config/dev.config
index a626e7f..1240684 100644
--- a/config/dev.config
+++ b/config/dev.config
@@ -16,10 +16,11 @@
config => #{type => standard_io},
filter_default => log,
filters => [
+ % Filter out supervisor progress reports so
+ % TLS certificates are not swamping the console...
{disable_progress, {fun logger_filters:progress/2, stop}}
]
}}
]}
]}
-
].
diff --git a/config/local.config b/config/local.config
index f663e70..0db4f98 100644
--- a/config/local.config
+++ b/config/local.config
@@ -33,6 +33,8 @@
config => #{type => standard_io},
filter_default => log,
filters => [
+ % Filter out supervisor progress reports so
+ % TLS certificates are not swamping the console...
{disable_progress, {fun logger_filters:progress/2, stop}}
]
}}
diff --git a/config/test.config b/config/test.config
index d84962f..bd9493d 100644
--- a/config/test.config
+++ b/config/test.config
@@ -23,6 +23,8 @@
config => #{type => standard_io},
filter_default => log,
filters => [
+ % Filter out supervisor progress reports so
+ % TLS certificates are not swamping the console...
{disable_progress, {fun logger_filters:progress/2, stop}}
]
}}
diff --git a/docs/grisp_connect_architecture.md b/docs/grisp_connect_architecture.md
index 05857c7..c417e48 100644
--- a/docs/grisp_connect_architecture.md
+++ b/docs/grisp_connect_architecture.md
@@ -59,11 +59,10 @@ It provides a high-level API to a JSON-RPC connection:
- Perform synchronous requests
- Start asynchronous requests
- Reply to a request
- - Send and error result for a request
+ - Send an error result for a request
- Send asynchronous notifications
- - Send generic errors
When performing an asynchronous request, the caller can give an opaque context
-term, that will given back when receiving a response or an error for this
+term, that will be given back when receiving a response or an error for this
request, allowing the caller to handle the asynchronous operation without having
to store information locally.
diff --git a/rebar.lock b/rebar.lock
index de05d82..df65b7f 100644
--- a/rebar.lock
+++ b/rebar.lock
@@ -1,9 +1,13 @@
{"1.2.0",
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.13.0">>},0},
- {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},1},
+ {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},2},
{<<"grisp">>,{pkg,<<"grisp">>,<<"2.7.0">>},0},
{<<"grisp_cryptoauth">>,{pkg,<<"grisp_cryptoauth">>,<<"2.4.0">>},0},
- {<<"gun">>,{pkg,<<"gun">>,<<"2.1.0">>},0},
+ {<<"gun">>,{pkg,<<"gun">>,<<"2.1.0">>},1},
+ {<<"jarl">>,
+ {git,"https://github.com/grisp/jarl.git",
+ {ref,"10085d38df19c67664d33ef61f515c92a8b0de56"}},
+ 0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
{<<"mapz">>,{pkg,<<"mapz">>,<<"2.4.0">>},1}]}.
[
diff --git a/src/grisp_connect.app.src b/src/grisp_connect.app.src
index 9c90847..76065bd 100644
--- a/src/grisp_connect.app.src
+++ b/src/grisp_connect.app.src
@@ -30,12 +30,12 @@
{logger, [
% Enable our own default handler,
% which will receive all events from boot
- {handler,
- grisp_connect_log_handler,
- grisp_connect_logger_bin,
- #{formatter => {grisp_connect_logger_bin, #{}},
+ {handler, grisp_connect_log_handler, grisp_connect_logger_bin, #{
+ formatter => {grisp_connect_logger_bin, #{}},
filter_default => log,
filters => [
+ % Filter out supervisor progress reports so
+ % TLS certificates are not swamping the console...
{disable_progress, {fun logger_filters:progress/2, stop}}
]
}}
diff --git a/src/grisp_connect_client.erl b/src/grisp_connect_client.erl
index 099ba24..e5af495 100644
--- a/src/grisp_connect_client.erl
+++ b/src/grisp_connect_client.erl
@@ -249,6 +249,7 @@ generic_errors() -> [
{device_already_linked, -3, <<"Device already linked">>},
{invalid_token, -4, <<"Invalid token">>},
{grisp_updater_unavailable, -10, <<"Software update unavailable">>},
+ {already_updating, -11, <<"Already updating">>},
{boot_system_not_validated, -12, <<"Boot system not validated">>},
{validate_from_unbooted, -13, <<"Validate from unbooted">>}
].
@@ -279,8 +280,11 @@ handle_connection_message(Data, {jarl_error, Reason,
handle_connection_message(Data, Msg) ->
case grisp_connect_api:handle_msg(Msg) of
ok -> keep_state_and_data;
+ {error, Code, Message, ErData, ReqRef} ->
+ conn_error(Data, Code, Message, ErData, ReqRef),
+ keep_state_and_data;
{reply, Result, ReqRef} ->
- conn_reply(Data, Result, ReqRef),
+ conn_result(Data, Result, ReqRef),
keep_state_and_data
end.
@@ -343,10 +347,14 @@ conn_notify(#data{conn = Conn}, Method, Type, Params)
Params2 = maps:put(type, Type, Params),
jarl:notify(Conn, Method, Params2).
-conn_reply(#data{conn = Conn}, Result, ReqRef)
+conn_result(#data{conn = Conn}, Result, ReqRef)
when Conn =/= undefined ->
jarl:reply(Conn, Result, ReqRef).
+conn_error(#data{conn = Conn}, Code, Message, ErData, ReqRef)
+ when Conn =/= undefined ->
+ jarl:reply(Conn, Code, Message, ErData, ReqRef).
+
% IP check functions
check_inet_ipv4() ->
diff --git a/test/grisp_connect_reconnect_SUITE.erl b/test/grisp_connect_reconnect_SUITE.erl
index 343c882..0f85155 100644
--- a/test/grisp_connect_reconnect_SUITE.erl
+++ b/test/grisp_connect_reconnect_SUITE.erl
@@ -89,5 +89,6 @@ reconnect_on_closed_frame_test(_) ->
connection_gun_pid() ->
{_, {data, _, _, _, _, ConnPid, _}} = sys:get_state(grisp_connect_client),
- {_, {data, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _}} = sys:get_state(ConnPid),
+ % Depends on the internal state of jarl_connection
+ {_, {data, _, _, _, _, _, _, _, _, _, _, _, GunPid, _, _, _}} = sys:get_state(ConnPid),
GunPid.