-->

How can I catch EPIPE in my NSFIleHandle handling?

2019-07-26 09:58发布

问题:

I'm having a problem with EPIPE in my iOS app, and it's not being caught in the @try/@catch/@finally block. How can I catch this signal (SIGPIPE, likely)...

I've built a "web proxy" into my app that will handle certain kinds of URLs - in this error case, it seems that the remote end (also in my app, but hiding in the iOS libraries) closes its end of the socket. I don't get a notification (should I? Is there something I should register for with the NSFileHandle that might help here?).

I've based this proxy on HTTPServer that Matt Gallagher put together (available here), and the problem is in a subclass of the HTTPRequestHandler class he put together. Here's the code (this code is the equivalent of the startResponse method in the base class):

-(void)proxyTS:(SSProxyTSResource *)proxyTS didReceiveResource:(NSData *)resource
{
    NSLog(@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));

CFHTTPMessageRef response =
    CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1);
    CFHTTPMessageSetHeaderFieldValue(response, 
                                    (CFStringRef)@"Content-Type", 
                                    (__bridge CFStringRef)s_MIMEtype);
    CFHTTPMessageSetHeaderFieldValue(response,
                                    (CFStringRef)@"Connection",
                                    (CFStringRef)@"close");
    CFHTTPMessageSetBody(response,
                        (__bridge CFDataRef)resource);

    CFDataRef headerData = CFHTTPMessageCopySerializedMessage(response);
    @try
    {
        NSLog(@" -> writing %u bytes to filehandle...",[((__bridge NSData *)headerData) length]);
        [self.fileHandle writeData:(__bridge NSData *)headerData];
    }
    @catch (NSException *exception)
    {
        // Ignore the exception, it normally just means the client
        // closed the connection from the other end.
    }
    @finally
    {
        NSLog(@" *ding*");
        CFRelease(headerData);
        CFRelease(response);
        [self.server closeHandler:self];
    }
}

And here's what shows up in the console log when it crashes:

Jan 15 14:55:10 AWT-NoTouch-iPhone-1 Streamer[1788] <Warning>: [SSProxyTSResponseHandler proxyTS:didReceiveResource:]
Jan 15 14:55:10 iPhone-1 Streamer[1788] <Warning>:  -> writing 261760 bytes to filehandle...
Jan 15 14:55:11 iPhone-1 com.apple.launchd[1] (UIKitApplication:com.XXX.Streamer[0xf58][1788]) <Warning>: (UIKitApplication:com.XXX.Streamer[0xf58]) Exited abnormally: Broken pipe: 13

It seems that because the other end closed the pipe the write() fails, so if someone can point me at how I can either discover that it's already closed and not try to write data to it OR whatever will make it not crash my program that would be very helpful.

回答1:

The immediate problem of crashing with SIGPIPE is solved. I'm not entirely giggly about this solution, but at least the app doesn't crash. It's not clear that it's working 100% correctly, but it does seem to be behaving quite a bit better.

I've resolved this issue by examining further what's going on. In doing some research, I found that perhaps I should be using NSFileHandle's writeabilityHandler property to install a block to do the writing. I'm not fully sold on that approach (it felt convoluted to me), but it might help.

Writability-handler solution:

In doing some web searching on writeabilityHandler, I stumbled on Bert Leung's blog entry on some issues he was having in a similar area. I took his code and modified it as follows, replacing the @try/@catch/@finally block above with this code:

self.pendingData = [NSMutableData dataWithData:(__bridge NSData *)(headerData)];

CFRelease(headerData);
CFRelease(response);

self.fileHandle.writeabilityHandler = ^(NSFileHandle* thisFileHandle)
    {
        int amountSent = send([thisFileHandle fileDescriptor],
                              [self.pendingData bytes],
                              [self.pendingData length],
                              MSG_DONTWAIT);
        if (amountSent < 0) {
            // errno is provided by system
            NSLog(@"[%@ %@] Error while sending response: %d", NSStringFromClass([self class]), NSStringFromSelector(_cmd), errno);
            // Setting the length to 0 will cause this handler to complete processing.
            self.pendingData.length = 0;
        } else {
            [self.pendingData replaceBytesInRange:NSMakeRange(0, amountSent)
                                   withBytes:NULL
                                      length:0];
        }

        if ([self.pendingData length] == 0) {
            thisFileHandle.writeabilityHandler = nil;
            // Hack to avoid ARC cycle with self. I don't like this, but...
            [[NSNotificationCenter defaultCenter] postNotification:self.myNotification];
        }
    };

That worked fine but it didn't solve the problem. I was still getting SIGPIPE/EPIPE.

SIGPIPE be gone!

This wasn't a surprise, exactly, as this does pretty much the same thing as the former writeData: did but does it using send() instead. The key difference though is that using send() allows errno to be set. This was quite helpful, actually - I was getting a couple of error codes (in errno), such as 54 (Connection reset by peer) and 32 (Broken pipe). The 54's were fine, but the 32's resulted in the SIGPIPE/EPIPE. Then it dawned on me - perhaps I should just ignore SIGPIPE.

Given that thought, I added a couple of hooks into my UIApplicationDelegate in application:didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    [self installSignalHandlers];

    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {

    ...

and applicationWillTerminate::

- (void)applicationWillTerminate:(UIApplication *)application
{
    // Saves changes in the application's managed object context before the application terminates.

    [self removeSignalHandlers];

    [self saveContext];
}

-(void)installSignalHandlers
{
    signal(SIGPIPE,SIG_IGN);
}

-(void)removeSignalHandlers
{
    signal(SIGPIPE, SIG_DFL);
}

Now at least the app doesn't crash. It's not clear that it's working 100% correctly, but it does seem to be behaving.

I also switched back to the @try/@catch/@finally structure because it's more direct. Further, after ignoring SIGPIPE, the @catch block does get triggered. Right now, I'm logging the exception, but only so I can see that it's working. In the released code, that log will be disabled.