Просмотр исходного кода

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 лет назад
Родитель
Сommit
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.
   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.

+ 13 - 2
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.

+ 10 - 3
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).

+ 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},
 		{"/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.