Mends.One

How do I execute a command, feed data to its stdin, and read from its stdout in MacRuby?

Pipe, Stdin, Stdout, Cocoa, Macruby

I'm attempting to execute a command, feed data to its stdin, and read from its stdout. I've tried using Ruby's Open3#popen3 as well as NSTask, exposed via MacRuby. The source for the program I'm writing is available here. I'm doing this in Xcode and MacRuby.

Here's some select code:

The entry point, just simply allowing me to easily switch between the two methods.

def do_gpg_cmd cmd
  do_gpg_cmd_nstask cmd
end

The ruby way, using Open3#popen3.

def do_gpg_cmd_ruby cmd
  gpg = "#{@gpg_path} --no-tty "
  cmd_output = ''
  logg "executing [#{cmd}]"
  Dispatch::Queue.concurrent.async do
    logg "new thread starting"
    Open3.popen3(gpg + cmd) do |stdin, stdout, stderr|
      stdin.write input_text
      stdin.close
      cmd_output = stdout.read
      output_text cmd_output
      stdout.close
      logg stderr.read
      stderr.close
    end
  end
  return cmd_output
end

In this approach, the application freezes (I'm testing by clicking the Sign button in the app, which runs gpg --clearsign --local-user $key).

When I kill the application, Xcode shows this in the thread diagnosic that automatically appears:

libsystem_kernel.dylib`__psynch_cvwait:
0x7fff84b390f0:  movl   $33554737, %eax
0x7fff84b390f5:  movq   %rcx, %r10
0x7fff84b390f8:  syscall
0x7fff84b390fa:  jae    0x7fff84b39101            ; __psynch_cvwait + 17 ; THIS LINE IS HIGHLIGHTED
0x7fff84b390fc:  jmpq   0x7fff84b3a4d4            ; cerror_nocancel
0x7fff84b39101:  ret    
0x7fff84b39102:  nop    
0x7fff84b39103:  nop  

The Cocoa way, using NSTask.

def do_gpg_cmd_nstask cmd
  Dispatch::Queue.concurrent.async do
    fcmd = "--no-tty " + cmd
    task = NSTask.alloc.init
    task.setLaunchPath(@gpg_path)
    task.setArguments(fcmd.split(" ") << nil)

    task.arguments.each {|a| puts "ARG: [#{a}]" }

    inpipe = NSPipe.pipe
    outpipe = NSPipe.pipe
    errpipe = NSPipe.pipe

    task.setStandardOutput(outpipe)
    task.setStandardInput(inpipe)
    task.setStandardError(errpipe)

    output = outpipe.fileHandleForReading
    errput = errpipe.fileHandleForReading
    input = inpipe.fileHandleForWriting

    task.launch

    input.writeData input_text.dataUsingEncoding(NSUTF8StringEncoding)
    input.closeFile

    outdata = output.readDataToEndOfFile
    errdata = errput.readDataToEndOfFile
    output.closeFile
    errput.closeFile
    outstring = NSString.alloc.initWithData(outdata, encoding: NSUTF8StringEncoding)
    errstring = NSString.alloc.initWithData(errdata, encoding: NSUTF8StringEncoding)

    output_text outstring
    logg errstring
  end
end

When I run this, I receive this error in the Xcode debug output. I'm obviously outputting the ARG parts myself as ultra dumb logging. The subprocess is not executed.

ARG: [--no-tty]
ARG: [--clearsign]
ARG: [--local-user]
ARG: [0xC2808780]
ARG: []
2013-03-12 23:27:39.305 GPGBoard[84924:3503] -[NSNull fileSystemRepresentation]: unrecognized selector sent to instance 0x7fff75b05310
*** Dispatch block exited prematurely because of an uncaught exception:
/Users/colin/Library/Developer/Xcode/DerivedData/GPGBoard-bradukgmaegxvmbukhwehepzyxcv/Build/Products/Debug/GPGBoard.app/Contents/Resources/AppDelegate.rb:81:in `block': NSInvalidArgumentException: -[NSNull fileSystemRepresentation]: unrecognized selector sent to instance 0x7fff75b05310 (RuntimeError)

I suspect that problems of either approach are mutually exclusive: the Open3#popen3 problem may be related to blocking read, while the problem with NSTask is related to a pipe problem.

0
C
Colin Dean
Jump to: Answer 1 Answer 2

Answers (2)

This piece of code works for me and prints out the files in the current directory:

framework "Cocoa"
task = NSTask.new
task.launchPath = "/bin/ls"
task.arguments = ["-l", "-a"]
stdoutPipe = NSPipe.pipe
task.standardOutput = stdoutPipe
task.launch
data = stdoutPipe.fileHandleForReading.readDataToEndOfFile
puts NSString.alloc.initWithData data, :encoding => NSASCIIStringEncoding

Now if I replace task.arguments = ["-l", "-a"] with task.arguments = "-l -a".split(" ") << nil I get the following error:

macruby[86209:707] -[NSNull fileSystemRepresentation]: unrecognized selector sent to instance 0x7fff77d6f310

So, I think your issue is task.setArguments(fcmd.split(" ") << nil). Change it to task.setArguments(fcmd.split(" ")) and you should no longer get the NSNull problem.

3
J
jtomschroeder

Comments:

Colin Dean said:
Thanks for answering. When I remove it, I get a message about something not being in the autozone. I read the code a bit more and decided to switch back to the regular-style hash (:sym=>:key instead of sym: :key) for the encoding argument to NSString#initWithData. Now, the behavior is the same as the fully Ruby version. I click the button and it just hangs. Have to Force Quit to get out of it.
Colin Dean said:
Actually, I had it still using the Ruby version. After switching back, I can actually get output from the command (yay!) but it hangs immediately after.
jtomschroeder said:
Depending on what the task is actually doing, you could try calling terminate
Colin Dean said:
How would I call terminate if the program appears to be hanging after the read?
Colin Dean said:
I dropped it in just before the read, as well as putting a 5 second sleep after it just to ensure that the program finished writing to the pipe. No dice. Edit: the Sign operation usually works now, but the Encrypt operation, which has a much larger output, seems still not working.

The problem is that the pipes have a buffer size. When the buffer is full, the write command blocks until the other end has read some data to make room for new data.

Your code first tries to write all the data to the command's stdin. Assuming the command does read some data, writes some output to stdout, then continues reading from its stdin. If there is much data being put through, some time the command's stdout pipe's buffer becomes full. The command blocks until someone reads data from the stdout pipe. However, your ruby code has not finished writing data to stdin yet, and continues to do so until the stdin pipe is full also. Now there is a dead-lock.

The solution is to write data to stdin and read data from stdout at the same time, either concurrently or simply block-wise (the block size not being larger than the pipe's buffer size.)

1
D
digory doo

Comments:

Colin Dean said:
Thanks! This has gotten me past a good bit, but I'm still not quite there. I encountered an issue after updating my code to try your strategy. Any additional pointers?
Colin Dean said:
After some more research, it looks like I'm blocked on this because rb_thread_create() is unimplemented as of rev 542dc07c.

Related Questions