SIGPIPE 13

Progress Indicator for Unarchiving

October 2nd, 2005

I added a software updater to my application, and one of the steps was uncompressing the archive (after downloading it). Since the archive size is a few megabytes, and I use bzip2 as compression, this step takes a few seconds, and thus I want to show a determinate progress indicator while it is working on this.

As of such, I should really uncompress while downloading (since the CPU is idle most of the time waiting for network bytes), but I'm using NSURLDownload, and didn't see any obvious way to do that (maybe give it a named pipe as the file to download into?).

Anyway, the problem is that extraction is done by running:

tar -jxf «archive» -C «destination»

Which gives us no output to parse for progress. So my solution is that instead of giving tar the archive as a filename, let it read it from stdin. Then I can feed it the archive 32 KB at a time, and thereby track the progress.

We can use popen to run a shell command with either input or output set to a normal C file pointer. So this is one way to do it:

// archivePath is pointing to our tbz archive
// progressBar is an NSProgressIndicator instance
 
[progressBar setIndeterminate:NO];
[progressBar setDoubleValue:0.0];
[progressBar setMaxValue:(double)«size of archive»];
[progressBar startAnimation:self];
 
current = 0;
if(FILE* fp = fopen([archivePath UTF8String], "r"))
{
    setenv("DESTINATION", [[archivePath stringByDeletingLastPathComponent] UTF8String], 1);
    if(FILE* cmdFP = popen("tar -jxC \"$DESTINATION\"", "w"))
    {
        std::vector<char> buf(32*1024);
        while(size_t len = fread(&buf[0], 1, buf.size(), fp))
        {
            current += len;
            [progressBar setDoubleValue:(double)current];
 
            while(NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])
                [NSApp sendEvent:event];
 
            fwrite(&buf[0], 1, len, cmdFP);
        }
        pclose(cmdFP);
    }
    fclose(fp);
}

And that shows a very nice and steady progress indicator for uncompressing the tbz archive.

Notice that I let the command string use a shell variable as the destination directory, instead of e.g. sprintf'ing the command. The command is handed to the shell interpreter (with option -c) so the variable will be expanded, and by doing it this way, I avoid the hassle of escaping special characters or spaces in the path (should there be any).

[by Allan Odgaard]


4 Responses to “Progress Indicator for Unarchiving”

  1. Daniel Eggert Says:
    September 3rd, 2006 at 12:17

    A more Cocoa-like approach would have been to use an NSTask and set the standard in / standard out on that. I guess your code is slightly more compact, though.

    /Daniel

  2. Alexander v. Below Says:
    January 10th, 2008 at 22:23

    I am trying to do the Cocoa NSTask approach, but I am getting a SIGPIPE when I do the write bytes (15k of data):

    NSTask * tarTask;
    NSPipe * inPipe;
    NSFileHandle * writeHandle;
    inPipe = [NSPipe pipe];
    writeHandle = [inPipe fileHandleForWriting];</p>
    
    tarTask = [[NSTask alloc] init];
    [tarTask setLaunchPath:@"/usr/bin/tar"];
    [tarTask setArguments:[NSArray arrayWithObjects:@"--extract", @"--read-full-blocks", [NSString stringWithFormat:@"--directory %@", NSTemporaryDirectory()], nil]];
    [tarTask setStandardInput:inPipe];
    [tarTask launch];       
    [writeHandle writeData:data];

    Any idea?

    Alex

  3. Allan Odgaard Says:
    January 15th, 2008 at 07:59

    A SIGPIPE signal would indicate that you are writing to a closed pipe.

    I.e. the task you launched closed the pipe (indirectly via exit maybe) before you wrote all the data.

    You can setup and ignore handler for this signal, but it would indicate that something is wrong, e.g. the data you send to tar/bzip2 is not in the proper format.

  4. leebert Says:
    July 25th, 2010 at 19:53

    Here's another way. You just need "pipeview" (pv) installed. I guess it mightn't help your app though.

    srcdir=$1
    outfile=$2
    
    tar -Ocf - $srcdir | pv -i 1 -w 50 -berps `du -bs $srcdir | awk '{print $1}'` | 7za a -si $outfile
    

Leave a Reply