ruby-changes:48382
From: normal <ko1@a...>
Date: Sat, 28 Oct 2017 08:26:54 +0900 (JST)
Subject: [ruby-changes:48382] normal:r60496 (trunk): socket: fix BasicSocket#*_nonblock buffering bugs from r58400
normal 2017-10-28 08:26:48 +0900 (Sat, 28 Oct 2017) New Revision: 60496 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=60496 Log: socket: fix BasicSocket#*_nonblock buffering bugs from r58400 IO#read_nonblock and IO#write_nonblock take into account buffered data, so the Linux-only BasicSocket#read_nonblock and BasicSocket#write_nonblock methods must, too. This bug was only introduced in r58400 ("socket: avoid fcntl for read/write_nonblock on Linux") and does not affect any stable release. * ext/socket/basicsocket.c (rsock_init_basicsocket): * ext/socket/init.c (rsock_s_recvfrom_nonblock): * ext/socket/init.c (rsock_init_socket_init): * ext/socket/lib/socket.rb (def read_nonblock): * ext/socket/lib/socket.rb (def write_nonblock): * ext/socket/rubysocket.h (static inline void rsock_maybe_wait_fd): * test/socket/test_basicsocket.rb (def test_read_write_nonblock): [Feature #13362] Modified files: trunk/ext/socket/basicsocket.c trunk/ext/socket/init.c trunk/ext/socket/lib/socket.rb trunk/ext/socket/rubysocket.h trunk/test/socket/test_basicsocket.rb Index: ext/socket/rubysocket.h =================================================================== --- ext/socket/rubysocket.h (revision 60495) +++ ext/socket/rubysocket.h (revision 60496) @@ -430,6 +430,9 @@ static inline void rsock_maybe_wait_fd(i https://github.com/ruby/ruby/blob/trunk/ext/socket/rubysocket.h#L430 # define MSG_DONTWAIT_RELIABLE 0 #endif +VALUE rsock_read_nonblock(VALUE sock, VALUE length, VALUE buf, VALUE ex); +VALUE rsock_write_nonblock(VALUE sock, VALUE buf, VALUE ex); + #if !defined HAVE_INET_NTOP && ! defined _WIN32 const char *inet_ntop(int, const void *, char *, size_t); #elif defined __MINGW32__ Index: ext/socket/init.c =================================================================== --- ext/socket/init.c (revision 60495) +++ ext/socket/init.c (revision 60496) @@ -280,6 +280,108 @@ rsock_s_recvfrom_nonblock(VALUE sock, VA https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L280 return rb_assoc_new(str, addr); } +#if MSG_DONTWAIT_RELIABLE +static VALUE sym_wait_writable; + +/* copied from io.c :< */ +static long +read_buffered_data(char *ptr, long len, rb_io_t *fptr) +{ + int n = fptr->rbuf.len; + + if (n <= 0) return 0; + if (n > len) n = (int)len; + MEMMOVE(ptr, fptr->rbuf.ptr+fptr->rbuf.off, char, n); + fptr->rbuf.off += n; + fptr->rbuf.len -= n; + return n; +} + +/* :nodoc: */ +VALUE +rsock_read_nonblock(VALUE sock, VALUE length, VALUE buf, VALUE ex) +{ + rb_io_t *fptr; + long n; + long len = NUM2LONG(length); + VALUE str = rsock_strbuf(buf, len); + char *ptr; + + OBJ_TAINT(str); + GetOpenFile(sock, fptr); + + if (len == 0) { + return str; + } + + ptr = RSTRING_PTR(str); + n = read_buffered_data(ptr, len, fptr); + if (n <= 0) { + n = (long)recv(fptr->fd, ptr, len, MSG_DONTWAIT); + if (n < 0) { + int e = errno; + if ((e == EWOULDBLOCK || e == EAGAIN)) { + if (ex == Qfalse) return sym_wait_readable; + rb_readwrite_syserr_fail(RB_IO_WAIT_READABLE, + e, "read would block"); + } + rb_syserr_fail_path(e, fptr->pathv); + } + } + if (len != n) { + rb_str_modify(str); + rb_str_set_len(str, n); + if (str != buf) { + rb_str_resize(str, n); + } + } + if (n == 0) { + if (ex == Qfalse) return Qnil; + rb_eof_error(); + } + + return str; +} + +/* :nodoc: */ +VALUE +rsock_write_nonblock(VALUE sock, VALUE str, VALUE ex) +{ + rb_io_t *fptr; + long n; + + if (!RB_TYPE_P(str, T_STRING)) + str = rb_obj_as_string(str); + + sock = rb_io_get_write_io(sock); + GetOpenFile(sock, fptr); + rb_io_check_writable(fptr); + + /* + * As with IO#write_nonblock, we may block if somebody is relying on + * buffered I/O; but nobody actually hits this because pipes and sockets + * are not userspace-buffered in Ruby by default. + */ + if (fptr->wbuf.len > 0) { + rb_io_flush(sock); + } + + n = (long)send(fptr->fd, RSTRING_PTR(str), RSTRING_LEN(str), MSG_DONTWAIT); + if (n < 0) { + int e = errno; + + if (e == EWOULDBLOCK || e == EAGAIN) { + if (ex == Qfalse) return sym_wait_writable; + rb_readwrite_syserr_fail(RB_IO_WAIT_WRITABLE, e, + "write would block"); + } + rb_syserr_fail_path(e, fptr->pathv); + } + + return LONG2FIX(n); +} +#endif /* MSG_DONTWAIT_RELIABLE */ + /* returns true if SOCK_CLOEXEC is supported */ int rsock_detect_cloexec(int fd) { @@ -680,4 +782,8 @@ rsock_init_socket_init(void) https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L782 #undef rb_intern sym_wait_readable = ID2SYM(rb_intern("wait_readable")); + +#if MSG_DONTWAIT_RELIABLE + sym_wait_writable = ID2SYM(rb_intern("wait_writable")); +#endif } Index: ext/socket/lib/socket.rb =================================================================== --- ext/socket/lib/socket.rb (revision 60495) +++ ext/socket/lib/socket.rb (revision 60496) @@ -449,16 +449,11 @@ class BasicSocket < IO https://github.com/ruby/ruby/blob/trunk/ext/socket/lib/socket.rb#L449 # Do other platforms support MSG_DONTWAIT reliably? if RUBY_PLATFORM =~ /linux/ && Socket.const_defined?(:MSG_DONTWAIT) def read_nonblock(len, str = nil, exception: true) # :nodoc: - case rv = __recv_nonblock(len, 0, str, exception) - when '' # recv_nonblock returns empty string on EOF - exception ? raise(EOFError, 'end of file reached') : nil - else - rv - end + __read_nonblock(len, str, exception) end def write_nonblock(buf, exception: true) # :nodoc: - __sendmsg_nonblock(buf, 0, nil, nil, exception) + __write_nonblock(buf, exception) end end end Index: ext/socket/basicsocket.c =================================================================== --- ext/socket/basicsocket.c (revision 60495) +++ ext/socket/basicsocket.c (revision 60496) @@ -725,6 +725,13 @@ rsock_init_basicsocket(void) https://github.com/ruby/ruby/blob/trunk/ext/socket/basicsocket.c#L725 rb_define_private_method(rb_cBasicSocket, "__recv_nonblock", bsock_recv_nonblock, 4); +#if MSG_DONTWAIT_RELIABLE + rb_define_private_method(rb_cBasicSocket, + "__read_nonblock", rsock_read_nonblock, 3); + rb_define_private_method(rb_cBasicSocket, + "__write_nonblock", rsock_write_nonblock, 2); +#endif + /* in ancdata.c */ rb_define_private_method(rb_cBasicSocket, "__sendmsg", rsock_bsock_sendmsg, 4); Index: test/socket/test_basicsocket.rb =================================================================== --- test/socket/test_basicsocket.rb (revision 60495) +++ test/socket/test_basicsocket.rb (revision 60496) @@ -205,4 +205,25 @@ class TestSocket_BasicSocket < Test::Uni https://github.com/ruby/ruby/blob/trunk/test/socket/test_basicsocket.rb#L205 assert_not_predicate(ssock, :nonblock?) unless set_nb end end + + def test_read_nonblock_mix_buffered + socks do |sserv, ssock, csock| + ssock.write("hello\nworld\n") + assert_equal "hello\n", csock.gets + IO.select([csock], nil, nil, 10) or + flunk 'socket did not become readable' + assert_equal "world\n", csock.read_nonblock(8) + end + end + + def test_write_nonblock_buffered + socks do |sserv, ssock, csock| + ssock.sync = false + ssock.write("h") + assert_equal :wait_readable, csock.read_nonblock(1, exception: false) + assert_equal 4, ssock.write_nonblock("ello") + ssock.close + assert_equal "hello", csock.read(5) + end + end end if defined?(BasicSocket) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/