diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl index 3d64906e..ed914df8 100644 --- a/src/mochiweb_request.erl +++ b/src/mochiweb_request.erl @@ -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). @@ -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). diff --git a/src/mochiweb_socket.erl b/src/mochiweb_socket.erl index 8ac24f5f..36346ef6 100644 --- a/src/mochiweb_socket.erl +++ b/src/mochiweb_socket.erl @@ -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). @@ -180,3 +181,112 @@ exit_if_closed({error, einval = Error}) -> exit({shutdown, Error}); exit_if_closed(Res) -> Res. + +%% @spec is_closed(Socket)) -> true | false | undefined. +%% +%% @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, _, _, <>}]} -> + 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]). diff --git a/test/mochiweb_request_tests.erl b/test/mochiweb_request_tests.erl index 119fc819..13c36fac 100644 --- a/test/mochiweb_request_tests.erl +++ b/test/mochiweb_request_tests.erl @@ -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.