Browse Source

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.
Loïc Hoguin 5 years ago
parent
commit
d52e84bdd9

+ 6 - 0
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.
   is now considered stable and has been documented.
   The old interface is now deprecated.
   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`
 * The experimental stream handlers `cowboy_metrics_h`
   and `cowboy_tracer_h` are now considered stable and
   and `cowboy_tracer_h` are now considered stable and
   have been documented.
   have been documented.

+ 13 - 2
doc/src/manual/cowboy_websocket.asciidoc

@@ -141,6 +141,7 @@ commands() :: [Command]
 Command :: {active, boolean()}
 Command :: {active, boolean()}
          | {deflate, boolean()}
          | {deflate, boolean()}
          | {set_options, #{idle_timeout => timeout()}}
          | {set_options, #{idle_timeout => timeout()}}
+         | {shutdown_reason, any()}
          | Frame :: cow_ws:frame()
          | Frame :: cow_ws:frame()
 ----
 ----
 
 
@@ -163,6 +164,15 @@ set_options::
 Set Websocket options. Currently only the option `idle_timeout`
 Set Websocket options. Currently only the option `idle_timeout`
 may be updated from a Websocket handler.
 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::
 Frame::
 
 
 Send the corresponding Websocket frame.
 Send the corresponding Websocket frame.
@@ -266,8 +276,9 @@ normal circumstances if necessary.
 
 
 == Changelog
 == 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.7*: The option `validate_utf8` has been added.
 * *2.6*: Deflate options can now be configured via `deflate_opts`.
 * *2.6*: Deflate options can now be configured via `deflate_opts`.
 * *2.0*: The Req object is no longer passed to Websocket callbacks.
 * *2.0*: The Req object is no longer passed to Websocket callbacks.

+ 10 - 3
src/cowboy_websocket.erl

@@ -35,6 +35,7 @@
 	| {active, boolean()}
 	| {active, boolean()}
 	| {deflate, boolean()}
 	| {deflate, boolean()}
 	| {set_options, map()}
 	| {set_options, map()}
+	| {shutdown_reason, any()}
 ].
 ].
 -export_type([commands/0]).
 -export_type([commands/0]).
 
 
@@ -95,7 +96,8 @@
 	utf8_state :: cow_ws:utf8_state(),
 	utf8_state :: cow_ws:utf8_state(),
 	deflate = true :: boolean(),
 	deflate = true :: boolean(),
 	extensions = #{} :: map(),
 	extensions = #{} :: map(),
-	req = #{} :: map()
+	req = #{} :: map(),
+	shutdown_reason = normal :: any()
 }).
 }).
 
 
 %% Because the HTTP/1.1 and HTTP/2 handshakes are so different,
 %% 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
 			State0
 	end,
 	end,
 	commands(Tail, State, Data);
 	commands(Tail, State, Data);
+commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) ->
+	commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data);
 commands([Frame|Tail], State, Data0) ->
 commands([Frame|Tail], State, Data0) ->
 	Data = [frame(Frame, State)|Data0],
 	Data = [frame(Frame, State)|Data0],
 	case is_close_frame(Frame) of
 	case is_close_frame(Frame) of
@@ -623,9 +627,12 @@ frame(Frame, #state{extensions=Extensions}) ->
 	cow_ws:frame(Frame, Extensions).
 	cow_ws:frame(Frame, Extensions).
 
 
 -spec terminate(#state{}, any(), terminate_reason()) -> no_return().
 -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),
 	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) ->
 handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) ->
 	cowboy_handler:terminate(Reason, Req, HandlerState, Handler).
 	cowboy_handler:terminate(Reason, Req, HandlerState, Handler).

+ 38 - 0
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}.

+ 20 - 1
test/ws_handler_SUITE.erl

@@ -52,7 +52,8 @@ init_dispatch(Name) ->
 		{"/info", ws_info_commands_h, RunOrHibernate},
 		{"/info", ws_info_commands_h, RunOrHibernate},
 		{"/active", ws_active_commands_h, RunOrHibernate},
 		{"/active", ws_active_commands_h, RunOrHibernate},
 		{"/deflate", ws_deflate_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.
 %% Support functions for testing using Gun.
@@ -286,3 +287,21 @@ websocket_set_options_idle_timeout(Config) ->
 	after 2000 ->
 	after 2000 ->
 		error(timeout)
 		error(timeout)
 	end.
 	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.