From cfd702a716f834c431abd46532e8cfa4debd1468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 8 May 2019 14:01:51 +0200 Subject: Add event_handler with init/connect_start/connect_end --- ebin/gun.app | 2 +- src/gun.erl | 80 ++++++++++++++++++++-------- src/gun_default_event_h.erl | 29 +++++++++++ src/gun_event.erl | 72 +++++++++++++++++++++++++ test/event_SUITE.erl | 124 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 284 insertions(+), 23 deletions(-) create mode 100644 src/gun_default_event_h.erl create mode 100644 src/gun_event.erl create mode 100644 test/event_SUITE.erl diff --git a/ebin/gun.app b/ebin/gun.app index 40fa4ca..e3ad981 100644 --- a/ebin/gun.app +++ b/ebin/gun.app @@ -1,7 +1,7 @@ {application, 'gun', [ {description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."}, {vsn, "1.3.0"}, - {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_http','gun_http2','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']}, + {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']}, {registered, [gun_sup]}, {applications, [kernel,stdlib,ssl,cowlib]}, {mod, {gun_app, []}}, diff --git a/src/gun.erl b/src/gun.erl index 7376a33..e958d2e 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -106,6 +106,7 @@ -type opts() :: #{ connect_timeout => timeout(), + event_handler => {module(), any()}, http_opts => http_opts(), http2_opts => http2_opts(), protocols => [http | http2], @@ -184,7 +185,9 @@ transport :: module(), messages :: {atom(), atom(), atom()}, protocol :: module(), - protocol_state :: any() + protocol_state :: any(), + event_handler :: module(), + event_handler_state :: any() }). %% Connection. @@ -233,6 +236,8 @@ check_options([{connect_timeout, infinity}|Opts]) -> check_options(Opts); check_options([{connect_timeout, T}|Opts]) when is_integer(T), T >= 0 -> check_options(Opts); +check_options([{event_handler, {Mod, _}}|Opts]) when is_atom(Mod) -> + check_options(Opts); check_options([{http_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> case gun_http:check_options(ProtoOpts) of ok -> @@ -724,18 +729,29 @@ init({Owner, Host, Port, Opts}) -> tls -> {<<"https">>, gun_tls} end, OwnerRef = monitor(process, Owner), + {EventHandler, EventHandlerState0} = maps:get(event_handler, Opts, + {gun_default_event_h, undefined}), + EventHandlerState = EventHandler:init(#{ + owner => Owner, + transport => OriginTransport, + origin_scheme => OriginScheme, + origin_host => Host, + origin_port => Port, + opts => Opts + }, EventHandlerState0), State = #state{owner=Owner, owner_ref=OwnerRef, host=Host, port=Port, origin_scheme=OriginScheme, origin_host=Host, origin_port=Port, opts=Opts, - transport=Transport, messages=Transport:messages()}, + transport=Transport, messages=Transport:messages(), + event_handler=EventHandler, event_handler_state=EventHandlerState}, {ok, not_connected, State, {next_event, internal, {retries, Retry}}}. default_transport(443) -> tls; default_transport(_) -> tcp. -not_connected(_, {retries, Retries}, - State=#state{host=Host, port=Port, opts=Opts, transport=Transport}) -> +not_connected(_, {retries, Retries}, State0=#state{host=Host, port=Port, opts=Opts, + transport=Transport, event_handler=EventHandler, event_handler_state=EventHandlerState0}) -> TransOpts0 = maps:get(transport_opts, Opts, []), TransOpts1 = case Transport of gun_tcp -> TransOpts0; @@ -743,27 +759,47 @@ not_connected(_, {retries, Retries}, end, TransOpts = [binary, {active, false}|TransOpts1], ConnectTimeout = maps:get(connect_timeout, Opts, infinity), + ConnectEvent = #{ + host => Host, + port => Port, + transport => Transport:name(), + transport_opts => TransOpts, + timeout => ConnectTimeout + }, + EventHandlerState1 = EventHandler:connect_start(ConnectEvent, EventHandlerState0), case Transport:connect(Host, Port, TransOpts, ConnectTimeout) of - {ok, Socket} when Transport =:= gun_tcp -> - Protocol = case maps:get(protocols, Opts, [http]) of - [http] -> gun_http; - [http2] -> gun_http2 + {ok, Socket} -> + Protocol = case Transport of + gun_tcp -> + case maps:get(protocols, Opts, [http]) of + [http] -> gun_http; + [http2] -> gun_http2 + end; + gun_tls -> + case ssl:negotiated_protocol(Socket) of + {ok, <<"h2">>} -> gun_http2; + _ -> gun_http + end end, - {next_state, connected, State, + EventHandlerState = EventHandler:connect_end(ConnectEvent#{ + socket => Socket, + protocol => Protocol:name() + }, EventHandlerState1), + {next_state, connected, State0#state{event_handler_state=EventHandlerState}, {next_event, internal, {connected, Socket, Protocol}}}; - {ok, Socket} when Transport =:= gun_tls -> - Protocol = case ssl:negotiated_protocol(Socket) of - {ok, <<"h2">>} -> gun_http2; - _ -> gun_http - end, - {next_state, connected, State, - {next_event, internal, {connected, Socket, Protocol}}}; - {error, Reason} when Retries =:= 0 -> - {stop, {shutdown, Reason}}; - {error, _Reason} -> - Timeout = maps:get(retry_timeout, Opts, 5000), - {keep_state, State, - {state_timeout, Timeout, {retries, Retries - 1}}} + {error, Reason} -> + EventHandlerState = EventHandler:connect_end(ConnectEvent#{ + error => Reason + }, EventHandlerState1), + State = State0#state{event_handler_state=EventHandlerState}, + case Retries of + 0 -> + {stop, {shutdown, Reason}, State}; + _ -> + Timeout = maps:get(retry_timeout, Opts, 5000), + {keep_state, State, + {state_timeout, Timeout, {retries, Retries - 1}}} + end end; not_connected({call, From}, {stream_info, _}, _) -> {keep_state_and_data, {reply, From, {error, not_connected}}}; diff --git a/src/gun_default_event_h.erl b/src/gun_default_event_h.erl new file mode 100644 index 0000000..e242e84 --- /dev/null +++ b/src/gun_default_event_h.erl @@ -0,0 +1,29 @@ +%% Copyright (c) 2019, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_default_event_h). +-behavior(gun_event). + +-export([init/2]). +-export([connect_start/2]). +-export([connect_end/2]). + +init(_EventData, State) -> + State. + +connect_start(_EventData, State) -> + State. + +connect_end(_EventData, State) -> + State. diff --git a/src/gun_event.erl b/src/gun_event.erl new file mode 100644 index 0000000..fffcafe --- /dev/null +++ b/src/gun_event.erl @@ -0,0 +1,72 @@ +%% Copyright (c) 2019, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_event). + +%% init. + +-type init_event() :: #{ + owner := pid(), + transport := tcp | tls, + origin_scheme := binary(), + origin_host := inet:hostname() | inet:ip_address(), + origin_port := inet:port_number(), + opts := gun:opts() +}. + +-callback init(init_event(), State) -> State. + +%% connect_start/connect_end. + +-type connect_event() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + transport := tcp | tls, + transport_opts := [gen_tcp:connect_option()] | [ssl:connect_option()], + timeout := timeout(), + socket => inet:socket() | ssl:sslsocket() | pid(), + protocol => http | http2, + error => any() +}. + +-callback connect_start(connect_event(), State) -> State. +-callback connect_end(connect_event(), State) -> State. + +%% @todo domain_lookup_start +%% @todo domain_lookup_end +%% @todo tls_handshake_start +%% @todo tls_handshake_end +%% @todo origin_changed +%% @todo transport_changed +%% @todo protocol_changed +%% @todo disconnected +%% @todo terminate +%% @todo stream_start +%% @todo stream_end +%% @todo request_start +%% @todo request_headers +%% @todo request_end +%% @todo response_start (call it once per inform + one for the response) +%% @todo response_inform +%% @todo response_headers +%% @todo response_end +%% @todo push_promise_start +%% @todo push_promise_end +%% @todo ws_upgrade_start +%% @todo ws_upgrade_end +%% @todo ws_frame_read_start +%% @todo ws_frame_read_header +%% @todo ws_frame_read_end +%% @todo ws_frame_write_start +%% @todo ws_frame_write_end diff --git a/test/event_SUITE.erl b/test/event_SUITE.erl new file mode 100644 index 0000000..383ac46 --- /dev/null +++ b/test/event_SUITE.erl @@ -0,0 +1,124 @@ +%% Copyright (c) 2019, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(event_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-behavior(gun_event). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +all() -> + ct_helper:all(?MODULE). + +init_per_suite(Config) -> + {ok, _} = cowboy:start_clear(?MODULE, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", ws_echo, []}]}]) + }}), + OriginPort = ranch:get_port(?MODULE), + [{origin_port, OriginPort}|Config]. + +end_per_suite(_) -> + ok = cowboy:stop_listener(?MODULE). + +%% init. + +init(_) -> + doc("Confirm that the init event callback is called."), + Self = self(), + Opts = #{event_handler => {?MODULE, Self}}, + {ok, Pid} = gun:open("localhost", 12345, Opts), + #{ + owner := Self, + transport := tcp, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := 12345, + opts := Opts + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +%% connect_start/connect_end. + +connect_start(_) -> + doc("Confirm that the connect_start event callback is called."), + Self = self(), + Opts = #{event_handler => {?MODULE, Self}}, + {ok, Pid} = gun:open("localhost", 12345, Opts), + #{ + host := "localhost", + port := 12345, + transport := tcp, + transport_opts := _, + timeout := _ + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +connect_end_error(_) -> + doc("Confirm that the connect_end event callback is called on connect failure."), + Self = self(), + Opts = #{event_handler => {?MODULE, Self}}, + {ok, Pid} = gun:open("localhost", 12345, Opts), + #{ + host := "localhost", + port := 12345, + transport := tcp, + transport_opts := _, + timeout := _, + error := _ + } = do_receive_event(connect_end), + gun:close(Pid). + +connect_end_ok(Config) -> + doc("Confirm that the connect_end event callback is called on connect success."), + Self = self(), + Opts = #{event_handler => {?MODULE, Self}}, + OriginPort = config(origin_port, Config), + {ok, Pid} = gun:open("localhost", OriginPort, Opts), + #{ + host := "localhost", + port := OriginPort, + transport := tcp, + transport_opts := _, + timeout := _, + socket := _, + protocol := http + } = do_receive_event(connect_end), + gun:close(Pid). + +%% Internal. + +do_receive_event(Event) -> + receive + {Event, EventData} -> + EventData + after 5000 -> + error(timeout) + end. + +%% gun_event callbacks. + +init(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +connect_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +connect_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. -- cgit v1.2.3