\subsubsection{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 {\it
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.

\subsubsection{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 {\tt read}
system call to perform non-blocking reads from {\tt stdout.} There is
a buffer where {\tt read} will store data. All of these are declared
below.

\begin{lstlisting}{}
#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];

\end{lstlisting}

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 {\tt 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 {\tt stdin} and {\tt 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 {\tt
stdin.} We also compute a date some time in the future (three seconds
after launch in this case.)

\begin{lstlisting}{}
    [cmdTask launch]; interrupted = NO;
    progDate = [NSDate dateWithTimeIntervalSinceNow:INTERVAL];

    if(inStr!=nil){
        [writer writeData:[inStr dataUsingEncoding:NSASCIIStringEncoding]];
    }
    [writer closeFile];

    while(len = read(fd, buf, BUFSIZE)){
\end{lstlisting}

The return value of the {\tt 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 {\tt 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 {\tt progPanel} was created elsewhere
in the application and contains a button that sets the flag {\tt
interrupted} to true. We start a modal session for the panel and set
the cursor.

\begin{lstlisting}{}
        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];
            }
\end{lstlisting}

We have to update the progress indicator should there be no data and
the panel is already displayed. Here we assume that the {\tt 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 {\tt 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.

\begin{lstlisting}{}
            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];
}
\end{lstlisting}

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


