Skip to content

Commit

Permalink
Merge pull request #1176 from bugsnag/nickdowell/app-hang-in-will-ter…
Browse files Browse the repository at this point in the history
…minate

[PLAT-7151] Do not report fatal app hangs if terminating
  • Loading branch information
nickdowell authored Aug 26, 2021
2 parents 8838981 + c6a8c8f commit 5c20d52
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 14 deletions.
5 changes: 0 additions & 5 deletions Bugsnag/Client/BugsnagClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,12 @@ NS_ASSUME_NONNULL_BEGIN

- (BugsnagEvent *)generateOutOfMemoryEvent;

/// @return A `BugsnagEvent` if the last run ended with a fatal app hang, `nil` otherwise.
- (nullable BugsnagEvent *)loadFatalAppHangEvent;

- (void)notifyInternal:(BugsnagEvent *)event block:(nullable BugsnagOnErrorBlock)block;

- (void)removeObserverWithBlock:(BugsnagObserverBlock)block; // Used in BugsnagReactNative

- (void)start;

- (void)startAppHangDetector;

@end

NS_ASSUME_NONNULL_END
13 changes: 11 additions & 2 deletions Bugsnag/Client/BugsnagClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ - (void)computeDidCrashLastLaunch {
didCrash = YES;
}
// Was the app terminated while the main thread was hung?
else if ((self.eventFromLastLaunch = [self loadFatalAppHangEvent])) {
else if ((self.eventFromLastLaunch = [self loadAppHangEvent]).unhandled) {
bsg_log_info(@"Last run terminated during an app hang.");
didCrash = YES;
}
Expand Down Expand Up @@ -1185,7 +1185,7 @@ - (void)appHangEnded {
self.appHangEvent = nil;
}

- (nullable BugsnagEvent *)loadFatalAppHangEvent {
- (nullable BugsnagEvent *)loadAppHangEvent {
NSError *error = nil;
NSDictionary *json = [BSGJSONSerialization JSONObjectWithContentsOfFile:BSGFileLocations.current.appHangEvent options:0 error:&error];
if (!json) {
Expand All @@ -1201,6 +1201,15 @@ - (nullable BugsnagEvent *)loadFatalAppHangEvent {
return nil;
}

// Receipt of the willTerminateNotification indicates that an app hang was not the cause of the termination, so treat as non-fatal.
if ([self.systemState.lastLaunchState[SYSTEMSTATE_KEY_APP][SYSTEMSTATE_APP_WAS_TERMINATED] boolValue]) {
if (self.configuration.appHangThresholdMillis == BugsnagAppHangThresholdFatalOnly) {
return nil;
}
event.session.handledCount++;
return event;
}

// Update event to reflect that the app hang was fatal.
event.errors.firstObject.errorMessage = @"The app was terminated while unresponsive";
// Cannot set event.severity directly because that sets severityReason.type to "userCallbackSetSeverity"
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Changelog
when the thermal state is critical will now be reported as a "Thermal Kill" rather than Out Of Memory error.
[#1171](https://github.com/bugsnag/bugsnag-cocoa/pull/1171)

### Bug fixes

* Fatal app hangs will no longer be reported if the `willTerminateNotification` is received.
[#1176](https://github.com/bugsnag/bugsnag-cocoa/pull/1176)

## 6.11.0 (2021-08-18)

### Enhancements
Expand Down
2 changes: 1 addition & 1 deletion Tests/BugsnagClientMirrorTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ - (void)setUp {
@"lastOrientation @16@0:8",
@"lastThermalState q16@0:8",
@"leaveBreadcrumbForEvent: v24@0:8@16",
@"loadFatalAppHangEvent @16@0:8",
@"loadAppHangEvent @16@0:8",
@"metadata @16@0:8",
@"metadataChanged: v24@0:8@16",
@"metadataFile @16@0:8",
Expand Down
13 changes: 13 additions & 0 deletions features/app_hangs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,16 @@ Feature: App hangs
And I background the app for 3 seconds
And I wait to receive an error
And the exception "message" equals "The app's main thread failed to respond to an event within 2000 milliseconds"

Scenario: App hangs that occur during app termination should be non-fatal
Given I run "AppHangInTerminationScenario"
And the app is not running
And I relaunch the app
And I configure Bugsnag for "AppHangInTerminationScenario"
Then I wait to receive an error
And the event "severity" equals "warning"
And the event "severityReason.type" equals "appHang"
And the event "unhandled" is false
And the exception "errorClass" equals "App Hang"
And the exception "message" equals "The app's main thread failed to respond to an event within 2000 milliseconds"
And the exception "type" equals "cocoa"
4 changes: 4 additions & 0 deletions features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
01E356C026CD5B6A00BE3F64 /* ThermalStateBreadcrumbScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E356BF26CD5B6A00BE3F64 /* ThermalStateBreadcrumbScenario.swift */; };
01E5EAD225B713990066EA8A /* OOMScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E5EAD125B713990066EA8A /* OOMScenario.m */; };
01F1474425F282E600C2DC65 /* AppHangScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F1474325F282E600C2DC65 /* AppHangScenarios.swift */; };
01FA9EC426D63BB20059FF4A /* AppHangInTerminationScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FA9EC326D63BB20059FF4A /* AppHangInTerminationScenario.swift */; };
6526A0D4248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */; };
8A14F0F62282D4AE00337B05 /* (null) in Sources */ = {isa = PBXBuildFile; };
8A32DB8222424E3000EDD92F /* NSExceptionShiftScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */; };
Expand Down Expand Up @@ -189,6 +190,7 @@
01E5EAD025B713990066EA8A /* OOMScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OOMScenario.h; sourceTree = "<group>"; };
01E5EAD125B713990066EA8A /* OOMScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OOMScenario.m; sourceTree = "<group>"; };
01F1474325F282E600C2DC65 /* AppHangScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHangScenarios.swift; sourceTree = "<group>"; };
01FA9EC326D63BB20059FF4A /* AppHangInTerminationScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangInTerminationScenario.swift; sourceTree = "<group>"; };
6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = "<group>"; };
8A32DB8022424E3000EDD92F /* NSExceptionShiftScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSExceptionShiftScenario.h; sourceTree = "<group>"; };
8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSExceptionShiftScenario.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -583,6 +585,7 @@
F49695AE2445476700105DA9 /* Plugin */,
0037410E2473CF2300BE41AA /* AppAndDeviceAttributesScenario.swift */,
01E0DB0A25E8EBD100A740ED /* AppDurationScenario.swift */,
01FA9EC326D63BB20059FF4A /* AppHangInTerminationScenario.swift */,
01F1474325F282E600C2DC65 /* AppHangScenarios.swift */,
01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */,
01DE903726CE99B800455213 /* CriticalThermalStateScenario.swift */,
Expand Down Expand Up @@ -954,6 +957,7 @@
E700EE55247D3204008CFFB6 /* OnSendOverwriteScenario.swift in Sources */,
F429538D8941382EC2C857CE /* AsyncSafeThreadScenario.m in Sources */,
F42955869D33EE0E510B9651 /* ReadGarbagePointerScenario.m in Sources */,
01FA9EC426D63BB20059FF4A /* AppHangInTerminationScenario.swift in Sources */,
8AEFC73420F8D1BB00A78779 /* ManualSessionWithUserScenario.m in Sources */,
E753F25424937A83001FB671 /* ThreadScenarios.m in Sources */,
F4295B56219D228FAA99BC14 /* ObjCExceptionScenario.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
01F47D30254B1B3100B184AD /* AutoContextNSExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47CBF254B1B3000B184AD /* AutoContextNSExceptionScenario.swift */; };
01F47D31254B1B3100B184AD /* OverwriteLinkRegisterScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F47CC0254B1B3000B184AD /* OverwriteLinkRegisterScenario.m */; };
01F47D32254B1B3100B184AD /* ResumeSessionOOMScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F47CC1254B1B3000B184AD /* ResumeSessionOOMScenario.m */; };
01FA9EC626D64FFF0059FF4A /* AppHangInTerminationScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FA9EC526D64FFF0059FF4A /* AppHangInTerminationScenario.swift */; };
CBB7878E2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB7878C2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.m */; };
E780377D264D703500430C11 /* AutoNotifyReenabledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7803779264D703500430C11 /* AutoNotifyReenabledScenario.swift */; };
E780377E264D703500430C11 /* AutoNotifyFalseHandledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = E780377A264D703500430C11 /* AutoNotifyFalseHandledScenario.swift */; };
Expand Down Expand Up @@ -346,6 +347,7 @@
01F47CC1254B1B3000B184AD /* ResumeSessionOOMScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResumeSessionOOMScenario.m; sourceTree = "<group>"; };
01F47CC2254B1B3000B184AD /* SIGBUSScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIGBUSScenario.h; sourceTree = "<group>"; };
01F47CC3254B1B3100B184AD /* SIGFPEScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIGFPEScenario.h; sourceTree = "<group>"; };
01FA9EC526D64FFF0059FF4A /* AppHangInTerminationScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHangInTerminationScenario.swift; sourceTree = "<group>"; };
2C49722B331FF4B0DC477462 /* Pods-macOSTestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-macOSTestApp.release.xcconfig"; path = "Target Support Files/Pods-macOSTestApp/Pods-macOSTestApp.release.xcconfig"; sourceTree = "<group>"; };
5C65BFC9838298CFA8A35072 /* Pods_macOSTestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_macOSTestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CBB7878C2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MarkUnhandledHandledScenario.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -378,6 +380,7 @@
01F47C5B254B1B2E00B184AD /* AccessNonObjectScenario.m */,
01F47C60254B1B2E00B184AD /* AppAndDeviceAttributesScenario.swift */,
01E0DB0425E8E90500A740ED /* AppDurationScenario.swift */,
01FA9EC526D64FFF0059FF4A /* AppHangInTerminationScenario.swift */,
01F1473925F2817100C2DC65 /* AppHangScenarios.swift */,
01018BAA25E417EC000312C6 /* AsyncSafeMallocScenario.m */,
01F47C73254B1B2E00B184AD /* AsyncSafeThreadScenario.h */,
Expand Down Expand Up @@ -809,6 +812,7 @@
01F47CCD254B1B3100B184AD /* NullPointerScenario.m in Sources */,
01F47D30254B1B3100B184AD /* AutoContextNSExceptionScenario.swift in Sources */,
01F47CE1254B1B3100B184AD /* ManualSessionWithUserScenario.m in Sources */,
01FA9EC626D64FFF0059FF4A /* AppHangInTerminationScenario.swift in Sources */,
01F47D01254B1B3100B184AD /* SessionCallbackOrderScenario.swift in Sources */,
01AF6A84258BB38A00FFC803 /* DispatchCrashScenario.swift in Sources */,
01F47D0A254B1B3100B184AD /* CxxExceptionScenario.mm in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// AppHangInTerminationScenario.swift
// iOSTestApp
//
// Created by Nick Dowell on 25/08/2021.
// Copyright © 2021 Bugsnag. All rights reserved.
//

#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif

class AppHangInTerminationScenario: Scenario {

override func startBugsnag() {
config.appHangThresholdMillis = 2_000
super.startBugsnag()
}

override func run() {
#if os(iOS)
let willTerminate = UIApplication.willTerminateNotification
#elseif os(macOS)
let willTerminate = NSApplication.willTerminateNotification
#endif

NotificationCenter.default.addObserver(forName: willTerminate, object: nil, queue: nil) {
NSLog("Received \($0.name.rawValue), simulating an app hang...")
Thread.sleep(forTimeInterval: 3)
}

#if os(iOS)
// Appium is not able to close apps gracefully, so we simulate this using private API
UIApplication.shared.perform(Selector(("terminateWithSuccess")))
#elseif os(macOS)
NSApp.terminate(self)
#endif
}
}
16 changes: 10 additions & 6 deletions features/steps/ios_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,26 @@ def click_if_present(element)

Then('the app is running in the foreground') do
wait_for_true do
status = Maze.driver.execute_script('mobile: queryAppState', {bundleId: 'com.bugsnag.iOSTestApp'})
status == 4
Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_foreground
end
end

Then('the app is running in the background') do
wait_for_true do
status = Maze.driver.execute_script('mobile: queryAppState', {bundleId: 'com.bugsnag.iOSTestApp'})
status == 3
Maze.driver.app_state('com.bugsnag.iOSTestApp') == :running_in_background
end
end

Then('the app is not running') do
wait_for_true do
status = Maze.driver.execute_script('mobile: queryAppState', {bundleId: 'com.bugsnag.iOSTestApp'})
status == 1
case Maze.driver.capabilities['platformName']
when 'iOS'
Maze.driver.app_state('com.bugsnag.iOSTestApp') == :not_running
when 'Mac'
`lsappinfo info -only pid -app com.bugsnag.macOSTestApp`.empty?
else
raise "Don't know how to query app state on this platform"
end
end
end

Expand Down

0 comments on commit 5c20d52

Please sign in to comment.