about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--ext/raindrops/extconf.rb105
-rw-r--r--ext/raindrops/linux_inet_diag.c79
-rw-r--r--ext/raindrops/linux_tcp_info.c196
-rw-r--r--ext/raindrops/raindrops.c16
-rw-r--r--ext/raindrops/tcp_info.c216
-rw-r--r--test/test_tcp_info.rb (renamed from test/test_linux_tcp_info.rb)40
6 files changed, 397 insertions, 255 deletions
diff --git a/ext/raindrops/extconf.rb b/ext/raindrops/extconf.rb
index 79d212c..86c7d78 100644
--- a/ext/raindrops/extconf.rb
+++ b/ext/raindrops/extconf.rb
@@ -1,4 +1,5 @@
 require 'mkmf'
+require 'shellwords'
 
 dir_config('atomic_ops')
 have_func('mmap', 'sys/mman.h') or abort 'mmap() not found'
@@ -6,9 +7,110 @@ have_func('munmap', 'sys/mman.h') or abort 'munmap() not found'
 
 $CPPFLAGS += " -D_GNU_SOURCE "
 have_func('mremap', 'sys/mman.h')
-have_header('linux/tcp.h')
+headers = %w(sys/types.h netdb.h string.h sys/socket.h netinet/in.h)
+if have_header('linux/tcp.h')
+  headers << 'linux/tcp.h'
+else
+  %w(netinet/tcp.h netinet/tcp_fsm.h).each { |h|
+    have_header(h, headers) and headers << h
+  }
+end
 
 $CPPFLAGS += " -D_BSD_SOURCE "
+
+if have_type("struct tcp_info", headers)
+  %w(
+    tcpi_state
+    tcpi_ca_state
+    tcpi_retransmits
+    tcpi_probes
+    tcpi_backoff
+    tcpi_options
+    tcpi_snd_wscale
+    tcpi_rcv_wscale
+    tcpi_rto
+    tcpi_ato
+    tcpi_snd_mss
+    tcpi_rcv_mss
+    tcpi_unacked
+    tcpi_sacked
+    tcpi_lost
+    tcpi_retrans
+    tcpi_fackets
+    tcpi_last_data_sent
+    tcpi_last_ack_sent
+    tcpi_last_data_recv
+    tcpi_last_ack_recv
+    tcpi_pmtu
+    tcpi_rcv_ssthresh
+    tcpi_rtt
+    tcpi_rttvar
+    tcpi_snd_ssthresh
+    tcpi_snd_cwnd
+    tcpi_advmss
+    tcpi_reordering
+    tcpi_rcv_rtt
+    tcpi_rcv_space
+    tcpi_total_retrans
+    tcpi_snd_wnd
+    tcpi_snd_bwnd
+    tcpi_snd_nxt
+    tcpi_rcv_nxt
+    tcpi_toe_tid
+    tcpi_snd_rexmitpack
+    tcpi_rcv_ooopack
+    tcpi_snd_zerowin
+  ).each do |field|
+    cfunc = "tcp_info_#{field}"
+    if have_struct_member('struct tcp_info', field, headers)
+      func_body = <<EOF
+static VALUE #{cfunc}(VALUE self)
+{
+        struct tcp_info *info = DATA_PTR(self);
+        return UINT2NUM((uint32_t)info->#{field});
+}
+EOF
+      func_body.delete!("\n")
+      $defs << "-DCFUNC_#{cfunc}=#{Shellwords.shellescape(func_body)}"
+    else
+      func_body = "static inline void #{cfunc}(void) {}"
+      $defs << "-DCFUNC_#{cfunc}=#{Shellwords.shellescape(func_body)}"
+      cfunc = 'rb_f_notimplement'.freeze
+    end
+    rbmethod = %Q("#{field.sub(/\Atcpi_/, ''.freeze)}")
+    $defs << "-DDEFINE_METHOD_tcp_info_#{field}=" \
+             "#{Shellwords.shellescape(
+                %Q[rb_define_method(cTCP_Info,#{rbmethod},#{cfunc},0)])}"
+  end
+  tcp_state_map = {
+    ESTABLISHED: %w(TCP_ESTABLISHED TCPS_ESTABLISHED),
+    SYN_SENT: %w(TCP_SYN_SENT TCPS_SYN_SENT),
+    SYN_RECV: %w(TCP_SYN_RECV TCPS_SYN_RECEIVED),
+    FIN_WAIT1: %w(TCP_FIN_WAIT1 TCPS_FIN_WAIT_1),
+    FIN_WAIT2: %w(TCP_FIN_WAIT2 TCPS_FIN_WAIT_2),
+    TIME_WAIT: %w(TCP_TIME_WAIT TCPS_TIME_WAIT),
+    CLOSE: %w(TCP_CLOSE TCPS_CLOSED),
+    CLOSE_WAIT: %w(TCP_CLOSE_WAIT TCPS_CLOSE_WAIT),
+    LAST_ACK: %w(TCP_LAST_ACK TCPS_LAST_ACK),
+    LISTEN: %w(TCP_LISTEN TCPS_LISTEN),
+    CLOSING: %w(TCP_CLOSING TCPS_CLOSING),
+  }
+  nstate = 0
+  tcp_state_map.each do |state, try|
+    try.each do |os_name|
+      have_const(os_name, headers) or next
+      tcp_state_map[state] = os_name
+      nstate += 1
+    end
+  end
+  if nstate == tcp_state_map.size
+    $defs << '-DRAINDROPS_TCP_STATES_ALL_KNOWN=1'
+    tcp_state_map.each do |state, name|
+      $defs << "-DRAINDROPS_TCP_#{state}=#{name}"
+    end
+  end
+end
+
 have_func("getpagesize", "unistd.h")
 have_func('rb_thread_blocking_region')
 have_func('rb_thread_io_blocking_region')
@@ -53,4 +155,5 @@ Users of Debian-based distros may run:
 
   apt-get install libatomic-ops-dev
 SRC
+create_header # generate extconf.h to avoid excessively long command-line
 create_makefile('raindrops_ext')
diff --git a/ext/raindrops/linux_inet_diag.c b/ext/raindrops/linux_inet_diag.c
index 8ad436b..f2db6a5 100644
--- a/ext/raindrops/linux_inet_diag.c
+++ b/ext/raindrops/linux_inet_diag.c
@@ -233,80 +233,70 @@ static void bug_warn_nogvl(const char *fmt, ...)
 
 static struct listen_stats *stats_for(st_table *table, struct inet_diag_msg *r)
 {
-        char *key, *port, *old_key;
+        char *host, *key, *port, *old_key;
         size_t alloca_len;
         struct listen_stats *stats;
-        socklen_t keylen;
+        socklen_t hostlen;
         socklen_t portlen = (socklen_t)sizeof("65535");
-        union any_addr sa;
-        socklen_t len = sizeof(struct sockaddr_storage);
-        int rc;
-        int flags = NI_NUMERICHOST | NI_NUMERICSERV;
+        int n;
+        const void *src = r->id.idiag_src;
 
-        switch ((sa.ss.ss_family = r->idiag_family)) {
+        switch (r->idiag_family) {
         case AF_INET: {
-                sa.in.sin_port = r->id.idiag_sport;
-                sa.in.sin_addr.s_addr = r->id.idiag_src[0];
-                keylen = INET_ADDRSTRLEN;
-                alloca_len = keylen + 1 + portlen;
-                key = alloca(alloca_len);
-                key[keylen] = 0; /* will be ':' later */
-                port = key + keylen + 1;
-                rc = getnameinfo(&sa.sa, len,
-                                 key, keylen, port, portlen, flags);
+                hostlen = INET_ADDRSTRLEN;
+                alloca_len = hostlen + portlen;
+                host = key = alloca(alloca_len);
                 break;
                 }
         case AF_INET6: {
-                sa.in6.sin6_port = r->id.idiag_sport;
-                memcpy(&sa.in6.sin6_addr, &r->id.idiag_src, sizeof(__be32[4]));
-                keylen = INET6_ADDRSTRLEN;
-                          /* [            ] */
-                alloca_len = 1 + keylen + 1 + 1 + portlen;
+                hostlen = INET6_ADDRSTRLEN;
+                alloca_len = 1 + hostlen + 1 + portlen;
                 key = alloca(alloca_len);
-                *key = '[';
-                key[1 + keylen + 1] = 0; /* will be ':' later */
-                port = 1 + key + keylen + 1 + 1;
-                rc = getnameinfo(&sa.sa, len,
-                                 key + 1, keylen, port, portlen, flags);
+                host = key + 1;
                 break;
                 }
         default:
                 assert(0 && "unsupported address family, could that be IPv7?!");
         }
-        if (rc != 0) {
-                bug_warn_nogvl("BUG: getnameinfo: %s\n", gai_strerror(rc));
-                *key = 0;
+        if (!inet_ntop(r->idiag_family, src, host, hostlen)) {
+                bug_warn_nogvl("BUG: inet_ntop: %s\n", strerror(errno));
+                *key = '\0';
+                *host = '\0';
         }
-
-        keylen = (socklen_t)strlen(key);
-        portlen = (socklen_t)strlen(port);
-
-        switch (sa.ss.ss_family) {
+        hostlen = (socklen_t)strlen(host);
+        switch (r->idiag_family) {
         case AF_INET:
-                key[keylen] = ':';
-                memmove(key + keylen + 1, port, portlen + 1);
+                host[hostlen] = ':';
+                port = host + hostlen + 1;
                 break;
         case AF_INET6:
-                key[keylen] = ']';
-                key[keylen + 1] = ':';
-                memmove(key + keylen + 2, port, portlen + 1);
-                keylen++;
+                key[0] = '[';
+                host[hostlen] = ']';
+                host[hostlen + 1] = ':';
+                port = host + hostlen + 2;
                 break;
         default:
                 assert(0 && "unsupported address family, could that be IPv7?!");
         }
 
+        n = snprintf(port, portlen, "%u", ntohs(r->id.idiag_sport));
+        if (n <= 0) {
+                bug_warn_nogvl("BUG: snprintf port: %d\n", n);
+                *key = '\0';
+        }
+
         if (st_lookup(table, (st_data_t)key, (st_data_t *)&stats))
                 return stats;
 
         old_key = key;
 
         if (r->idiag_state == TCP_ESTABLISHED) {
-                int n = snprintf(key, alloca_len, "%s:%u",
-                                 addr_any(sa.ss.ss_family),
+                n = snprintf(key, alloca_len, "%s:%u",
+                                 addr_any(r->idiag_family),
                                  ntohs(r->id.idiag_sport));
                 if (n <= 0) {
                         bug_warn_nogvl("BUG: snprintf: %d\n", n);
+                        *key = '\0';
                 }
                 if (st_lookup(table, (st_data_t)key, (st_data_t *)&stats))
                         return stats;
@@ -319,8 +309,9 @@ static struct listen_stats *stats_for(st_table *table, struct inet_diag_msg *r)
                         memcpy(key, old_key, n + 1);
                 }
         } else {
-                key = xmalloc(keylen + 1 + portlen + 1);
-                memcpy(key, old_key, keylen + 1 + portlen + 1);
+                size_t old_len = strlen(old_key) + 1;
+                key = xmalloc(old_len);
+                memcpy(key, old_key, old_len);
         }
         stats = xcalloc(1, sizeof(struct listen_stats));
         st_insert(table, (st_data_t)key, (st_data_t)stats);
diff --git a/ext/raindrops/linux_tcp_info.c b/ext/raindrops/linux_tcp_info.c
deleted file mode 100644
index 83001a5..0000000
--- a/ext/raindrops/linux_tcp_info.c
+++ /dev/null
@@ -1,196 +0,0 @@
-#if defined(__linux__) && defined(HAVE_LINUX_TCP_H)
-#include <ruby.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <linux/tcp.h>
-#ifdef TCP_INFO
-#include "my_fileno.h"
-
-#define TCPI_ATTR_READER(x) \
-static VALUE tcp_info_##x(VALUE self) \
-{ \
-        struct tcp_info *info = DATA_PTR(self); \
-        return UINT2NUM((uint32_t)info->tcpi_##x); \
-}
-
-TCPI_ATTR_READER(state)
-TCPI_ATTR_READER(ca_state)
-TCPI_ATTR_READER(retransmits)
-TCPI_ATTR_READER(probes)
-TCPI_ATTR_READER(backoff)
-TCPI_ATTR_READER(options)
-TCPI_ATTR_READER(snd_wscale)
-TCPI_ATTR_READER(rcv_wscale)
-TCPI_ATTR_READER(rto)
-TCPI_ATTR_READER(ato)
-TCPI_ATTR_READER(snd_mss)
-TCPI_ATTR_READER(rcv_mss)
-TCPI_ATTR_READER(unacked)
-TCPI_ATTR_READER(sacked)
-TCPI_ATTR_READER(lost)
-TCPI_ATTR_READER(retrans)
-TCPI_ATTR_READER(fackets)
-TCPI_ATTR_READER(last_data_sent)
-TCPI_ATTR_READER(last_ack_sent)
-TCPI_ATTR_READER(last_data_recv)
-TCPI_ATTR_READER(last_ack_recv)
-TCPI_ATTR_READER(pmtu)
-TCPI_ATTR_READER(rcv_ssthresh)
-TCPI_ATTR_READER(rtt)
-TCPI_ATTR_READER(rttvar)
-TCPI_ATTR_READER(snd_ssthresh)
-TCPI_ATTR_READER(snd_cwnd)
-TCPI_ATTR_READER(advmss)
-TCPI_ATTR_READER(reordering)
-TCPI_ATTR_READER(rcv_rtt)
-TCPI_ATTR_READER(rcv_space)
-TCPI_ATTR_READER(total_retrans)
-
-static size_t tcpi_memsize(const void *ptr)
-{
-        return sizeof(struct tcp_info);
-}
-
-static const rb_data_type_t tcpi_type = {
-        "tcp_info",
-        { NULL, RUBY_TYPED_DEFAULT_FREE, tcpi_memsize, /* reserved */ },
-        /* parent, data, [ flags ] */
-};
-
-static VALUE alloc(VALUE klass)
-{
-        struct tcp_info *info;
-
-        return TypedData_Make_Struct(klass, struct tcp_info, &tcpi_type, info);
-}
-
-/*
- * call-seq:
- *
- *        Raindrops::TCP_Info.new(tcp_socket)        -> TCP_Info object
- *
- * Reads a TCP_Info object from any given +tcp_socket+.  See the tcp(7)
- * manpage and /usr/include/linux/tcp.h for more details.
- */
-static VALUE init(VALUE self, VALUE io)
-{
-        int fd = my_fileno(io);
-        struct tcp_info *info = DATA_PTR(self);
-        socklen_t len = (socklen_t)sizeof(struct tcp_info);
-        int rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, info, &len);
-
-        if (rc != 0)
-                rb_sys_fail("getsockopt");
-
-        return self;
-}
-
-void Init_raindrops_linux_tcp_info(void)
-{
-        VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
-        VALUE cTCP_Info;
-
-        /*
-         * Document-class: Raindrops::TCP_Info
-         *
-         * This is used to wrap "struct tcp_info" as described in tcp(7)
-         * and /usr/include/linux/tcp.h.  The following readers methods
-         * are defined corresponding to the "tcpi_" fields in the
-         * tcp_info struct.
-         *
-         * In particular, the +last_data_recv+ field is useful for measuring
-         * the amount of time a client spent in the listen queue before
-         * +accept()+, but only if +TCP_DEFER_ACCEPT+ is used with the
-         * listen socket (it is on by default in Unicorn).
-         *
-         * - state
-         * - ca_state
-         * - retransmits
-         * - probes
-         * - backoff
-         * - options
-         * - snd_wscale
-         * - rcv_wscale
-         * - rto
-         * - ato
-         * - snd_mss
-         * - rcv_mss
-         * - unacked
-         * - sacked
-         * - lost
-         * - retrans
-         * - fackets
-         * - last_data_sent
-         * - last_ack_sent
-         * - last_data_recv
-         * - last_ack_recv
-         * - pmtu
-         * - rcv_ssthresh
-         * - rtt
-         * - rttvar
-         * - snd_ssthresh
-         * - snd_cwnd
-         * - advmss
-         * - reordering
-         * - rcv_rtt
-         * - rcv_space
-         * - total_retrans
-         *
-         * https://kernel.org/doc/man-pages/online/pages/man7/tcp.7.html
-         */
-        cTCP_Info = rb_define_class_under(cRaindrops, "TCP_Info", rb_cObject);
-        rb_define_alloc_func(cTCP_Info, alloc);
-        rb_define_private_method(cTCP_Info, "initialize", init, 1);
-
-        /*
-         * Document-method: Raindrops::TCP_Info#get!
-         *
-         * call-seq:
-         *
-         *        info = Raindrops::TCP_Info.new(tcp_socket)
-         *        info.get!(tcp_socket)
-         *
-         * Update an existing TCP_Info objects with the latest stats
-         * from the given socket.  This even allows sharing TCP_Info
-         * objects between different sockets to avoid garbage.
-         */
-        rb_define_method(cTCP_Info, "get!", init, 1);
-
-#define TCPI_DEFINE_METHOD(x) \
-        rb_define_method(cTCP_Info, #x, tcp_info_##x, 0)
-
-        TCPI_DEFINE_METHOD(state);
-        TCPI_DEFINE_METHOD(ca_state);
-        TCPI_DEFINE_METHOD(retransmits);
-        TCPI_DEFINE_METHOD(probes);
-        TCPI_DEFINE_METHOD(backoff);
-        TCPI_DEFINE_METHOD(options);
-        TCPI_DEFINE_METHOD(snd_wscale);
-        TCPI_DEFINE_METHOD(rcv_wscale);
-        TCPI_DEFINE_METHOD(rto);
-        TCPI_DEFINE_METHOD(ato);
-        TCPI_DEFINE_METHOD(snd_mss);
-        TCPI_DEFINE_METHOD(rcv_mss);
-        TCPI_DEFINE_METHOD(unacked);
-        TCPI_DEFINE_METHOD(sacked);
-        TCPI_DEFINE_METHOD(lost);
-        TCPI_DEFINE_METHOD(retrans);
-        TCPI_DEFINE_METHOD(fackets);
-        TCPI_DEFINE_METHOD(last_data_sent);
-        TCPI_DEFINE_METHOD(last_ack_sent);
-        TCPI_DEFINE_METHOD(last_data_recv);
-        TCPI_DEFINE_METHOD(last_ack_recv);
-        TCPI_DEFINE_METHOD(pmtu);
-        TCPI_DEFINE_METHOD(rcv_ssthresh);
-        TCPI_DEFINE_METHOD(rtt);
-        TCPI_DEFINE_METHOD(rttvar);
-        TCPI_DEFINE_METHOD(snd_ssthresh);
-        TCPI_DEFINE_METHOD(snd_cwnd);
-        TCPI_DEFINE_METHOD(advmss);
-        TCPI_DEFINE_METHOD(reordering);
-        TCPI_DEFINE_METHOD(rcv_rtt);
-        TCPI_DEFINE_METHOD(rcv_space);
-        TCPI_DEFINE_METHOD(total_retrans);
-}
-#endif /* TCP_INFO */
-#endif /* defined(__linux__) && defined(HAVE_LINUX_TCP_H) */
diff --git a/ext/raindrops/raindrops.c b/ext/raindrops/raindrops.c
index 390b8b8..837084c 100644
--- a/ext/raindrops/raindrops.c
+++ b/ext/raindrops/raindrops.c
@@ -117,7 +117,9 @@ retry:
         r->drops = mmap(NULL, tmp,
                         PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
         if (r->drops == MAP_FAILED) {
-                if ((errno == EAGAIN || errno == ENOMEM) && tries-- > 0) {
+                int err = errno;
+
+                if ((err == EAGAIN || err == ENOMEM) && tries-- > 0) {
                         rb_gc();
                         goto retry;
                 }
@@ -153,7 +155,9 @@ static void resize(struct raindrops *r, size_t new_rd_size)
 
         rv = mremap(old_address, old_size, new_size, MREMAP_MAYMOVE);
         if (rv == MAP_FAILED) {
-                if (errno == EAGAIN || errno == ENOMEM) {
+                int err = errno;
+
+                if (err == EAGAIN || err == ENOMEM) {
                         rb_gc();
                         rv = mremap(old_address, old_size, new_size, 0);
                 }
@@ -336,7 +340,9 @@ static VALUE aref(VALUE self, VALUE index)
 
 #ifdef __linux__
 void Init_raindrops_linux_inet_diag(void);
-void Init_raindrops_linux_tcp_info(void);
+#endif
+#ifdef HAVE_TYPE_STRUCT_TCP_INFO
+void Init_raindrops_tcp_info(void);
 #endif
 
 #ifndef _SC_NPROCESSORS_CONF
@@ -441,6 +447,8 @@ void Init_raindrops_ext(void)
 
 #ifdef __linux__
         Init_raindrops_linux_inet_diag();
-        Init_raindrops_linux_tcp_info();
+#endif
+#ifdef HAVE_TYPE_STRUCT_TCP_INFO
+        Init_raindrops_tcp_info();
 #endif
 }
diff --git a/ext/raindrops/tcp_info.c b/ext/raindrops/tcp_info.c
new file mode 100644
index 0000000..3e241a1
--- /dev/null
+++ b/ext/raindrops/tcp_info.c
@@ -0,0 +1,216 @@
+#include <ruby.h>
+#include "extconf.h"
+#include <sys/socket.h>
+#include <netinet/in.h>
+#if defined(HAVE_LINUX_TCP_H)
+#  include <linux/tcp.h>
+#else
+#  if defined(HAVE_NETINET_TCP_H)
+#    include <netinet/tcp.h>
+#  endif
+#  if defined(HAVE_NETINET_TCP_FSM_H)
+#    include <netinet/tcp_fsm.h>
+#  endif
+#endif
+
+#ifdef HAVE_TYPE_STRUCT_TCP_INFO
+#include "my_fileno.h"
+
+CFUNC_tcp_info_tcpi_state
+CFUNC_tcp_info_tcpi_ca_state
+CFUNC_tcp_info_tcpi_retransmits
+CFUNC_tcp_info_tcpi_probes
+CFUNC_tcp_info_tcpi_backoff
+CFUNC_tcp_info_tcpi_options
+CFUNC_tcp_info_tcpi_snd_wscale
+CFUNC_tcp_info_tcpi_rcv_wscale
+CFUNC_tcp_info_tcpi_rto
+CFUNC_tcp_info_tcpi_ato
+CFUNC_tcp_info_tcpi_snd_mss
+CFUNC_tcp_info_tcpi_rcv_mss
+CFUNC_tcp_info_tcpi_unacked
+CFUNC_tcp_info_tcpi_sacked
+CFUNC_tcp_info_tcpi_lost
+CFUNC_tcp_info_tcpi_retrans
+CFUNC_tcp_info_tcpi_fackets
+CFUNC_tcp_info_tcpi_last_data_sent
+CFUNC_tcp_info_tcpi_last_ack_sent
+CFUNC_tcp_info_tcpi_last_data_recv
+CFUNC_tcp_info_tcpi_last_ack_recv
+CFUNC_tcp_info_tcpi_pmtu
+CFUNC_tcp_info_tcpi_rcv_ssthresh
+CFUNC_tcp_info_tcpi_rtt
+CFUNC_tcp_info_tcpi_rttvar
+CFUNC_tcp_info_tcpi_snd_ssthresh
+CFUNC_tcp_info_tcpi_snd_cwnd
+CFUNC_tcp_info_tcpi_advmss
+CFUNC_tcp_info_tcpi_reordering
+CFUNC_tcp_info_tcpi_rcv_rtt
+CFUNC_tcp_info_tcpi_rcv_space
+CFUNC_tcp_info_tcpi_total_retrans
+
+static size_t tcpi_memsize(const void *ptr)
+{
+        return sizeof(struct tcp_info);
+}
+
+static const rb_data_type_t tcpi_type = {
+        "tcp_info",
+        { NULL, RUBY_TYPED_DEFAULT_FREE, tcpi_memsize, /* reserved */ },
+        /* parent, data, [ flags ] */
+};
+
+static VALUE alloc(VALUE klass)
+{
+        struct tcp_info *info;
+
+        return TypedData_Make_Struct(klass, struct tcp_info, &tcpi_type, info);
+}
+
+/*
+ * call-seq:
+ *
+ *        Raindrops::TCP_Info.new(tcp_socket)        -> TCP_Info object
+ *
+ * Reads a TCP_Info object from any given +tcp_socket+.  See the tcp(7)
+ * manpage and /usr/include/linux/tcp.h for more details.
+ */
+static VALUE init(VALUE self, VALUE io)
+{
+        int fd = my_fileno(io);
+        struct tcp_info *info = DATA_PTR(self);
+        socklen_t len = (socklen_t)sizeof(struct tcp_info);
+        int rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, info, &len);
+
+        if (rc != 0)
+                rb_sys_fail("getsockopt");
+
+        return self;
+}
+
+void Init_raindrops_tcp_info(void)
+{
+        VALUE cRaindrops = rb_define_class("Raindrops", rb_cObject);
+        VALUE cTCP_Info;
+
+        /*
+         * Document-class: Raindrops::TCP_Info
+         *
+         * This is used to wrap "struct tcp_info" as described in tcp(7)
+         * and /usr/include/linux/tcp.h.  The following readers methods
+         * are defined corresponding to the "tcpi_" fields in the
+         * tcp_info struct.
+         *
+         * In particular, the +last_data_recv+ field is useful for measuring
+         * the amount of time a client spent in the listen queue before
+         * +accept()+, but only if +TCP_DEFER_ACCEPT+ is used with the
+         * listen socket (it is on by default in Unicorn).
+         *
+         * - state
+         * - ca_state
+         * - retransmits
+         * - probes
+         * - backoff
+         * - options
+         * - snd_wscale
+         * - rcv_wscale
+         * - rto
+         * - ato
+         * - snd_mss
+         * - rcv_mss
+         * - unacked
+         * - sacked
+         * - lost
+         * - retrans
+         * - fackets
+         * - last_data_sent
+         * - last_ack_sent
+         * - last_data_recv
+         * - last_ack_recv
+         * - pmtu
+         * - rcv_ssthresh
+         * - rtt
+         * - rttvar
+         * - snd_ssthresh
+         * - snd_cwnd
+         * - advmss
+         * - reordering
+         * - rcv_rtt
+         * - rcv_space
+         * - total_retrans
+         *
+         * https://kernel.org/doc/man-pages/online/pages/man7/tcp.7.html
+         */
+        cTCP_Info = rb_define_class_under(cRaindrops, "TCP_Info", rb_cObject);
+        rb_define_alloc_func(cTCP_Info, alloc);
+        rb_define_private_method(cTCP_Info, "initialize", init, 1);
+
+        /*
+         * Document-method: Raindrops::TCP_Info#get!
+         *
+         * call-seq:
+         *
+         *        info = Raindrops::TCP_Info.new(tcp_socket)
+         *        info.get!(tcp_socket)
+         *
+         * Update an existing TCP_Info objects with the latest stats
+         * from the given socket.  This even allows sharing TCP_Info
+         * objects between different sockets to avoid garbage.
+         */
+        rb_define_method(cTCP_Info, "get!", init, 1);
+
+        DEFINE_METHOD_tcp_info_tcpi_state;
+        DEFINE_METHOD_tcp_info_tcpi_ca_state;
+        DEFINE_METHOD_tcp_info_tcpi_retransmits;
+        DEFINE_METHOD_tcp_info_tcpi_probes;
+        DEFINE_METHOD_tcp_info_tcpi_backoff;
+        DEFINE_METHOD_tcp_info_tcpi_options;
+        DEFINE_METHOD_tcp_info_tcpi_snd_wscale;
+        DEFINE_METHOD_tcp_info_tcpi_rcv_wscale;
+        DEFINE_METHOD_tcp_info_tcpi_rto;
+        DEFINE_METHOD_tcp_info_tcpi_ato;
+        DEFINE_METHOD_tcp_info_tcpi_snd_mss;
+        DEFINE_METHOD_tcp_info_tcpi_rcv_mss;
+        DEFINE_METHOD_tcp_info_tcpi_unacked;
+        DEFINE_METHOD_tcp_info_tcpi_sacked;
+        DEFINE_METHOD_tcp_info_tcpi_lost;
+        DEFINE_METHOD_tcp_info_tcpi_retrans;
+        DEFINE_METHOD_tcp_info_tcpi_fackets;
+        DEFINE_METHOD_tcp_info_tcpi_last_data_sent;
+        DEFINE_METHOD_tcp_info_tcpi_last_ack_sent;
+        DEFINE_METHOD_tcp_info_tcpi_last_data_recv;
+        DEFINE_METHOD_tcp_info_tcpi_last_ack_recv;
+        DEFINE_METHOD_tcp_info_tcpi_pmtu;
+        DEFINE_METHOD_tcp_info_tcpi_rcv_ssthresh;
+        DEFINE_METHOD_tcp_info_tcpi_rtt;
+        DEFINE_METHOD_tcp_info_tcpi_rttvar;
+        DEFINE_METHOD_tcp_info_tcpi_snd_ssthresh;
+        DEFINE_METHOD_tcp_info_tcpi_snd_cwnd;
+        DEFINE_METHOD_tcp_info_tcpi_advmss;
+        DEFINE_METHOD_tcp_info_tcpi_reordering;
+        DEFINE_METHOD_tcp_info_tcpi_rcv_rtt;
+        DEFINE_METHOD_tcp_info_tcpi_rcv_space;
+        DEFINE_METHOD_tcp_info_tcpi_total_retrans;
+
+#ifdef RAINDROPS_TCP_STATES_ALL_KNOWN
+        {
+#define TCPSET(n,v) rb_hash_aset(tcp, ID2SYM(rb_intern(#n)), INT2NUM(v))
+                VALUE tcp = rb_hash_new();
+                TCPSET(ESTABLISHED, RAINDROPS_TCP_ESTABLISHED);
+                TCPSET(SYN_SENT, RAINDROPS_TCP_SYN_SENT);
+                TCPSET(SYN_RECV, RAINDROPS_TCP_SYN_RECV);
+                TCPSET(FIN_WAIT1, RAINDROPS_TCP_FIN_WAIT1);
+                TCPSET(FIN_WAIT2, RAINDROPS_TCP_FIN_WAIT2);
+                TCPSET(TIME_WAIT, RAINDROPS_TCP_TIME_WAIT);
+                TCPSET(CLOSE, RAINDROPS_TCP_CLOSE);
+                TCPSET(CLOSE_WAIT, RAINDROPS_TCP_CLOSE_WAIT);
+                TCPSET(LAST_ACK, RAINDROPS_TCP_LAST_ACK);
+                TCPSET(LISTEN, RAINDROPS_TCP_LISTEN);
+                TCPSET(CLOSING, RAINDROPS_TCP_CLOSING);
+#undef TCPSET
+                OBJ_FREEZE(tcp);
+                rb_define_const(cRaindrops, "TCP", tcp);
+        }
+#endif
+}
+#endif /* HAVE_STRUCT_TCP_INFO */
diff --git a/test/test_linux_tcp_info.rb b/test/test_tcp_info.rb
index c947211..b107565 100644
--- a/test/test_linux_tcp_info.rb
+++ b/test/test_tcp_info.rb
@@ -5,15 +5,15 @@ require 'raindrops'
 require 'socket'
 require 'pp'
 $stderr.sync = $stdout.sync = true
-class TestLinuxTCP_Info < Test::Unit::TestCase
+class TestTCP_Info < Test::Unit::TestCase
 
   TEST_ADDR = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
 
   # Linux kernel commit 5ee3afba88f5a79d0bff07ddd87af45919259f91
   TCP_INFO_useful_listenq = `uname -r`.strip >= '2.6.24'
 
-
-  def test_tcp_server
+  def test_tcp_server_unacked
+    return if RUBY_PLATFORM !~ /linux/ # unacked not implemented on others...
     s = TCPServer.new(TEST_ADDR, 0)
     rv = Raindrops::TCP_Info.new s
     c = TCPSocket.new TEST_ADDR, s.addr[1]
@@ -29,10 +29,8 @@ class TestLinuxTCP_Info < Test::Unit::TestCase
     tmp.get!(s)
     assert_equal before, tmp.object_id
 
-    ensure
-      c.close if c
-      a.close if a
-      s.close
+  ensure
+    [ c, a, s ].compact.each(&:close)
   end
 
   def test_accessors
@@ -42,12 +40,14 @@ class TestLinuxTCP_Info < Test::Unit::TestCase
     assert tcp_info_methods.size >= 32
     tcp_info_methods.each do |m|
       next if m.to_sym == :get!
+      next if ! tmp.respond_to?(m)
       val = tmp.__send__ m
       assert_kind_of Integer, val
       assert val >= 0
     end
-    ensure
-      s.close
+    assert tmp.respond_to?(:state), 'every OS knows about TCP state, right?'
+  ensure
+    s.close
   end
 
   def test_tcp_server_delayed
@@ -65,4 +65,24 @@ class TestLinuxTCP_Info < Test::Unit::TestCase
       a.close if a
       s.close
   end
-end if RUBY_PLATFORM =~ /linux/
+
+  def test_tcp_server_state_closed
+    s = TCPServer.new(TEST_ADDR, 0)
+    c = TCPSocket.new(TEST_ADDR, s.addr[1])
+    i = Raindrops::TCP_Info.allocate
+    a = s.accept
+    i.get!(a)
+    state = i.state
+    if Raindrops.const_defined?(:TCP)
+      assert_equal state, Raindrops::TCP[:ESTABLISHED]
+    end
+    c = c.close
+    sleep(0.01) # wait for kernel to update state
+    i.get!(a)
+    assert_not_equal state, i.state
+  ensure
+    s.close if s
+    c.close if c
+    a.close if a
+  end
+end if defined? Raindrops::TCP_Info