readInBackgroundAndNotify方法直到NSTask完成才更新

时间:2011-11-24 00:44:12

标签: objective-c cocoa nsnotifications nsnotificationcenter nstask

我正在尝试在后台线程中运行NSTask,并使用readInBackgroundAndNotify在NSTextview中显示其输出,该NSTextview位于附加到我的窗口(Preference Pane)的NSPanel上 我似乎没有收到通知,因为应该调用的方法不是。

我有控制器类(PreferencePane.m)初始化负责运行NSTask的类(Inventory.m)

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

然后我发送一个NSNotification来启动后台(来自PreferencePane.m):

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

此类(Inventory.m)是其init重写中的此常量(NotificationInventoryRequested)的观察者

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

}

这将运行inventoryRequested方法(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];
}

}

这会运行我的NSTask方法,我已经从示例中重构了几次

设置BOOL以帮助重复运行sanity由inventoryRequested使用ivar inventoryIsRunning处理

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

我仔细检查我的任务并设置readInBackgroundAndNotify将我的自我添加为观察者。

 ....
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;
}

这一切似乎都运行良好。但是这个方法永远不会被调用(即我没有看到窗口更新或控制台中的NSLog):

-(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];


}

这一切似乎都运行良好。但是这个方法永远不会被调用(即我没有看到窗口更新或控制台中的NSLog)。我看到了这个thread,并认为可能是我的NSPanel阻止了运行循环,所以我把它设置为非模态。我还记得读过有关NSNotification不同步的内容,所以我想也许是因为我正在通过NSNotification调用这个方法来测试我只是快速地做了这个:

- (IBAction)updateInventoryButtonPressed:(id)sender
{

inventory = [[Inventory alloc] init];

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

显然自我在那里无效,但它告诉我,即使直接调用这种方法似乎没有帮助(因此让我觉得这不是关于NSNotification“阻塞”。)

关于我缺少的任何想法,我已经在我的代码中的任何地方检查了removeObserver(我知道我需要将它添加到dealloc并且可能在readPipe中:当命令运行完成时)。如果它有帮助,那么需要工作的是小NSTextview包装器,因为我现在不在字符串中进行排序。

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];
}

任何对此的帮助也会被视为我的下一个绊脚石。

更新: 看起来这个readData方法正在被调用,但是在NSTask完成之前它没有更新我的Textview,所以我遇到了流量控制问题。

3 个答案:

答案 0 :(得分:6)

我能够通过添加以下

来实现这一目标
NSDictionary *defaultEnvironment = [[NSProcessInfo processInfo] environment];
NSMutableDictionary *environment = [[NSMutableDictionary alloc] initWithDictionary:defaultEnvironment];
[environment setObject:@"YES" forKey:@"NSUnbufferedIO"];
[task setEnvironment:environment];

这停止了通知只发送缓冲区(在我的情况下我的所有输出都是一次)。

答案 1 :(得分:2)

根据所描述的阻塞行为和 NSUnbufferedIO解决方案方法,它似乎是一个标准的I / O流缓冲问题(它不时会在shell脚本中养成它的丑陋头脑)。

有关这个棘手问题的详细文章,请参阅:buffering in standard streams

避免标准I / O流缓冲的一种方法是在命令行工具本身的一部分上启用行缓冲模式。示例:

  • tcpdump -l
  • grep --line-buffered
  • sed -l

通常可以通过使用script命令在伪终端(pty)中运行命令行工具来启用行缓冲输出模式。

/usr/bin/script -q /dev/null /usr/bin/local/inventory --force | ...

除了来自Apple的 Moriarity示例代码外,还有NSTask个示例代码值得一看,包括AMShellWrapperasynctask.mPseudoTTY.app (列于http://cocoadev.com/index.pl?NSTask)。

答案 2 :(得分:1)

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

[[[task standardOutput] fileHandleForReading] readInBackgroundAndNotify];

您假设管道将同时返回相同的NSFileHandle对象。

如果没有,则发布通知的对象和您正在观察的对象将不是同一个对象。这可以解释为什么你没有看到通知,所以我不会指望它。

尝试单独获取NSFileHandle并将其分配给变量,然后在addObserver:selector:name:object:readInBackgroundAndNotify消息表达式中使用该变量。

[task waitUntilExit];

在任务完成之前,您将阻止UI。 waitUntilExit至少应该被认为是有害的,至少可能是其他原因。

waitUntilExit使用了运行循环,所以如果窗口在等待的时候没有机会进行绘制,我会感到有些惊讶,但无论如何都值得改变,这两者都是潜在的修复(如果确实是这样的话)问题)因为阻止UI本身就很糟糕。

请考虑观察NSTaskDidTerminateNotification(您应该在启动之前设置)。您可以在收到通知后发布自己的通知。