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