tags:

views:

213

answers:

3

I'm looking to expose an interactive command line program via JSON or another RPC style service using Ruby. I've found a couple tricks to do this, but im missing something when redirecting the output and input.

One method at least on linux is to redirect the stdin and stdout to a file then read and write to that file asynchronously with file reads and writes. Another method ive been trying after googling around was to use open4. Here is the code I wrote so far, but its getting stuck after reading a few lines from standard output.

require "open4"
include Open4

status = popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6") do |pid, stdin, stdout, stderr|
  puts "PID #{pid}"
  lines=""
  while (line=stdout.gets)
    lines+=line
    puts line
  end
  while (line=stderr.gets)
    lines+=line
    puts line
  end
end

Any help on this or some insight would be appreciated!

A: 

What I would recommend is using Xinetd (or similar) to run the command on some socket and then using the ruby network code. One of the problems you've already run into in your code here is that your two while loops are sequential, which can cause problems.

Joshua Smith
A: 

EDIT

Your should consider integrating with AnyTerm. You can then either expose AnyTerm directly e.g. via Apache mod_proxy, or have your Rails controller act as the reverse proxy (handling authentication/session validation, then playing back controller.request minus any cookies to localhost:<AnyTerm-daemon-port>, and sending back as a response whatever AnyTerm replies with.)

class ConsoleController < ApplicationController
  # AnyTerm speaks via HTTP POST only
  def update
    # validate session
    ...
    # forward request to AnyTerm
    response = Net::HTTP.post_form(URI.parse('http://localhost:#{AnyTermPort}/', request.params))
    headers['Content-Type'] = response['Content-Type']
    render_text response.body, response.status
  end

Otherwise, you'd need to use IO::Select or IO::read_noblock to know when data is available to be read (from either network or subprocess) so you don't deadlock. See this too. Also check that either your Rails is used in a multi-threaded environment or that your Ruby version is not affected by this IO::Select bug.

You can start with something along these lines:

status = POpen4::popen4("ping localhost") do |stdout, stderr, stdin, pid|  
  puts "PID #{pid}"  
  # our buffers 
  stdout_lines="" 
  stderr_lines=""
  begin
    loop do
      # check whether stdout, stderr or both are 
      #  ready to be read from without blocking 
      IO.select([stdout,stderr]).flatten.compact.each { |io|
        # stdout, if ready, goes to stdout_lines 
        stdout_lines += io.readpartial(1024) if io.fileno == stdout.fileno 
        # stderr, if ready, goes to stdout_lines 
        stderr_lines += io.readpartial(1024) if io.fileno == stderr.fileno 
      }
      break if stdout.closed? && stderr.closed? 
      # if we acumulated any complete lines (\n-terminated) 
      #  in either stdout/err_lines, output them now 
      stdout_lines.sub!(/.*\n/m) { puts $& ; '' } 
      stderr_lines.sub!(/.*\n/m) { puts $& ; '' } 
    end
  rescue EOFError
    puts "Done"
  end 
end 

To also handle stdin, change to:

      IO.select([stdout,stderr],[stdin]).flatten.compact.each { |io|
        # program ready to get stdin? do we have anything for it?
        if io.fileno == stdin.fileno && <got data from client?>
          <write a small chunk from client to stdin>
        end
        # stdout, if ready, goes to stdout_lines 

Cheers, V.

vladr
I get an error with this example: 13:in `select': can't convert Fixnum into IO (TypeError)
Aaron
your popen4 version may be different than mine; check the order of stdout, stderr, pid etc. in `... do |stdout, stderr, stdin, pid|` with respect to your popen4 documentation.
vladr
Tried your fix and it works, but this doesnt solve the input issue either. I'd basically like to have a command prompt while the stdout/err is still going so I can give input.
Aaron
See the latest edit re. performing `select` on `stdin`; also check out AnyTerm.
vladr
A: 

Another trick you might try is to re-direct stderr to stdout in your command, so that your program only has to read the stdout. Something like this:

popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6 2>&1")

The other benefit of this is that you get all the messages/errors in the order they happen during the program run.

Ron Savage
I tried that, but I also want to keep an interactive console with this solution. Its a really difficult problem to solve and I've tried many solutions.
Aaron