fredag, september 22, 2006

The difference between Kernel#` and Kernel#system

Today I had a fun learning experience. It cost me several hours of work, so I will post a small notice about it here so Google can make other developers lives easier. Or maybe I'm the only one who did this mistake.

Anyway. What I was trying to do was to start an external Ruby script (from another Ruby script). This other Ruby script went daemon, but since I didn't want to install the daemonize package (another bad decision, probably), I just wrote the script in question to fork and detach. Now, I have condensed the question a little, to this Ruby script:
 `ruby -e'if pid=fork; Process.detach(pid); else; sleep(5); end'`
Everyone please raise their hands if it is obvious that this script will sleep for 5 seconds before giving back my prompt. It wasn't obvious for me, since it was a long time I did UNIX System programming.

For those who still want to know, the problem is that backtick binds to the started process' STDIN, STDOUT and STDERR. As long as STDOUT is live, backtick will wait. And since the forking and detaching doesn't redirect all the STD* streams, this will wait until both processes has finished.

There are two ways to fix this. One right way, and one fast way. The right way is to detach the rebind the streams after forking. This can easily be done with this code:
 STDIN.reopen('/dev/null')
STDOUT.reopen('/dev/null')
STDERR.reopen('/dev/null')
The faster way is to replace backtick with a system call. Since system isn't interested in the output from the process, it will not bind those streams. So just running this instead, will work:
 system "ruby -e'if pid=fork; Process.detach(pid); else; sleep(5); end'"
I have learned the lesson. I have bought a copy of the Stevens book. (UNIX Network Programming, which detail the interaction between fork and ports, which was what my original problem was about.)

3 kommentarer:

Anonym sa...

What about doing something like `ruby ruby_script &`

You lose any control over the script, but it will run in the background and return right away.

Ola Bini sa...

Yeah, I tried that too. Won't work. The backtick starts a shell and then runs the command in that. Ruby binds the STDOUT of the _shell_ not the process inside the shell, which means that will get bound too in the external process. Try it and see. =)

Dominic Mitchell sa...

I really, really recommend Steven's Advanced Programming in the Unix Environment. There's a newer version out recently. It covers this sort of thing in great detail.

I completely sympathise with this problem. It bit me too recently in a Perl script I was writing...

I did notice the daemons gem today. Calling daemonize in the child process is probably the correct solution in the long run.