Wednesday, May 13, 2009

fork and exec on the JVM? JRuby to the Rescue!

Today David R. MacIver pinged me in #scala and asked "headius: Presumably you guys have spent quite a lot of time trying to make things like system("vim") work correctly in JRuby and failing? i.e. I'm probably wasting my time to attempt similar?"

My first answer was "yes", since there's no direct way to exec a program like vim (which wants a real terminal) and have it work on the JVM. The JVM's process launching gives the newly-spawned processes the child side of piped streams, which you then have to manually pump (which is what we do in JRuby's system, backtick, and exec methods). Under these circumstances, vim may start up, but it's certainly not functional.

But then I got to thinking...if you were doing this in C, you'd fork+exec and all would be happy. But we can't fork+exec on the JVM..OR CAN WE?

As you should know by now, JRuby ships with FFI, a library that allows you to bind any arbitrary C function in Ruby code. So getting fork+exec to work was a simple matter of writing a little Ruby code:
require 'ffi'

module Exec
extend FFI::Library

attach_function :my_exec, :execl, [:string, :string, :varargs], :int
attach_function :fork, [], :int
end

vim1 = '/usr/bin/vim'
vim2 = 'vim'
if Exec.fork == 0
Exec.my_exec vim1, vim2, :pointer, nil
end

Process.waitall

Running that with JRuby (I tried master, David tried 1.3.0RC1, and 1.2.0 works too) brings up a full-screen vim session, just like you'd expect, and it all just works. No other JVM language can do this so quickly and easily.

We'll probably try to generalize this into an optional library JRubyists can load (require 'jruby/real_exec' or similar) and perhaps add fork and exec to jna-posix so that the other JVM languages can have sweet, sweet process launching too.

JRuby rocks.

Update: The biggest problem with using fork+exec in this way is that you can't guarantee *nothing* happens between the fork call and the exec call. If, for example, the JVM decides to GC or move memory around, you can have a fatal crash at the JVM process level. Because of that, I don't recommend using fork + exec via FFI in JRuby, even though it's pretty cool.

However, since this post I've learned of the "posix_spawn" function available on most Unix variants. It's basically fork + exec in a single function, plus most of the typical security and IO tweaks you might do after forking and before execing. It's definitely my recommended alternative to fork+exec for JRuby, and to make that easier I've bundled it up as the "spoon" gem (gem install spoon) which provides spawn and spawnp to JRuby users directly. Here's an example session using Spoon to launch JRuby as a daemon. If you just need fork+exec on the JVM, posix_spawn or the Spoon gem are the best way to do it.

16 comments:

  1. Will this be used for system and akin in future versions?

    ReplyDelete
  2. Konstantin: Maybe. Currently we do a bit of magic to make sure that if you system or exec something like 'ruby blah blah blah' it will just start another JRuby instance in the same process. But it may be desirable for other cases to actually fork+exec so that e.g. terminal applications work correctly.

    ReplyDelete
  3. Why make this a separate library? Just integrate it into Kernel.

    If you can add fork & exec, can you add setsid as well? And if so, well, true background processes for JRuby. :)

    ReplyDelete
  4. @overtheline --

    The point is, with a real fork/exec you don't need to tend the pipes like you would with Runtime.exec.

    -- MarkusQ

    ReplyDelete
  5. @overtheline: How about you have a try getting the example use case (launch vim in a way that inherits the console STDIN and STDOUT) to work with Runtime.exec and report back on how it goes? I'll be delighted if you can figure out a way to make it work, but I'm pretty sure you won't.

    Runtime.exec is very very limited in what it allows. This is amongst the many examples where it falls down.

    ReplyDelete
  6. Sun was very serious about "run anywhere" Most of this discussion is about what was not done on Unix. But since most of this discussion assumes Unix, it's sort of beside the point to harsh java for something it was NOT trying to do, ie expose unix. Java is by it's nature a least common denominator. Love it or leave it. But at least understand it before you complain.

    ReplyDelete
  7. @Anonymous - I understand it, and it doesn't run everywhere. That strategy always ends in failure. They could have recognized this and standardized optional libraries for the most popular server OS in the world... unix.... which sun *also* had a sabre rattling interest in.

    How many of you remember the Java OS? Seriously, not supporting unix/posix APIs just reinforces that old Bill Joy quote ... "there are more smart people outside the company than in it"

    ReplyDelete
  8. Daniel,

    To add setsid(), you would do this:

    attach_function :setsid, [], :int

    in the natural place, then you'll have Exec.setsid.

    ReplyDelete
  9. Awesome job Charles. JRuby really is awesome.

    ReplyDelete
  10. You JRruby guys are superb! The work you've been doing makes everyones life lot easier. JRuby has always been such a great project and now this! Thank you!!

    ReplyDelete
  11. Jan Berkel: We should probably talk about it more, make sure we've got our ducks in a row. But it works fine for me on OS X, so file a bug or pop on IRC and we'll figure out why it's not working for you.

    ReplyDelete
  12. Making subprocessess reliable is hard.
    E.g. I think you need to close
    file descriptors in the child
    before you exec, to avoid some
    deadlocks.

    I'm surprised that you can execute ruby
    code after the fork(); I expect that
    the JVM itself
    should not be fully functional then.
    What happens if you gc after fork()?

    ReplyDelete
  13. Is this going to be solved with the new ProcessBuilder.Redirect addition (Redirect.INHERIT for example) in JDK7?

    ReplyDelete
  14. Anonymous (immediately above): Very likely...I have not had a lot of time to look at NIO2 stuff, but there's a good chance it will solve many of our current issues, including this sort of thing.

    ReplyDelete
  15. I assume that using fork with jruby is discouraged?

    ReplyDelete
  16. Is it possible to use spoon to replace something like the below or would I need to replace the `exec` call?

    https://gist.github.com/2907212

    ```
    def run
    exec 'some_command'
    end

    def run_in_background
    pid=fork do
    run
    end
    end
    ```

    ReplyDelete