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

fix(oom): Improve background app state detection #394

Merged
merged 4 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
Changelog
=========

## TBD

### Bug fixes

* Support adding pre-delivery metadata to out-of-memory reports
[#393](https://github.com/bugsnag/bugsnag-cocoa/pull/393)
* Fix erroneously reporting out-of-memory events from iOS app extensions
[#394](https://github.com/bugsnag/bugsnag-cocoa/pull/394)
* Fix erroneously reporting out-of-memory events when an iOS app is in the
foreground but inactive
[#394](https://github.com/bugsnag/bugsnag-cocoa/pull/394)
* Fix erroneously reporting out-of-memory events when the app terminates
normally and is issued a "will terminate" notification, but is terminated
prior to the out-of-memory watchdog processing the notification
kattrali marked this conversation as resolved.
Show resolved Hide resolved
[#394](https://github.com/bugsnag/bugsnag-cocoa/pull/394)

## 5.22.3 (2019-07-15)

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Run the integration tests using `make e2e` (end-to-end)
- [ ] Have the changelog and README been updated?
- [ ] Are there pull requests for installation changes on the [dashboard](https://github.com/bugsnag/dashboard-js)?
- [ ] Are there pull requests for new features/behavior on the [docs site](https://github.com/bugsnag/docs.bugsnag.com)?
- [ ] Run `./Tests/prerelease/run_prerelease_checks.sh`
- [ ] Has all new functionality been manually tested on a release build?
- [ ] Do the installation instructions work when creating an example app from scratch?
- [ ] If a response is not received from the server, is the report queued for later?
Expand Down
54 changes: 53 additions & 1 deletion Source/BSGOutOfMemoryWatchdog.m
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ - (void)enable {
selector:@selector(handleTransitionToForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[center addObserver:self
selector:@selector(handleTransitionToActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[center addObserver:self
selector:@selector(handleTransitionToInactive:)
name:UIApplicationWillResignActiveNotification
object:nil];
[center addObserver:self
selector:@selector(handleLowMemoryChange:)
name:UIApplicationDidReceiveMemoryWarningNotification
Expand Down Expand Up @@ -114,6 +122,17 @@ - (void)observeValueForKeyPath:(NSString *)keyPath
self.cachedFileInfo[@"app"][@"releaseStage"] = change[NSKeyValueChangeNewKey];
[self writeSentinelFile];
}

- (void)handleTransitionToActive:(NSNotification *)note {
self.cachedFileInfo[@"app"][@"isActive"] = @YES;
[self writeSentinelFile];
kattrali marked this conversation as resolved.
Show resolved Hide resolved
}

- (void)handleTransitionToInactive:(NSNotification *)note {
self.cachedFileInfo[@"app"][@"isActive"] = @NO;
[self writeSentinelFile];
}

- (void)handleTransitionToForeground:(NSNotification *)note {
self.cachedFileInfo[@"app"][@"inForeground"] = @YES;
[self writeSentinelFile];
Expand Down Expand Up @@ -154,6 +173,8 @@ - (BOOL)computeDidOOMLastLaunchWithConfig:(BugsnagConfiguration *)config {
[lastBootInfo valueForKeyPath:@"device.osBuild"];
BOOL lastBootInForeground =
[[lastBootInfo valueForKeyPath:@"app.inForeground"] boolValue];
BOOL lastBootWasActive =
[[lastBootInfo valueForKeyPath:@"app.isActive"] boolValue];
NSString *osVersion = [BSG_KSSystemInfo osBuildVersion];
NSDictionary *appInfo = [[NSBundle mainBundle] infoDictionary];
NSString *bundleVersion =
Expand All @@ -163,7 +184,8 @@ - (BOOL)computeDidOOMLastLaunchWithConfig:(BugsnagConfiguration *)config {
BOOL sameVersions = [lastBootOSVersion isEqualToString:osVersion] &&
[lastBootBundleVersion isEqualToString:bundleVersion] &&
[lastBootAppVersion isEqualToString:appVersion];
BOOL shouldReport = config.reportOOMs && (config.reportBackgroundOOMs || lastBootInForeground);
BOOL shouldReport = config.reportOOMs
&& (config.reportBackgroundOOMs || (lastBootInForeground && lastBootWasActive));
[self deleteSentinelFile];
return sameVersions && shouldReport;
}
Expand Down Expand Up @@ -221,7 +243,24 @@ - (NSMutableDictionary *)generateCacheInfoWithConfig:(BugsnagConfiguration *)con
app[@"releaseStage"] = config.releaseStage;
app[@"version"] = systemInfo[@BSG_KSSystemField_BundleShortVersion] ?: @"";
app[@"bundleVersion"] = systemInfo[@BSG_KSSystemField_BundleVersion] ?: @"";
#if BSGOOMAvailable
UIApplicationState state = [self currentAppState];
// The app is in the foreground if the current state is "active" or
// "inactive". From the UIApplicationState docs:
// > UIApplicationStateActive
// > The app is running in the foreground and currently receiving events.
// > UIApplicationStateInactive
// > The app is running in the foreground but is not receiving events.
// > This might happen as a result of an interruption or because the app
// > is transitioning to or from the background.
// > UIApplicationStateBackground
// > The app is running in the background.
app[@"inForeground"] = @(state == UIApplicationStateInactive
kattrali marked this conversation as resolved.
Show resolved Hide resolved
|| state == UIApplicationStateActive);
app[@"isActive"] = @(state == UIApplicationStateActive);
#else
app[@"inForeground"] = @YES;
#endif
#if TARGET_OS_TV
app[@"type"] = @"tvOS";
#elif TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE
Expand All @@ -246,4 +285,17 @@ - (NSMutableDictionary *)generateCacheInfoWithConfig:(BugsnagConfiguration *)con
return cache;
}

// Only available on iOS/tvOS
#if BSGOOMAvailable
- (UIApplicationState)currentAppState {
// Only checked outside of app extensions since sharedApplication is
// unavailable to extension UIKit APIs
if ([BSG_KSSystemInfo isRunningInAppExtension]) {
return UIApplicationStateActive;
}
UIApplication *app = [UIApplication performSelector:@selector(sharedApplication)];
kattrali marked this conversation as resolved.
Show resolved Hide resolved
return [app applicationState];
}
#endif

@end
5 changes: 0 additions & 5 deletions Source/BugsnagBreadcrumb.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,4 @@ typedef void (^BSGBreadcrumbConfiguration)(BugsnagBreadcrumb *_Nonnull);
*/
- (NSArray *_Nullable)arrayValue;

/**
* Reads and return breadcrumb data currently stored on disk
*/
- (NSDictionary *_Nullable)cachedBreadcrumbs;

@end
6 changes: 3 additions & 3 deletions Source/BugsnagBreadcrumb.m
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ - (void)addBreadcrumbWithBlock:
}
}

- (NSDictionary *)cachedBreadcrumbs {
__block NSDictionary *cache = nil;
- (NSArray *)cachedBreadcrumbs {
__block NSArray *cache = nil;
dispatch_barrier_sync(self.readWriteQueue, ^{
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:self.cachePath options:0 error:&error];
Expand All @@ -245,7 +245,7 @@ - (NSDictionary *)cachedBreadcrumbs {
bsg_log_err(@"Failed to read breadcrumbs from disk: %@", error);
}
});
return cache;
return [cache isKindOfClass:[NSArray class]] ? cache : nil;
}

@synthesize capacity = _capacity;
Expand Down
40 changes: 32 additions & 8 deletions Source/BugsnagNotifier.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "BugsnagNotifier.h"
#import "BSGConnectivity.h"
#import "Bugsnag.h"
#import "Private.h"
#import "BugsnagCrashSentry.h"
#import "BugsnagHandledState.h"
#import "BugsnagLogger.h"
Expand All @@ -36,6 +37,7 @@
#import "BSG_RFC3339DateTool.h"
#import "BSG_KSCrashType.h"
#import "BSG_KSCrashState.h"
#import "BSG_KSSystemInfo.h"
#import "BSG_KSMach.h"

#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE
Expand Down Expand Up @@ -387,7 +389,16 @@ - (void)start {
#endif

_started = YES;
if (self.configuration.reportOOMs && !bsg_ksmachisBeingTraced() && self.configuration.autoNotify) {
// autoNotify disables all unhandled event reporting
BOOL configuredToReportOOMs = self.configuration.reportOOMs && self.configuration.autoNotify;
// Disable if a debugger is enabled, since the development cycle of starting
// and restarting an app is also an uncatchable kill
BOOL noDebuggerEnabled = !bsg_ksmachisBeingTraced();
// Disable if in an app extension, since app extensions have a different
// app lifecycle and the heuristic used for finding app terminations rooted
// in fixable code does not apply
BOOL notInAppExtension = ![BSG_KSSystemInfo isRunningInAppExtension];
if (configuredToReportOOMs && noDebuggerEnabled && notInAppExtension) {
[self.oomWatchdog enable];
}

Expand Down Expand Up @@ -579,16 +590,29 @@ - (void)notifyOutOfMemoryEvent {
static NSString *const BSGOutOfMemoryErrorClass = @"Out Of Memory";
static NSString *const BSGOutOfMemoryMessageFormat = @"The app was likely terminated by the operating system while in the %@";
NSMutableDictionary *lastLaunchInfo = [[self.oomWatchdog lastBootCachedFileInfo] mutableCopy];
BOOL wasInForeground = [[lastLaunchInfo valueForKeyPath:@"app.inForeground"] boolValue];
NSString *message = [NSString stringWithFormat:BSGOutOfMemoryMessageFormat, wasInForeground ? @"foreground" : @"background"];
BugsnagHandledState *handledState = [BugsnagHandledState
handledStateWithSeverityReason:LikelyOutOfMemory
severity:BSGSeverityError
attrValue:nil];
NSDictionary *crumbs = [self.configuration.breadcrumbs cachedBreadcrumbs];
NSArray *crumbs = [self.configuration.breadcrumbs cachedBreadcrumbs];
if (crumbs.count > 0) {
lastLaunchInfo[@"breadcrumbs"] = crumbs;
}
for (NSDictionary *crumb in crumbs) {
if ([crumb isKindOfClass:[NSDictionary class]]
&& [crumb[@"name"] isKindOfClass:[NSString class]]) {
NSString *name = crumb[@"name"];
// If the termination breadcrumb is set, the app entered a normal
// termination flow but expired before the watchdog sentinel could
// be updated. In this case, no report should be sent.
if ([name isEqualToString:kAppWillTerminate]) {
return;
}
}
}

BOOL wasInForeground = [[lastLaunchInfo valueForKeyPath:@"app.inForeground"] boolValue];
NSString *message = [NSString stringWithFormat:BSGOutOfMemoryMessageFormat, wasInForeground ? @"foreground" : @"background"];
BugsnagHandledState *handledState = [BugsnagHandledState
handledStateWithSeverityReason:LikelyOutOfMemory
severity:BSGSeverityError
attrValue:nil];
NSDictionary *appState = @{@"oom": lastLaunchInfo, @"didOOM": @YES};
[self.crashSentry reportUserException:BSGOutOfMemoryErrorClass
reason:message
Expand Down
5 changes: 5 additions & 0 deletions Source/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,9 @@
*/
+ (NSString *)osBuildVersion;

/**
* Whether the current main bundle is an iOS app extension
*/
+ (BOOL)isRunningInAppExtension;

@end
20 changes: 20 additions & 0 deletions Source/KSCrash/Source/KSCrash/Recording/BSG_KSSystemInfo.m
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,26 @@ + (NSString *)osBuildVersion {
return [self stringSysctl:@"kern.osversion"];
}

+ (BOOL)isRunningInAppExtension {
#if BSG_KSCRASH_HOST_IOS
NSBundle *mainBundle = [NSBundle mainBundle];
// From the App Extension Programming Guide:
// > When you build an extension based on an Xcode template, you get an
// > extension bundle that ends in .appex.
return [[mainBundle executablePath] containsString:@".appex"]
// In the case that the extension bundle was renamed or generated
// outside of the Xcode template, check the Bundle OS Type Code:
// > This key consists of a four-letter code for the bundle type. For
// > apps, the code is APPL, for frameworks, it's FMWK, and for bundles,
// > it's BNDL.
// If the main bundle type is not "APPL", assume this is an extension
// context.
|| ![[mainBundle infoDictionary][@"CFBundlePackageType"] isEqualToString:@"APPL"];
#else
return NO;
#endif
}

@end

const char *bsg_kssysteminfo_toJSON(void) {
Expand Down
13 changes: 13 additions & 0 deletions Source/Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef BUGSNAG_PRIVATE_H
#define BUGSNAG_PRIVATE_H

#import "BugsnagBreadcrumb.h"

@interface BugsnagBreadcrumbs ()
/**
* Reads and return breadcrumb data currently stored on disk
*/
- (NSArray *_Nullable)cachedBreadcrumbs;
@end

#endif // BUGSNAG_PRIVATE_H
12 changes: 12 additions & 0 deletions Tests/prerelease/features/out_of_memory.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Feature: Out of memory events

Scenario: The OS kills the application while inactive
The application is in the foreground but inactive when interrupted
by a phone call or Siri

When I run "OOMScenario"
And the app is interrupted by Siri
And the app is unexpectedly terminated
And I relaunch the app
And I wait for 10 seconds
Then I should receive 0 requests
6 changes: 6 additions & 0 deletions Tests/prerelease/run_prerelease_checks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

# Run visibly to allow scripting access (pressing buttons, etc)
open $(xcode-select -p)/Applications/Simulator.app

bundle exec maze-runner Tests/prerelease/features/
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSExceptionShiftScenario.m; sourceTree = "<group>"; };
8A42FD33225DEE04007AE561 /* SessionOOMScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SessionOOMScenario.m; sourceTree = "<group>"; };
8A42FD34225DEE04007AE561 /* SessionOOMScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SessionOOMScenario.h; sourceTree = "<group>"; };
8A56EE7F22E22ED80066B9DC /* OOMWillTerminateScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOMWillTerminateScenario.m; sourceTree = "<group>"; };
8A56EE8022E22ED80066B9DC /* OOMWillTerminateScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMWillTerminateScenario.h; sourceTree = "<group>"; };
8A840FB921AF5C450041DBFA /* SwiftAssertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftAssertion.swift; sourceTree = "<group>"; };
8A98400120FD11BF0023ECD1 /* AutoSessionCustomVersionScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AutoSessionCustomVersionScenario.h; sourceTree = "<group>"; };
8A98400220FD11BF0023ECD1 /* AutoSessionCustomVersionScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AutoSessionCustomVersionScenario.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -233,6 +235,8 @@
F42953DE2BB41023C0B07F41 /* scenarios */ = {
isa = PBXGroup;
children = (
8A56EE8022E22ED80066B9DC /* OOMWillTerminateScenario.h */,
8A56EE7F22E22ED80066B9DC /* OOMWillTerminateScenario.m */,
8A14F0F42282D4AE00337B05 /* ReportBackgroundOOMsEnabledScenario.h */,
8A14F0F02282D4AD00337B05 /* ReportBackgroundOOMsEnabledScenario.m */,
8A14F0EF2282D4AD00337B05 /* ReportOOMsDisabledReportBackgroundOOMsEnabledScenario.h */,
Expand Down Expand Up @@ -446,6 +450,7 @@
F42955E0916B8851F074D9B3 /* UserEmailScenario.swift in Sources */,
F4295968571A4118D6A4606A /* UserEnabledScenario.swift in Sources */,
F4295A036B228AF608641699 /* UserDisabledScenario.swift in Sources */,
8A56EE8222E22ED80066B9DC /* OOMWillTerminateScenario.m in Sources */,
8A14F0F62282D4AE00337B05 /* ReportOOMsDisabledScenario.m in Sources */,
E7767F13221C21E30006648C /* ResumedSessionScenario.swift in Sources */,
8AEFC73120F8D1A000A78779 /* AutoSessionWithUserScenario.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#import "Scenario.h"

@interface OOMWillTerminateScenario : Scenario

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

#import "OOMWillTerminateScenario.h"
#import <signal.h>
#import <UIKit/UIKit.h>

@implementation OOMWillTerminateScenario

- (void)startBugsnag {
self.config.shouldAutoCaptureSessions = NO;
[super startBugsnag];
}

- (void)run {
dispatch_async(dispatch_get_main_queue(), ^{
exit(0);
});
}
@end
22 changes: 22 additions & 0 deletions features/out_of_memory.feature
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ Feature: Reporting out of memory events
And the event "metaData.extra.shape" equals "line"
And the event breadcrumbs contain "Crumb left before crash"

Scenario: The app is terminated normally
The application can be gracefully terminated by the OS if more
memory is needed for other applications or directly by calling
exit(0)

When I crash the app using "OOMWillTerminateScenario"
And I wait for 4 seconds
And I relaunch the app
And I wait for 10 seconds
Then I should receive 0 requests

Scenario: The app is terminated normally
The application can be gracefully terminated by the OS if more
memory is needed for other applications or directly by calling
exit(0)

When I crash the app using "OOMWillTerminateScenario"
And I wait for 4 seconds
And I relaunch the app
kattrali marked this conversation as resolved.
Show resolved Hide resolved
And I wait for 10 seconds
Then I should receive 0 requests

Scenario: The OS kills the application in the background
When I run "OOMScenario"
And I put the app in the background
Expand Down
17 changes: 17 additions & 0 deletions features/scripts/activate_siri.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env osascript

tell application "Simulator"
activate
end tell

tell application "System Events"
tell process "Simulator"
tell menu bar 1
tell menu bar item "Hardware"
tell menu "Hardware"
click menu item "Siri"
end tell
end tell
end tell
end tell
end tell
Loading