0
votes

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.

2

2 Answers

3
votes

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.

1
votes

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