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
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];
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With