Skip to content
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

Merged
merged 10 commits into from
Oct 2, 2018
Merged

Conversation

NickEntin
Copy link
Collaborator

No description provided.

Copy link
Collaborator

@dfed dfed left a 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.

@import Foundation;


/// Enables logging uncaught exceptions.
Copy link
Collaborator

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.

/// Enables logging uncaught exceptions.
OBJC_EXTERN void ARKEnableLogOnUncaughtException(void);

/// Disables logging uncaught exceptions.
Copy link
Collaborator

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.

@@ -0,0 +1,28 @@
//
// ARKExceptionLogging.h
// Aardvark
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.


void ARKEnableLogOnUncaughtException()
{
ARKPreviousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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).


void ARKHandleUncaughtException(NSException *exception)
{
ARKLogWithType(ARKLogTypeError, nil, @"Uncaught exception '%@':\n%@", exception.name, exception.debugDescription);
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@dfed
Copy link
Collaborator

dfed commented Oct 22, 2017

One more thing: we should hook this code into to the sample app :)

{
ARKLogWithType(ARKLogTypeError, nil, @"Uncaught exception '%@':\n%@", exception.name, exception.debugDescription);
dispatch_semaphore_t logDistributionSemaphore = dispatch_semaphore_create(0);
[[ARKLogDistributor defaultDistributor] distributeAllPendingLogsWithCompletionHandler:^{
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #68 to track

Copy link
Collaborator

@dfed dfed left a 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.
Copy link
Collaborator

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.

@NickEntin
Copy link
Collaborator Author

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")
Copy link
Collaborator

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

Copy link
Collaborator Author

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dope nullable annotation


void ARKHandleUncaughtException(NSException *exception)
{
for (ARKLogDistributor *logDistributor in ARKUncaughtExceptionLogDistributors) {
Copy link
Collaborator

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.
Copy link
Collaborator

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.

Copy link
Collaborator

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;


Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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. :)

if (ARKUncaughtExceptionLogDistributors == nil) {
ARKUncaughtExceptionLogDistributors = [NSMutableArray arrayWithObject:logDistributor];

ARKPreviousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
Copy link
Collaborator

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 ARKPreviousUncaughtExceptionHandlers. Otherwise the following code

ARKEnableLogOnUncaughtException()
ARKEnableLogOnUncaughtExceptionToLogDistributor(mySpecialDistributor)

will nuke whatever uncaught exception handler was previously set.

Copy link
Collaborator

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?

Copy link
Collaborator Author

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:

  1. Exception logging is disabled. ARKHandleUncaughtException is not the specified uncaught exception handler. ARKUncaughtExceptionLogDistributors is nil. ARKPreviousUncaughtExceptionHandler is nil.

  2. 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.

Copy link
Collaborator

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 🙂

{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ARKUncaughtExceptionLogDistributorsLock = [[NSLock alloc] init];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supnit: new

void ARKHandleUncaughtException(NSException *exception)
{
for (ARKLogDistributor *logDistributor in ARKUncaughtExceptionLogDistributors) {
Copy link
Collaborator

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?

Copy link
Collaborator

@dfed dfed left a 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];
Copy link
Collaborator

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

Copy link
Collaborator Author

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the new tests!

@NickEntin NickEntin merged commit ae05c36 into square:master Oct 2, 2018
@NickEntin NickEntin deleted the entin/exception-logging branch October 2, 2018 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants