Search code examples
objective-ccocoansnotificationsnsnotificationcenternstask

readInBackgroundAndNotify method not updating until NSTask complete


I am trying to run a NSTask in a background thread and display its output in a NSTextview that is on a NSPanel attached to my window ( Preference Pane ) using readInBackgroundAndNotify It does not seem like I am receiving the notifications as the method that should be called is not.

I have the controller class (PreferencePane.m) init the class (Inventory.m) that is in charge of running the NSTask

- (IBAction)updateInventoryButtonPressed:(id)sender
{
    inventory = [[Inventory alloc] init];
....

Then I send it a NSNotification to start the background (from PreferencePane.m):

....
[[NSNotificationCenter defaultCenter]
 postNotificationName:NotificationInventoryRequested
 object:self];
}

This class (Inventory.m) is an observer of this constant (NotificationInventoryRequested) in its init override

 - (id)init
{   
        [super init];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(inventoryRequested:) 
                                                 name:NotificationInventoryRequested    
                                               object:nil];
    return self;

}

This runs the inventoryRequested method (of Inventory.m)

-(void)inventoryRequested:(NSNotification*)aNotification
{
    if (inventoryIsRunning) {
        NSLog(@"Inventory is already running, ignoring request");
    }
    else {
        NSLog(@"Starting Inventory in background...");
        [NSThread detachNewThreadSelector:@selector(runInventoryTask)
                                 toTarget:self
                               withObject:nil];
}

}

This runs my NSTask method which I have refactored a few times from examples

Set a BOOL to help with duplicate runs sanity is handled by inventoryRequested using ivar inventoryIsRunning

    -(void)runInventoryTask
  {
    inventoryIsRunning = YES;
 ....

I double check my task and setup the readInBackgroundAndNotify adding my self as an observer.

 ....
if (task) {
NSLog(@"Found existing task...releasing");
    [task release];
}
task = [[NSTask alloc] init];
NSLog(@"Setting up pipe");
[task setStandardOutput: [NSPipe pipe]];
[task setStandardError: [task standardOutput]];
// Setup our arguments
[task setLaunchPath:@"/usr/bin/local/inventory"];
[task setArguments:[NSArray arrayWithObjects:@"--force",
                    nil]];
//Said to help with Xcode now showing logs
//[task setStandardInput:[NSPipe pipe]];

[self performSelectorOnMainThread:@selector(addLogText:)
                       withObject:@"Launching Task..."
                    waitUntilDone:false];

[[NSNotificationCenter defaultCenter] addObserver:self 
                                         selector:@selector(readPipe:) 
                                             name: NSFileHandleReadCompletionNotification 
                                           object: [[task standardOutput] fileHandleForReading]];

[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];
[task launch];
[task waitUntilExit];
// Let Any Observers know we are finished with Inventory
[[NSNotificationCenter defaultCenter]
 postNotificationName:NotificationInventoryComplete
 object:self];
inventoryIsRunning = NO;
}

This all seems to run fine. But this method never gets called (i.e. I don't see the window update or the NSLog in the console ):

-(void)readPipe:(NSNotification *)notification
{
NSData *data;
NSString *text;
NSLog(@"Read Pipe was called");

data = [[notification userInfo] 
        objectForKey:NSFileHandleNotificationDataItem];
if ([data length]){
    text = [[NSString alloc] initWithData:data 
                                 encoding:NSASCIIStringEncoding];
    // Update the text in our text view

    [self performSelectorOnMainThread:@selector(addLogText:)
                           withObject:text
                        waitUntilDone:false];
    NSLog(@"%@",text);
    [text release];

}
[[notification object] readInBackgroundAndNotify];


}

This all seems to run fine. But this method never gets called (i.e. I don't see the window update or the NSLog in the console ). I saw this thread and thought maybe it was my NSPanel blocking the run loop, so I set it as non-modal. I also remember reading about NSNotification's not being synchronous, so I thought perhaps because the method I is being called by a NSNotification, to test I just did this real quick:

- (IBAction)updateInventoryButtonPressed:(id)sender
{

inventory = [[Inventory alloc] init];

/*[[NSNotificationCenter defaultCenter]
 postNotificationName:NotificationInventoryRequested
 object:self];*/
[inventory inventoryRequested:self];
[self showPanel:sender];

Obviously self is not valid there, but it served to show me that even calling this method directly did not seem to help ( thus making me think this is not about NSNotification "blocking".)

Any thoughts on what I am missing, i have checked for removeObserver anywhere in my code ( I know I need to add it to dealloc and probably in readPipe: when the command run is done). If it helps here is the little NSTextview wrapper which needs work as I does not do line \n in the strings right now.

Inventory.h

//NSTextview
IBOutlet NSTextView  *      inventoryTextView;

Inventory.m

-(void)addLogText:(NSString *)text
{
NSRange myRange = NSMakeRange([[inventoryTextView textStorage] length], 0);
[[inventoryTextView textStorage] replaceCharactersInRange:myRange                                                          withString:text];
}

Any help with this would be appreciated too as its my next stumbling block.

UPDATED: Looks like this readData method is being called, however its not updating my Textview until the NSTask is complete,so I have a flow control problem.


Solution

  • I was able to get this working by adding the following

    NSDictionary *defaultEnvironment = [[NSProcessInfo processInfo] environment];
    NSMutableDictionary *environment = [[NSMutableDictionary alloc] initWithDictionary:defaultEnvironment];
    [environment setObject:@"YES" forKey:@"NSUnbufferedIO"];
    [task setEnvironment:environment];
    

    This stopped the notification from only sending me the buffer ( all my output at once in my case ).