How to read all remaining output of readInBackgroundAndNotify after NSTask has ended? - macos

I'm invoking various command line tools via NSTask. The tools may run for several seconds, and output text constantly to stdout. Eventually, the tool will terminate on its own. My app reads its output asynchronously with readInBackgroundAndNotify.
If I stop processing the async output as soon as the tool has exited, I will often lose some of its output that hasn't been delivered by then.
Which means I have to wait a little longer, allowing the RunLoop to process pending read notifications. How do I tell when I've read everything the tool has written to the pipe?
This problem can be verified in the code below by removing the line with the runMode: call - then the program will print that zero lines were processed. So it appears that at the time the process has exited, there's already a notification in the queue that is waiting to be delivered, and that delivery happens thru the runMode: call.
Now, it might appear that simply calling runMode: once after the tool's exit may be enough, but my testing shows that it isn't - sometimes (with larger amounts of output data), this will still only process parts of the remaining data.
Note: A work-around such as making the invoked tool outout some end-of-text marker is not a solution I seek. I believe there must be some proper way to do this, whereby the end of the pipe stream is signalled somehow, and that's what I'm looking for in an answer.
Sample Code
The code below can be pasted into a new Xcode project's AppDelegate.m file.
When run, it invokes a tool that generates some longer output and then waits for the termination of the tool with waitUntilExit. If it would then immediately remove the outputFileHandleReadCompletionObserver, most of the tool's output would be missed. By adding the runMode: invocation for the duration of a second, all output from the tool is received - Of course, this timed loop is less than optimal.
And I would like to keep the runModal function synchronous, i.e. it shall not return before it has received all output from the tool. It does run in its own tread in my actual program, if that matters (I saw a comment from Peter Hosey warning that waitUntilExit would block the UI, but that would not be an issue in my case).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[self runTool];
}
- (void)runTool
{
// Retrieve 200 lines of text by invoking `head -n 200 /usr/share/dict/words`
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", #"200", #"/usr/share/dict/words"];
__block int lineCount = 0;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
id <NSObject> outputFileHandleReadCompletionObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait one more second so that we can process any remaining output from the tool
NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1];
while ([NSDate.date compare:endDate] == NSOrderedAscending) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
[[NSNotificationCenter defaultCenter] removeObserver:outputFileHandleReadCompletionObserver];
NSLog(#"Lines processed: %d", lineCount);
}

