3.1 User may interrupt read from task

by Marko Riedel

3.1.1 Idea

We want to collect output from a task, possibly after writing some data to its standard input. The caller of the method should have the option of a progress display that pops up after a certain time and allows the user to interrupt the task. The method returns an enumerator that iterates over the lines that were collected from the standard output of the task.

3.1.2 Implementation

We will need two pipes, one for writing to the standard input of the task and another for reading from its standard output. We also need a modal session for the progress display. We will use the read system call to perform non-blocking reads from stdout. There is a buffer where read will store data. All of these are declared below.

 
#define BUFSIZE 4096 
#define INTERVAL  3 
 
#define ERRMSG ”ERRGSMBrowser_--transfer_interrupted” 
 
- (NSEnumerator *)runCommand:(NSString *)cmd arguments:(NSArray *)args 
                        input:(NSString *)inStr 
                     progress:(BOOL)prog 
{ 
    NSPipe *inputPipe, *outputPipe; 
    NSFileHandle *reader, *writer; 
    NSMutableData *data = [NSMutableData dataWithCapacity:1024]; 
    NSTask *cmdTask = [NSTask new]; 
 
    int ticks = 0; 
 
    int fd, len; 
    char *buf[BUFSIZE]; 
 
    unsigned length; 
    const char *bytes; 
 
    NSApplication *app = [NSApplication sharedApplication]; 
    NSModalSession progSession; 
    BOOL pflag = NO; 
    NSDate *progDate; 
 
    int mask = NSLeftMouseDownMask  NSLeftMouseUpMask  
        NSLeftMouseDraggedMask; 
    NSDate *evDate; 
    NSEvent *event; 
 
    NSLog(@”%@_%@”, cmd, args); 
 
    outputPipe = [NSPipe pipe]; 
    reader = [outputPipe fileHandleForReading]; 
 
    fd = [reader fileDescriptor]; 
    fcntl(fd, F_SETFL, O_NONBLOCK); 
 
    inputPipe = [NSPipe pipe]; 
    writer = [inputPipe fileHandleForWriting]; 
 
    [cmdTask setLaunchPath:cmd]; 
    [cmdTask setArguments:args]; 
 
    [cmdTask setStandardInput:inputPipe]; 
    [cmdTask setStandardOutput:outputPipe]; 
    [cmdTask setStandardError:outputPipe];

The first part of the routine allocates the two pipes and collects file handles for reading and writing, resp. Furthermore we activate non-blocking reads for the file for stdout. This key idea will allow us to animate the progress panel while we are reading data. We prepare for launch by setting the tasks stdin and stdout as well as the path to the execultable and an array containing the arguments that are to be passed to the task. We are ready to launch the task. The first action is to write and close the task’s stdin. We also compute a date some time in the future (three seconds after launch in this case.)

 
    [cmdTask launch]; interrupted = NO; 
    progDate = [NSDate dateWithTimeIntervalSinceNow:INTERVAL]; 
 
    if(inStr!=nil){ 
        [writer writeData:[inStr dataUsingEncoding:NSASCIIStringEncoding]]; 
    } 
    [writer closeFile]; 
 
    while(len = read(fd, buf, BUFSIZE)){

The return value of the read call tells us what occured: there are no data when it is less than zero, it is zero on EOF and gives the number of bytes if data were copied into the buffer. We simply store the data in an NSData object allocated for this purpose. We exit the loop at EOF.

The case when there are no data but we are not at EOF is special. We need to start a modal session should the caller have requested a progress display and we have been waiting longer than three seconds. Assume that the panel progPanel was created elsewhere in the application and contains a button that sets the flag interrupted to true. We start a modal session for the panel and set the cursor.

 
        if(len>0){ 
            [data appendBytes:buf length:len]; 
        } 
        else{ 
            if(prog==YES && pflag==NO && 
               [progDate timeIntervalSinceNow]<0.0){ 
                [progPanel 
                     setTitle:[NSString stringWithFormat:@”Running_%@”, cmd]]; 
 
                progSession = [app beginModalSessionForWindow:progPanel]; 
                pflag = YES; 
                [[NSCursor arrowCursor] push]; 
            }

We have to update the progress indicator should there be no data and the panel is already displayed. Here we assume that the progInd can display a value between zero and one hundred. We update the indicator and wait for 0.025 seconds whether the progress panel gets a mouse click event (mask defined above). We process the event if this is the case. This is where the user is able to click the stop button and set the flag interrupted. We terminate the task if we received an interrupt and set the data to contain an error message. The program sleeps for 0.025 seconds if there were no data, so that no busy-waiting occurs.

 
            else if(pflag==YES){ 
                ticks = (ticks+1)%200; 
                [progInd 
                     setPercent:(float)(ticks<100 ? ticks : 199-ticks)]; 
                [progInd display]; 
                [progPanel flushWindow]; 
 
                evDate = [NSDate dateWithTimeIntervalSinceNow:0.025]; 
                event = [app nextEventMatchingMask:mask 
                               untilDate:evDate 
                               inMode:NSDefaultRunLoopMode 
                               dequeue:YES]; 
                if([event window]==progPanel){ 
                     [app sendEvent:event]; 
                } 
                if(interrupted==YES){ 
                     [cmdTask terminate]; 
                     data = [NSData dataWithBytes:ERRMSG 
                                    length:strlen(ERRMSG)]; 
                     break; 
                } 
            } 
            [NSThread 
                sleepUntilDate: 
                     [NSDate dateWithTimeIntervalSinceNow:0.025]]; 
        } 
    } 
 
    if(pflag==YES){ 
        [app endModalSession:progSession]; 
        [progPanel close]; 
        [NSCursor pop]; 
    } 
 
    length = [data length]; 
    bytes = [data bytes]; 
 
    if(length && bytes[length-1]==\n’){ 
        length--; 
    } 
 
    return 
        [[[NSString stringWithCString:bytes length:length] 
             componentsSeparatedByString:@\n”] 
            objectEnumerator]; 
}

The remainder of the code closes the progress panel if it was displayed and ends the modal session. The return statement splits the data from stdout into lines and returns an enumerator that can be used to iterate over those lines.