diff --git a/README.md b/README.md index 497b276..eaff1e4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Grisp2 Seawater Client Library ## Application env options +### connect + +This option is set to `true` as default. Set it to `false` to prevent automatic connection to GRiSP.io on boot. +In such case the state machine that maintains the connection can be started manually using `grisp_io_connection:connect()`. + ### ntp An optional NTP client can be started using option `{ntp, true}`. diff --git a/src/grisp_io_connection.erl b/src/grisp_io_connection.erl new file mode 100644 index 0000000..ae8af47 --- /dev/null +++ b/src/grisp_io_connection.erl @@ -0,0 +1,133 @@ +%% @doc State machine to ensure connectivity with grisp.io +-module(grisp_io_connection). + +% External API +-export([start_link/0]). +-export([connect/0]). + +% Internal API +-export([disconnected/0]). + +-behaviour(gen_statem). +-export([init/1, terminate/3, code_change/4, callback_mode/0, handle_event/4]). + +-include_lib("kernel/include/logger.hrl"). + +-define(STD_TIMEOUT, 1000). + +% API + +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +connect() -> + gen_statem:cast(?MODULE, ?FUNCTION_NAME). + +disconnected() -> + gen_statem:castl(?MODULE, ?FUNCTION_NAME). + +% gen_statem CALLBACKS --------------------------------------------------------- + +init([]) -> + {ok, Connect} = application:get_env(grisp_seawater, connect), + NextState = case Connect of + true -> waiting_ip; + false -> idle + end, + {ok, NextState, []}. + +terminate(_Reason, _State, _Data) -> ok. + +code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. + +callback_mode() -> [handle_event_function, state_enter]. + +%%% STATE CALLBACKS ------------------------------------------------------------ + +% IDLE +handle_event(enter, _OldState, idle, _Data) -> + keep_state_and_data; +handle_event(cast, connect, idle, Data) -> + {next_state, waiting_ip, Data}; + +% WAITING_IP +handle_event(enter, _OldState, waiting_ip, Data) -> + {next_state, waiting_ip, Data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, waiting_ip, Data) -> + case check_inet_ipv4() of + {ok, IP} -> + ?LOG_INFO(#{event => checked_ip, ip => IP}), + {next_state, connecting, Data}; + invalid -> + ?LOG_INFO(#{event => waiting_ip}), + {next_state, waiting_ip, Data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% CONNECTING +handle_event(enter, _OldState, connecting, _Data) -> + {ok, Domain} = application:get_env(grisp_seawater, seawater_domain), + {ok, Port} = application:get_env(grisp_seawater, seawater_port), + ?LOG_NOTICE(#{event => connecting, domain => Domain, port => Port}), + grisp_seawater_ws:connect(Domain, Port), + {keep_state_and_data, [{state_timeout, 0, retry}]}; +handle_event(state_timeout, retry, connecting, Data) -> + case grisp_seawater_ws:is_connected() of + true -> + ?LOG_NOTICE(#{event => connected}), + {next_state, connected, Data}; + false -> + ?LOG_NOTICE(#{event => waiting_ws_connection}), + {keep_state_and_data, [{state_timeout, ?STD_TIMEOUT, retry}]} + end; + +% CONNECTED +handle_event(enter, _OldState, connected, _Data) -> + keep_state_and_data; +handle_event(cast, disconnected, connected, Data) -> + ?LOG_WARNING(#{event => disconnected}), + {next_state, waiting_ip, Data}; + +handle_event(E, OldS, NewS, Data) -> + ?LOG_ERROR(#{event => unhandled_gen_statem_event, + gen_statem_event => E, + old_state => OldS, + new_state => NewS}), + {keep_state, Data}. + +% INTERNALS -------------------------------------------------------------------- + +check_inet_ipv4() -> + case get_ip_of_valid_interfaces() of + {IP1,_,_,_} = IP when IP1 =/= 127 -> {ok, IP}; + _ -> invalid + end. + +get_ipv4_from_opts([]) -> + undefined; +get_ipv4_from_opts([{addr, {_1, _2, _3, _4}} | _]) -> + {_1, _2, _3, _4}; +get_ipv4_from_opts([_ | TL]) -> + get_ipv4_from_opts(TL). + +has_ipv4(Opts) -> + get_ipv4_from_opts(Opts) =/= undefined. + +flags_are_ok(Flags) -> + lists:member(up, Flags) and + lists:member(running, Flags) and + not lists:member(loopback, Flags). + +get_valid_interfaces() -> + {ok, Interfaces} = inet:getifaddrs(), + [ + Opts + || {_Name, [{flags, Flags} | Opts]} <- Interfaces, + flags_are_ok(Flags), + has_ipv4(Opts) + ]. + +get_ip_of_valid_interfaces() -> + case get_valid_interfaces() of + [Opts | _] -> get_ipv4_from_opts(Opts); + _ -> undefined + end. diff --git a/src/grisp_seawater.app.src b/src/grisp_seawater.app.src index 76eff09..af9ba50 100644 --- a/src/grisp_seawater.app.src +++ b/src/grisp_seawater.app.src @@ -16,6 +16,7 @@ {env, [ {seawater_domain, "grisp.io"}, {seawater_port, 7777}, + {connect, true}, % keeps a constant connection with grisp.io {ntp, false}, % if set to true, starts the NTP client {ws_requests_timeout, 5_000} ]}, diff --git a/src/grisp_seawater_sup.erl b/src/grisp_seawater_sup.erl index a045584..5eeeac4 100644 --- a/src/grisp_seawater_sup.erl +++ b/src/grisp_seawater_sup.erl @@ -38,7 +38,10 @@ init([]) -> {ok, true} -> [worker(grisp_seawater_ntp, [])]; {ok, false} -> [] end, - ChildSpecs = NTP ++ [worker(grisp_seawater_ws, [])], + ChildSpecs = NTP ++ [ + worker(grisp_seawater_ws, []), + worker(grisp_io_connection, []) + ], {ok, {SupFlags, ChildSpecs}}. %% internal functions diff --git a/src/grisp_seawater_ws.erl b/src/grisp_seawater_ws.erl index 886784c..efb6daf 100644 --- a/src/grisp_seawater_ws.erl +++ b/src/grisp_seawater_ws.erl @@ -124,6 +124,7 @@ handle_info({gun_down, Pid, ws, closed, [Stream]}, erlang:cancel_timer(Tref), gen_server:reply(Caller, {error, ws_closed}) end || {Caller, Tref} <- maps:values(Requests)], + grisp_io_connection:disconnected(), {noreply, shutdown_gun(S#state{requests = #{}})}; handle_info(M, S) -> ?LOG_WARNING(#{event => unhandled_info, info => M}),