1

I'm using NSTask to run an external utility which returns a long string of data. The problem is that when the returned string exceeds a large amount of data (around 32759 chars) it becomes null or truncates the returned string. How do I return the full output?

NSTask *myTask = [[NSTask alloc] init];

[myTask setLaunchPath:myExternalCommand];
[myTask setArguments:[NSArray arrayWithObjects: arg1, arg2, nil]];

NSPipe *pipe = [NSPipe pipe];
[myTask setStandardOutput:pipe];

NSFileHandle *taskHandle;
taskHandle = [pipe fileHandleForReading];

[myTask launch];
[myTask waitUntilExit];

NSData *taskData;
taskData = [taskHandle readDataToEndOfFile];

NSString *outputString = [[NSString alloc] initWithData:taskData
                         encoding:NSUTF8StringEncoding];

NSLog(@"Output: \n%@", outputString);
// (null or truncated) when stdout exceeds x amount of stdout

To test the functionality use cat or similar on a large file for the myExternalCommand. The issue seems to happen right after the character length of 32759...

solution? I'm not sure, but what might need to happen is to somehow read the return stdout in chunks, then append the outputString data if possible.

update: I tried moving waitUntilExit after readDataToEndOfFile per suggestion, but it did not affect the outcome.

*please note, I'm looking for an Obj-C solution, thanks.

Joe Habadas
  • 628
  • 8
  • 21
  • Read the data before waiting for the task to exit. – Ken Thomases Mar 25 '19 at 04:23
  • @KenThomases: Hi Ken, so you are meaning put `[myTask waitUntilExit];` at the end of the current function instead? thanks! – Joe Habadas Mar 25 '19 at 05:22
  • Well, it just should be after the call to `-readDataToEndOfFile`. If it works, I'll write up a full answer with an explanation of what's going on. – Ken Thomases Mar 25 '19 at 05:37
  • Hi Ken, unfortunately it didn't change the outcome. I think what needs to happen is to somehow allow NSTask to read chunks of the `stdout` and append the data, although I'm not sure how or if that is possible. – Joe Habadas Mar 25 '19 at 06:54
  • Have you tried logging `taskData` instead of the string you try to create from it? I'm guessing you've got it all but it's not valid UTF-8 data so the attempt to create a string from it fails. – Ken Thomases Mar 25 '19 at 15:03
  • @KenThomases: I just logged `taskData` and it only returns 29120 characters (as hex bytes), if I covert those bytes to `Ascii` it is equal to 14560 characters. I'm testing it without any special characters just `A-Z`, so should be okay in regard to `UTF-8`, thanks. – Joe Habadas Mar 25 '19 at 16:55

2 Answers2

4

Found on CocoaDev:

“The data that passes through the pipe is buffered; the size of the buffer is determined by the underlying operating system.”

from: http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSPipe_Class/index.html

The NSPipe buffer limit seems to be 4096 bytes (cf. /usr/include/limits.h: “… #define _POSIX_ARG_MAX 4096 …”)

You can read the output from your NSTask asynchronously, using readabilityHandler. Within the handler, use availableData to read the output piece-by-piece.

Use a terminationHandler to get notified once the task exits, and then set your readabilityHandler to nil to stop it from reading.

It's all async, so you'll need to block and wait until the task exits.

Here is a complete sample that works well enough for me. I used a printf instead of NSLog as it seems that NSLog is truncating the output on the console (not sure if that's a bug or a feature). Error checking is omitted and adds some complexity, you will probably want to read standardError as well in the same way.

dispatch_semaphore_t waitHandle;
NSTask *myTask;
NSMutableData* taskOutput;
        
waitHandle = dispatch_semaphore_create(0);
        
myTask = [[NSTask alloc] init];
[myTask setLaunchPath:@"/bin/cat"];
[myTask setArguments:[NSArray arrayWithObjects: @"/path/to/a/big/file", nil]];
[myTask setStandardOutput:[NSPipe pipe]];
        
taskOutput = [[NSMutableData alloc] init];
        
[[myTask.standardOutput fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
    NSData *data = [file availableData];
    [taskOutput appendData:data];
}];
        
[myTask setTerminationHandler:^(NSTask *task) {
    [task.standardOutput fileHandleForReading].readabilityHandler = nil;
            
    NSString *outputString = [[NSString alloc] initWithData:taskOutput encoding:NSUTF8StringEncoding];
    printf("Output: \n%s\n", [outputString UTF8String]);
            
    dispatch_semaphore_signal(waitHandle);
}];
        
[myTask launch];
        
dispatch_semaphore_wait(waitHandle, DISPATCH_TIME_FOREVER);
Community
  • 1
  • 1
TheNextman
  • 12,428
  • 2
  • 36
  • 75
  • See this answer: https://stackoverflow.com/a/40283623/233944. Since macOS 10.12 / iOS 10, `NSLog` delegates to the new unified logging, which restricts messages to 1024 characters. For anything longer than that, Apple thinks you should use your own logging system. – TheNextman Mar 27 '19 at 03:28
  • Hi, thanks for the answer. It appears that my original method works also if I use `printf`, so it seems the `NSLog` limitation is one of the main issues. I was also getting `(null)` when binary data was > `4096` so I'll need to test out your code to see if that resolves things completely. Do you have any idea why NSLog has the limitation like it has? - it seems odd that a logging function wouldn't log (everything). – Joe Habadas Mar 27 '19 at 03:29
  • sorry, I deleted my last comment by accident, but copied/pasted it again. Would you know if it's possible to stream stdout into the `NSTask` pipe almost continously? Just curious, because that might be useful too. – Joe Habadas Mar 27 '19 at 03:32
  • The readabilityHandler will be called whenever there's something to read; it's as close to a continuous stream as you could ask for I think. – TheNextman Mar 27 '19 at 03:38
  • 1
    It appears your example is much more efficient from the quick testing I did. It seems to use less cpu and memory consumption, thank you, much appreciated. – Joe Habadas Mar 27 '19 at 03:53
0

I had a task that was throwing a large error immediately, and causing a hang, adding a reader to the stderr solved the issue

[[myTask.standardError fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
    NSData *data = [file availableData];
    [taskError appendData:data];
}];
Klajd Deda
  • 355
  • 2
  • 7