swift Process() return values

Apple Recommended

Replies

Where did you get that run returns a string ?


AFAICT, run returns void.

The

which
command returns its result by printing
stdout
. The
run
method doesn’t return that; it actually returns nothing (
Void
) because the process executes asynchronously. To collect the output you have to do two things:
  • Wait for the process to finish

  • Collect its output

Doing this correctly is tricky for obscure low-level reasons. Pasted in below is some code I had lying around for this. If I run it like so:

try! launch(tool: URL(fileURLWithPath: "/usr/bin/which"), arguments: ["sips"]) { (status, outputData) in
    let output = String(data: outputData, encoding: .utf8) ?? ""
    print("done, status: \(status), output: \(output)")
}

it prints:

done, status: 0, output: /usr/bin/sips

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
func launch(tool: URL, arguments: [String], completionHandler: @escaping (Int32, Data) -> Void) throws {
    let group = DispatchGroup()
    let pipe = Pipe()
    var standardOutData = Data()

    group.enter()
    let proc = Process()
    proc.executableURL = tool
    proc.arguments = arguments
    proc.standardOutput = pipe.fileHandleForWriting
    proc.terminationHandler = { _ in
        proc.terminationHandler = nil
        group.leave()
    }

    group.enter()
    DispatchQueue.global().async {
        // Doing long-running synchronous I/O on a global concurrent queue block
        // is less than ideal, but I’ve convinced myself that it’s acceptable
        // given the target ‘market’ for this code.

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        pipe.fileHandleForReading.closeFile()
        DispatchQueue.main.async {
            standardOutData = data
            group.leave()
        }
    }

    group.notify(queue: .main) {
        completionHandler(proc.terminationStatus, standardOutData)
    }

    try proc.run()

    // We have to close our reference to the write side of the pipe so that the
    // termination of the child process triggers EOF on the read side.

    pipe.fileHandleForWriting.closeFile()
}
  • @eskimo When pipe.fileHandleForWriting.closeFile() is called, the process exits with singal 9 (SIGKILL). When I try to acquire the file handle manually, the NSException thrown says [NSConcreteFileHandle fileDescriptor]: Bad file descriptor.

    So what's up with that last line?

  • Same issue there

Add a Comment

Wow! Thanks Eskimo.


I intended to use 'which' to locate sips as a convenience if other OS's had it somewhere other than /usr/bin/ , which may not be true. I could just continue to stipulate that the user provide it there. Since 'which' itself must be in /usr/bin , I guess I'm OK with that stipulation.


Nevertheless, your answer taught me what I sought, and more. Should I execute a Process() in the future, that prints to stdout, I'll know how to do it.


Thanks again.

Someone asked me for an Objective-C version of the code I posted upthread and so here it is.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Code Block
static BOOL launchToolWithArgumentsError(
NSURL * _Nonnull url,
NSArray<NSString *> * _Nonnull arguments,
void (^completionHandler)(int status, NSData * _Nonnull outputData),
NSError * _Nullable * _Nullable errorPtr
) {
dispatch_group_t group = dispatch_group_create();
NSPipe * pipe = [NSPipe pipe];
NSMutableData * standardOutData = [NSMutableData data];
dispatch_group_enter(group);
NSTask * task = [[NSTask alloc] init];
task.executableURL = url;
task.arguments = arguments;
task.standardOutput = pipe.fileHandleForWriting;
task.terminationHandler = ^(NSTask * taskB) {
taskB.terminationHandler = nil;
dispatch_group_leave(group);
};
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
// Doing long-running synchronous I/O on a global concurrent queue block
// is less than ideal, but I’ve convinced myself that it’s acceptable
// given the target ‘market’ for this code.
NSData * newData = [pipe.fileHandleForReading readDataToEndOfFile];
[pipe.fileHandleForReading closeFile];
dispatch_async(dispatch_get_main_queue(), ^{
[standardOutData appendData:newData];
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
completionHandler(task.terminationStatus, standardOutData);
});
BOOL result = [task launchAndReturnError:errorPtr];
// We have to close our reference to the write side of the pipe so that the
// termination of the child process triggers EOF on the read side.
[pipe.fileHandleForWriting closeFile];
return result;
}

So what's up with that last line?

What’s up with the last line is pretty well explained by the comment. I’m not sure why it’s crashing for you; the code I posted continues to work for me [1]. How are you testing this?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] On macOS 11.4, building with Xcode 12.5, and testing it like so:

let tool = URL(fileURLWithPath: "/bin/echo")
try! launch(tool: tool, arguments: ["Hello", "Cruel", "World!"]) { status, output in
    print(status, output)
}

Same issue for me

Crashed Thread:        24  Dispatch queue: com.apple.root.utility-qos

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

Application Specific Information:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '*** -[NSConcreteFileHandle availableData]: Bad file descriptor'
terminating with uncaught exception of type NSException
abort() called

macOS 11.4 Xcode 12.5.1

Same issue for me

Weird. Your setup is almost identical to mine, so it’s not obvious why you’re seeing the problem while I’m not. My advice is that either you or eaigner open a DTS tech support incident so that I can allocate more time to dig into this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Also I'm wondering why we are setting pipe.fileHandleForWriting in process output instead of pipe.fileHandleForReading in case if we just reading ?

Strings still return empty for me. If I do a search for files such as ls or which itself it returns them just fine. If I search for avr-gcc it returns empty as it has been doing for me all day. I can use "which avr-gcc" just fine from the terminal but in my swift code it returns nothing.

Edit: By that same token, it seems Whereis works for common programs in terminal but it returns nothing for apps like avr-gcc or anothers added via homebrew. I'm not sure the significance of that

VitaliiKizlov wrote:

Also I'm wondering why we are setting pipe.fileHandleForWriting in process output instead of pipe.fileHandleForReading in case if we just reading ?

Yeah, that’s confusing. A pipe creates two file handles, one for reading and one for writing. You need to pass the one for writing to the child process’s stdout and then read its output via the one for reading.

Having said that, my new advice, as shown by the code in Running a Child Process with Standard Input and Output, is to pass the Pipe itself to Process, so it can pick up the correct file handle based on the direction of the I/O.


kelvin75 wrote:

Strings still return empty for me.

Let’s tackle this issue on the thread you created for it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hey eskimo 🙂 Big thanks to your new solution 🙏 Maybe you can help me how to correctly add progress while child process is running ?

Maybe you can help me how to correctly add progress while child process is running ?

Sure. How about you start a new thread with the details? Use the same tags as this thread so that I see it go by.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"