NSTask's real-time output
Asked Answered
R

2

20

I have a PHP script which has mutliple sleep() commands. I would like to execute it in my application with NSTask. My script looks like this:

echo "first\n"; sleep(1); echo "second\n"; sleep(1); echo "third\n";

I can execute my task asynchronously using notifications:

- (void)awakeFromNib {
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath: @"/usr/bin/php"];

    NSArray *arguments;
    arguments = [NSArray arrayWithObjects: @"-r", @"echo \"first\n\"; sleep(1); echo \"second\n\"; sleep(1); echo \"third\n\";", nil];
    [task setArguments: arguments];

    NSPipe *p = [NSPipe pipe];
    [task setStandardOutput:p];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskExited:) name:NSTaskDidTerminateNotification object:task];

    [task launch];

}

- (void)taskExited:(NSNotification *)notif {
    NSTask *task = [notif object];
    NSData *data = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile];
    NSString *str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
    NSLog(@"%@",str);
}

My output is (after 2 seconds, of course):

2011-08-03 20:45:19.474 MyApp[3737:903] first
second
third

My question is: how can I get theese three words immediately after they are printed?

Ridings answered 3/8, 2011 at 18:55 Comment(0)
P
23

You can use NSFileHandle's waitForDataInBackgroundAndNotify method to receive a notification when the script writes data to its output. This will only work, however, if the interpreter sends the strings immediately. If it buffers output, you will get a single notification after the task exits.

- (void)awakeFromNib {
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath: @"/usr/bin/php"];

    NSArray *arguments;
    arguments = [NSArray arrayWithObjects: @"-r", @"echo \"first\n\"; sleep(1); echo \"second\n\"; sleep(1); echo \"third\n\";", nil];
    [task setArguments: arguments];

    NSPipe *p = [NSPipe pipe];
    [task setStandardOutput:p];
    NSFileHandle *fh = [p fileHandleForReading];
    [fh waitForDataInBackgroundAndNotify];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedData:) name:NSFileHandleDataAvailableNotification object:fh];

    [task launch];

}

- (void)receivedData:(NSNotification *)notif {
    NSFileHandle *fh = [notif object];
    NSData *data = [fh availableData];
    if (data.length > 0) { // if data is found, re-register for more data (and print)
        [fh waitForDataInBackgroundAndNotify];
        NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@" ,str);
    }
}
Prank answered 3/8, 2011 at 19:6 Comment(6)
I tried that, but it logs only "first". "second" and "third" are logged on task's termination...Ridings
Solved it by placing extra readInBackgroundAndNotify in -receiveData. Thanks for the idea!Ridings
I updated the code after struggling for a while. You needed another waitForDataInBackgroundAndNotify, not readInBackgroundAndNotify.Desirable
Tried this code snippet with find command which creates long result and the snippet required an extra [fh waitForDataInBackgroundAndNotify] in order to get the complete data. Hope this helps beginners like me.Nonillion
I tried using this code in Swift (below) in iOS, but interestingly, I only get the call back in real time when I have my app attached to the debugger... If I run it stand-alone, then I only get 1 call at the end with everything instead of a nice stream in real time.. Any idea why?Cb
@Cb most likely the debugger changes buffering settings on one end or both. Probably something you can configure but I don’t know how off the top of my head.Prank
M
10

For reference, here is ughoavgfhw's answer in swift.

override func awakeFromNib() {
    // Setup the task
    let task = NSTask()
    task.launchPath = "/usr/bin/php"
    task.arguments = ["-r", "echo \"first\n\"; sleep(1); echo \"second\n\"; sleep(1); echo \"third\n\";"]

    // Pipe the standard out to an NSPipe, and set it to notify us when it gets data
    let pipe = NSPipe()
    task.standardOutput = pipe
    let fh = pipe.fileHandleForReading
    fh.waitForDataInBackgroundAndNotify()

    // Set up the observer function
    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.addObserver(self, selector: "receivedData:", name: NSFileHandleDataAvailableNotification, object: nil)

    // You can also set a function to fire after the task terminates
    task.terminationHandler = {task -> Void in
           // Handle the task ending here
    }

    task.launch()
}

func receivedData(notif : NSNotification) {
    // Unpack the FileHandle from the notification
    let fh:NSFileHandle = notif.object as NSFileHandle
    // Get the data from the FileHandle
    let data = fh.availableData
    // Only deal with the data if it actually exists
    if data.length > 1 {
    // Since we just got the notification from fh, we must tell it to notify us again when it gets more data
        fh.waitForDataInBackgroundAndNotify()
        // Convert the data into a string
        let string = NSString(data: data, encoding: NSASCIIStringEncoding)
        println(string!)
    }
}

This construct will be necessary if your task produces lots of output into the pipe. Simply calling pipe.fileHandleForReading.readDataToEndOfFile() will not work because the task is waiting for the pipe to be emptied so it can write more while your program is waiting for the end of the data. Thus, your program will hang. This notification and observer construct allows the pipe to be read asynchronously and thus prevents the aforementioned stalemate.

Messiaen answered 3/10, 2014 at 3:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.