Search code examples
rubypopen3

Kill a process called using open3 in ruby


I'm using a command line program, it works as mentioned below:

$ ROUTE_TO_FOLDER/app < "long text"

If "long text" is written using the parameters "app" needs, then it will fill a text file with results. If not, it will fill the text file with dots continuously (I can't handle or modify the code of "app" in order to avoid this).

In a ruby script there's a line like this:

text = "long text that will be used by app"
output = system("ROUTE_TO_FOLDER/app < #{text}")

Now, if text is well written, there won't be problems and I will get an output file as mentioned before. The problem comes when text is not well written. What happens next is that my ruby script hangs and I'm not sure how to kill it.

I've found Open3 and I've used the method like this:

irb> cmd = "ROUTE_TO_FOLDER/app < #{text}"
irb> stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
=> [#<IO:fd 10>, #<IO:fd 11>, #<IO:fd 13>, #<Thread:0x007f3a1a6f8820 run>]

When I do:

irb> wait_thr.value

it also hangs, and :

irb> wait_thr.status
=> "sleep"

How can I avoid these problems? Is it not recognizing that "app" has failed?


Solution

  • wait_thr.pid provides you the pid of the started process. Just do

    Process.kill("KILL",wait_thr.pid)
    

    when you need to kill it.

    You can combine it with detecting if the process is hung (continuously outputs dots) in one of the two ways.

    1) Set a timeout for waiting for the process:

    get '/process' do
      text = "long text that will be used by app"
      cmd = "ROUTE_TO_FOLDER/app < #{text}"
      Open3.popen3(cmd) do |i,o,e,w|
        begin
          Timeout.timeout(10) do # timeout set to 10 sec, change if needed
            # process output of the process. it will produce EOF when done.
            until o.eof? do
              # o.read_nonblock(N) ...
            end
          end
        rescue Timeout::Error
          # here you know that the process took longer than 10 seconds
          Process.kill("KILL", w.pid)
          # do whatever other error processing you need
        end
      end
    end
    

    2) Check the process output. (The code below is simplified - you probably don't want to read the output of your process into a single String buf first and then process, but I guess you get the idea).

    get '/process' do
      text = "long text that will be used by app"
      cmd = "ROUTE_TO_FOLDER/app < #{text}"
      Open3.popen3(cmd) do |i,o,e,w|
        # process output of the process. it will produce EOF when done. 
        # If you get 16 dots in a row - the process is in the continuous loop
        # (you may want to deal with stderr instead - depending on where these dots are sent to)
        buf = ""
        error = false
        until o.eof? do
          buf << o.read_nonblock(16)
          if buf.size>=16 && buf[-16..-1] == '.'*16
            # ok, the process is hung
            Process.kill("KILL", w.pid)
            error = true
            # you should also get o.eof? the next time you check (or after flushing the pipe buffer),
            # so you will get out of the until o.eof? loop
          end
        end
        if error
          # do whatever error processing you need
        else
          # process buf, it contains all the output
        end
      end
    end