-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Log uncaught exceptions #53
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the work here! Some comments below. Would be great to add a section to the README as well.
Aardvark/ARKExceptionLogging.h
Outdated
@import Foundation; | ||
|
||
|
||
/// Enables logging uncaught exceptions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to explain here how this will interop with crash reporters. i.e. if this call should be executed before or after setting up a crash reporter.
Aardvark/ARKExceptionLogging.h
Outdated
/// Enables logging uncaught exceptions. | ||
OBJC_EXTERN void ARKEnableLogOnUncaughtException(void); | ||
|
||
/// Disables logging uncaught exceptions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to explain what state the app is returned to here, especially with regards to crash reporters.
Aardvark/ARKExceptionLogging.h
Outdated
@@ -0,0 +1,28 @@ | |||
// | |||
// ARKExceptionLogging.h | |||
// Aardvark |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a part of CoreAardvark
? This seems like core functionality, but I'm not sure if the APIs we're using here are extension safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be fine in CoreAardvark
. I'll do some testing, but don't see anything that says it isn't extension safe.
Aardvark/ARKExceptionLogging.m
Outdated
|
||
void ARKEnableLogOnUncaughtException() | ||
{ | ||
ARKPreviousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to consider threading here? Should this be called only from the Main thread? Or is this call thread safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anything about threading in the documentation and from some testing it seems to work fine setting it from either the main thread or a background thread. In either case, the exception handler gets called on the thread on which the exception occurred, and the previously-set exception handler gets chained correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Makes sense that Apple would make this thread safe (though sucky that they didn't make this explicit in the documentation).
Aardvark/ARKExceptionLogging.m
Outdated
|
||
void ARKHandleUncaughtException(NSException *exception) | ||
{ | ||
ARKLogWithType(ARKLogTypeError, nil, @"Uncaught exception '%@':\n%@", exception.name, exception.debugDescription); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use debugDescription
rather than description
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debugDescription
includes both the description
and the stack trace
One more thing: we should hook this code into to the sample app :) |
a723c01
to
58221d7
Compare
CoreAardvark/ARKExceptionLogging.m
Outdated
{ | ||
ARKLogWithType(ARKLogTypeError, nil, @"Uncaught exception '%@':\n%@", exception.name, exception.debugDescription); | ||
dispatch_semaphore_t logDistributionSemaphore = dispatch_semaphore_create(0); | ||
[[ARKLogDistributor defaultDistributor] distributeAllPendingLogsWithCompletionHandler:^{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably add a comment here that we're trying to flush all logs prior to crashing. It may also be worth giving ARKLogObserver
a - (void)processAllPendingLogsWithCompletionHandler:(nonnull dispatch_block_t)completionHandler;
method, and calling it here on each log distributor.
Right now, you're distributing all logs to your log observers, but your log observers may be doing async tasks & not be complete processing them by the time this callback is executed.
Also, I'm curious: what happens if an exception is triggered while we wait for all logs to be distributed? Do we enter an infinite crash loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I can tell, it won't reenter the exception handler if an exception occurs.
Added a call to -[ARKLogDistributor distributeAllPendingLogsWithCompletionHandler:]
to bump the thread priority. My goal here was to make sure that the final exception log gets written to disk (so only the default log distributor), not to flush all log distributors/observers. Flushing all logs might be helpful though, since that could give context around the crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since adding that functionality to the log observers would be a breaking change, what do you think about making it only flush the default distributor for now, and file an issue to flush all observers too in 4.0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #68 to track
e5ec963
to
81b5013
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Solid job making the default distributor distribute all logs. Adding an issue for all distributors seems reasonable to me.
However, thoughts on ARKEnableLogOnUncaughtException
taking an array of distributors to flush as an argument? That wouldn't be a breaking change, right?
@@ -61,6 +61,9 @@ Want to look at logs on device? Push an instance of [ARKLogTableViewController]( | |||
## Performance | |||
Logs are distributed to loggers on an internal background queue that will never slow down your app. Logs observed by the log store are incrementally appended to disk and not stored in memory. | |||
|
|||
## Exception Logging | |||
To turn on logging of uncaught exceptions, call `ARKEnableLogOnUncaughtException()`. When an uncaught exception occurs, the stack trace will be logged to the default log distributor. To test this out in the sample app, hold one finger down on the screen for at least 5 seconds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add in here that we'll propagate the exceptions back to any existing crash handler.
Instead of taking an array, I made it so you can specify which distributor you want, and call it multiple times to support multiple log distributors. |
// MARK: - Uncaught Exception Handling | ||
|
||
func existingUncaughtExceptionHandler(exception: NSException) { | ||
print("Existing uncaught exception handler got called") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we print the exception here?
this is super nifty btw
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure 👍
OBJC_EXTERN void ARKEnableLogOnUncaughtException(void); | ||
|
||
/// Enables logging uncaught exceptions to the specified log distributor. Calls into previously-set uncaught exception handler after logging if one has been set. | ||
OBJC_EXTERN void ARKEnableLogOnUncaughtExceptionToLogDistributor(ARKLogDistributor *_Nonnull logDistributor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dope nullable annotation
CoreAardvark/ARKExceptionLogging.m
Outdated
|
||
void ARKHandleUncaughtException(NSException *exception) | ||
{ | ||
for (ARKLogDistributor *logDistributor in ARKUncaughtExceptionLogDistributors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
😉
(super non-blocking)
{ | ||
[ARKUncaughtExceptionLogDistributors removeObject:logDistributor]; | ||
|
||
// If that was the last log distributor, clean up the handler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love this. Like, really love the thoroughness.
but: where are the tests?
Best way I can think of to test is creating an internal object that has the NSSetUncaughtExceptionHandler
and NSGetUncaughtExceptionHandler
methods handed to it via blocks so you can swap them out for test methods. Then use that internal object under the hood here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the new tests!
|
||
@class ARKLogDistributor; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have a note in this file about thread safety? Given what's happening under the hood, these methods should all be called from the same thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'll add a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, added locks to make it thread safe. :)
CoreAardvark/ARKExceptionLogging.m
Outdated
if (ARKUncaughtExceptionLogDistributors == nil) { | ||
ARKUncaughtExceptionLogDistributors = [NSMutableArray arrayWithObject:logDistributor]; | ||
|
||
ARKPreviousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need a stack of ARKPreviousUncaughtExceptionHandler
s. Otherwise the following code
ARKEnableLogOnUncaughtException()
ARKEnableLogOnUncaughtExceptionToLogDistributor(mySpecialDistributor)
will nuke whatever uncaught exception handler was previously set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if you do the above, how many times will ARKHandleUncaughtException
(which logs to all registered log distributors) be called? I think twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should only ever get called once. Unless I missed something (or they get called on different threads, as you pointed out above), it should be in one of two states:
-
Exception logging is disabled.
ARKHandleUncaughtException
is not the specified uncaught exception handler.ARKUncaughtExceptionLogDistributors
isnil
.ARKPreviousUncaughtExceptionHandler
isnil
. -
Exception logging is enabled.
ARKHandleUncaughtException
is the specified uncaught exception handler.ARKUncaughtExceptionLogDistributors
has at least one log distributor.ARKPreviousUncaughtExceptionHandler
is set to the previously-set uncaught exception handler.
When you enable logging and there are no existing log distributors set up, it will perform the initial setup (setting itself as the uncaught exception handler, and storing the existing one to chain). When you enable logging on additional distributors, it will only add them to the list of distributors to use.
When you disable logging, it will remove the log distributor from the list. Then, it checks if the list is now empty, and if so reverses the setup process, removing itself as the uncaught exception handler (and restoring the previous one) and clearing out the distributor list.
FWIW, I tested calling enable twice, and it logged the message twice, then called the existing handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦♂️ I missed the ARKUncaughtExceptionLogDistributors == nil
check somehow. Thank you for the detailed explanation. I agree that this works and my fears are unfounded. This is a really simple/clever way to make this work.
A comment above this line saying "we only want to register ourselves as an exception handler the first time a log distributor is registered" might help with long-term maintenance, but so would reading more carefully 🙂
CoreAardvark/ARKExceptionLogging.m
Outdated
{ | ||
static dispatch_once_t onceToken; | ||
dispatch_once(&onceToken, ^{ | ||
ARKUncaughtExceptionLogDistributorsLock = [[NSLock alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supnit: new
CoreAardvark/ARKExceptionLogging.m
Outdated
void ARKHandleUncaughtException(NSException *exception) | ||
{ | ||
for (ARKLogDistributor *logDistributor in ARKUncaughtExceptionLogDistributors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should lock around this method too, since the lock should be protecting ARKUncaughtExceptionLogDistributors
in addition to ARKPreviousUncaughtExceptionHandler
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests are really great! Only blocking comment is where we unlock
. Besides that, this is ready to merge.
|
||
if (ARKPreviousUncaughtExceptionHandler != nil) { | ||
|
||
[logDistributorsLock unlock]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unlock
should go after we access ARKPreviousUncaughtExceptionHandler
below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the reading of ARKPreviousUncaughtExceptionHandler
inside the lock, but left the calling outside, to avoid deadlock in case the previous handler somehow calls back into this one.
{ | ||
[ARKUncaughtExceptionLogDistributors removeObject:logDistributor]; | ||
|
||
// If that was the last log distributor, clean up the handler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the new tests!
No description provided.