Why is @try ... @ catching work with - [NSFileHandle writeData]?

I have a method similar to tee utility. He receives a notification that the data has been read on the channel, and then writes data to one or more channels (connected to slave applications). If the subordinate application crashes, then this channel is interrupted, and of course I get an exception, which is then processed in the @try ... @catch block.

This works most of the time. I am puzzled that sometimes an exception completely disables the application with an uncaught exception and points to the writeData line. I could not understand what a pattern is when it falls, but why should it never be caught? (Note that this is not done inside the debugger.)

Here is 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 installed a signal handler in my AppDelegate> appDidFinishLaunching:

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

And it really leads to the application crashing or signal. Here is an example of a reverse stroke:

 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 
+6
source share
2 answers

So the good people at Crashlytics were able to help me. To quote them:

Here is the story:

  • The pipe dies due to a malfunction of the child process. Subsequent read / write will cause an error.
  • This entry occurs, resulting in SIGPIPE (and not an exception at runtime).
  • If SIGPIPE is masked / ignored, NSFileHandle checks errno and throws a run-time exception that it throws.
  • The function is deeper than your tee: the method wrapped this entry in @ try / @ catch (proved by setting a breakpoint on __cxa_begin_catch )
    • This function, which turns out to be "_dispatch_client_callout", which calls 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 very poor reputation for being a good citizen in the face of runtime exceptions.

So, you are right that the NSFileHandle throws an exception at runtime, the pipe dies, but not before the signal that kills to process is raised. Others ran into this exact issue (on iOS, which has much better semantics regarding runtime exceptions).

How can I catch EPIPE in my NSFIleHandle handling?

In short, I don't think you can catch this exception. But by ignoring SIGPIPE and using the lower level API to read / write to this file descriptor, I believe you can get around this. As a general rule, I would recommend against ignoring signals, but in this case, it seems reasonable.

Thus, the updated code 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]; } } } } 
+4
source

I know that this does not help much to answer the question why the exception trap seems to be broken, but I hope this is a useful answer to solve this problem.

I ran into a similar problem trying to read / write to a socket wrapped in NSFileHandle . I worked on it, checking channel availability directly using fileDescriptor , as shown

 - (BOOL)socketIsValid { return (write([fh fileDescriptor], NULL, 0) == 0); } 

then I tested with this method before trying to call writeData:

+1
source

Source: https://habr.com/ru/post/974989/


All Articles