From d52e84bdd97b93d7d9cea827de57bd4a0edea9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 10 Oct 2019 11:33:35 +0200 Subject: Add shutdown_reason Websocket command This allows changing the normal exit reason of Websocket processes, providing a way to signal other processes of why the exit occurred. --- doc/src/guide/migrating_from_2.6.asciidoc | 6 ++++ doc/src/manual/cowboy_websocket.asciidoc | 15 ++++++++-- src/cowboy_websocket.erl | 13 +++++++-- test/handlers/ws_shutdown_reason_commands_h.erl | 38 +++++++++++++++++++++++++ test/ws_handler_SUITE.erl | 21 +++++++++++++- 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 test/handlers/ws_shutdown_reason_commands_h.erl diff --git a/doc/src/guide/migrating_from_2.6.asciidoc b/doc/src/guide/migrating_from_2.6.asciidoc index a582ee4..91d1588 100644 --- a/doc/src/guide/migrating_from_2.6.asciidoc +++ b/doc/src/guide/migrating_from_2.6.asciidoc @@ -81,6 +81,12 @@ Cowboy 2.7 requires Erlang/OTP 20.0 or greater. is now considered stable and has been documented. The old interface is now deprecated. +* A new Websocket handler command `shutdown_reason` + can be used to change the normal exit reason of + Websocket processes. By default `normal` is used; + with this command the exit reason can be changed + to `{shutdown, ShutdownReason}`. + * The experimental stream handlers `cowboy_metrics_h` and `cowboy_tracer_h` are now considered stable and have been documented. diff --git a/doc/src/manual/cowboy_websocket.asciidoc b/doc/src/manual/cowboy_websocket.asciidoc index 440a0e8..59d412d 100644 --- a/doc/src/manual/cowboy_websocket.asciidoc +++ b/doc/src/manual/cowboy_websocket.asciidoc @@ -141,6 +141,7 @@ commands() :: [Command] Command :: {active, boolean()} | {deflate, boolean()} | {set_options, #{idle_timeout => timeout()}} + | {shutdown_reason, any()} | Frame :: cow_ws:frame() ---- @@ -163,6 +164,15 @@ set_options:: Set Websocket options. Currently only the option `idle_timeout` may be updated from a Websocket handler. +shutdown_reason:: + +Change the shutdown reason. The Websocket process will exit +with reason `normal` by default. This command can be used to +exit with reason `{shutdown, ShutdownReason}` under normal +conditions. This command has no effect when the Websocket +process exits abnormally, for example following a crash in a +handler callback. + Frame:: Send the corresponding Websocket frame. @@ -266,8 +276,9 @@ normal circumstances if necessary. == Changelog -* *2.7*: The commands based interface has been added. The old - interface is now deprecated. +* *2.7*: The commands based interface has been documented. + The old interface is now deprecated. +* *2.7*: The command `shutdown_reason` was introduced. * *2.7*: The option `validate_utf8` has been added. * *2.6*: Deflate options can now be configured via `deflate_opts`. * *2.0*: The Req object is no longer passed to Websocket callbacks. diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index ad0dad5..31103ac 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -35,6 +35,7 @@ | {active, boolean()} | {deflate, boolean()} | {set_options, map()} + | {shutdown_reason, any()} ]. -export_type([commands/0]). @@ -95,7 +96,8 @@ utf8_state :: cow_ws:utf8_state(), deflate = true :: boolean(), extensions = #{} :: map(), - req = #{} :: map() + req = #{} :: map(), + shutdown_reason = normal :: any() }). %% Because the HTTP/1.1 and HTTP/2 handshakes are so different, @@ -546,6 +548,8 @@ commands([{set_options, SetOpts}|Tail], State0=#state{opts=Opts}, Data) -> State0 end, commands(Tail, State, Data); +commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) -> + commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data); commands([Frame|Tail], State, Data0) -> Data = [frame(Frame, State)|Data0], case is_close_frame(Frame) of @@ -623,9 +627,12 @@ frame(Frame, #state{extensions=Extensions}) -> cow_ws:frame(Frame, Extensions). -spec terminate(#state{}, any(), terminate_reason()) -> no_return(). -terminate(State, HandlerState, Reason) -> +terminate(State=#state{shutdown_reason=Shutdown}, HandlerState, Reason) -> handler_terminate(State, HandlerState, Reason), - exit(normal). + case Shutdown of + normal -> exit(normal); + _ -> exit({shutdown, Shutdown}) + end. handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> cowboy_handler:terminate(Reason, Req, HandlerState, Handler). diff --git a/test/handlers/ws_shutdown_reason_commands_h.erl b/test/handlers/ws_shutdown_reason_commands_h.erl new file mode 100644 index 0000000..90b435c --- /dev/null +++ b/test/handlers/ws_shutdown_reason_commands_h.erl @@ -0,0 +1,38 @@ +%% This module sends the process pid to the test pid +%% found in the x-test-pid header, then changes the +%% shutdown reason and closes the connection normally. + +-module(ws_shutdown_reason_commands_h). +-behavior(cowboy_websocket). + +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, RunOrHibernate) -> + TestPid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), + {cowboy_websocket, Req, {TestPid, RunOrHibernate}}. + +websocket_init(State={TestPid, RunOrHibernate}) -> + TestPid ! {ws_pid, self()}, + ShutdownReason = receive + {TestPid, SR} -> + SR + after 1000 -> + error(timeout) + end, + Commands = [ + {shutdown_reason, ShutdownReason}, + close + ], + case RunOrHibernate of + run -> {Commands, State}; + hibernate -> {Commands, State, hibernate} + end. + +websocket_handle(_, State) -> + {[], State}. + +websocket_info(_, State) -> + {[], State}. diff --git a/test/ws_handler_SUITE.erl b/test/ws_handler_SUITE.erl index 67d50d2..872b152 100644 --- a/test/ws_handler_SUITE.erl +++ b/test/ws_handler_SUITE.erl @@ -52,7 +52,8 @@ init_dispatch(Name) -> {"/info", ws_info_commands_h, RunOrHibernate}, {"/active", ws_active_commands_h, RunOrHibernate}, {"/deflate", ws_deflate_commands_h, RunOrHibernate}, - {"/set_options", ws_set_options_commands_h, RunOrHibernate} + {"/set_options", ws_set_options_commands_h, RunOrHibernate}, + {"/shutdown_reason", ws_shutdown_reason_commands_h, RunOrHibernate} ]}]). %% Support functions for testing using Gun. @@ -286,3 +287,21 @@ websocket_set_options_idle_timeout(Config) -> after 2000 -> error(timeout) end. + +websocket_shutdown_reason(Config) -> + doc("The command {shutdown_reason, any()} can be used to " + "change the shutdown reason of a Websocket connection."), + ConnPid = gun_open(Config), + StreamRef = gun:ws_upgrade(ConnPid, "/shutdown_reason", [ + {<<"x-test-pid">>, pid_to_list(self())} + ]), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + WsPid = receive {ws_pid, P} -> P after 1000 -> error(timeout) end, + MRef = monitor(process, WsPid), + WsPid ! {self(), {?MODULE, ?FUNCTION_NAME}}, + receive + {'DOWN', MRef, process, WsPid, {shutdown, {?MODULE, ?FUNCTION_NAME}}} -> + ok + after 1000 -> + error(timeout) + end. -- cgit v1.2.3