Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't @try...@catch work with -[NSFileHandle writeData]?

I have a method that is similar to the tee utility. It receives a notification that data has been read on a pipe, and then writes that data to one or more pipes (connected to subordinate applications). If a subordinate app crashes, then that pipe is broken, and I naturally get an exception, which is then handled in a @try...@catch block.

This works most of the time. What I'm puzzled by is that occasionally, the exception crashes the app entirely with an uncaught exception, and pointing to the writeData line . I haven't been able to figure out what the pattern is on when it crashes, but why should it ever NOT be caught? (Note this is not executing inside the debugger.)

Here's the code:

//in setup:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tee:) name:NSFileHandleReadCompletionNotification object:fileHandle];

 -(void)tee:(NSNotification *)notification
{
//    NSLog(@"Got read for tee ");

NSData *readData = notification.userInfo[NSFileHandleNotificationDataItem];
totalDataRead += readData.length;
//    NSLog(@"Total Data Read %ld",totalDataRead);
NSArray *pipes = [teeBranches objectForKey:notification.object];

if (readData.length) {
    for (NSPipe *pipe in pipes {
           @try {
                [[pipe fileHandleForWriting] writeData:readData];
            }
            @catch (NSException *exception) {
                NSLog(@"download write fileHandleForWriting fail: %@", exception.reason);
                if (!_download.isCanceled) {
                    [_download rescheduleOnMain];
                    NSLog(@"Rescheduling");
                }
                return; 
            }
            @finally {
            }
    }
 }

I should mention that I have set a signal handler in my AppDelegate>appDidFinishLaunching:

signal(SIGPIPE, &signalHandler);
signal(SIGABRT, &signalHandler );

void signalHandler(int signal)
{
    NSLog(@"Got signal %d",signal);
}

And that does execute whether the app crashes or the signal is caught. Here's a sample crash backtrace:

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000

Application Specific Information:
*** Terminating app due to uncaught exception 'NSFileHandleOperationException', reason: '*** -[NSConcreteFileHandle writeData:]: Broken pipe'
abort() called
terminating with uncaught exception of type NSException

Application Specific Backtrace 1:
0   CoreFoundation                      0x00007fff838cbbec __exceptionPreprocess + 172
1   libobjc.A.dylib                     0x00007fff90e046de objc_exception_throw + 43
2   CoreFoundation                      0x00007fff838cba9d +[NSException raise:format:] + 205
3   Foundation                          0x00007fff90a2be3c __34-[NSConcreteFileHandle writeData:]_block_invoke + 81
4   Foundation                          0x00007fff90c53c17 __49-[_NSDispatchData enumerateByteRangesUsingBlock:]_block_invoke + 32
5   libdispatch.dylib                   0x00007fff90fdfb76 _dispatch_client_callout3 + 9
6   libdispatch.dylib                   0x00007fff90fdfafa _dispatch_data_apply + 110
7   libdispatch.dylib                   0x00007fff90fe9e73 dispatch_data_apply + 31
8   Foundation                          0x00007fff90c53bf0 -[_NSDispatchData enumerateByteRangesUsingBlock:] + 83
9   Foundation                          0x00007fff90a2bde0 -[NSConcreteFileHandle writeData:] + 150
10  myApp                               0x000000010926473e -[MTTaskChain tee:] + 2030
11  CoreFoundation                      0x00007fff838880dc __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
12  CoreFoundation                      0x00007fff83779634 _CFXNotificationPost + 3140
13  Foundation                          0x00007fff909bb9b1 -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
14  Foundation                          0x00007fff90aaf8e6 _performFileHandleSource + 1622
15  CoreFoundation                      0x00007fff837e9ae1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
16  CoreFoundation                      0x00007fff837dbd3c __CFRunLoopDoSources0 + 476
17  CoreFoundation                      0x00007fff837db29f __CFRunLoopRun + 927
18  CoreFoundation                      0x00007fff837dacb8 CFRunLoopRunSpecific + 296
19  HIToolbox                           0x00007fff90664dbf RunCurrentEventLoopInMode + 235
20  HIToolbox                           0x00007fff90664b3a ReceiveNextEventCommon + 431
21  HIToolbox                           0x00007fff9066497b _BlockUntilNextEventMatchingListInModeWithFilter + 71
22  AppKit                              0x00007fff8acf5cf5 _DPSNextEvent + 1000
23  AppKit                              0x00007fff8acf5480 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] + 194
24  AppKit                              0x00007fff8ace9433 -[NSApplication run] + 594
25  AppKit                              0x00007fff8acd4834 NSApplicationMain + 1832
26  myApp                               0x00000001091b16a2 main + 34
27  myApp                               0x00000001091ab864 start + 52
like image 285
mackworth Avatar asked Sep 08 '14 16:09

mackworth


1 Answers

So, the nice folks at Crashlytics were able to help me here. To quote them:

Here's the story:

  • The pipe dies because the child process crashes. The very next read/write will cause a fault.
  • That write occurs, which results in a SIGPIPE (not a runtime exception).
  • If that SIGPIPE is masked/ignored, NSFileHandle checks errno and creates a runtime exception which it throws.
  • A function deeper than your tee: method has wrapped this write in a @try/@catch (proved by setting a breakpoint on __cxa_begin_catch)
    • That function, which turns out to be "_dispatch_client_callout", which makes a call to objc_terminate, which effectively kills the process.

Why does _dispatch_client_callout do this? I'm not sure, but you can see the code here: http://www.opensource.apple.com/source/libdispatch/libdispatch-228.23/src/object.m

Unfortunately, AppKit has a really poor track record of being a good citizen in the face of runtime exceptions.

So, you are right that NSFileHandle raises a runtime exception about the pipe dying, but not before a signal is raised that kills the process. Others have encountered this exact issue (on iOS, which has much better semantics about runtime exceptions).

How can I catch EPIPE in my NSFIleHandle handling?

In short, I don't believe it is possible for you to catch this exception. But, by ignoring SIGPIPE and using lower-level APIs to read/write to this file handle, I believe you can work around this. As a general rule, I'd recommend against ignoring signals, but in this case, it seems reasonable.

Thus the revised code is now:

-(void)tee:(NSNotification *)notification {
    NSData *readData = notification.userInfo[NSFileHandleNotificationDataItem];
    totalDataRead += readData.length;
    //    NSLog(@"Total Data Read %ld",totalDataRead);
    NSArray *pipes = [teeBranches objectForKey:notification.object];

    if (readData.length) {
        for (NSPipe *pipe in pipes ) {
            NSInteger numTries = 3;
            size_t bytesLeft = readData.length;
            while (bytesLeft > 0 && numTries > 0 ) {
                ssize_t amountSent= write ([[pipe fileHandleForWriting] fileDescriptor], [readData bytes]+readData.length-bytesLeft, bytesLeft);
                if (amountSent < 0) {
                     NSLog(@"write fail; tried %lu bytes; error: %zd", bytesLeft, amountSent);
                    break;
                } else {
                    bytesLeft = bytesLeft- amountSent;
                    if (bytesLeft > 0) {
                        NSLog(@"pipe full, retrying; tried %lu bytes; wrote %zd", (unsigned long)[readData length], amountSent);
                        sleep(1);  //probably too long, but this is quite rare
                        numTries--;
                    }
                }
            }
            if (bytesLeft >0) {
                if (numTries == 0) {
                    NSLog(@"Write Fail4: couldn't write to pipe after three tries; giving up");
                 }
                 [self rescheduleOnMain];
             }

        }
    }
}
like image 77
mackworth Avatar answered Sep 28 '22 06:09

mackworth