JRuby (please use github issues at http://bugs.jruby.org)
  1. JRuby (please use github issues at http://bugs.jruby.org)
  2. JRUBY-5222

Reopening a file descriptor on a socket doesn't work (gives "could not reopen: null (IOError)")

    Details

    • Type: Bug Bug
    • Status: Resolved Resolved
    • Priority: Minor Minor
    • Resolution: Fixed
    • Affects Version/s: JRuby 1.1.6, JRuby 1.5.5
    • Fix Version/s: JRuby 1.7.0.pre1
    • Component/s: Core Classes/Modules
    • Labels:
      None
    • Environment:
      Tested under Ubuntu 10.04, OpenJDK 1.6.0_18
    • Number of attachments :
      0

      Description

      STDIN.reopen doesn't work for me if I specify an open TCP socket, as it does in MRI. Here's a test:

      reopen
      p RUBY_VERSION
      begin
        p JRUBY_VERSION
        p Java::java::lang::System.getProperty("java.vendor")
        p Java::java::lang::System.getProperty("java.version")
      rescue NameError => name
      end
      
      require 'socket'
      server = TCPServer.new("127.0.0.1",11112)
      another_thread = Thread.new { TCPSocket.new("127.0.0.1",11112).write("x"*99) }
      client = server.accept
      
      STDIN.reopen(client)
      p STDIN.read(99)
      

      And here's me running it under a couple of different Javas, and JRuby 1.5.5. The same thing happened under the ancient system JRuby (1.1.6):

       
      mattbee@desk4:~$ JAVA_HOME=/usr/lib/jvm/java-6-sun /usr/local/jruby-1.5.5/bin/jruby reopen
      "1.8.7"
      "1.5.5"
      "Sun Microsystems Inc."
      "1.6.0_22"
      reopen:14: could not reopen: null (IOError)
      mattbee@desk4:~$ JAVA_HOME=/usr/lib/jvm/java-1.6.0-openjdk /usr/local/jruby-1.5.5/bin/jruby reopen
      "1.8.7"
      "1.5.5"
      "Sun Microsystems Inc."
      "1.6.0_18"
      reopen:14: could not reopen: null (IOError)
      mattbee@desk4:~$ ruby reopen
      "1.8.7"
      "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      mattbee@desk4:~$ 
      

        Activity

        Hide
        Matthew Bloch added a comment -

        I just double-checked the behaviour on a bleeding-edge git build, and it does the same thing.

        Show
        Matthew Bloch added a comment - I just double-checked the behaviour on a bleeding-edge git build, and it does the same thing.
        Hide
        Charles Oliver Nutter added a comment -

        There's numerous problems here...I really hate reopen.

        • Reopen attempts to capture the current position from any incoming readable stream and re-set it, to force it to flush before it returns from reopen. In this case, since it's a socket, it's not seekable, so we can't get a position from it. A fix for this is to not try to get position if it's not seekable.
        • With that fixed, read fails because in checking whether the stream is readable it sees that it is also writable and tries to force a flush, again by seeking to the same position it is already in. The same check would work here.

        The larger systemic problem is that files, STDIO, and sockets behave differently in JRuby and have different setup/teardown/read/write logic. When we reopen one type with a completely different type, a lot of assumptions about how those streams have been set up are broken. It may be possible to fix these various issues, but there's a systemic problem with reopen itself: it requires that an object be able to physically change its type. In JRuby, where a TCPSocket is implemented by RubyTCPSocket and STDIO streams are simple RubyIO objects, it's not possible for us to switch the actual backing store.

        Is this a case you actually ran into?

        Show
        Charles Oliver Nutter added a comment - There's numerous problems here...I really hate reopen. Reopen attempts to capture the current position from any incoming readable stream and re-set it, to force it to flush before it returns from reopen. In this case, since it's a socket, it's not seekable, so we can't get a position from it. A fix for this is to not try to get position if it's not seekable. With that fixed, read fails because in checking whether the stream is readable it sees that it is also writable and tries to force a flush, again by seeking to the same position it is already in. The same check would work here. The larger systemic problem is that files, STDIO, and sockets behave differently in JRuby and have different setup/teardown/read/write logic. When we reopen one type with a completely different type, a lot of assumptions about how those streams have been set up are broken. It may be possible to fix these various issues, but there's a systemic problem with reopen itself: it requires that an object be able to physically change its type. In JRuby, where a TCPSocket is implemented by RubyTCPSocket and STDIO streams are simple RubyIO objects, it's not possible for us to switch the actual backing store. Is this a case you actually ran into?
        Hide
        Matthew Bloch added a comment -

        Yes - I was building an inetd program, copying the logic from an ancient inetd.c. So I wanted to to answer a socket, fork, reopen STDIN as that socket, and then exec. I just boiled down a test case to show you the differences. Having said that it seems that reopen is a bit unloved on MRI too. MRI does achieve changing an object's type through skulduggery, but it seems to elicit that same pukey reaction from Ruby programmers as e.g. C++ programmers exhibit when they discover Ruby monkey-patching - everyone has their limits

        The problem is surely in the pattern of representing an I/O descriptor as a single object, and also letting the user also call dup2() ? You just have to change the type of an object to support that properly. OR maybe an acceptable hack would be for IO to go into a "proxy mode" once you call reopen, i.e. once you call IO#reopen on a valid other IO object, it just turns into a proxy for the new one. So it won't be the new object but it will behave like it.

        I'm not sure it's a particularly important use of anyone's time to "fix" this, and I know when I'm fighting an uphill battle - having read around it I think I'm lucky my code works on MRI! I can probably make it work portably by simply dropping down to FFI methods to do all my low-level reopens, and ignoring the object model as I'm about to exec() it all away.

        A more explicit error in JRuby might save another programmer some head-scratching, e.g. just when you IO#reopen to an IO of a different type, throw "Yuck, do you realise what you're asking?" and link to this discussion

        Show
        Matthew Bloch added a comment - Yes - I was building an inetd program, copying the logic from an ancient inetd.c. So I wanted to to answer a socket, fork, reopen STDIN as that socket, and then exec. I just boiled down a test case to show you the differences. Having said that it seems that reopen is a bit unloved on MRI too . MRI does achieve changing an object's type through skulduggery, but it seems to elicit that same pukey reaction from Ruby programmers as e.g. C++ programmers exhibit when they discover Ruby monkey-patching - everyone has their limits The problem is surely in the pattern of representing an I/O descriptor as a single object, and also letting the user also call dup2() ? You just have to change the type of an object to support that properly. OR maybe an acceptable hack would be for IO to go into a "proxy mode" once you call reopen, i.e. once you call IO#reopen on a valid other IO object, it just turns into a proxy for the new one. So it won't be the new object but it will behave like it. I'm not sure it's a particularly important use of anyone's time to "fix" this, and I know when I'm fighting an uphill battle - having read around it I think I'm lucky my code works on MRI! I can probably make it work portably by simply dropping down to FFI methods to do all my low-level reopens, and ignoring the object model as I'm about to exec() it all away. A more explicit error in JRuby might save another programmer some head-scratching, e.g. just when you IO#reopen to an IO of a different type, throw "Yuck, do you realise what you're asking?" and link to this discussion
        Hide
        Charles Oliver Nutter added a comment -

        Fixed on master@30095bb. Perhaps someone can write up some tests for our repository, MRI's repository, or (ideally) RubySpec?

        commit 30095bb6455512e7e4e98ea0fd53d3b04f9921e2
        Author: Charles Oliver Nutter <headius@headius.com>
        Date:   Mon Mar 12 17:59:57 2012 -0500
        
            Fix JRUBY-5222
            
            Reopening a file descriptor on a socket doesn't work (gives "could not reopen: null (IOError)")
            
            The errors were all from trying to seek the socket as though it were
            a file, which obviously doesn't work. Added guards to only attempt
            seeking streams which are seekable.
        
        Show
        Charles Oliver Nutter added a comment - Fixed on master@30095bb. Perhaps someone can write up some tests for our repository, MRI's repository, or (ideally) RubySpec? commit 30095bb6455512e7e4e98ea0fd53d3b04f9921e2 Author: Charles Oliver Nutter <headius@headius.com> Date: Mon Mar 12 17:59:57 2012 -0500 Fix JRUBY-5222 Reopening a file descriptor on a socket doesn't work (gives "could not reopen: null (IOError)") The errors were all from trying to seek the socket as though it were a file, which obviously doesn't work. Added guards to only attempt seeking streams which are seekable.

          People

          • Assignee:
            Charles Oliver Nutter
            Reporter:
            Matthew Bloch
          • Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: