diff --git a/doc/src/guide/handlers.ezdoc b/doc/src/guide/handlers.ezdoc
index c0fb97e18..4b741cd3f 100644
--- a/doc/src/guide/handlers.ezdoc
+++ b/doc/src/guide/handlers.ezdoc
@@ -97,3 +97,61 @@ Note that while this function may be called in a Websocket
handler, it is generally not useful to do any clean up as
the process terminates immediately after calling this callback
when using Websocket.
+
+:: Code change
+
+All handlers coming with Cowboy allow the use of the optional
+`code_change/4` callback.
+
+``` erlang
+code_change(_OldVsn, Req, State, _Extra) ->
+ {ok, Req, State}
+```
+
+This callback is strictly reserved for changing the state structure
+of the handler when a code change occurs. The return value must a
+tuple as above. If this callback is not implemented the `Req` and
+`State` will remain unchanged after a code change.
+
+This function may be called when a handler uses the `cowboy_loop` or
+`cowboy_websocket` sub protocols.
+
+:: Formatting status
+
+All handlers coming with Cowboy allow the use of the optional
+`format_status/2` callback.
+
+``` erlang
+format_status(_Opt, [_PDict, _Req, State]) ->
+ State
+```
+
+`Opt` is the atom `normal` when the status is being formatted due to
+a system call and the atom `terminate` when a handler's state is being
+formatted when terminating. `PDict` is the contents of the process
+dictionary.
+
+This callback should be used to provide nicely formatted status for
+debug tools and logging. It is strictly reserved for formatting the
+status of a handler.
+
+If this callback is not implemented the following default
+implementation will be used:
+
+``` erlang
+format_status(normal, [_PDict, _Req, State]) ->
+ [{data, [{"Handler state", State}]}];
+format_status(terminate, [_PDict, _Req, State]) ->
+ State.
+```
+
+Notice the structure of the return in the case of `normal`. Returning
+a list of 2-tuples with the first element the atoms `header` or `data`
+will allow observer - and other OTP tools - to do extra formatting of
+the status. In the case of `header` the second element should be a
+string describing the process status. For `data` the second element
+should be a list of 2-tuples with first element a string and the
+second a term. The string should describe the term as in the default
+implementation. For `cowboy_loop` and `cowboy_websocket` sub protocols
+a header and additional data will be included along with the handlers
+formatted status.
diff --git a/doc/src/guide/middlewares.ezdoc b/doc/src/guide/middlewares.ezdoc
index 0c142f9dd..8ac9b3012 100644
--- a/doc/src/guide/middlewares.ezdoc
+++ b/doc/src/guide/middlewares.ezdoc
@@ -23,12 +23,18 @@ Middlewares can return one of four different values:
* `{ok, Req, Env}` to continue the request processing
* `{suspend, Module, Function, Args}` to hibernate
+* `{system, From, Msg, Module, Req, Env, Misc}` to handle system
+messages
* `{halt, Req}` to stop processing and move on to the next request
Of note is that when hibernating, processing will resume on the given
MFA, discarding all previous stacktrace. Make sure you keep the `Req`
and `Env` in the arguments of this MFA for later use.
+When handling system messages the given module must implement the
+`cowboy_system` callbacks. For more information about handling system
+messages see `System Messages` below.
+
If an error happens during middleware processing, Cowboy will not try
to send an error back to the socket, the process will just crash. It
is up to the middleware to make sure that a reply is sent if something
@@ -41,14 +47,18 @@ In the previous chapters we saw it briefly when we needed to pass
the routing information. It is a list of tuples with the first
element being an atom and the second any Erlang term.
-Two values in the environment are reserved:
+Four values in the environment are reserved:
* `listener` contains the name of the listener
* `result` contains the result of the processing
+* `parent` contains the pid of the parent process
+* `dbg` contains a list of `sys` debug objects
-The `listener` value is always defined. The `result` value can be
-set by any middleware. If set to anything other than `ok`, Cowboy
-will not process any subsequent requests on this connection.
+The `listener`, `parent` and `dbg` values are always defined. If an
+exit signal is received from the parent process the middleware must
+exit with the same reason. The `result` value can be set by any
+middleware. If set to anything other than `ok`, Cowboy will not
+process any subsequent requests on this connection.
The middlewares that come with Cowboy may define or require other
environment values to perform.
@@ -66,3 +76,232 @@ and `handler_opts` values of the environment, respectively.
The handler middleware requires the `handler` and `handler_opts`
values. It puts the result of the request handling into `result`.
+
+:: System Messages
+
+A middleware may choose to handle system messages. Handling system
+messages will help OTP tools to debug and code reload the middleware.
+
+To handle system message a middleware implements the 3 required
+`cowboy_system` callbacks and possibly 3 optional callbacks. Note
+these are not the same as the standard `sys` callbacks though are
+very similar.
+
+A system message is of the form:
+
+``` erlang
+{system, From, Msg}
+```
+
+To handle the system message a middleware returns:
+
+``` erlang
+{system, From, Msg, Module, Req, Env, Misc}
+```
+
+`Module` is the module with the `cowboy_system` callbacks. This module
+will be used to handle system requests. `Misc` should be any
+additional data that is required by the middleware, i.e. its state.
+
+Once the middleware has returned, the `sys` module will control the
+process. Note the process might be hibernated.
+
+The first callback is `continue/3`. This is called when `sys` returns
+control of the process to cowboy and the middleware. Therefore it
+should continue to act as normal and return the same values as
+`execute/2`:
+
+``` erlang
+continue(Req, Env, Misc) ->
+ {ok, Req, Env}
+```
+
+The second callback is `terminate/4`. This is called when the process
+should terminate because its parent did. The middleware should exit
+with the same reason as the first argument (after handling any
+termination or clean up):
+
+``` erlang
+terminate(Reason, _Req, _Env, _Misc) ->
+ exit(Reason).
+```
+
+The third callback is `code_change/4`. This is called when the process
+should change it's internal state structure because a new version of
+the module has been loaded:
+
+``` erlang
+code_change(Req, Misc, _Module, _OldVsn, _Extra) ->
+ {ok, Req, Misc}.
+```
+
+`OldVsn` is a term representing the previous version of the module
+being code changed, which may not be the middleware. This should be
+used to know if and how a state should be converted to a new
+structure.
+
+`Extra` can be any term and can be used to provide extra information
+required in a code change.
+
+To code change a process it must be suspended by the `sys` module:
+
+``` erlang
+sys:suspend(Pid),
+sys:change_code(Pid, Module, OldVsn, Extra),
+sys:resume(Pid).
+```
+
+Remember to resume the process afterwards, with `sys:resume/1`,
+otherwise the process will remain `sys` suspended.
+
+To code change the state of a handler a middleware may call
+`cowboy_handler:code_change/6`, to alter the `Req` and handler state:
+
+``` erlang
+cowboy_handler:code_change(OldVsn, Req, State, Module, Extra, Handler)
+```
+
+This is turn will call the handler's optional callback `code_change/4`
+if it is implemented and `Module` is the handlers module:
+
+```erlang
+Handler:code_change(OldVsn, Req, State, Extra)
+```
+
+Both `cowboy_loop` and `cowboy_websocket` will code change a handler
+in this way.
+
+The first optional callback is `get_state/2`. This is used to retrieve
+the state of the middleware when debugging. The return value should be
+of the form:
+
+``` erlang
+{ok, {Module, Req, State}}
+```
+
+Where `Module` is the module of the middleware, or handler, and
+`State` is its the state. For example:
+
+``` erlang
+get_state(Req, Misc) ->
+ {ok, {?MODULE, Req, Misc}}.
+```
+
+To get the state of a process call (only for use while debugging):
+
+``` erlang
+sys:get_state(Pid)
+```
+
+If a middleware does not implement this callback the `Module` will be
+middleware module and `State` its data.
+
+The `cowboy_loop` and `cowboy_websocket` sub protocols will return the
+module and state of the current handler, rather than their own.
+Middlewares with handler modules may wish to do this too.
+
+The second optional callback is `replace_state/3`. This function is
+called during debugging to replace the internal state of a middleware
+or a handler.
+
+The first argument is an anonymous function that expects an argument
+of the form returned by `get_state/2`.
+
+``` erlang
+replace_state(Replace, Req, Misc) ->
+ Result = {?Module, Req2, Misc2} = Replace({?MODULE, Req, Misc}),
+ {ok, Result, Req2, Misc2}.
+```
+Where `State` is the state of the middleware, or handler, with module
+`Module`.
+
+The middleware will receive the new `Req2` and `Misc2` on
+`continue/3`, or the next system callback called.
+
+To replace the state of a process call (only for use while debugging):
+
+``` erlang
+sys:replace_state(Replace, Pid)
+```
+
+For a cowboy process the `Replace` anonymous function should
+receive, and return, an argument similar to this example:
+
+``` erlang
+fun({Module, Req, State}) ->
+ {Module, Req, State}
+end.
+```
+
+If a middleware does not implement this callback the `Module` will be
+the module of the middleware and `State` its data. Note that in this
+case the middleware module can not be changed. An attempt to do so
+will result in the replace failing. However the cowboy process will
+continue as if the call had not been made.
+
+For `cowboy_loop` and `cowboy_websocket` sub protocols the `Module`
+and `State` will refer to the handler module and its state. These two
+will not allow the current handler to be changed, and an attempt to do
+so will fail. Should a `Replace` function raise an exception the
+internal state of the process will stay the same and it will continue
+running as if nothing happened.
+
+Calling a cowboy process when it is between requests will results in
+the `Req` being the atom `undefined` because there is currently no
+request.
+
+A third optional callback can be implemented, `format_status/2`. This
+function is called during debugging to fetch the full status of a
+middleware.
+
+The first argument is the atom `normal`. The second argument is a
+list of information. The first element is the contents of the process
+dictionary. The second is an atom `suspended` or `running`, depending
+whether the process is suspended by `sys` or not. The third, cowboy
+request object; the fourth, middleware environment; the fifth, the
+data for the middleware.
+
+``` erlang
+format_status(normal, [_PDict, _SysState, Req, Env, Misc]) ->
+ [
+ {header, "Middleware"},
+ {data, [
+ {"Request", cowboy_req:to_list(Req)},
+ {"Environment", Env},
+ {"Misc", Misc}
+ ]}
+ ].
+```
+
+Note that the `Req` will be locked and should only be used to retrieve
+information.
+
+If a middleware does not implement this callback the `Misc` data will
+be used to format the status.
+
+To view the status of a process call:
+
+``` erlang
+sys:get_status(Pid)
+```
+
+To format the state of a handler a middleware (or sub protocol) may
+call `cowboy_handler:format_status/4`:
+
+```erlang
+cowboy_handler:format_status(normal, PDict, Req, State, Handler)
+```
+
+In turn this will call the optional handler callback
+`format_status/2`:
+
+```erlang
+Handler:format_status(normal, [PDict, Req, State])
+```
+
+If a handler does not implement the callback its `State` will be
+used to provide data for the status:
+
+``` erlang
+{data, [{"Handler state", State}]}
+```
diff --git a/doc/src/guide/sub_protocols.ezdoc b/doc/src/guide/sub_protocols.ezdoc
index d34f21e26..2b23b8bc2 100644
--- a/doc/src/guide/sub_protocols.ezdoc
+++ b/doc/src/guide/sub_protocols.ezdoc
@@ -57,7 +57,8 @@ upgrade(Req, Env, Handler, HandlerOpts, Timeout, Hibernate) ->
```
This callback is expected to behave like a middleware and to
-return an updated environment and Req object.
+return an updated environment and Req object. Note that sub protocols
+may halt, hibernate and handle system messages like a middleware.
Sub protocols are expected to call the `cowboy_handler:terminate/4`
function when they terminate. This function will make sure that
diff --git a/doc/src/manual/cowboy_handler.ezdoc b/doc/src/manual/cowboy_handler.ezdoc
index b440b60e7..dd0592c7b 100644
--- a/doc/src/manual/cowboy_handler.ezdoc
+++ b/doc/src/manual/cowboy_handler.ezdoc
@@ -17,8 +17,9 @@ Environment output:
This module also defines the `cowboy_handler` behaviour that
defines the basic interface for handlers. All Cowboy handlers
-implement at least the `init/2` callback, and may implement
-the `terminate/3` callback optionally.
+implement at least the `init/2` callback, and may implement any of
+the optional callbacks: `terminate/3`, `code_change/4` and
+`format_status/2`.
:: Types
@@ -108,3 +109,38 @@ Call the optional `terminate/3` callback if it exists.
This function should always be called at the end of the execution
of a handler, to give it a chance to clean up or perform
miscellaneous operations.
+
+: code_change(OldVsn, Req, State, Module, Extra, Handler) -> {ok, Req, State}
+
+Types:
+
+* OldVsn :: any()
+* Req = cowboy_req:req()
+* State = any()
+* Module = module()
+* Extra = any()
+* Handler = module()
+
+Call the optional `code_change/4` callback if it exists.
+
+This function should always be called when a code change occurs as
+the structure of the handlers state may need to be changed.
+
+: format_status(Opt, PDict, Req, State, Handler) -> Status
+
+Types:
+
+* Opt = normal | terminate
+* PDict = [{any(), any()}]
+* Req = cowboy_req:req()
+* State = any()
+* Handler = module()
+* Status = any()
+
+Call the optional `format_status/2` callback if it exists.
+
+This function should always be called when formatting the handlers
+state. The `Opt` value of `normal` should be used when formatting
+the state as part of a `format_status/2` system call. The `Opt`
+value of `terminate` should be used when creating a suitable
+value for logging on termination.
diff --git a/doc/src/manual/cowboy_loop.ezdoc b/doc/src/manual/cowboy_loop.ezdoc
index 1f3ab9e42..9efff4f2e 100644
--- a/doc/src/manual/cowboy_loop.ezdoc
+++ b/doc/src/manual/cowboy_loop.ezdoc
@@ -71,6 +71,10 @@ received first.
A socket error ocurred.
+: {shutdown, Reason}
+
+The parent process exited with reason `Reason`.
+
:: Callbacks
: info(Info, Req, State)
diff --git a/doc/src/manual/cowboy_middleware.ezdoc b/doc/src/manual/cowboy_middleware.ezdoc
index 2275d35e8..81d64c2e3 100644
--- a/doc/src/manual/cowboy_middleware.ezdoc
+++ b/doc/src/manual/cowboy_middleware.ezdoc
@@ -21,7 +21,8 @@ optionally with its contents modified.
: execute(Req, Env)
-> {ok, Req, Env}
| {suspend, Module, Function, Args}
- | {halt, Req}
+ | {system, From, Msg, Module, Req, Env, Misc}
+ | {halt, Req, Env}
Types:
@@ -30,6 +31,9 @@ Types:
* Module = module()
* Function = atom()
* Args = [any()]
+* From = {pid(), any()}
+* Msg = any()
+* Misc = any()
Execute the middleware.
@@ -41,6 +45,10 @@ The `suspend` return value will hibernate the process until
an Erlang message is received. Note that when resuming, any
previous stacktrace information will be gone.
+The `system` return value will handle the system message.
+The given module will be used for `cowboy_system` callbacks when
+handling system messages.
+
The `halt` return value stops Cowboy from doing any further
processing of the request, even if there are middlewares
that haven't been executed yet. The connection may be left
diff --git a/doc/src/manual/cowboy_sub_protocol.ezdoc b/doc/src/manual/cowboy_sub_protocol.ezdoc
index 2ad0cf76c..c0a5847dc 100644
--- a/doc/src/manual/cowboy_sub_protocol.ezdoc
+++ b/doc/src/manual/cowboy_sub_protocol.ezdoc
@@ -12,7 +12,8 @@ None.
: upgrade(Req, Env, Handler, Opts)
-> {ok, Req, Env}
| {suspend, Module, Function, Args}
- | {halt, Req}
+ | {system, From, Msg, Module, Req, Env, Misc}
+ | {halt, Req, Env}
| {error, StatusCode, Req}
Types:
@@ -24,6 +25,9 @@ Types:
* Module = module()
* Function = atom()
* Args = [any()]
+* From = {pid(), any()}
+* Msg = any()
+* Misc = any()
* StatusCode = cowboy:http_status()
Upgrade the protocol.
diff --git a/doc/src/manual/cowboy_websocket.ezdoc b/doc/src/manual/cowboy_websocket.ezdoc
index 889ddd747..d1606833c 100644
--- a/doc/src/manual/cowboy_websocket.ezdoc
+++ b/doc/src/manual/cowboy_websocket.ezdoc
@@ -104,6 +104,10 @@ received first.
A socket error ocurred.
+: {shutdown, Reason}
+
+The parent process exited with reason `Reason`.
+
:: Callbacks
: websocket_handle(InFrame, Req, State)
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
index e3faf6605..6e89d0be4 100644
--- a/src/cowboy_handler.erl
+++ b/src/cowboy_handler.erl
@@ -22,6 +22,8 @@
-export([execute/2]).
-export([terminate/4]).
+-export([code_change/6]).
+-export([format_status/5]).
-callback init(Req, any())
-> {ok | module(), Req, any()}
@@ -30,6 +32,10 @@
| {module(), Req, any(), timeout(), hibernate}
when Req::cowboy_req:req().
%% @todo optional -callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
+%% @todo optional -callback code_change(any(), cowboy_req:req(), any(), any())
+%% -> {ok, cowboy_req:req(), any()}.
+%% @todo optional -callback format_status(normal | terminate,
+%% [[{any(), any()}], cowboy_req:req(), any()]) -> any().
-spec execute(Req, Env) -> {ok, Req, Env}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
@@ -52,7 +58,8 @@ execute(Req, Env) ->
Stacktrace = erlang:get_stacktrace(),
cowboy_req:maybe_reply(Stacktrace, Req),
terminate({crash, Class, Reason}, Req, HandlerOpts, Handler),
- erlang:Class([
+ erlang:exit([
+ {class, Class},
{reason, Reason},
{mfa, {Handler, init, 2}},
{stacktrace, Stacktrace},
@@ -68,15 +75,63 @@ terminate(Reason, Req, State, Handler) ->
try
Handler:terminate(Reason, cowboy_req:lock(Req), State)
catch Class:Reason2 ->
- erlang:Class([
+ erlang:exit([
+ {class, Class},
{reason, Reason2},
{mfa, {Handler, terminate, 3}},
{stacktrace, erlang:get_stacktrace()},
{req, cowboy_req:to_list(Req)},
- {state, State},
+ {state, terminate_state(Req, State, Handler)},
{terminate_reason, Reason}
])
end;
false ->
ok
end.
+
+terminate_state(Req, State, Handler) ->
+ case erlang:function_exported(Handler, format_status, 2) of
+ true ->
+ call_format_status(terminate, get(), Req, State, Handler);
+ false ->
+ default_format_status(terminate, State)
+ end.
+
+-spec code_change(any(), cowboy_req:req(), any(), module(), any(), module())
+ -> {ok, cowboy_req:req(), any()}.
+code_change(OldVsn, Req, State, Handler, Extra, Handler) ->
+ case erlang:function_exported(Handler, code_change, 4) of
+ true ->
+ handler_code_change(OldVsn, Req, State, Extra, Handler);
+ false ->
+ {ok, Req, State}
+ end;
+code_change(_OldVsn, Req, State, _Module, _Extra, _Handler) ->
+ {ok, Req, State}.
+
+handler_code_change(OldVsn, Req, State, Extra, Handler) ->
+ {ok, _Req2, _State2} = Handler:code_change(OldVsn, Req, State, Extra).
+
+-spec format_status(normal | terminate, [{term(), term()}], cowboy_req:req(),
+ any(), module()) -> any().
+format_status(Opt, PDict, Req, State, Handler) ->
+ case erlang:function_exported(Handler, format_status, 2) of
+ true ->
+ call_format_status(Opt, PDict, Req, State, Handler);
+ false ->
+ default_format_status(Opt, State)
+ end.
+
+call_format_status(Opt, PDict, Req, State, Handler) ->
+ try Handler:format_status(Opt, [PDict, cowboy_req:lock(Req), State]) of
+ Status ->
+ Status
+ catch
+ _:_ ->
+ default_format_status(Opt, State)
+ end.
+
+default_format_status(normal, State) ->
+ [{data, [{"Handler state", State}]}];
+default_format_status(terminate, State) ->
+ State.
diff --git a/src/cowboy_loop.erl b/src/cowboy_loop.erl
index b9eb8cda1..0493acf37 100644
--- a/src/cowboy_loop.erl
+++ b/src/cowboy_loop.erl
@@ -23,9 +23,16 @@
%% {error, overflow} reason if this threshold is reached.
-module(cowboy_loop).
-behaviour(cowboy_sub_protocol).
+-behaviour(cowboy_system).
-export([upgrade/6]).
-export([loop/4]).
+-export([continue/3]).
+-export([terminate/4]).
+-export([code_change/5]).
+-export([get_state/2]).
+-export([replace_state/3]).
+-export([format_status/2]).
-callback init(Req, any())
-> {ok | module(), Req, any()}
@@ -42,6 +49,7 @@
-record(state, {
env :: cowboy_middleware:env(),
+ parent :: pid(),
hibernate = false :: boolean(),
buffer_size = 0 :: non_neg_integer(),
max_buffer = 5000 :: non_neg_integer() | infinity,
@@ -50,15 +58,22 @@
resp_sent = false :: boolean()
}).
+-type misc() :: {#state{}, module(), any()}.
+
-spec upgrade(Req, Env, module(), any(), timeout(), run | hibernate)
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req, Env, misc()}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, Timeout, run) ->
- State = #state{env=Env, max_buffer=get_max_buffer(Env), timeout=Timeout},
+ {_, Parent} = lists:keyfind(parent, 1, Env),
+ State = #state{env=Env, parent=Parent, max_buffer=get_max_buffer(Env),
+ timeout=Timeout},
State2 = timeout(State),
after_call(Req, State2, Handler, HandlerState);
upgrade(Req, Env, Handler, HandlerState, Timeout, hibernate) ->
- State = #state{env=Env, max_buffer=get_max_buffer(Env), hibernate=true, timeout=Timeout},
+ {_, Parent} = lists:keyfind(parent, 1, Env),
+ State = #state{env=Env, parent=Parent, max_buffer=get_max_buffer(Env),
+ hibernate=true, timeout=Timeout},
State2 = timeout(State),
after_call(Req, State2, Handler, HandlerState).
@@ -103,10 +118,12 @@ timeout(State=#state{timeout=Timeout,
-spec loop(Req, #state{}, module(), any())
-> {ok, Req, cowboy_middleware:env()} | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
loop(Req, State=#state{buffer_size=NbBytes,
max_buffer=Threshold, timeout_ref=TRef,
- resp_sent=RespSent}, Handler, HandlerState) ->
+ resp_sent=RespSent, env=Env, parent=Parent}, Handler, HandlerState) ->
[Socket, Transport] = cowboy_req:get([socket, transport], Req),
{OK, Closed, Error} = Transport:messages(),
receive
@@ -131,6 +148,10 @@ loop(Req, State=#state{buffer_size=NbBytes,
after_loop(Req, State, Handler, HandlerState, timeout);
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
loop(Req, State, Handler, HandlerState);
+ {'EXIT', Parent, Reason} ->
+ terminate(Req, State, Handler, HandlerState, {shutdown, Reason});
+ {system, From, Msg} ->
+ {system, From, Msg, ?MODULE, Req, Env, {State, Handler, HandlerState}};
Message ->
%% We set the socket back to {active, false} mode in case
%% the handler is going to call recv. We also flush any
@@ -161,12 +182,14 @@ call(Req, State=#state{resp_sent=RespSent},
cowboy_req:maybe_reply(Stacktrace, Req)
end,
cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler),
- erlang:Class([
+ State = cowboy_handler:format_status(terminate, get(), Req, HandlerState, Handler),
+ erlang:exit([
+ {class, Class},
{reason, Reason},
{mfa, {Handler, info, 3}},
{stacktrace, Stacktrace},
{req, cowboy_req:to_list(Req)},
- {state, HandlerState}
+ {state, State}
])
end.
@@ -203,3 +226,61 @@ flush_timeouts() ->
after 0 ->
ok
end.
+
+-spec continue(cowboy_req:req(), cowboy_middleware:env(), misc())
+ -> {ok, cowboy_req:req(), cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), cowboy_req:req(),
+ cowboy_middleware:env(), misc()}.
+continue(Req, Env, {State, Handler, HandlerState}) ->
+ loop(Req, State#state{env=Env}, Handler, HandlerState).
+
+-spec terminate(any(), cowboy_req:req(), cowboy_middleware:env(), misc())
+ -> no_return().
+terminate(Reason, Req, _Env,
+ {#state{resp_sent=RespSent}, Handler, HandlerState}) ->
+ if RespSent -> ok; true ->
+ cowboy_req:maybe_reply([], Req)
+ end,
+ _ = cowboy_handler:terminate({shutdown, Reason}, Req, HandlerState, Handler),
+ exit(Reason).
+
+-spec code_change(cowboy_req:req(), misc(), module(), any(), any())
+ -> {ok, cowboy_req:req(), misc()}.
+code_change(Req, {State, Handler, HandlerState}, Module, OldVsn, Extra) ->
+ {ok, Req2, HandlerState2} = cowboy_handler:code_change(OldVsn, Req, HandlerState,
+ Module, Extra, Handler),
+ {ok, Req2, {State, Handler, HandlerState2}}.
+
+-spec get_state(cowboy_req:req(), misc())
+ -> {ok, {module(), cowboy_req:req(), any()}}.
+get_state(Req, {_State, Handler, HandlerState}) ->
+ {ok, {Handler, Req, HandlerState}}.
+
+-spec replace_state(cowboy_system:replace_state(), cowboy_req:req(), misc())
+ -> {ok, {module(), cowboy_req:req(), any()}, cowboy_req:req(), misc()}.
+replace_state(Replace, Req, {State, Handler, HandlerState}) ->
+ Result = {Handler, Req2, HandlerState2} = Replace({Handler, Req, HandlerState}),
+ {ok, Result, Req2, {State, Handler, HandlerState2}}.
+
+-spec format_status(normal, [[{term(), term()}] | running | suspended |
+ cowboy_req:req() | cowboy_middleware:env() | misc()])
+ -> maybe_improper_list().
+format_status(Opt, [PDict, SysState, Req, Env, {State, Handler, HandlerState}]) ->
+ Parent = lists:keyfind(parent, 1, Env),
+ {dbg, Dbg} = lists:keyfind(dbg, 1, Env),
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"Request", cowboy_req:to_list(Req)},
+ {"Environment", Env},
+ {"Loop state", state_to_list(State)},
+ {"Handler", Handler}
+ ],
+ HandlerStatus = cowboy_handler:format_status(Opt, PDict, Req, HandlerState, Handler),
+ [{header, "Cowboy loop handler"}, {data, Data} | HandlerStatus].
+
+state_to_list(State) ->
+ lists:zip(record_info(fields, state), tl(tuple_to_list(State))).
diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl
index 7ff947e98..2503465d2 100644
--- a/src/cowboy_middleware.erl
+++ b/src/cowboy_middleware.erl
@@ -14,11 +14,157 @@
-module(cowboy_middleware).
+%% API.
+-export([execute/4]).
+-export([resume/5]).
+
+%% System.
+-export([system_continue/3]).
+-export([system_terminate/4]).
+-export([system_code_change/4]).
+-export([system_get_state/1]).
+-export([system_replace_state/2]).
+-export([format_status/2]).
+
-type env() :: [{atom(), any()}].
-export_type([env/0]).
-callback execute(Req, Env)
-> {ok, Req, Env}
| {suspend, module(), atom(), [any()]}
- | {halt, Req}
+ | {system, {pid(), term()}, any(), module(), Req, Env, any()}
+ | {halt, Req, Env}
when Req::cowboy_req:req(), Env::env().
+
+-record(misc, {
+ req :: cowboy_req:req(),
+ env :: env(),
+ halt :: {module(), atom(), list()},
+ tail :: [module()],
+ module :: module(),
+ module_misc :: any()
+}).
+
+%%API.
+
+-spec execute(cowboy_req:req(), env(), {module(), atom(), list()}, [module()])
+ -> ok.
+execute(Req, Env, {Module, Function, Args}, []) ->
+ apply(Module, Function, [Req, Env | Args]);
+execute(Req, Env, Halt, [Middleware | Tail]) ->
+ case Middleware:execute(Req, Env) of
+ {ok, Req2, Env2} ->
+ execute(Req2, Env2, Halt, Tail);
+ {suspend, Module, Function, Args} ->
+ proc_lib:hibernate(?MODULE, resume,
+ [Halt, Tail, Module, Function, Args]);
+ {system, From, Msg, Module, Req2, Env2, ModMisc} ->
+ {_, Parent} = lists:keyfind(parent, 1, Env2),
+ {_, Dbg} = lists:keyfind(dbg, 1, Env2),
+ Misc = #misc{req=Req2, env=Env2, halt=Halt, tail=Tail,
+ module=Module, module_misc=ModMisc},
+ sys:handle_system_msg(Msg, From, Parent, ?MODULE, Dbg, Misc);
+ {halt, Req2, Env2} ->
+ execute(Req2, Env2, Halt, [])
+ end.
+
+-spec resume({module(), atom(), list()}, [module()], module(), atom(), list())
+ -> ok.
+resume(Halt, Tail, Module, Function, Args) ->
+ case apply(Module, Function, Args) of
+ {ok, Req2, Env2} ->
+ execute(Req2, Env2, Halt, Tail);
+ {suspend, Module, Function, Args2} ->
+ proc_lib:hibernate(?MODULE, resume,
+ [Halt, Tail, Module, Function, Args2]);
+ {system, From, Msg, Module, Req2, Env2, ModMisc} ->
+ {_, Parent} = lists:keyfind(parent, 1, Env2),
+ {_, Dbg} = lists:keyfind(dbg, 1, Env2),
+ Misc = #misc{req=Req2, env=Env2, halt=Halt, tail=Tail,
+ module=Module, module_misc=ModMisc},
+ sys:handle_system_msg(Msg, From, Parent, ?MODULE, Dbg, Misc);
+ {halt, Req2, Env2} ->
+ execute(Req2, Env2, Halt, [])
+ end.
+
+%% System.
+
+-spec system_continue(pid(), [sys:dbg_opt()], #misc{}) -> ok.
+system_continue(_Parent, Dbg, #misc{req=Req, env=Env, halt=Halt,
+ tail=Tail, module=Module, module_misc=ModMisc}) ->
+ Env2 = lists:keystore(dbg, 1, Env, {dbg, Dbg}),
+ resume(Halt, Tail, Module, continue, [Req, Env2, ModMisc]).
+
+-spec system_terminate(any(), pid(), [sys:dbg_opt()], #misc{}) -> no_return().
+system_terminate(Reason, _Parent, Dbg,
+ #misc{req=Req, env=Env, module=Module, module_misc=ModMisc}) ->
+ Env2 = lists:keystore(dbg, 1, Env, {dbg, Dbg}),
+ Module:terminate(Reason, Req, Env2, ModMisc).
+
+-spec system_code_change(#misc{}, module(), any(), any())
+ -> {ok, #misc{}} | {error | exit | throw, any()}.
+system_code_change(Misc=#misc{req=Req, module=Module, module_misc=ModMisc},
+ Module2, OldVsn, Extra) ->
+ try Module:code_change(Req, ModMisc, Module2, OldVsn, Extra) of
+ {ok, Req2, ModMisc2} ->
+ {ok, Misc#misc{req=Req2, module_misc=ModMisc2}}
+ catch
+ Class:Reason ->
+ {Class, Reason}
+ end.
+
+-spec system_get_state(#misc{}) -> {ok, {module(), cowboy_req:req(), any()}}.
+system_get_state(#misc{req=Req, module=Module, module_misc=ModMisc}) ->
+ case erlang:function_exported(Module, get_state, 2) of
+ true ->
+ {ok, {_, _, _}} = Module:get_state(Req, ModMisc);
+ false ->
+ {ok, {Module, Req, ModMisc}}
+ end.
+
+-spec system_replace_state(cowboy_system:replace_state(), #misc{})
+ -> {ok, {module(), cowboy_req:req(), any()}, #misc{}}.
+system_replace_state(Replace, Misc=#misc{req=Req, module=Module, module_misc=ModMisc}) ->
+ case erlang:function_exported(Module, replace_state, 3) of
+ true ->
+ {ok, Result, Req2, ModMisc2} = Module:replace_state(Replace, Req, ModMisc),
+ {ok, Result, Misc#misc{req=Req2, module_misc=ModMisc2}};
+ false ->
+ {Module, Req2, ModMisc2} = Result = Replace({Module, Req, ModMisc}),
+ {ok, Result, Misc#misc{req=Req2, module_misc=ModMisc2}}
+ end.
+
+-spec format_status(normal, [[{term(), term()}] | running | suspended |
+ pid() | [sys:dbg_opt()] | #misc{}]) -> any().
+format_status(Opt, [PDict, SysState, Parent, Dbg,
+ #misc{req=Req, env=Env, module=Module, module_misc=ModMisc}]) ->
+ Env2 = lists:keystore(dbg, 1, Env, {dbg, Dbg}),
+ case erlang:function_exported(Module, format_status, 2) of
+ true ->
+ format_status(Opt, PDict, SysState, Parent, Dbg, Req, Env2, ModMisc, Module);
+ false ->
+ default_format_status(SysState, Parent, Dbg, Req, Env, Module, ModMisc)
+ end.
+
+format_status(Opt, PDict, SysState, Parent, Dbg, Req, Env, ModMisc, Module) ->
+ Req2 = cowboy_req:lock(Req),
+ try Module:format_status(Opt, [PDict, SysState, Req2, Env, ModMisc]) of
+ Status ->
+ Status
+ catch
+ _:_ ->
+ default_format_status(SysState, Parent, Dbg, Req, Env, Module, ModMisc)
+ end.
+
+default_format_status(SysState, Parent, Dbg, Req, Env, Module, ModMisc) ->
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"Request", cowboy_req:to_list(Req)},
+ {"Environment", Env},
+ {"Middleware", Module},
+ {"Middleware state", ModMisc}
+ ],
+ [{header, "Cowboy middleware"}, {data, Data}].
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index 82f1f3879..6ae921f4e 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -19,9 +19,17 @@
-export([start_link/4]).
%% Internal.
--export([init/4]).
+-export([init/5]).
-export([parse_request/3]).
--export([resume/6]).
+-export([next_request/4]).
+
+%% System.
+-export([system_continue/3]).
+-export([system_terminate/4]).
+-export([system_code_change/4]).
+-export([system_get_state/1]).
+-export([system_replace_state/2]).
+-export([format_status/2]).
-type opts() :: [{compress, boolean()}
| {env, cowboy_middleware:env()}
@@ -42,6 +50,7 @@
middlewares :: [module()],
compress :: boolean(),
env :: cowboy_middleware:env(),
+ parent :: pid(),
onresponse = undefined :: undefined | cowboy:onresponse_fun(),
max_empty_lines :: non_neg_integer(),
req_keepalive = 1 :: non_neg_integer(),
@@ -60,7 +69,7 @@
-spec start_link(ranch:ref(), inet:socket(), module(), opts()) -> {ok, pid()}.
start_link(Ref, Socket, Transport, Opts) ->
- Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]),
+ Pid = proc_lib:spawn_link(?MODULE, init, [Ref, self(), Socket, Transport, Opts]),
{ok, Pid}.
%% Internal.
@@ -72,8 +81,8 @@ get_value(Key, Opts, Default) ->
_ -> Default
end.
--spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok.
-init(Ref, Socket, Transport, Opts) ->
+-spec init(ranch:ref(), pid(), inet:socket(), module(), opts()) -> ok.
+init(Ref, Parent, Socket, Transport, Opts) ->
Compress = get_value(compress, Opts, false),
MaxEmptyLines = get_value(max_empty_lines, Opts, 5),
MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64),
@@ -82,12 +91,13 @@ init(Ref, Socket, Transport, Opts) ->
MaxKeepalive = get_value(max_keepalive, Opts, 100),
MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096),
Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]),
- Env = [{listener, Ref}|get_value(env, Opts, [])],
+ Dbg = sys:debug_options(get_value(debug, Opts, [])),
+ Env = [{listener, Ref}, {parent, Parent}, {dbg, Dbg}|get_value(env, Opts, [])],
OnResponse = get_value(onresponse, Opts, undefined),
Timeout = get_value(timeout, Opts, 5000),
ok = ranch:accept_ack(Ref),
wait_request(<<>>, #state{socket=Socket, transport=Transport,
- middlewares=Middlewares, compress=Compress, env=Env,
+ middlewares=Middlewares, compress=Compress, env=Env, parent=Parent,
max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive,
max_request_line_length=MaxRequestLineLength,
max_header_name_length=MaxHeaderNameLength,
@@ -414,45 +424,19 @@ request(Buffer, State=#state{socket=Socket, transport=Transport,
-spec execute(cowboy_req:req(), #state{}) -> ok.
execute(Req, State=#state{middlewares=Middlewares, env=Env}) ->
- execute(Req, State, Env, Middlewares).
+ Halt = {?MODULE, next_request, [State, ok]},
+ cowboy_middleware:execute(Req, Env, Halt, Middlewares).
--spec execute(cowboy_req:req(), #state{}, cowboy_middleware:env(), [module()])
+-spec next_request(cowboy_req:req(), cowboy_middleware:env(), #state{}, any())
-> ok.
-execute(Req, State, Env, []) ->
- next_request(Req, State, get_value(result, Env, ok));
-execute(Req, State, Env, [Middleware|Tail]) ->
- case Middleware:execute(Req, Env) of
- {ok, Req2, Env2} ->
- execute(Req2, State, Env2, Tail);
- {suspend, Module, Function, Args} ->
- erlang:hibernate(?MODULE, resume,
- [State, Env, Tail, Module, Function, Args]);
- {halt, Req2} ->
- next_request(Req2, State, ok)
- end.
-
--spec resume(#state{}, cowboy_middleware:env(), [module()],
- module(), module(), [any()]) -> ok.
-resume(State, Env, Tail, Module, Function, Args) ->
- case apply(Module, Function, Args) of
- {ok, Req2, Env2} ->
- execute(Req2, State, Env2, Tail);
- {suspend, Module2, Function2, Args2} ->
- erlang:hibernate(?MODULE, resume,
- [State, Env, Tail, Module2, Function2, Args2]);
- {halt, Req2} ->
- next_request(Req2, State, ok)
- end.
-
--spec next_request(cowboy_req:req(), #state{}, any()) -> ok.
-next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout},
+next_request(Req, Env, State=#state{req_keepalive=Keepalive, timeout=Timeout},
HandlerRes) ->
cowboy_req:ensure_response(Req, 204),
%% If we are going to close the connection,
%% we do not want to attempt to skip the body.
case cowboy_req:get(connection, Req) of
close ->
- terminate(State);
+ terminate(State#state{env=Env});
_ ->
%% Skip the body if it is reasonably sized. Close otherwise.
Buffer = case cowboy_req:body(Req) of
@@ -461,15 +445,30 @@ next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout},
end,
%% Flush the resp_sent message before moving on.
if HandlerRes =:= ok, Buffer =/= close ->
- receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- ?MODULE:parse_request(Buffer,
- State#state{req_keepalive=Keepalive + 1,
- until=until(Timeout)}, 0);
+ next_request(Buffer, State#state{req_keepalive=Keepalive + 1,
+ until=until(Timeout), env=Env});
true ->
- terminate(State)
+ terminate(State#state{env=Env})
end
end.
+-spec next_request(binary(), #state{}) -> ok.
+next_request(Buffer, State=#state{env=Env, parent=Parent}) ->
+ receive
+ {cowboy_req, resp_sent} ->
+ next_request(Buffer, State);
+ {'EXIT', Parent, Reason} ->
+ terminate(State),
+ exit(Reason);
+ {system, From, Msg} ->
+ Parent = get_value(parent, Env, self()),
+ Dbg = get_value(dbg, Env, []),
+ Misc = {Buffer, State},
+ sys:handle_system_msg(Msg, From, Parent, ?MODULE, Dbg, Misc)
+ after 0 ->
+ ?MODULE:parse_request(Buffer, State, 0)
+ end.
+
-spec error_terminate(cowboy:http_status(), #state{}) -> ok.
error_terminate(Status, State=#state{socket=Socket, transport=Transport,
compress=Compress, onresponse=OnResponse}) ->
@@ -486,3 +485,51 @@ error_terminate(Status, Req, State) ->
terminate(#state{socket=Socket, transport=Transport}) ->
Transport:close(Socket),
ok.
+
+%% System.
+
+-spec system_continue(pid(), [sys:dbg_opt()], {binary(), #state{}}) -> ok.
+system_continue(_Parent, Dbg, {Buffer, State=#state{env=Env}}) ->
+ Env2 = lists:keystore(dbg, 1, Env, {dbg, Dbg}),
+ next_request(Buffer, State#state{env=Env2}).
+
+-spec system_terminate(any(), pid(), [sys:dbg_opt()], {binary(), #state{}})
+ -> no_return().
+system_terminate(Reason, _Parent, _Dbg, {_Buffer, State}) ->
+ terminate(State),
+ exit(Reason).
+
+-spec system_code_change({binary(), #state{}}, module(), any(), any())
+ -> {ok, #state{}}.
+system_code_change(Misc, _Module, _OldVsn, _Extra) ->
+ {ok, Misc}.
+
+-spec system_get_state({binary(), #state{}})
+ -> {ok, {module(), undefined, {binary(), #state{}}}}.
+system_get_state(Misc) ->
+ {ok, {?MODULE, undefined, Misc}}.
+
+-spec system_replace_state(cowboy_system:replace_state(), {binary(), #state{}})
+ -> {ok, {module(), undefined, {binary(), #state{}}}, {binary(), #state{}}}.
+system_replace_state(Replace, Misc) ->
+ {?MODULE, undefined, Misc2={_, _}} = Replace({?MODULE, undefined, Misc}),
+ {ok, {?MODULE, undefined, Misc2}, Misc2}.
+
+-spec format_status(normal, list()) -> [{atom(), term()}].
+format_status(normal, [_PDict, SysState, Parent, Dbg,
+ {Buffer, State=#state{env=Env}}]) ->
+ Env2 = lists:keystore(dbg, 1, Env, {dbg, Dbg}),
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"Environment", Env2},
+ {"Protocol buffer", Buffer},
+ {"Protocol state", state_to_list(State)}
+ ],
+ [{header, "Cowboy protocol"}, {data, Data}].
+
+-spec state_to_list(#state{}) -> [{atom(), term()}].
+state_to_list(State) ->
+ lists:zip(record_info(fields, state), tl(tuple_to_list(State))).
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 98c6f3b00..2dae54076 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -976,17 +976,21 @@ next(Req, State, StatusCode) when is_integer(StatusCode) ->
respond(Req, State, StatusCode) ->
terminate(cowboy_req:reply(StatusCode, Req), State).
+-spec error_terminate(cowboy_req:req(), #state{}, error | exit | throw, any(), atom())
+ -> no_return().
error_terminate(Req, #state{handler=Handler, handler_state=HandlerState},
Class, Reason, Callback) ->
Stacktrace = erlang:get_stacktrace(),
cowboy_req:maybe_reply(Stacktrace, Req),
cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler),
- erlang:Class([
+ State = cowboy_handler:format_status(terminate, get(), Req, HandlerState, Handler),
+ erlang:exit([
+ {class, Class},
{reason, Reason},
{mfa, {Handler, Callback, 2}},
{stacktrace, Stacktrace},
{req, cowboy_req:to_list(Req)},
- {state, HandlerState}
+ {state, State}
]).
terminate(Req, #state{env=Env, handler=Handler, handler_state=HandlerState}) ->
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
index b09051eef..b2de5eec1 100644
--- a/src/cowboy_router.erl
+++ b/src/cowboy_router.erl
@@ -157,7 +157,7 @@ compile_brackets_split(<< C, Rest/binary >>, Acc, N) ->
compile_brackets_split(Rest, << Acc/binary, C >>, N).
-spec execute(Req, Env)
- -> {ok, Req, Env} | {halt, Req}
+ -> {ok, Req, Env} | {halt, Req, Env}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
execute(Req, Env) ->
{_, Dispatch} = lists:keyfind(dispatch, 1, Env),
@@ -168,11 +168,11 @@ execute(Req, Env) ->
Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req),
{ok, Req2, [{handler, Handler}, {handler_opts, HandlerOpts}|Env]};
{error, notfound, host} ->
- {halt, cowboy_req:reply(400, Req)};
+ {halt, cowboy_req:reply(400, Req), Env};
{error, badrequest, path} ->
- {halt, cowboy_req:reply(400, Req)};
+ {halt, cowboy_req:reply(400, Req), Env};
{error, notfound, path} ->
- {halt, cowboy_req:reply(404, Req)}
+ {halt, cowboy_req:reply(404, Req), Env}
end.
%% Internal.
diff --git a/src/cowboy_spdy.erl b/src/cowboy_spdy.erl
index 3057cca2b..d661e29e1 100644
--- a/src/cowboy_spdy.erl
+++ b/src/cowboy_spdy.erl
@@ -22,10 +22,11 @@
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
+-export([format_status/2]).
%% Internal request process.
--export([request_init/10]).
--export([resume/5]).
+-export([request_init/12]).
+-export([request_terminate/2]).
-export([reply/4]).
-export([stream_reply/3]).
-export([stream_data/2]).
@@ -54,11 +55,13 @@
-record(state, {
parent = undefined :: pid(),
+ dbg :: [sys:dbg_opt()],
socket,
transport,
buffer = <<>> :: binary(),
middlewares,
env,
+ debug,
onresponse,
peer,
zdef,
@@ -69,7 +72,8 @@
-type opts() :: [{env, cowboy_middleware:env()}
| {middlewares, [module()]}
- | {onresponse, cowboy:onresponse_fun()}].
+ | {onresponse, cowboy:onresponse_fun()}
+ | {debug, list()}].
-export_type([opts/0]).
%% API.
@@ -95,15 +99,16 @@ init(Parent, Ref, Socket, Transport, Opts) ->
{ok, Peer} = Transport:peername(Socket),
Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]),
Env = [{listener, Ref}|get_value(env, Opts, [])],
+ Debug = get_value(debug, Opts, []),
OnResponse = get_value(onresponse, Opts, undefined),
Zdef = cow_spdy:deflate_init(),
Zinf = cow_spdy:inflate_init(),
ok = ranch:accept_ack(Ref),
loop(#state{parent=Parent, socket=Socket, transport=Transport,
- middlewares=Middlewares, env=Env,
+ middlewares=Middlewares, env=Env, debug=Debug,
onresponse=OnResponse, peer=Peer, zdef=Zdef, zinf=Zinf}).
-loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
+loop(State=#state{parent=Parent, dbg=Dbg, socket=Socket, transport=Transport,
buffer=Buffer, children=Children}) ->
{OK, Closed, Error} = Transport:messages(),
Transport:setopts(Socket, [{active, once}]),
@@ -192,7 +197,7 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
%% @todo Report the error if any.
loop(delete_child(Pid, State));
{system, From, Request} ->
- sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
+ sys:handle_system_msg(Request, From, Parent, ?MODULE, Dbg, State);
%% Calls from the supervisor module.
{'$gen_call', {To, Tag}, which_children} ->
Workers = [{?MODULE, Pid, worker, [?MODULE]}
@@ -214,8 +219,8 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
end.
-spec system_continue(_, _, #state{}) -> ok.
-system_continue(_, _, State) ->
- loop(State).
+system_continue(_, Dbg, State) ->
+ loop(State#state{dbg=Dbg}).
-spec system_terminate(any(), _, _, _) -> no_return().
system_terminate(Reason, _, _, _) ->
@@ -225,6 +230,21 @@ system_terminate(Reason, _, _, _) ->
system_code_change(Misc, _, _, _) ->
{ok, Misc}.
+-spec format_status(normal, [[{term(), term()}] | running | suspended |
+ pid() | [sys:dbg_opt()] | #state{}]) -> any().
+format_status(_Opt, [_PDict, SysState, Parent, Dbg, Misc]) ->
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"State", state_to_list(Misc)}
+ ],
+ [{header, "Cowboy SPDY"}, {data, Data}].
+
+state_to_list(State) ->
+ lists:zip(record_info(fields, state), tl(tuple_to_list(State))).
+
parse_frame(State=#state{zinf=Zinf}, Data) ->
case cow_spdy:split(Data) of
{true, Frame, Rest} ->
@@ -254,12 +274,12 @@ handle_frame(State, {syn_stream, StreamID, AssocToStreamID,
%% Erlang does not allow us to control the priority of processes
%% so we ignore that value entirely.
handle_frame(State=#state{middlewares=Middlewares, env=Env,
- onresponse=OnResponse, peer=Peer},
+ debug=Debug, onresponse=OnResponse, peer=Peer},
{syn_stream, StreamID, _, IsFin, _, _,
Method, _, Host, Path, Version, Headers}) ->
- Pid = spawn_link(?MODULE, request_init, [
+ Pid = proc_lib:spawn_link(?MODULE, request_init, [
{self(), StreamID}, Peer, OnResponse,
- Env, Middlewares, Method, Host, Path, Version, Headers
+ Env, self(), Debug, Middlewares, Method, Host, Path, Version, Headers
]),
new_child(State, StreamID, Pid, IsFin);
%% RST_STREAM.
@@ -382,46 +402,26 @@ delete_child(Pid, State=#state{children=Children}) ->
%% Request process.
-spec request_init(socket(), {inet:ip_address(), inet:port_number()},
- cowboy:onresponse_fun(), cowboy_middleware:env(), [module()],
- binary(), binary(), binary(), binary(), [{binary(), binary()}])
+ cowboy:onresponse_fun(), cowboy_middleware:env(), pid(), list(),
+ [module()], binary(), binary(), binary(), binary(),
+ [{binary(), binary()}])
-> ok.
request_init(FakeSocket, Peer, OnResponse,
- Env, Middlewares, Method, Host, Path, Version, Headers) ->
+ Env, Parent, Debug, Middlewares, Method, Host, Path, Version, Headers) ->
{Host2, Port} = cow_http:parse_fullhost(Host),
{Path2, Qs} = cow_http:parse_fullpath(Path),
Version2 = cow_http:parse_version(Version),
Req = cowboy_req:new(FakeSocket, ?MODULE, Peer,
Method, Path2, Qs, Version2, Headers,
Host2, Port, <<>>, true, false, OnResponse),
- execute(Req, Env, Middlewares).
-
--spec execute(cowboy_req:req(), cowboy_middleware:env(), [module()])
- -> ok.
-execute(Req, _, []) ->
- cowboy_req:ensure_response(Req, 204);
-execute(Req, Env, [Middleware|Tail]) ->
- case Middleware:execute(Req, Env) of
- {ok, Req2, Env2} ->
- execute(Req2, Env2, Tail);
- {suspend, Module, Function, Args} ->
- erlang:hibernate(?MODULE, resume,
- [Env, Tail, Module, Function, Args]);
- {halt, Req2} ->
- cowboy_req:ensure_response(Req2, 204)
- end.
-
--spec resume(cowboy_middleware:env(), [module()],
- module(), module(), [any()]) -> ok.
-resume(Env, Tail, Module, Function, Args) ->
- case apply(Module, Function, Args) of
- {ok, Req2, Env2} ->
- execute(Req2, Env2, Tail);
- {suspend, Module2, Function2, Args2} ->
- erlang:hibernate(?MODULE, resume,
- [Env, Tail, Module2, Function2, Args2]);
- {halt, Req2} ->
- cowboy_req:ensure_response(Req2, 204)
- end.
+ Dbg = sys:debug_options(Debug),
+ Env2 = [{parent, Parent}, {dbg, Dbg} | Env],
+ Halt = {?MODULE, request_terminate, []},
+ cowboy_middleware:execute(Req, Env2, Halt, Middlewares).
+
+-spec request_terminate(cowboy_req:req(), cowboy_middleware:env()) -> ok.
+request_terminate(Req, _Env) ->
+ cowboy_req:ensure_response(Req, 204).
%% Reply functions used by cowboy_req.
diff --git a/src/cowboy_sub_protocol.erl b/src/cowboy_sub_protocol.erl
index f177263e7..1babd4af6 100644
--- a/src/cowboy_sub_protocol.erl
+++ b/src/cowboy_sub_protocol.erl
@@ -16,5 +16,8 @@
-module(cowboy_sub_protocol).
-callback upgrade(Req, Env, module(), any(), timeout(), run | hibernate)
- -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {halt, Req}
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req, Env, any()}
+ | {halt, Req, Env}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
diff --git a/src/cowboy_system.erl b/src/cowboy_system.erl
new file mode 100644
index 000000000..da68b6ea2
--- /dev/null
+++ b/src/cowboy_system.erl
@@ -0,0 +1,37 @@
+%% Copyright (c) 2014, James Fish
+%%
+%% 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(cowboy_system).
+
+-type replace_state() ::
+ fun(({module(), cowboy_req:req() | undefined, any()})
+ -> {module(), cowboy_req:req() | undefined, any()}).
+-export_type([replace_state/0]).
+
+-callback continue(cowboy_req:req(), cowboy_middleware:env(), any())
+ -> {ok, cowboy_req:req(), cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), term()}, any(), module(), cowboy_req:req(),
+ cowboy_middleware:env(), any()}
+ | {halt, cowboy_req:req(), cowboy_middleware:env()}.
+-callback terminate(any(), cowboy_req:req(), cowboy_middleware:env(), any())
+ -> no_return().
+-callback code_change(cowboy_req:req(), any(), module(), any(), any())
+ -> {ok, cowboy_req:req(), any()}.
+%% @todo optional -callback get_state(cowboy_req:req(), any())
+%% -> {ok, {module(), cowboy_req:req(), any()}}.
+%% @todo optional -callback replace_state(replace_state(), cowboy_req:req(), any())
+%% -> {ok, {module(), cowboy_req:req(), any()}, cowboy_req:req(), any()}.
+%% @todo optional -callback format_status(normal, [[{any(), any()}] |
+%% running | suspended | cowboy_req:req() | cowboy_middleware:env() | any()])
+%% -> any().
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index c700af90a..20419f4cc 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -16,9 +16,16 @@
%% It also supports RFC6455, the proposed standard for Websocket.
-module(cowboy_websocket).
-behaviour(cowboy_sub_protocol).
+-behaviour(cowboy_system).
-export([upgrade/6]).
-export([handler_loop/4]).
+-export([continue/3]).
+-export([terminate/4]).
+-export([code_change/5]).
+-export([get_state/2]).
+-export([replace_state/3]).
+-export([format_status/2]).
-type close_code() :: 1000..4999.
-export_type([close_code/0]).
@@ -36,7 +43,8 @@
-type terminate_reason() :: normal | shutdown | timeout
| remote | {remote, close_code(), binary()}
| {error, badencoding | badframe | closed | atom()}
- | {crash, error | exit | throw, any()}.
+ | {crash, error | exit | throw, any()}
+ | {shutdown, any()}.
-callback init(Req, any())
-> {ok | module(), Req, any()}
@@ -62,6 +70,7 @@
-record(state, {
env :: cowboy_middleware:env(),
+ parent :: pid(),
socket = undefined :: inet:socket(),
transport = undefined :: module(),
handler :: module(),
@@ -77,15 +86,22 @@
deflate_state :: undefined | port()
}).
+-type misc() :: {handler_loop, #state{}, any(), binary()}
+ | {websocket_payload_loop, #state{}, any(), {opcode(), non_neg_integer(),
+ mask_key(), binary(), non_neg_integer(), rsv()}}.
+
-spec upgrade(Req, Env, module(), any(), timeout(), run | hibernate)
- -> {ok, Req, Env} | {suspend, module(), atom(), [any()]}
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req, Env, misc()}
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, Timeout, Hibernate) ->
{_, Ref} = lists:keyfind(listener, 1, Env),
ranch:remove_connection(Ref),
+ {_, Parent} = lists:keyfind(parent, 1, Env),
[Socket, Transport] = cowboy_req:get([socket, transport], Req),
- State = #state{env=Env, socket=Socket, transport=Transport, handler=Handler,
- timeout=Timeout, hibernate=Hibernate =:= hibernate},
+ State = #state{env=Env, parent=Parent, socket=Socket, transport=Transport,
+ handler=Handler, timeout=Timeout, hibernate=Hibernate =:= hibernate},
try websocket_upgrade(State, Req) of
{ok, State2, Req2} ->
websocket_handshake(State2, Req2, HandlerState)
@@ -147,6 +163,8 @@ websocket_extensions(State, Req) ->
-spec websocket_handshake(#state{}, Req, any())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
websocket_handshake(State=#state{
transport=Transport, key=Key, deflate_frame=DeflateFrame},
@@ -168,6 +186,8 @@ websocket_handshake(State=#state{
-spec handler_before_loop(#state{}, Req, any(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
handler_before_loop(State=#state{
socket=Socket, transport=Transport, hibernate=true},
@@ -192,9 +212,11 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
-spec handler_loop(#state{}, Req, any(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
- timeout_ref=TRef}, Req, HandlerState, SoFar) ->
+ timeout_ref=TRef, env=Env, parent=Parent}, Req, HandlerState, SoFar) ->
receive
{OK, Socket, Data} ->
State2 = handler_loop_timeout(State),
@@ -208,6 +230,11 @@ handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
websocket_close(State, Req, HandlerState, timeout);
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
handler_loop(State, Req, HandlerState, SoFar);
+ {'EXIT', Parent, Reason} ->
+ websocket_close(State, Req, HandlerState, {shutdown, Reason});
+ {system, From, Msg} ->
+ Misc = {handler_loop, State, HandlerState, SoFar},
+ {system, From, Msg, ?MODULE, Req, Env, Misc};
Message ->
handler_call(State, Req, HandlerState,
SoFar, websocket_info, Message, fun handler_before_loop/4)
@@ -219,6 +246,8 @@ handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
-spec websocket_data(#state{}, Req, any(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
%% RSV bits MUST be 0 unless an extension is negotiated
%% that defines meanings for non-zero values.
@@ -291,6 +320,8 @@ websocket_data(State, Req, HandlerState, Data) ->
opcode(), non_neg_integer(), mask_key(), binary(), rsv(), 0 | 1)
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
%% The opcode is only included in the first frame fragment.
websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
@@ -317,6 +348,8 @@ websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, Rsv, 1) ->
binary(), rsv())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
%% Close control frames with a payload MUST contain a valid close code.
websocket_payload(State, Req, HandlerState,
@@ -496,9 +529,11 @@ is_utf8(_) ->
non_neg_integer(), rsv())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
- messages={OK, Closed, Error}, timeout_ref=TRef},
+ messages={OK, Closed, Error}, timeout_ref=TRef, env=Env, parent=Parent},
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv) ->
Transport:setopts(Socket, [{active, once}]),
receive
@@ -515,6 +550,12 @@ websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
websocket_payload_loop(State, Req, HandlerState,
Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv);
+ {'EXIT', Parent, Reason} ->
+ websocket_close(State, Req, HandlerState, {shutdown, Reason});
+ {system, From, Msg} ->
+ Args = {Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv},
+ Misc = {websocket_payload_loop, State, HandlerState, Args},
+ {system, From, Msg, ?MODULE, Req, Env, Misc};
Message ->
handler_call(State, Req, HandlerState,
<<>>, websocket_info, Message,
@@ -527,6 +568,8 @@ websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
%% Continuation frame.
websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}},
@@ -567,6 +610,8 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
-spec handler_call(#state{}, Req, any(), binary(), atom(), any(), fun())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), Req,
+ cowboy_middleware:env(), misc()}
when Req::cowboy_req:req().
handler_call(State=#state{handler=Handler}, Req, HandlerState,
RemainingData, Callback, Message, NextState) ->
@@ -620,13 +665,15 @@ handler_call(State=#state{handler=Handler}, Req, HandlerState,
websocket_close(State, Req2, HandlerState2, shutdown)
catch Class:Reason ->
_ = websocket_close(State, Req, HandlerState, {crash, Class, Reason}),
- erlang:Class([
+ State = cowboy_handler:format_status(terminate, get(), Req, HandlerState, Handler),
+ erlang:exit([
+ {class, Class},
{reason, Reason},
{mfa, {Handler, Callback, 3}},
{stacktrace, erlang:get_stacktrace()},
{msg, Message},
{req, cowboy_req:to_list(Req)},
- {state, HandlerState}
+ {state, State}
])
end.
@@ -718,6 +765,8 @@ websocket_close(State=#state{socket=Socket, transport=Transport},
case Reason of
Normal when Normal =:= shutdown; Normal =:= timeout ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>);
+ {shutdown, _} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>);
{error, badframe} ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>);
{error, badencoding} ->
@@ -738,3 +787,73 @@ handler_terminate(#state{env=Env, handler=Handler},
Req, HandlerState, Reason) ->
cowboy_handler:terminate(Reason, Req, HandlerState, Handler),
{ok, Req, [{result, closed}|Env]}.
+
+-spec continue(cowboy_req:req(), cowboy_middleware:env(), misc())
+ -> {ok, cowboy_req:req(), cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ | {system, {pid(), any()}, any(), module(), cowboy_req:req(),
+ cowboy_middleware:env(), misc()}.
+continue(Req, Env, {handler_loop, State, HandlerState, SoFar}) ->
+ handler_loop(State#state{env=Env}, Req, HandlerState, SoFar);
+continue(Req, Env, {websocket_payload_loop, State, HandlerState,
+ {Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv}}) ->
+ websocket_payload_loop(State#state{env=Env}, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv).
+
+-spec terminate(any(), cowboy_req:req(), cowboy_middleware:env(), misc())
+ -> no_return().
+terminate(Reason, Req, Env, {_Fun, State, HandlerState, _Args}) ->
+ _ = websocket_close(State#state{env=Env}, Req, HandlerState, {shutdown, Reason}),
+ exit(Reason).
+
+-spec code_change(cowboy_req:req(), misc(), module(), any(), any())
+ -> {ok, cowboy_req:req(), misc()}.
+code_change(Req, {Fun, State=#state{handler=Handler}, HandlerState, Args},
+ Module, OldVsn, Extra) ->
+ {ok, Req2, HandlerState2} = cowboy_handler:code_change(OldVsn, Req, HandlerState,
+ Module, Extra, Handler),
+ {ok, Req2, {Fun, State, HandlerState2, Args}}.
+
+-spec get_state(cowboy_req:req(), misc())
+ -> {ok, {module(), cowboy_req:req(), any()}}.
+get_state(Req, {_Fun, #state{handler=Handler}, HandlerState, _Args}) ->
+ {ok, {Handler, Req, HandlerState}}.
+
+-spec replace_state(cowboy_system:replace_state(), cowboy_req:req(), misc())
+ -> {ok, {module(), cowboy_req:req(), any()}, cowboy_req:req(), misc()}.
+replace_state(Replace, Req,
+ {Fun, State=#state{handler=Handler}, HandlerState, Args}) ->
+ {Handler, Req2, HandlerState2} = Result = Replace({Handler, Req, HandlerState}),
+ {ok, Result, Req2, {Fun, State, HandlerState2, Args}}.
+
+-spec format_status(normal, [[{term(), term()}] | running | suspended |
+ cowboy_req:req() | cowboy_middleware:env() | misc()])
+ -> maybe_improper_list().
+format_status(Opt, [PDict, SysState, Req, Env,
+ {Fun, State=#state{handler=Handler}, HandlerState, Args}]) ->
+ {_, Parent} = lists:keyfind(parent, 1, Env),
+ {_, Dbg} = lists:keyfind(dbg, 1, Env),
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"Request", cowboy_req:to_list(Req)},
+ {"Environment", Env},
+ {"Websocket state", state_to_list(State)},
+ {"Websocket loop function", Fun},
+ {"Websocket loop data", format_args(Fun, Args)},
+ {"Handler", Handler}
+ ],
+ HandlerStatus = cowboy_handler:format_status(Opt, PDict, Req, HandlerState, Handler),
+ [{header, "Cowboy websocket handler"}, {data, Data} | HandlerStatus].
+
+state_to_list(State) ->
+ lists:zip(record_info(fields, state), tl(tuple_to_list(State))).
+
+format_args(handler_loop, SoFar) ->
+ [{so_far, SoFar}];
+format_args(websocket_payload_loop,
+ {Opcode, Len, MaskKey, Unmasked, UnmaskedLen, Rsv}) ->
+ [{opcode, Opcode}, {length, Len}, {mask_key, MaskKey},
+ {unmasked_length, UnmaskedLen}, {unmasked, Unmasked}, {rsv, Rsv}].
diff --git a/test/handlers/loop_handler_system_h.erl b/test/handlers/loop_handler_system_h.erl
new file mode 100644
index 000000000..803091f70
--- /dev/null
+++ b/test/handlers/loop_handler_system_h.erl
@@ -0,0 +1,31 @@
+%% This module implements a loop handler that sends
+%% a message to loop_system_tester and then waits
+%% for a timeout. That process will attempt to use
+%% system messages on the process. The result of
+%% the request will be a 204 reply.
+
+-module(loop_handler_system_h).
+
+-export([init/2]).
+-export([info/3]).
+-export([terminate/3]).
+-export([code_change/4]).
+-export([format_status/2]).
+
+init(Req, _) ->
+ loop_tester_system ! {loop_handler_system_h, self()},
+ {cowboy_loop, Req, 0, 500}.
+
+info(_Info, Req, State) ->
+ {shutdown, cowboy_req:reply(500, Req), State}.
+
+terminate(_, _, _) ->
+ ok.
+
+code_change(_OldVsn, Req, _State, State2) ->
+ {ok, Req, State2}.
+
+format_status(normal, [_PDict, _Req, State]) ->
+ [{data, [{"Handler state", {formatted, State}}]}];
+format_status(terminate, [_PDict, _Req, State]) ->
+ {formatted, State}.
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 3783b6e42..5828522f6 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -195,7 +195,8 @@ init_dispatch(Config) ->
{"/rest_expires_binary", rest_expires_binary, []},
{"/rest_empty_resource", rest_empty_resource, []},
{"/loop_stream_recv", http_loop_stream_recv, []},
- {"/", http_handler, []}
+ {"/", http_handler, []},
+ {"/system", http_system, []}
]}
]).
@@ -1027,3 +1028,27 @@ te_identity(Config) ->
{response, nofin, 200, _} = gun:await(ConnPid, Ref),
{ok, Body} = gun:await_body(ConnPid, Ref),
ok.
+
+system(Config) ->
+ ConnPid = gun_open(Config),
+ register(http_system_tester, self()),
+ Ref = gun:get(ConnPid, "/system", [{<<"connection">>, <<"keep-alive">>}]),
+ Pid = receive {http_system, P} -> P after 500 -> exit(timeout) end,
+ unregister(http_system_tester),
+ ok = sys:suspend(Pid),
+ ok = sys:change_code(Pid, undefined, undefined, undefined),
+ {cowboy_protocol, undefined, State} = sys:get_state(Pid),
+ Replace = fun({Mod, undefined, State2}) -> {Mod, undefined, State2} end,
+ {cowboy_protocol, undefined, State} = sys:replace_state(Pid, Replace),
+ %% Not allowed to change module from cowboy_protocol.
+ BadReplace = fun({_Mod2, undefined, State3}) -> {undefined, undefined, State3} end,
+ {'EXIT', {{callback_failed, _, _}, _}} = (catch sys:replace_state(Pid, BadReplace)),
+ {cowboy_protocol, undefined, _} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, suspended, _Parent, [], Misc] = Status,
+ [{header, "Cowboy protocol"}, {data, Data}] = Misc,
+ {_, <<>>} = lists:keyfind("Protocol buffer", 1, Data),
+ {_, [{_, _} | _]} = lists:keyfind("Protocol state", 1, Data),
+ ok = sys:resume(Pid),
+ {response, nofin, 200, _} = gun:await(ConnPid, Ref),
+ ok.
diff --git a/test/http_SUITE_data/http_system.erl b/test/http_SUITE_data/http_system.erl
new file mode 100644
index 000000000..1e100ec34
--- /dev/null
+++ b/test/http_SUITE_data/http_system.erl
@@ -0,0 +1,12 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_system).
+
+-export([init/2]).
+
+init(Req, Opts) ->
+ http_system_tester ! {http_system, self()},
+ timer:sleep(100),
+ Headers = proplists:get_value(headers, Opts, []),
+ Body = proplists:get_value(body, Opts, "http_system"),
+ {ok, cowboy_req:reply(200, Headers, Body, Req), Opts}.
diff --git a/test/loop_handler_SUITE.erl b/test/loop_handler_SUITE.erl
index 5f69490df..765929e27 100644
--- a/test/loop_handler_SUITE.erl
+++ b/test/loop_handler_SUITE.erl
@@ -39,7 +39,8 @@ init_dispatch(_) ->
cowboy_router:compile([{'_', [
{"/long_polling", long_polling_h, []},
{"/loop_body", loop_handler_body_h, []},
- {"/loop_timeout", loop_handler_timeout_h, []}
+ {"/loop_timeout", loop_handler_timeout_h, []},
+ {"/loop_system", loop_handler_system_h, []}
]}]).
%% Tests.
@@ -85,3 +86,25 @@ loop_timeout(Config) ->
Ref = gun:get(ConnPid, "/loop_timeout"),
{response, fin, 204, _} = gun:await(ConnPid, Ref),
ok.
+
+loop_system(Config) ->
+ doc("Ensure that the loop handler can handle system messages"),
+ ConnPid = gun_open(Config),
+ register(loop_tester_system, self()),
+ Ref = gun:get(ConnPid, "/loop_system"),
+ Pid = receive {loop_handler_system_h, P} -> P after 500 -> exit(timeout) end,
+ unregister(loop_tester_system),
+ ok = sys:suspend(Pid),
+ %% code_change sets state to extra (1)
+ ok = sys:change_code(Pid, loop_handler_system_h, undefined, 1),
+ ok = sys:resume(Pid),
+ {loop_handler_system_h, _Req, 1} = sys:get_state(Pid),
+ Replace = fun({Mod, Req2, 1}) -> {Mod, Req2, 2} end,
+ {loop_handler_system_h, _Req3, 2} = sys:replace_state(Pid, Replace),
+ {loop_handler_system_h, _Req4, 2} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, running, _Parent, [], Misc] = Status,
+ [{header, "Cowboy loop handler"},
+ {data, _}, {data, [{"Handler state", {formatted, 2}}]}] = Misc,
+ {response, fin, 204, _} = gun:await(ConnPid, Ref),
+ ok.
diff --git a/test/system_SUITE.erl b/test/system_SUITE.erl
new file mode 100644
index 000000000..9cb177365
--- /dev/null
+++ b/test/system_SUITE.erl
@@ -0,0 +1,131 @@
+%% Copyright (c) 2014, James Fish
+%%
+%% 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(system_SUITE).
+-compile(export_all).
+
+-import(cowboy_test, [config/2]).
+-import(cowboy_test, [doc/1]).
+-import(cowboy_test, [gun_open/1]).
+
+%% ct.
+
+all() ->
+ cowboy_test:common_all().
+
+groups() ->
+ cowboy_test:common_groups(cowboy_test:all(?MODULE)).
+
+init_per_group(Name, Config) ->
+ cowboy_test:init_common_groups(Name, Config, ?MODULE).
+
+end_per_group(Name, _) ->
+ cowboy:stop_listener(Name).
+
+%% Dispatch configuration.
+
+init_dispatch(_) ->
+ cowboy_router:compile([{'_', [
+ {"/default", system_default_h, system_default_sp},
+ {"/default/handler", system_default_handler_h, system_full_sp},
+ {"/full", system_full_h, system_full_sp}
+ ]}]).
+
+%% Tests.
+
+default(Config) ->
+ doc("Ensure that a minimal sub_protocol or middleware handles system messages"),
+ ConnPid = gun_open(Config),
+ register(system_default_tester, self()),
+ Ref = gun:get(ConnPid, "/default"),
+ Pid = receive {system_default_h, P} -> P after 500 -> exit(timeout) end,
+ unregister(system_default_tester),
+ ok = sys:suspend(Pid),
+ {system_default_sp, _Req, undefined} = sys:get_state(Pid),
+ %% code_change will do nothing to sub_protocol
+ ok = sys:change_code(Pid, system_default_sp, undefined, code_change),
+ ok = sys:resume(Pid),
+ Replace = fun({Mod, Req2, undefined}) -> {Mod, Req2, new_state} end,
+ {system_default_sp, _Req3, new_state} = sys:replace_state(Pid, Replace),
+ {system_default_sp, _Req5, new_state} = sys:get_state(Pid),
+ %% Not allowed to change the middleware module
+ BadReplace = fun({_Mod2, Req4, State}) -> {undefined, Req4, State} end,
+ {'EXIT', {{callback_failed, _, _}, _}} = (catch sys:replace_state(Pid, BadReplace)),
+ {system_default_sp, _Req5, new_state} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, running, _Parent, [], Misc] = Status,
+ [{header, "Cowboy middleware"}, {data, Data}] = Misc,
+ {_, system_default_sp} = lists:keyfind("Middleware", 1, Data),
+ {_, new_state} = lists:keyfind("Middleware state", 1, Data),
+ {response, fin, 204, _} = gun:await(ConnPid, Ref),
+ ok.
+
+default_handler(Config) ->
+ doc("Ensure that a sub_protocol with default handler can handle system messages"),
+ ConnPid = gun_open(Config),
+ register(system_default_handler_tester, self()),
+ Ref = gun:get(ConnPid, "/default/handler"),
+ Pid = receive {system_default_handler_h, P} -> P after 500 -> exit(timeout) end,
+ unregister(system_default_handler_tester),
+ ok = sys:suspend(Pid),
+ {system_default_handler_h, _Req, undefined} = sys:get_state(Pid),
+ %% code_change will do nothing to sub_protocol (or handler)
+ ok = sys:change_code(Pid, system_default_handler_h, undefined, code_change),
+ ok = sys:resume(Pid),
+ Replace = fun({system_default_handler_h, Req2, undefined}) ->
+ {system_default_handler_h, Req2, new_state}
+ end,
+ {system_default_handler_h, _Req3, new_state} = sys:replace_state(Pid, Replace),
+ {system_default_handler_h, _Req4, new_state} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, running, _Parent, [], Misc] = Status,
+ [{header, "Cowboy system test"}, {data, Data},
+ {data, Data2}] = Misc,
+ {_, system_default_handler_h} = lists:keyfind("Handler", 1, Data),
+ %% Default handler state format
+ {_, new_state} = lists:keyfind("Handler state", 1, Data2),
+ {response, fin, 204, _} = gun:await(ConnPid, Ref),
+ ok.
+
+full(Config) ->
+ doc("Ensure that a sub_protocol with fully implemented handler can handle system messages"),
+ ConnPid = gun_open(Config),
+ register(system_full_tester, self()),
+ Ref = gun:get(ConnPid, "/full"),
+ Pid = receive {system_full_h, P} -> P after 500 -> exit(timeout) end,
+ unregister(system_full_tester),
+ ok = sys:suspend(Pid),
+ {system_full_h, _Req, undefined} = sys:get_state(Pid),
+ %% code_change will change handler state to extra (code_change) if
+ %% handler module.
+ ok = sys:change_code(Pid, system_full_h, undefined, code_change),
+ {system_full_h, _Req, code_change} = sys:get_state(Pid),
+ %% But not if the module is not the handler.
+ ok = sys:change_code(Pid, system_full_sp, undefined, code_change2),
+ {system_full_h, _Req, code_change} = sys:get_state(Pid),
+ ok = sys:resume(Pid),
+ Replace = fun({system_full_h, Req2, code_change}) ->
+ {system_full_h, Req2, new_state}
+ end,
+ {system_full_h, _Req3, new_state} = sys:replace_state(Pid, Replace),
+ {system_full_h, _Req4, new_state} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, running, _Parent, [], Misc] = Status,
+ [{header, "Cowboy system test"}, {data, Data},
+ {data, Data2}] = Misc,
+ {_, system_full_h} = lists:keyfind("Handler", 1, Data),
+ %% Handler state is formatted
+ {_, {formatted, new_state}} = lists:keyfind("Handler state", 1, Data2),
+ {response, fin, 204, _} = gun:await(ConnPid, Ref),
+ ok.
diff --git a/test/system_SUITE_data/system_default_h.erl b/test/system_SUITE_data/system_default_h.erl
new file mode 100644
index 000000000..8c8a99dc1
--- /dev/null
+++ b/test/system_SUITE_data/system_default_h.erl
@@ -0,0 +1,7 @@
+-module(system_default_h).
+
+-export([init/2]).
+
+init(Req, Upgrade) ->
+ system_default_tester ! {?MODULE, self()},
+ {Upgrade, Req, undefined, 500}.
diff --git a/test/system_SUITE_data/system_default_handler_h.erl b/test/system_SUITE_data/system_default_handler_h.erl
new file mode 100644
index 000000000..1e7536fed
--- /dev/null
+++ b/test/system_SUITE_data/system_default_handler_h.erl
@@ -0,0 +1,7 @@
+-module(system_default_handler_h).
+
+-export([init/2]).
+
+init(Req, Upgrade) ->
+ system_default_handler_tester ! {?MODULE, self()},
+ {Upgrade, Req, undefined, 500}.
diff --git a/test/system_SUITE_data/system_default_sp.erl b/test/system_SUITE_data/system_default_sp.erl
new file mode 100644
index 000000000..286eccd21
--- /dev/null
+++ b/test/system_SUITE_data/system_default_sp.erl
@@ -0,0 +1,26 @@
+-module(system_default_sp).
+
+-export([upgrade/6]).
+-export([continue/3]).
+-export([terminate/4]).
+-export([code_change/5]).
+
+upgrade(Req, Env, _, State, _, _) ->
+ loop(Req, Env, State).
+
+loop(Req, Env, State) ->
+ receive
+ {system, From, Msg} ->
+ {system, From, Msg, ?MODULE, Req, Env, State}
+ after 500 ->
+ {ok, Req, [{result, ok} | Env]}
+ end.
+
+continue(Req, Env, State) ->
+ loop(Req, Env, State).
+
+terminate(Reason, _Req, _Env, _state) ->
+ exit(Reason).
+
+code_change(Req, State, _Module, _OldVsn, _Extra) ->
+ {ok, Req, State}.
diff --git a/test/system_SUITE_data/system_full_h.erl b/test/system_SUITE_data/system_full_h.erl
new file mode 100644
index 000000000..2d0f223ca
--- /dev/null
+++ b/test/system_SUITE_data/system_full_h.erl
@@ -0,0 +1,21 @@
+-module(system_full_h).
+
+-export([init/2]).
+-export([terminate/3]).
+-export([code_change/4]).
+-export([format_status/2]).
+
+init(Req, Upgrade) ->
+ system_full_tester ! {?MODULE, self()},
+ {Upgrade, Req, undefined, 500}.
+
+terminate(_Reason, _Req, _State) ->
+ ok.
+
+code_change(_OldVsn, Req, _State, Extra) ->
+ {ok, Req, Extra}.
+
+format_status(normal, [_PDict, _Req, State]) ->
+ [{data, [{"Handler state", {formatted, State}}]}];
+format_status(terminate, [_PDict, _Req, State]) ->
+ State.
diff --git a/test/system_SUITE_data/system_full_sp.erl b/test/system_SUITE_data/system_full_sp.erl
new file mode 100644
index 000000000..cd50fbefc
--- /dev/null
+++ b/test/system_SUITE_data/system_full_sp.erl
@@ -0,0 +1,55 @@
+-module(system_full_sp).
+
+-export([upgrade/6]).
+-export([continue/3]).
+-export([terminate/4]).
+-export([code_change/5]).
+-export([get_state/2]).
+-export([replace_state/3]).
+-export([format_status/2]).
+
+upgrade(Req, Env, Handler, State, _, _) ->
+ loop(Req, Env, {Handler, State}).
+
+loop(Req, Env, {Handler, State}) ->
+ receive
+ {system, From, Msg} ->
+ {system, From, Msg, ?MODULE, Req, Env, {Handler, State}}
+ after 500 ->
+ Result = cowboy_handler:terminate(timeout, Req, State, Handler),
+ {ok, Req, [{result, Result} | Env]}
+ end.
+
+continue(Req, Env, Misc) ->
+ loop(Req, Env, Misc).
+
+terminate(Reason, Req, _Env, {Handler, State}) ->
+ _ = cowboy_handler:terminate({shutdown, Reason}, Req, State, Handler),
+ exit(Reason).
+
+code_change(Req, {Handler, State}, Module, OldVsn, Extra) ->
+ {ok, Req2, State2} = cowboy_handler:code_change(OldVsn, Req, State,
+ Module, Extra, Handler),
+ {ok, Req2, {Handler, State2}}.
+
+get_state(Req, {Handler, State}) ->
+ {ok, {Handler, Req, State}}.
+
+replace_state(Replace, Req, {Handler, State}) ->
+ {Handler, Req2, State2} = Result = Replace({Handler, Req, State}),
+ {ok, Result, Req2, {Handler, State2}}.
+
+format_status(Opt, [PDict, SysState, Req, Env, {Handler, HandlerState}]) ->
+ Parent = lists:keyfind(parent, 1, Env),
+ {dbg, Dbg} = lists:keyfind(dbg, 1, Env),
+ Log = sys:get_debug(log, Dbg, []),
+ Data = [
+ {"Status", SysState},
+ {"Parent", Parent},
+ {"Logged events", Log},
+ {"Request", cowboy_req:to_list(Req)},
+ {"Environment", Env},
+ {"Handler", Handler}
+ ],
+ HandlerStatus = cowboy_handler:format_status(Opt, PDict, Req, HandlerState, Handler),
+ [{header, "Cowboy system test"}, {data, Data} | HandlerStatus].
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
index e341e10b3..552a7ef37 100644
--- a/test/ws_SUITE.erl
+++ b/test/ws_SUITE.erl
@@ -80,7 +80,8 @@ init_dispatch() ->
{text, <<"won't be received">>}]}
]},
{"/ws_timeout_hibernate", ws_timeout_hibernate, []},
- {"/ws_timeout_cancel", ws_timeout_cancel, []}
+ {"/ws_timeout_cancel", ws_timeout_cancel, []},
+ {"/ws_system", ws_system, []}
]}
]).
@@ -651,6 +652,48 @@ ws_timeout_reset(Config) ->
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
+ws_system(Config) ->
+ %% Websocket sub protocol should handle system messages
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ register(ws_system_tester, self()),
+ ok = gen_tcp:send(Socket, [
+ "GET /ws_system HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 8\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest}
+ = erlang:decode_packet(http, Handshake, []),
+ [Headers, <<>>] = do_decode_headers(
+ erlang:decode_packet(httph, Rest, []), []),
+ {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
+ {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
+ {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ Pid = receive {ws_system, P} -> P after 500 -> exit(timeout) end,
+ unregister(ws_system_tester),
+ ok = sys:suspend(Pid),
+ %% code_change sets state to extra (1)
+ ok = sys:change_code(Pid, ws_system, undefined, 1),
+ ok = sys:resume(Pid),
+ {ws_system, _Req, 1} = sys:get_state(Pid),
+ Replace = fun({Mod, Req2, 1}) -> {Mod, Req2, 2} end,
+ {ws_system, _Req3, 2} = sys:replace_state(Pid, Replace),
+ {ws_system, _Req4, 2} = sys:get_state(Pid),
+ {status, Pid, {module, _Module}, Status} = sys:get_status(Pid),
+ [_PDict, running, _Parent, [], Misc] = Status,
+ [{header, "Cowboy websocket handler"},
+ {data, _}, {data, [{"Handler state", {formatted, 2}}]}] = Misc,
+ {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
%% Internal.
do_decode_headers({ok, http_eoh, Rest}, Acc) ->
diff --git a/test/ws_SUITE_data/ws_system.erl b/test/ws_SUITE_data/ws_system.erl
new file mode 100644
index 000000000..adeae39ca
--- /dev/null
+++ b/test/ws_SUITE_data/ws_system.erl
@@ -0,0 +1,29 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(ws_system).
+
+-export([init/2]).
+-export([websocket_handle/3]).
+-export([websocket_info/3]).
+-export([code_change/4]).
+-export([format_status/2]).
+
+init(Req, _) ->
+ ws_system_tester ! {ws_system, self()},
+ {cowboy_websocket, Req, 0, 1000}.
+
+websocket_handle({text, Data}, Req, State) ->
+ {reply, {text, Data}, Req, State};
+websocket_handle({binary, Data}, Req, State) ->
+ {reply, {binary, Data}, Req, State}.
+
+websocket_info(_Info, Req, State) ->
+ {ok, Req, State}.
+
+code_change(_OldVsn, Req, _State, State2) ->
+ {ok, Req, State2}.
+
+format_status(normal, [_PDict, _Req, State]) ->
+ [{data, [{"Handler state", {formatted, State}}]}];
+format_status(terminate, [_PDict, _Req, State]) ->
+ {formatted, State}.