From 8b2d844d6fcd4c8f4e77d1455781e8ea68747eb7 Mon Sep 17 00:00:00 2001 From: Nick Vatamaniuc Date: Thu, 31 Aug 2023 12:14:41 -0400 Subject: [PATCH] Add mochiweb_request:is_closed/1 function This function can used during long running request callbacks to detect if the client connection is closed. If the request callback periodically streams data back to the client, the act of writting to the client socket will detect if it is closed or not. However, in cases when no data is sent back, and the client times-out and closes the connection, it may be useful to be able to find out early and stop processing the request on the server. It turns out there is no easy way to detect if a passive mode socket is closed in Erlang/OTP [1]. Neither one of inet:monitor/1, inet:info/1, inet:getstat/1 work. However, it is possible to do it by querying the TCP state info of the socket. That option available in Linux since kernel 2.4 and on other Unix-like OSes (NetBSD, OpenBSD, FreeBSD and MacOS). Windows also has a tcp info query method however it is not reacheable via the gensockopts(2) standard socket API, so it can't be queried from Erlang's inet:getopts/2 API. [1] Using the newer socket module it's possible to detect if a socket is closed by attempting a recv with a MSG_PEEK option. However, the regular gen_tcp OTP module doesn't have a recv() variant which takes extra options. In addition, the new socket implementation still feels rather experimental. (It's not the default even in the latest OTP 26 release). --- src/mochiweb_request.erl | 12 ++++ src/mochiweb_socket.erl | 110 +++++++++++++++++++++++++++++++- test/mochiweb_request_tests.erl | 23 +++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl index 3d64906e..2b8c5e24 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..279f4336 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,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, _, _, <>}]} -> + 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.