Skip to content

Commit

Permalink
Merge pull request #258 from nickva/add-request-is-closed-function
Browse files Browse the repository at this point in the history
Add mochiweb_request:is_closed/1 function
  • Loading branch information
etrepum committed Aug 31, 2023
2 parents 0733494 + 8b2d844 commit af661f1
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/mochiweb_request.erl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
-export([accepted_content_types/2,
accepts_content_type/2]).

-export([is_closed/1]).

-define(SAVE_QS, mochiweb_request_qs).

-define(SAVE_PATH, mochiweb_request_path).
Expand Down Expand Up @@ -1140,3 +1142,13 @@ accept_header({?MODULE,
undefined -> "*/*";
Value -> Value
end.

%% @spec is_closed(request()) -> true | false | undefined
%% @doc Check if a request connection is closing or already closed. This may be
%% useful when processing long running request callbacks, when the client
%% disconnects after a short timeout. This function works on Linux, NetBSD,
%% OpenBSD, FreeBSD and MacOS. On other operating systems, like Windows for
%% instance, it will return undefined.
is_closed({?MODULE,
[Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
mochiweb_socket:is_closed(Socket).
110 changes: 109 additions & 1 deletion src/mochiweb_socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
-export([listen/4,
accept/1, transport_accept/1, finish_accept/1,
recv/3, send/2, close/1, port/1, peername/1,
setopts/2, getopts/2, type/1, exit_if_closed/1]).
setopts/2, getopts/2, type/1, exit_if_closed/1,
is_closed/1]).

-define(ACCEPT_TIMEOUT, 2000).
-define(SSL_TIMEOUT, 10000).
Expand Down Expand Up @@ -180,3 +181,110 @@ exit_if_closed({error, einval = Error}) ->
exit({shutdown, Error});
exit_if_closed(Res) ->
Res.

%% @doc Check if the socket is closing or already closed. This function works
%% with passive mode sockets on Linux, OpenBSD, NetBSD, FreeBSD and MacOS. On
%% unsupported OS-es, like Windows, it returns undefined.
is_closed(Socket) ->
OsType = os:type(),
case tcp_info_opt(OsType) of
{raw, _, _, _} = InfoOpt ->
case getopts(Socket, [InfoOpt]) of
{ok, [{raw, _, _, <<State:8/native, _/binary>>}]} ->
tcp_is_closed(State, OsType);
{ok, []} ->
undefined;
{error, einval} ->
% Already cleaned up
true;
{error, _} ->
undefined
end;
undefined ->
undefined
end.

% All OS-es have the tcpi_state (uint8) as first member of tcp_info struct

tcp_info_opt({unix, linux}) ->
%% netinet/in.h
%% IPPROTO_TCP = 6
%%
%% netinet/tcp.h
%% #define TCP_INFO 11
%%
{raw, 6, 11, 1};
tcp_info_opt({unix, darwin}) ->
%% netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% netinet/tcp.h
%% #define TCP_CONNECTION_INFO 0x106
%%
{raw, 6, 16#106, 1};
tcp_info_opt({unix, freebsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 32
%%
{raw, 6, 32, 1};
tcp_info_opt({unix, netbsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 9
{raw, 6, 9, 1};
tcp_info_opt({unix, openbsd}) ->
%% sys/netinet/in.h
%% #define IPPROTO_TCP 6
%%
%% sys/netinet/tcp.h
%% #define TCP_INFO 0x09
{raw, 6, 16#09, 1};
tcp_info_opt({_, _}) ->
undefined.

tcp_is_closed(State, {unix, linux}) ->
%% netinet/tcp.h
%% enum
%% {
%% TCP_ESTABLISHED = 1,
%% TCP_SYN_SENT,
%% TCP_SYN_RECV,
%% TCP_FIN_WAIT1,
%% TCP_FIN_WAIT2,
%% TCP_TIME_WAIT,
%% TCP_CLOSE,
%% TCP_CLOSE_WAIT,
%% TCP_LAST_ACK,
%% TCP_LISTEN,
%% TCP_CLOSING
%% }
%%
lists:member(State, [4, 5, 6, 7, 8, 9, 11]);
tcp_is_closed(State, {unix, Type})
when
Type =:= darwin;
Type =:= freebsd;
Type =:= netbsd;
Type =:= openbsd
->
%% tcp_fsm.h states are the same on macos, freebsd, netbsd and openbsd
%%
%% netinet/tcp_fsm.h
%% #define TCPS_CLOSED 0 /* closed */
%% #define TCPS_LISTEN 1 /* listening for connection */
%% #define TCPS_SYN_SENT 2 /* active, have sent syn */
%% #define TCPS_SYN_RECEIVED 3 /* have send and received syn */
%% #define TCPS_ESTABLISHED 4 /* established */
%% #define TCPS_CLOSE_WAIT 5 /* rcvd fin, waiting for close */
%% #define TCPS_FIN_WAIT_1 6 /* have closed, sent fin */
%% #define TCPS_CLOSING 7 /* closed xchd FIN; await FIN ACK */
%% #define TCPS_LAST_ACK 8 /* had fin and close; await FIN ACK */
%% #define TCPS_FIN_WAIT_2 9 /* have closed, fin is acked */
%% #define TCPS_TIME_WAIT 10 /* in 2*msl quiet wait after close */
%%
lists:member(State, [0, 5, 6, 7, 8, 9, 10]).
23 changes: 23 additions & 0 deletions test/mochiweb_request_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,27 @@ should_close_test() ->
(F({1, 0}, [{"Connection", "Keep-Alive"}]))),
ok.

is_closed_test() ->
Headers = mochiweb_headers:make([{"Accept", "text/html"}]),
{ok, Socket} = gen_tcp:listen(0, [{active, false}]),
Req = mochiweb_request:new(Socket, 'GET', "/foo", {1, 1}, Headers),
case is_closed_supported() of
true ->
?assertNot(mochiweb_request:is_closed(Req)),
gen_tcp:close(Socket),
?assert(mochiweb_request:is_closed(Req));
false ->
?assertEqual(undefined, mochiweb_request:is_closed(Req)),
gen_tcp:close(Socket),
?assertEqual(undefined, mochiweb_request:is_closed(Req))
end.

is_closed_supported() ->
case os:type() of
{unix, OsName} ->
lists:member(OsName, [linux, openbsd, netbsd, freebsd, darwin]);
{_, _} ->
false
end.

-endif.

0 comments on commit af661f1

Please sign in to comment.