Thursday, May 14, 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.

19 comments:

Konstantin Haase said...

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

Charles Oliver Nutter said...

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.

Daniel Berger said...

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. :)

Anonymous said...

Dont you realize that java Runtime.exec does a fork exec on unix systems? whats your point? Cant you call Runtime.exec from jRuby or what?

This seems idiotic.

Anonymous said...

@overtheline --

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

-- MarkusQ

Patrick Mueller said...

overtheline, the Runtime.exec() stuff in Java isn't terribly precise, compared to the options you have with fork/exec, and also has issues dealing with the streams, and even determining when the process completes - I wrote about this a while back.

Unknown said...
This comment has been removed by the author.
Unknown said...

Charles, this is great work!
I was just researching this the other day. Glad you posted this solution.

Java definitely made some huge mistakes. Trying to hide/ignore the Unix API was one of them. Even stranger considering this was from Sun.

Now maybe if they can make the VM startup really fast, the JVM could finally dent all the scripting languages.

Anyways, what you guys are doing with JRuby is just plain great. Thank you!

David R. MacIver said...

@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.

Anonymous said...

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.

Unknown said...

@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"

Hiro said...

Daniel,

To add setsid(), you would do this:

attach_function :setsid, [], :int

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

Greg said...

Awesome job Charles. JRuby really is awesome.

Nizar Jouini said...

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!!

Charles Oliver Nutter said...

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.

Jan Berkel said...

JRUBY-3665

Anonymous said...

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()?

Anonymous said...

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

Charles Oliver Nutter said...

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.