From 9d53208a8ca4cb124eba230b6b47fee6b69e5854 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 25 Aug 2021 13:56:36 +0100 Subject: [PATCH 1/2] Add E2E scenario for app hang during termination --- features/app_hangs.feature | 13 ++++++ .../ios/iOSTestApp.xcodeproj/project.pbxproj | 4 ++ .../macOSTestApp.xcodeproj/project.pbxproj | 4 ++ .../AppHangInTerminationScenario.swift | 41 +++++++++++++++++++ features/steps/ios_steps.rb | 16 +++++--- 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 features/fixtures/shared/scenarios/AppHangInTerminationScenario.swift diff --git a/features/app_hangs.feature b/features/app_hangs.feature index 5acfd85b7..1210a6151 100644 --- a/features/app_hangs.feature +++ b/features/app_hangs.feature @@ -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" diff --git a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj index f1dbfd62f..60a8dd2e2 100644 --- a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj @@ -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 */; }; @@ -189,6 +190,7 @@ 01E5EAD025B713990066EA8A /* OOMScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OOMScenario.h; sourceTree = ""; }; 01E5EAD125B713990066EA8A /* OOMScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OOMScenario.m; sourceTree = ""; }; 01F1474325F282E600C2DC65 /* AppHangScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHangScenarios.swift; sourceTree = ""; }; + 01FA9EC326D63BB20059FF4A /* AppHangInTerminationScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangInTerminationScenario.swift; sourceTree = ""; }; 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; 8A32DB8022424E3000EDD92F /* NSExceptionShiftScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSExceptionShiftScenario.h; sourceTree = ""; }; 8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSExceptionShiftScenario.m; sourceTree = ""; }; @@ -583,6 +585,7 @@ F49695AE2445476700105DA9 /* Plugin */, 0037410E2473CF2300BE41AA /* AppAndDeviceAttributesScenario.swift */, 01E0DB0A25E8EBD100A740ED /* AppDurationScenario.swift */, + 01FA9EC326D63BB20059FF4A /* AppHangInTerminationScenario.swift */, 01F1474325F282E600C2DC65 /* AppHangScenarios.swift */, 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */, 01DE903726CE99B800455213 /* CriticalThermalStateScenario.swift */, @@ -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 */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index 0aaf0bb90..786deb32d 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -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 */; }; @@ -346,6 +347,7 @@ 01F47CC1254B1B3000B184AD /* ResumeSessionOOMScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResumeSessionOOMScenario.m; sourceTree = ""; }; 01F47CC2254B1B3000B184AD /* SIGBUSScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIGBUSScenario.h; sourceTree = ""; }; 01F47CC3254B1B3100B184AD /* SIGFPEScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SIGFPEScenario.h; sourceTree = ""; }; + 01FA9EC526D64FFF0059FF4A /* AppHangInTerminationScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHangInTerminationScenario.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 */, @@ -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 */, diff --git a/features/fixtures/shared/scenarios/AppHangInTerminationScenario.swift b/features/fixtures/shared/scenarios/AppHangInTerminationScenario.swift new file mode 100644 index 000000000..8a3026f5e --- /dev/null +++ b/features/fixtures/shared/scenarios/AppHangInTerminationScenario.swift @@ -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 + } +} diff --git a/features/steps/ios_steps.rb b/features/steps/ios_steps.rb index 9ef100248..9d1b786b9 100644 --- a/features/steps/ios_steps.rb +++ b/features/steps/ios_steps.rb @@ -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 From c6a8c8f2b73649dd7335a27d5c45120df9aa8b23 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 25 Aug 2021 14:38:56 +0100 Subject: [PATCH 2/2] Do not report fatal app hangs if terminating --- Bugsnag/Client/BugsnagClient+Private.h | 5 ----- Bugsnag/Client/BugsnagClient.m | 13 +++++++++++-- CHANGELOG.md | 5 +++++ Tests/BugsnagClientMirrorTest.m | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index 194b2dba2..553e31acc 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -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 diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index eba386a41..f764ebe6e 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -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; } @@ -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) { @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index c5074aaa7..5342ea536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Tests/BugsnagClientMirrorTest.m b/Tests/BugsnagClientMirrorTest.m index 299cdc390..f024e8a14 100644 --- a/Tests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagClientMirrorTest.m @@ -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",