2

I would like to execute a command with NSTask, and be able to see the progress in a modal window. For example if I execute 'ls -R /' i would like to see the chunks appearing in a NSTextView.

I came up with the following, and everything works fine, except the update part. The task get executed (with the spinning beachbal) and when it is finished i see the result appear in the textview.

@interface ICA_RunWindowController ()

@property (strong) IBOutlet NSTextView* textResult;
@property (strong) IBOutlet NSButton* buttonAbort;
@property (strong) IBOutlet NSButton* buttonOK;

- (IBAction) doOK:(id) sender;
- (IBAction) doAbort:(id) sender;

@end

@implementation ICA_RunWindowController {
  NSTask * executionTask;
  id taskObserver;
  NSFileHandle * errorFile;
  id errorObserver;
  NSFileHandle * outputFile;
  id outputObserver;
  }

@synthesize textResult,buttonAbort,buttonOK;  

- (IBAction)doOK:(id)sender {
  [[self window] close];
  [NSApp stopModal];
  }

- (IBAction)doAbort:(id)sender {
  [executionTask terminate];
  }

- (void) taskCompleted {
NSLog(@"Task completed");
  [[NSNotificationCenter defaultCenter] removeObserver:taskObserver];
  [[NSNotificationCenter defaultCenter] removeObserver:errorObserver];
  [[NSNotificationCenter defaultCenter] removeObserver:outputObserver];
  [self outputAvailable];
  [self errorAvailable];
  executionTask = nil;
  [buttonAbort setEnabled:NO];
  [buttonOK setEnabled:YES];
  }


- (void) appendText:(NSString *) text inColor:(NSColor *) textColor {
  NSDictionary * makeUp = [NSDictionary dictionaryWithObject:textColor forKey:NSForegroundColorAttributeName];
  NSAttributedString * extraText = [[NSAttributedString alloc] initWithString:text attributes:makeUp];
  [textResult setEditable:YES];
  [textResult setSelectedRange:NSMakeRange([[textResult textStorage] length], 0)];
  [textResult insertText:extraText];
  [textResult setEditable:NO];
  [textResult display];
  }

- (void) outputAvailable {
  NSData * someData = [outputFile readDataToEndOfFile];
  if ([someData length] > 0) {
NSLog(@"output Available");
    NSString * someText = [[NSString alloc] initWithData:someData encoding:NSUTF8StringEncoding];
    [self appendText:someText inColor:[NSColor blackColor]];
    }
  }

- (void) errorAvailable {
  NSData * someData = [errorFile readDataToEndOfFile];
  if ([someData length] > 0) {
NSLog(@"Error Available");
    NSString * someText = [[NSString alloc] initWithData:someData encoding:NSUTF8StringEncoding];
    [self appendText:someText inColor:[NSColor redColor]];
    }
  }

- (void) runCommand:(NSString *) command {
// make sure all views are initialized
  [self showWindow:[self window]];
// some convience vars
  NSArray * runLoopModes = @[NSDefaultRunLoopMode, NSRunLoopCommonModes];
  NSNotificationCenter * defCenter = [NSNotificationCenter defaultCenter];
// create an task
  executionTask = [[NSTask alloc] init];
// fill the parameters for the task
  [executionTask setLaunchPath:@"/bin/sh"];
  [executionTask setArguments:@[@"-c",command]];
// create an observer for Termination
  taskObserver = [defCenter addObserverForName:NSTaskDidTerminateNotification
                                        object:executionTask 
                                         queue:[NSOperationQueue mainQueue]
                                    usingBlock:^(NSNotification *note) 
    {
      [self taskCompleted];        
      }
    ];

// Create a pipe and a filehandle for reading errors
  NSPipe * error = [[NSPipe alloc] init];
  [executionTask setStandardError:error];
  errorFile = [error fileHandleForReading];
  errorObserver = [defCenter addObserverForName:NSFileHandleDataAvailableNotification
                                         object:errorFile 
                                          queue:[NSOperationQueue mainQueue]
                                     usingBlock:^(NSNotification *note) 
    {
      [self errorAvailable];        
      [errorFile waitForDataInBackgroundAndNotifyForModes:runLoopModes];
      }
    ];
  [errorFile waitForDataInBackgroundAndNotifyForModes:runLoopModes];

// Create a pipe and a filehandle for reading output
  NSPipe * output = [[NSPipe alloc] init];
  [executionTask setStandardOutput:output];
  outputFile = [output fileHandleForReading];
  outputObserver = [defCenter addObserverForName:NSFileHandleDataAvailableNotification
                                          object:outputFile 
                                           queue:[NSOperationQueue mainQueue]
                                      usingBlock:^(NSNotification *note) 
    {
      [self outputAvailable];        
      [outputFile waitForDataInBackgroundAndNotifyForModes:runLoopModes];
      }
    ];
  [outputFile waitForDataInBackgroundAndNotifyForModes:runLoopModes];

// start task
  [executionTask launch];

// show our window as modal
  [NSApp runModalForWindow:[self window]];
  }

My question: Is it possible to update the output while the task is running? And, if yes, how could I achieve that?

Germo
  • 23
  • 3

1 Answers1

3

A modal window runs the run loop in NSModalPanelRunLoopMode, so you need to add that to your runLoopModes.

You should not be getting the spinning beach ball. The cause is that you're calling -readDataToEndOfFile in your -outputAvailable and -errorAvailable methods. Given that you're using -waitForDataInBackgroundAndNotifyForModes:, you would use the -availableData method to get what data is available without blocking.

Alternatively, you could use -readInBackgroundAndNotifyForModes:, monitor the NSFileHandleReadCompletionNotification notification, and, in your handler, obtain the data from the notification object using [[note userInfo] objectForKey:NSFileHandleNotificationDataItem]. In other words, let NSFileHandle do the work of reading the data for you.

Either way, though, once you get the end-of-file indicator (an empty NSData), you should not re-issue the ...InBackgroundAndNotifyForModes: call. If you do, you'll busy-spin as it keeps feeding you the same end-of-file indicator over and over.

It shouldn't be necessary to manually -display your text view. Once you fix the blocking calls that were causing the spinning color wheel cursor, that will also allow the normal window updating to happen automatically.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • Thanks, everything works as desired now. I changed the -..Available methods to – Germo Jul 26 '14 at 03:06
  • - (BOOL) outputAvailable { NSData * someData = [outputFile availableData]; if ([someData length] > 0) { NSString * someText = [[NSString alloc] initWithData:someData encoding:NSUTF8StringEncoding]; [self appendText:someText inColor:[NSColor blackColor]]; return YES; } return NO; } that way i can control the re-issue of the waitFor... call if ([self outputAvailable]) [outputFile waitForDataInBackgroundAndNotifyForModes:runLoopModes]; – Germo Jul 26 '14 at 03:13