Sheets and long running tasks - cocoa

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.

Related

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

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.

Working of NSCondition lock

I have been using NSCondition lately, and I really don't understand the lock and unlock part.
For example,
NSCondition lock = [NSCondition new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Do stuff
[lock signal];
};
[lock wait];
//Do rest
This is working fine. What difference does it make from this..
NSCondition lock = [[NSCondition alloc] init];
[lock lock];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Do stuff
[lock signal];
[lock unlock];
};
[lock wait];
//Do rest
You must hold the lock on a condition variable before waiting.
Waiting releases the lock, then is guaranteed to block until it has been signalled and the lock has been re-taken. Note that if multiple threads are waiting, only one will be notified of the signal, the others will continue to wait for further signals. Alternatively, if the predicate will remain true after execution, then the signal can be broadcast to all threads.
The lock is a mutual exclusion lock (mutex), meaning only one thread can hold the lock at any one time. As well as using this to protect whatever you're using the condition variable for, it also protects the internal workings of the condition variable. If you attempted to use it without the lock then there would be race conditions on checking/setting the condition across multiple threads.
For correct use of a condition variable, neither of your examples are correct. The whole point is to be protecting the state of a predicate. Here is an example is based on the pseudo-code from the documentation.
NSCondition condvar = [NSCondition new];
__block BOOL workRequired = NO;
// something somewhere else does this at some point ^{
[condvar lock];
workRequired = YES;
[condvar signal];
[condvar unlock];
};
[condvar lock];
while (!workRequired) {
[condvar wait];
}
// Do the work
workRequired = NO;
[condvar unlock];
In your example, you don't have any condition beyond "the block finished". In which case you should just use dispatch_sync.
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do stuff
};
// Do rest
If for some reason you don't know if you're already running on that queue, then a semaphore will suffice.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0L);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do stuff
dispatch_semaphore_signal(semaphore);
};
dispatch_semaphore_wait(semaphore);
// Do rest

CADisplaylink code fires even when display doesn't refresh

Note: as of iOS7 this problem may only manifest in the simulator -- still testing.
I have an implementation of CADisplayLink, and I need the code to run if and only if the display actually refreshes
This doesn't happen.
Here's a very simple test program:
I start the display link running; in the first frame aLabel should display "WordFlash"; for the next 19 frames it should display "--------" and for the next 100 it should be blank; then the cycle should repeat.
Occasionally (say 1 in 8 times), and unpredictably, the screen won't refresh to display "WordFlash" though the code has indeed fired (as the counter has advanced). I need the counter to advance only if "WordFlash" has successfully displayed for exactly 1 frame.
Any ideas? I am totally stumped.
Note: This display-refresh-skipping seems to happen without correlation to the time it takes the device to execute the simple code (as in the NSLogged time-to-execute the code can be identical in two different cycles while only one cycle has successfully flashed the word.
#import "HomePwnerViewController.h"
#interface HomePwnerViewController ()
#end
#implementation HomePwnerViewController {
int counter;
UILabel *aLabel;
}
- (void)viewDidLoad
{
[super viewDidLoad];
aLabel = [[UILabel alloc] initWithFrame:self.view.frame];
[self.view addSubview:aLabel];
aLabel.alpha = 1;
aLabel.text = #"";
aLabel.textAlignment = NSTextAlignmentCenter;
[aLabel setFont:[UIFont fontWithName:#"Courier" size:50]];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(testWordFlashMethod) userInfo:nil repeats:NO];
}
- (void) displaySensitiveProcedure: (CADisplayLink *) sender {
CFTimeInterval startTime = CACurrentMediaTime();
if (counter == 0)
aLabel.text = #"FlashWord";
else if (counter < 20)
aLabel.text = #"---------";
else
aLabel.text = #"";
CFTimeInterval endTime = CACurrentMediaTime();
if (counter == 0)
NSLog(#"frame time %f frame length %f ratio %f", endTime - startTime, sender.duration, (endTime - startTime)/sender.duration);
counter++;
if (counter == 120)
counter = 0;
}
- (void) testWordFlashMethod {
CADisplayLink *DL = [CADisplayLink displayLinkWithTarget:self selector:#selector(displaySensitiveProcedure:)];
DL.frameInterval = 1;
counter = 0;
[DL addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Much obliged,
b
If I were to guess, I'd say that animating a UILabel on screen refresh is not the best way to do things. In my experience changing the text on them can take a bit of time to render. If you want to draw a dash across the screen, you might be better served by using Core Graphics API's, or a CALayer based drawing approach.
Your displaySensitiveProcedure is probably taking more than a single frame (16.7ms) to update the screen, which makes it skip a frame. If you consistently take more than that (say 20ms), you can have the DisplayLink fire less frequently to get smooth animation at a slower framerate - see the frameInterval property. I would suggest timing your callback by doing something like this:
- (void) displaySensitiveProcedure: (CADisplayLink *) sender {
CFTimeInterval startTime = CACurrentMediaTime();
... <code> ...
CFTimeInterval endTime = CACurrentMediaTime();
NSLog(#"frame time: %f", endTime - startTime);
}

OS X main.m running a server style program

I writing my first OS X command line program which is a server style program. It's job is to process various information and respond to other events.
I have the following code in my main.m
int main(int argc, const char * argv[]) {
#autoreleasepool {
PIPieman *pieman = [[[PIPieman alloc] init] autorelease];
[pieman start];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
while (!pieman.finished && [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
}
return 0;
}
I got this code from various documents and the basic idea is that once pieman.finished is set to YES, that the program then exits.
The problem I have is that the flag is being set by code inside pieman, but the run loop is not being triggered, so the program does not exit. I've been looking for ways to trigger the run loop and there seem to be a variety, but none that feel like a good solution. For example I could reduce the beforeDate: to a few seconds to cause periodic triggering of the run loop.
My preference would be that something triggers the run loop on the change of the finished boolean value.
Any suggestions?
You need to tell the run loop to return from runMode:beforeDate:. The NSRunLoop class doesn't define a message to do that, but NSRunLoop is built on CFRunLoop, and the CFRunLoopStop function does what you need.
#implementation PIPieman
...
- (void)setFinished:(BOOL)finished {
_finished = finished;
CFRunLoopStop(CFRunLoopGetMain());
}

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

Resources