It's quite simple. In the observer block when data.length is 0 remove the observer and call terminate.
The code will continue after the waitUntilExit line.
- (void)runTool
{
// Retrieve 20000 lines of text by invoking `head -n 20000 /usr/share/dict/words`
const int expected = 20000;
NSTask *theTask = [[NSTask alloc] init];
theTask.qualityOfService = NSQualityOfServiceUserInitiated;
theTask.launchPath = #"/usr/bin/head";
theTask.arguments = #[#"-n", [#(expected) stringValue], #"/usr/share/dict/words"];
__block int lineCount = 0;
__block bool finished = false;
NSPipe *outputPipe = [NSPipe pipe];
theTask.standardOutput = outputPipe;
NSFileHandle *outputFileHandle = outputPipe.fileHandleForReading;
NSString __block *prevPartialLine = #"";
[[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleReadCompletionNotification object:outputFileHandle queue:nil usingBlock:^(NSNotification * _Nonnull note)
{
// Read the output from the cmdline tool
NSData *data = [note.userInfo objectForKey:NSFileHandleNotificationDataItem];
if (data.length > 0) {
// go over each line
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *lines = [[prevPartialLine stringByAppendingString:output] componentsSeparatedByString:#"\n"];
prevPartialLine = [lines lastObject];
NSInteger lastIdx = lines.count - 1;
[lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx == lastIdx) return; // skip the last (= incomplete) line as it's not terminated by a LF
// now we can process `line`
lineCount += 1;
}];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleReadCompletionNotification object:nil];
[theTask terminate];
finished = true;
}
[note.object readInBackgroundAndNotify];
}];
NSParameterAssert(outputFileHandle);
[outputFileHandle readInBackgroundAndNotify];
// Start the task
[theTask launch];
// Wait until it is finished
[theTask waitUntilExit];
// Wait until all data from the pipe has been received
while (!finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(#"Lines processed: %d (should be: %d)", lineCount, expected);
}

The problem with waitUntilExit is that it doesn't always behave the way one might think. The following is mentioned in the documenation:
waitUntilExit does not guarantee that the terminationHandler
block has been fully executed before waitUntilExit returns.
It appears this is precisely the problem you are having; it's a race condition. The waitUntilExit is not waiting long enough and the lineCount variable is reached before the NSTask completes. The solution would likely be to use a semaphore or dispatch_group, although it's unclear if you want to go that route — this is not an easy problem to resolve it seems.
*I experienced a similar issue from months back that still isn't resolved unfortunately.

Related

How do you wait for an application to close in OS X?

I am using the code below to check if an application is running and close it. Can someone provide an example of how to request an application calose and wait for it to close before proceeding?
+ (BOOL)isApplicationRunningWithName:(NSString *)applicationName {
BOOL isAppActive = NO;
NSDictionary *aDictionary;
NSArray *selectedApps = [[NSWorkspace sharedWorkspace] runningApplications];
for (aDictionary in selectedApps) {
if ([[aDictionary valueForKey:#"NSApplicationName"] isEqualToString: applicationName]) {
isAppActive = YES;
break;
}
}
return isAppActive;
}
+ (void)stopApplication:(NSString *)pathToApplication {
NSString *appPath = [[NSWorkspace sharedWorkspace] fullPathForApplication:pathToApplication];
NSString *identifier = [[NSBundle bundleWithPath:appPath] bundleIdentifier];
NSArray *selectedApps = [NSRunningApplication runningApplicationsWithBundleIdentifier:identifier];
// quit all
[selectedApps makeObjectsPerformSelector:#selector(terminate)];
}
You can use Key-Value Observing to observe the terminated property of each running application. This way, you'll get notified when each application terminates, without having to poll.
One way would be to periodically call isApplicationRunningWithName on a timer, and wait until that function returns NO.
The commandline timelimit will let you send a close signal to an app, wait x seconds, then kill it (or send any other signal you like, kill is -9) if hasn't obeyed the "warning" signal.
(Note: I haven't tried compiling it on Mac, but I believe it's fairly POSIX-compliant code and not Linux-specific as it runs on BSD and others.)

Sheets and long running tasks

I need to run a complex (ie long) task after the user clicks on a button.
The button opens a sheet and the long running operation is started using dispatch_async and other Grand Central Dispatch stuff.
I've written the code and it works fine but I need help to understand if I've done everything correctly or if I've ignored (due to my ignorance) any potential problem.
The user clicks the button and opens sheet, the block contains the long task (in this example it only runs a for(;;) loop
The block contains also the logic to close the sheet when task completes.
-(IBAction)openPanel:(id)sender {
[NSApp beginSheet:panel
modalForWindow:[self window]
modalDelegate:nil
didEndSelector:NULL
contextInfo:nil];
void (^progressBlock)(void);
progressBlock = ^{
running = YES; // this is a instance variable
for (int i = 0; running && i < 1000000; i++) {
[label setStringValue:[NSString stringWithFormat:#"Step %d", i]];
[label setNeedsDisplay: YES];
}
running = NO;
[NSApp endSheet:panel];
[panel orderOut:sender];
};
//Finally, run the block on a different thread.
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_async(queue,progressBlock);
}
The panel contains a Stop button that allows user to stop the task before its completion
-(IBAction)closePanel:(id)sender {
running = NO;
[NSApp endSheet:panel];
[panel orderOut:sender];
}
This code has a potential problem where it sets value of the status text. Basically all objects in AppKit are only allowed to be called from the main thread and can break in weird ways if they're not. You're calling the setStringValue: and setNeedsDisplay: methods on the label from whatever thread the global queue is running on. To fix this you should write the loop like so:
for (int i = 0; running && i < 1000000; i++) {
dispatch_async(dispatch_get_main_queue(), ^{
[label setStringValue:[NSString stringWithFormat:#"Step %d", i]];
[label setNeedsDisplay: YES];
});
}
This will set the label text from the main thread as AppKit expects.

Cocoa - Trim all leading whitespace from NSString

(have searched, but not been able to find a simple solution to this one either here, or in Cocoa docs)
Q. How can I trim all leading whitespace only from an NSString? (i.e. leaving any other whitespace intact.)
Unfortunately, for my purposes, NSString's stringByTrimmingCharactersInSet method works on both leading and trailing.
Mac OS X 10.4 compatibility needed, manual GC.
This creates an NSString category to do what you need. With this, you can call NSString *newString = [mystring stringByTrimmingLeadingWhitespace]; to get a copy minus leading whitespace. (Code is untested, may require some minor debugging.)
#interface NSString (trimLeadingWhitespace)
-(NSString*)stringByTrimmingLeadingWhitespace;
#end
#implementation NSString (trimLeadingWhitespace)
-(NSString*)stringByTrimmingLeadingWhitespace {
NSInteger i = 0;
while ((i < [self length])
&& [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[self characterAtIndex:i]]) {
i++;
}
return [self substringFromIndex:i];
}
#end
This is another solution using Regular Expressions (requires iOS 3.2):
NSRange range = [string rangeOfString:#"^\\s*" options:NSRegularExpressionSearch];
NSString *result = [string stringByReplacingCharactersInRange:range withString:#""];
And if you want to trim the trailing whitespaces only you can use #"\\s*$" instead.
This code is taking blanks.
NSString *trimmedText = [strResult stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSLog(#"%#",trimmedText);
Here is a very efficient (uses CoreFoundation) way of doing it (Taken from kissxml):
- (NSString *)trimWhitespace {
NSMutableString *mStr = [self mutableCopy];
CFStringTrimWhitespace((CFMutableStringRef)mStr);
NSString *result = [mStr copy];
[mStr release];
return [result autorelease];
}
NSString *myText = #" foo ";
NSString *trimmedText = [myText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSLog(#"old = [%#], trimmed = [%#]", myText, trimmedText);
Here's what I would do, and it doesn't involve categories!
NSString* outputString = inputString;
NSRange range = [inputString rangeOfCharacterFromSet: [NSCharacterSet whitespaceCharacterSet]
options:0];
if (range.location == 0)
outputString = [inputString substringFromIndex: range.location + range.length];
This is much less code.
I didn't really have much time to test this, and I'm not sure if 10.4 contains the UTF8String method for NSString, but here's how I'd do it:
NSString+Trimming.h
#import <Foundation/Foundation.h>
#interface NSString (Trimming)
-(NSString *) stringByTrimmingWhitespaceFromFront;
#end
NSString+Trimming.m
#import "NSString+Trimming.h"
#implementation NSString (Trimming)
-(NSString *) stringByTrimmingWhitespaceFromFront
{
const char *cStringValue = [self UTF8String];
int i;
for (i = 0; cStringValue[i] != '\0' && isspace(cStringValue[i]); i++);
return [self substringFromIndex:i];
}
#end
It may not be the most efficient way of doing this but it should work.
str = [str stringByReplacingOccurrencesOfString:#" " withString:#""];

Getting URL From beginSheetModalForWindow:

I'm using an OpenPanel to get a file path URL. This works:
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
NSURL *pathToFile = nil;
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
In general, any attempt to extract pathToFile from the context of oPanel has failed. This isn't such a big deal for small situations, but as my code grows, I'm forced to stuff everything -- XML parsing, core data, etc -- inside an inappropriate region. What can I do to extract pathToFile?
Thanks.
This doesn't, resulting in an 'assignment of read-only variable' error:
NSURL *pathToFile = nil;
[oPanel beginSheetModalForWindow:theWindow completionHandler:^(NSInteger returnCode)
{
if (returnCode == NSOKButton)
pathToFile = [[oPanel URLs] objectAtIndex:0];
}];
return pathToFile;
Yes, because you're trying to assign to the copy of the pathToFile variable that gets made when the block is created. You're not assigning to the original pathToFile variable that you declared outside the block.
You could use the __block keyword to let the block assign to this variable, but I don't think this will help because beginSheetModalForWindow:completionHandler: doesn't block. (The documentation doesn't mention this, but there's no reason for the method to block, and you can verify with logging that it doesn't.) The message returns immediately, while the panel is still running.
So, you're trying to have your completion-handler block assign to a local variable, but your method in which you declared the local variable will probably have returned by the time block runs, so it won't be able to work with the value that the block left will leave in the variable.
Whatever you do with pathToFile should be either in the block itself, or in a method (taking an NSURL * argument) that the block can call.
you can also runModal after you begin the sheet you just need to make sure you end the sheet later. This way you don't have to bend to apple's will, it isn't deprecated and it should still work perfectly.
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel beginSheetModalForWindow:window completionHandler:nil];
NSInteger result = [openPanel runModal];
NSURL *url = nil;
if (result == NSFileHandlingPanelOKButton)
{
url = [openPanel URL];
}
[NSApp endSheet:openPanel];
It seems a little bit like black magic coding but it does work.

Making an NSMutableString transformation without leaking memory?

I have this function within an iPhone project Objective C class.
While it's correct in terms of the desired functionality, after a few calls, it crashes into the debugger.
So I think it's a case of bad memory management, but I'm not sure where.
- (NSString *)stripHtml:(NSString *)originalText {
// remove all html tags (<.*>) from the originalText string
NSMutableString *strippedText = [[NSMutableString alloc] init];
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSString *current = [originalText substringWithRange:NSMakeRange(i, 1)];
if( [current isEqualTo:#"<"] )
appendFlag = NO;
if( appendFlag )
[strippedText appendString:current];
if( [current isEqualTo:#">"] )
appendFlag = YES;
}
NSString *newText = [NSString stringWithString:strippedText];
[strippedText release];
return newText;
}
Every time you iterate over your for loop, you're allocating a new NSString. While these NSStrings are autoreleased, they won't actually be released until after all the processing of your last input is finished. In the meantime, you'll allocate a potentially infinite amount of memory. The solution is to create your own autorelease pool and drain it every trip through the for loop. It'll look something like this:
BOOL appendFlag = YES;
for( int i=0; i<[originalText length]; i++ ) {
NSAutoreleasePool *pool = [NSAutoreleasePool new];
// rest of for loop body
[pool drain];
}
That'll free up the memory used by your current pointer right away.

Resources