From 7288728eb874607fe753293f4906ec123ba20fe9 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sat, 25 May 2024 15:28:59 -0400 Subject: [PATCH 1/3] add lifecycle swizzling --- ...ateTracker.m => KSCrashAppStateTracker.mm} | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) rename Sources/KSCrashRecording/{KSCrashAppStateTracker.m => KSCrashAppStateTracker.mm} (56%) diff --git a/Sources/KSCrashRecording/KSCrashAppStateTracker.m b/Sources/KSCrashRecording/KSCrashAppStateTracker.mm similarity index 56% rename from Sources/KSCrashRecording/KSCrashAppStateTracker.m rename to Sources/KSCrashRecording/KSCrashAppStateTracker.mm index a2fa10d82..c79cbacb6 100644 --- a/Sources/KSCrashRecording/KSCrashAppStateTracker.m +++ b/Sources/KSCrashRecording/KSCrashAppStateTracker.mm @@ -29,6 +29,8 @@ #import #import +#import +#import #if KSCRASH_HAS_UIAPPLICATION #import @@ -110,7 +112,11 @@ @interface KSCrashAppStateTracker () { os_unfair_lock _lock; KSCrashAppTransitionState _transitionState; NSMutableArray> *_observers; + BOOL _proxied; } + +@property (nonatomic, assign) BOOL proxied; + @end @implementation KSCrashAppStateTracker @@ -156,6 +162,15 @@ - (void)dealloc [self stop]; } +- (void)setProxied:(BOOL)proxied +{ + _proxied = proxied; + if (proxied) { + [self stop]; + _registrations = @[]; + } +} + // Observers are either an object passed in that // implements `KSCrashAppStateTrackerObserving` or a block. // Both will be wrapped in a `KSCrashAppStateTrackerBlockObserver`. @@ -322,4 +337,239 @@ - (void)stop } } +static BOOL SwizzleInstanceMethod(Class klass, SEL originalSelector, SEL swizzledSelector) +{ + //Obtaining original and swizzled method: + Method original = class_getInstanceMethod(klass, originalSelector); + Method swizzled = class_getInstanceMethod(klass, swizzledSelector); + + if (!original || !swizzled) { + return NO; + } + + method_exchangeImplementations(original, swizzled); + + return YES; +} + +typedef BOOL (*ApplicationDelegate_TwoArgs)(id, SEL, id, id); +typedef void (*ApplicationDelegate_OneArg)(id, SEL, id); + +static std::map gMappings = {}; + +static void __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg) +{ + std::string name(sel_getName(cmd)); + NSLog(@"[MAP] %s", name.c_str()); + const auto it = gMappings.find(name); + if (it != gMappings.end()) { + NSLog(@"[MAP:implemented] %s", name.c_str()); + ApplicationDelegate_OneArg imp = (ApplicationDelegate_OneArg)method_getImplementation(it->second); + imp(self, cmd, arg); + } +} + +static BOOL __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg1, id arg2) +{ + std::string name(sel_getName(cmd)); + NSLog(@"[MAP] %s", name.c_str()); + const auto it = gMappings.find(name); + if (it != gMappings.end()) { + NSLog(@"[MAP:implemented] %s", name.c_str()); + ApplicationDelegate_TwoArgs imp = (ApplicationDelegate_TwoArgs)method_getImplementation(it->second); + return imp(self, cmd, arg1, arg2); + } + return YES; +} + +@end + +@interface __KS_CALLING_DELEGATE_TEMPLATE__ : NSObject +@end + +@interface UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) +- (void)__ks_proxyDelegate; +@end + +@implementation __KS_CALLING_DELEGATE_TEMPLATE__ + ++ (void)load +{ +#if KSCRASH_HAS_UIAPPLICATION + SwizzleInstanceMethod(UIApplication.class, @selector(setDelegate:), @selector(__ks_setDelegate:)); + + [[NSNotificationCenter defaultCenter] addObserverForName:UISceneWillConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification * _Nonnull notification) { + UIScene *scene = notification.object; + [scene __ks_proxyDelegate]; + }]; +#endif +} + +#pragma - app delegate + +- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunching]; + return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunching]; + return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateTerminating]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +#pragma - scene delegate + +- (void)sceneWillEnterForeground:(UIScene *)scene +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneDidBecomeActive:(UIScene *)scene +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneWillResignActive:(UIScene *)scene +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneDidEnterBackground:(UIScene *)scene +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +@end + +@interface Proxier : NSObject +@end + +@implementation Proxier + ++ (void)copyMethodsFromClass:(Class)fromClass toClass:(Class)toClass baseClass:(Class)baseClass +{ + unsigned int count = 0; + Method *methods = class_copyMethodList(fromClass, &count); + for (unsigned int i = 0; i < count; i++) { + + SEL name = method_getName(methods[i]); + IMP imp = method_getImplementation(methods[i]); + const char *type = method_getTypeEncoding(methods[i]); + + NSLog(@"Adding %s", name); + + Method originalMethod = class_getInstanceMethod(baseClass, name); + if (originalMethod) { + gMappings[sel_getName(name)] = originalMethod; + NSLog(@"-> original exists"); + } else { + NSLog(@"-> no original"); + } + + if (!class_addMethod(toClass, name, imp, type)) { + NSLog(@"-> Failed to add %s", name); + } + } + free(methods); +} + ++ (Class)subclassClass:(Class)klass copyMethodsFromClass:(Class)methodSourceClass +{ + NSString *subclassName = [[[@"__KSCrash__" + stringByAppendingString:NSStringFromClass(klass)] + stringByAppendingString:@"_"] + stringByAppendingString:[NSUUID UUID].UUIDString]; + Class subclass = objc_allocateClassPair(klass, subclassName.UTF8String, 0); + if (!subclass) { + return nil; + } + + [Proxier copyMethodsFromClass:methodSourceClass + toClass:subclass + baseClass:klass]; + + objc_registerClassPair(subclass); + + return subclass; +} + ++ (void)proxyObject:(NSObject *)object withMethodsFromClass:(Class)methodSourceClass +{ + Class subclass = [self subclassClass:object.class copyMethodsFromClass:methodSourceClass]; + Class originalClass = object_setClass(object, subclass); + if (originalClass) { + NSLog(@"[AC] Swizzled '%@' with '%@'", NSStringFromClass(originalClass), NSStringFromClass(subclass)); + } else { + NSLog(@"[AC] Swizzled failed"); + } +} + +@end + +@implementation UIApplication (__KS_CALLING_DELEGATE_TEMPLATE__) + +- (void)__ks_setDelegate:(id)delegate +{ + if (delegate) { + [Proxier proxyObject:delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; + KSCrashAppStateTracker.shared.proxied = YES; + } + [self __ks_setDelegate:delegate]; +} + +@end + +@interface UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) +- (void)__ks_proxyDelegate; +@end + +@implementation UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) + +- (void)__ks_proxyDelegate +{ + if (self.delegate) { + [Proxier proxyObject:self.delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; + KSCrashAppStateTracker.shared.proxied = YES; + } +} + @end From 31589c4d9b9bf7a6b543fa9d4054625d03cdd36f Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sun, 26 May 2024 15:19:39 -0400 Subject: [PATCH 2/3] Finalize first working take --- .../KSCrashAppStateTracker+Private.h | 13 + ...ateTracker.mm => KSCrashAppStateTracker.m} | 286 +++------------- .../KSCrashLifecycleHandler.mm | 307 ++++++++++++++++++ .../include/KSCrashAppStateTracker.h | 1 + .../include/KSCrashAppTransitionState.h | 1 + .../KSCrashMonitor_Memory_Tests.m | 9 +- 6 files changed, 373 insertions(+), 244 deletions(-) create mode 100644 Sources/KSCrashRecording/KSCrashAppStateTracker+Private.h rename Sources/KSCrashRecording/{KSCrashAppStateTracker.mm => KSCrashAppStateTracker.m} (57%) create mode 100644 Sources/KSCrashRecording/KSCrashLifecycleHandler.mm diff --git a/Sources/KSCrashRecording/KSCrashAppStateTracker+Private.h b/Sources/KSCrashRecording/KSCrashAppStateTracker+Private.h new file mode 100644 index 000000000..bb4c43bf5 --- /dev/null +++ b/Sources/KSCrashRecording/KSCrashAppStateTracker+Private.h @@ -0,0 +1,13 @@ +#import "KSCrashAppStateTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface KSCrashAppStateTracker () + +- (void)_setTransitionState:(KSCrashAppTransitionState)transitionState; + +@property (nonatomic, assign) BOOL proxied; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/KSCrashRecording/KSCrashAppStateTracker.mm b/Sources/KSCrashRecording/KSCrashAppStateTracker.m similarity index 57% rename from Sources/KSCrashRecording/KSCrashAppStateTracker.mm rename to Sources/KSCrashRecording/KSCrashAppStateTracker.m index c79cbacb6..9ded79ba7 100644 --- a/Sources/KSCrashRecording/KSCrashAppStateTracker.mm +++ b/Sources/KSCrashRecording/KSCrashAppStateTracker.m @@ -29,8 +29,6 @@ #import #import -#import -#import #if KSCRASH_HAS_UIAPPLICATION #import @@ -42,6 +40,7 @@ case KSCrashAppTransitionStateStartupPrewarm: return "prewarm"; case KSCrashAppTransitionStateActive: return "active"; case KSCrashAppTransitionStateLaunching: return "launching"; + case KSCrashAppTransitionStateLaunched: return "launched"; case KSCrashAppTransitionStateBackground: return "background"; case KSCrashAppTransitionStateTerminating: return "terminating"; case KSCrashAppTransitionStateExiting: return "exiting"; @@ -62,6 +61,7 @@ bool ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionState state) case KSCrashAppTransitionStateStartup: case KSCrashAppTransitionStateLaunching: + case KSCrashAppTransitionStateLaunched: case KSCrashAppTransitionStateForegrounding: case KSCrashAppTransitionStateActive: case KSCrashAppTransitionStateDeactivating: @@ -111,6 +111,7 @@ @interface KSCrashAppStateTracker () { // transition state and observers protected by the lock os_unfair_lock _lock; KSCrashAppTransitionState _transitionState; + BOOL _transitionComplete; NSMutableArray> *_observers; BOOL _proxied; } @@ -248,20 +249,62 @@ - (KSCrashAppTransitionState)transitionState return ret; } +- (void)_locked_completeState:(KSCrashAppTransitionState)transitionState +{ + if (_transitionState != transitionState || _transitionComplete) { + return; + } + + _transitionComplete = YES; + NSLog(@"[AC] %s, completed: %d", ksapp_transition_state_to_string(transitionState), _transitionComplete); + + // send delegate + // ie: didCompleteTransition or something... +} + +- (void)_completeState:(KSCrashAppTransitionState)transitionState +{ + os_unfair_lock_lock(&_lock); + [self _locked_completeState:transitionState]; + os_unfair_lock_unlock(&_lock); +} + - (void)_setTransitionState:(KSCrashAppTransitionState)transitionState { + BOOL completeOnNextLoop = NO; NSArray> *observers = nil; { os_unfair_lock_lock(&_lock); + if (_transitionState != transitionState) { + + // finalize the current state + [self _locked_completeState:_transitionState]; + + // move to the next state _transitionState = transitionState; + _transitionComplete = NO; + completeOnNextLoop = YES; observers = [_observers copy]; } + os_unfair_lock_unlock(&_lock); } - for (id obs in observers) { - [obs appStateTracker:self didTransitionToState:transitionState]; + if (observers) { + NSLog(@"[AC] %s, completed: %d", ksapp_transition_state_to_string(transitionState), _transitionComplete); + + for (id obs in observers) { + [obs appStateTracker:self didTransitionToState:transitionState]; + } + } + + + if (completeOnNextLoop) { + __weak typeof(self)weakMe = self; + CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ + [weakMe _locked_completeState:transitionState]; + }); } } @@ -302,7 +345,7 @@ - (void)start _registrations = @[ OBSERVE(_center, UIApplicationDidFinishLaunchingNotification, { - [weakMe _setTransitionState:KSCrashAppTransitionStateLaunching]; + [weakMe _setTransitionState:KSCrashAppTransitionStateLaunched]; }), OBSERVE(_center, UIApplicationWillEnterForegroundNotification, { [weakMe _setTransitionState:KSCrashAppTransitionStateForegrounding]; @@ -337,239 +380,6 @@ - (void)stop } } -static BOOL SwizzleInstanceMethod(Class klass, SEL originalSelector, SEL swizzledSelector) -{ - //Obtaining original and swizzled method: - Method original = class_getInstanceMethod(klass, originalSelector); - Method swizzled = class_getInstanceMethod(klass, swizzledSelector); - - if (!original || !swizzled) { - return NO; - } - - method_exchangeImplementations(original, swizzled); - - return YES; -} - -typedef BOOL (*ApplicationDelegate_TwoArgs)(id, SEL, id, id); -typedef void (*ApplicationDelegate_OneArg)(id, SEL, id); - -static std::map gMappings = {}; - -static void __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg) -{ - std::string name(sel_getName(cmd)); - NSLog(@"[MAP] %s", name.c_str()); - const auto it = gMappings.find(name); - if (it != gMappings.end()) { - NSLog(@"[MAP:implemented] %s", name.c_str()); - ApplicationDelegate_OneArg imp = (ApplicationDelegate_OneArg)method_getImplementation(it->second); - imp(self, cmd, arg); - } -} - -static BOOL __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg1, id arg2) -{ - std::string name(sel_getName(cmd)); - NSLog(@"[MAP] %s", name.c_str()); - const auto it = gMappings.find(name); - if (it != gMappings.end()) { - NSLog(@"[MAP:implemented] %s", name.c_str()); - ApplicationDelegate_TwoArgs imp = (ApplicationDelegate_TwoArgs)method_getImplementation(it->second); - return imp(self, cmd, arg1, arg2); - } - return YES; -} - -@end - -@interface __KS_CALLING_DELEGATE_TEMPLATE__ : NSObject -@end - -@interface UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) -- (void)__ks_proxyDelegate; -@end - -@implementation __KS_CALLING_DELEGATE_TEMPLATE__ - -+ (void)load -{ -#if KSCRASH_HAS_UIAPPLICATION - SwizzleInstanceMethod(UIApplication.class, @selector(setDelegate:), @selector(__ks_setDelegate:)); - - [[NSNotificationCenter defaultCenter] addObserverForName:UISceneWillConnectNotification - object:nil - queue:nil - usingBlock:^(NSNotification * _Nonnull notification) { - UIScene *scene = notification.object; - [scene __ks_proxyDelegate]; - }]; -#endif -} - -#pragma - app delegate - -- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunching]; - return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunching]; - return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); -} - -- (void)applicationDidBecomeActive:(UIApplication *)application -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; - __KS_CALLING_DELEGATE__(self, _cmd, application); -} - -- (void)applicationWillResignActive:(UIApplication *)application -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; - __KS_CALLING_DELEGATE__(self, _cmd, application); -} - -- (void)applicationDidEnterBackground:(UIApplication *)application -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; - __KS_CALLING_DELEGATE__(self, _cmd, application); -} - -- (void)applicationWillEnterForeground:(UIApplication *)application -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; - __KS_CALLING_DELEGATE__(self, _cmd, application); -} - -- (void)applicationWillTerminate:(UIApplication *)application -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateTerminating]; - __KS_CALLING_DELEGATE__(self, _cmd, application); -} - -#pragma - scene delegate - -- (void)sceneWillEnterForeground:(UIScene *)scene -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; - __KS_CALLING_DELEGATE__(self, _cmd, scene); -} - -- (void)sceneDidBecomeActive:(UIScene *)scene -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; - __KS_CALLING_DELEGATE__(self, _cmd, scene); -} - -- (void)sceneWillResignActive:(UIScene *)scene -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; - __KS_CALLING_DELEGATE__(self, _cmd, scene); -} - -- (void)sceneDidEnterBackground:(UIScene *)scene -{ - [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; - __KS_CALLING_DELEGATE__(self, _cmd, scene); -} - -@end - -@interface Proxier : NSObject -@end - -@implementation Proxier - -+ (void)copyMethodsFromClass:(Class)fromClass toClass:(Class)toClass baseClass:(Class)baseClass -{ - unsigned int count = 0; - Method *methods = class_copyMethodList(fromClass, &count); - for (unsigned int i = 0; i < count; i++) { - - SEL name = method_getName(methods[i]); - IMP imp = method_getImplementation(methods[i]); - const char *type = method_getTypeEncoding(methods[i]); - - NSLog(@"Adding %s", name); - - Method originalMethod = class_getInstanceMethod(baseClass, name); - if (originalMethod) { - gMappings[sel_getName(name)] = originalMethod; - NSLog(@"-> original exists"); - } else { - NSLog(@"-> no original"); - } - - if (!class_addMethod(toClass, name, imp, type)) { - NSLog(@"-> Failed to add %s", name); - } - } - free(methods); -} - -+ (Class)subclassClass:(Class)klass copyMethodsFromClass:(Class)methodSourceClass -{ - NSString *subclassName = [[[@"__KSCrash__" - stringByAppendingString:NSStringFromClass(klass)] - stringByAppendingString:@"_"] - stringByAppendingString:[NSUUID UUID].UUIDString]; - Class subclass = objc_allocateClassPair(klass, subclassName.UTF8String, 0); - if (!subclass) { - return nil; - } - - [Proxier copyMethodsFromClass:methodSourceClass - toClass:subclass - baseClass:klass]; - - objc_registerClassPair(subclass); - - return subclass; -} - -+ (void)proxyObject:(NSObject *)object withMethodsFromClass:(Class)methodSourceClass -{ - Class subclass = [self subclassClass:object.class copyMethodsFromClass:methodSourceClass]; - Class originalClass = object_setClass(object, subclass); - if (originalClass) { - NSLog(@"[AC] Swizzled '%@' with '%@'", NSStringFromClass(originalClass), NSStringFromClass(subclass)); - } else { - NSLog(@"[AC] Swizzled failed"); - } -} - @end -@implementation UIApplication (__KS_CALLING_DELEGATE_TEMPLATE__) - -- (void)__ks_setDelegate:(id)delegate -{ - if (delegate) { - [Proxier proxyObject:delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; - KSCrashAppStateTracker.shared.proxied = YES; - } - [self __ks_setDelegate:delegate]; -} - -@end -@interface UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) -- (void)__ks_proxyDelegate; -@end - -@implementation UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) - -- (void)__ks_proxyDelegate -{ - if (self.delegate) { - [Proxier proxyObject:self.delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; - KSCrashAppStateTracker.shared.proxied = YES; - } -} - -@end diff --git a/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm b/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm new file mode 100644 index 000000000..d68c18720 --- /dev/null +++ b/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm @@ -0,0 +1,307 @@ +// +// KSCrashLifecycleHandler.mm +// +// Created by Alexander Cohen on 2024-05-20. +// +// Copyright (c) 2024 Alexander Cohen. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +#import "KSSystemCapabilities.h" + +/** + * If we can, we try and swizzle App and Scene delegates. + * + * Lifecycle delegate and notifications happen at different times, + * here's what it looks like: + * + * run loop cycle 1: Application -> Delegate + * run loop cycle 2: Application -> Notification + * + * Due to this, it's very hard for a system to receive lifecycle + * events before the app it is being installed in. This can lead + * to mismatched user perception (foreground when the app is actually + * background). That mismatch leads to wrongly categorizing + * issues reported by the system. + * + * To fix this, we need to swizzle app an scene delegates. This allows + * us to receive delegate callbacks before anyone else and correctly + * record the state of the app before the app has a chance to run any + * code in those transitions and possibly cause a reliability event. + * + * + */ +// We need to have UIApplication to do any of this. +#if KSCRASH_HAS_UIAPPLICATION + +#import "KSCrashAppStateTracker+Private.h" + +#import +#import + +#import + +@interface __KS_CALLING_DELEGATE_TEMPLATE__ : NSObject +@end + +#define UISCENE_AVAILABLE API_AVAILABLE(ios(13.0)) + +UISCENE_AVAILABLE +@interface UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) +- (void)__ks_proxyDelegate; +@end + +@interface UIApplication (__KS_CALLING_DELEGATE_TEMPLATE__) +- (void)__ks_setDelegate:(id)delegate; +@end + +UISCENE_AVAILABLE +@interface __KS_CALLING_DELEGATE_TEMPLATE__ () +@end + +@implementation __KS_CALLING_DELEGATE_TEMPLATE__ + +static BOOL SwizzleInstanceMethod(Class klass, SEL originalSelector, SEL swizzledSelector) +{ + Method original = class_getInstanceMethod(klass, originalSelector); + Method swizzled = class_getInstanceMethod(klass, swizzledSelector); + + if (!original || !swizzled) { + return NO; + } + + method_exchangeImplementations(original, swizzled); + return YES; +} + +typedef BOOL (*ApplicationDelegate_TwoArgs)(id, SEL, id, id); +typedef void (*ApplicationDelegate_OneArg)(id, SEL, id); + +static std::map gMappings = {}; + +static void __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg) +{ + std::string name(sel_getName(cmd)); + NSLog(@"[MAP] %s", name.c_str()); + const auto it = gMappings.find(name); + if (it != gMappings.end()) { + NSLog(@"[MAP:implemented] %s", name.c_str()); + ApplicationDelegate_OneArg imp = (ApplicationDelegate_OneArg)method_getImplementation(it->second); + imp(self, cmd, arg); + } +} + +static BOOL __KS_CALLING_DELEGATE__(id self, SEL cmd, id arg1, id arg2) +{ + std::string name(sel_getName(cmd)); + NSLog(@"[MAP] %s", name.c_str()); + const auto it = gMappings.find(name); + if (it != gMappings.end()) { + NSLog(@"[MAP:implemented] %s", name.c_str()); + ApplicationDelegate_TwoArgs imp = (ApplicationDelegate_TwoArgs)method_getImplementation(it->second); + return imp(self, cmd, arg1, arg2); + } + return YES; +} + ++ (void)load +{ + static BOOL sDontSwizzle = [NSProcessInfo.processInfo.environment[@"KSCRASH_APP_SCENE_DELEGATE_SWIZZLE_DISABLED"] boolValue]; + if (sDontSwizzle) { + return; + } +#if KSCRASH_HAS_UIAPPLICATION + SwizzleInstanceMethod(UIApplication.class, @selector(setDelegate:), @selector(__ks_setDelegate:)); + + if (@available(iOS 13.0, tvOS 13.0, *)) { + [[NSNotificationCenter defaultCenter] addObserverForName:UISceneWillConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification * _Nonnull notification) { + UIScene *scene = notification.object; + [scene __ks_proxyDelegate]; + }]; + } +#endif +} + +#pragma - app delegate + +- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunching]; + return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateLaunched]; + return __KS_CALLING_DELEGATE__(self, _cmd, application, launchOptions); +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateTerminating]; + __KS_CALLING_DELEGATE__(self, _cmd, application); +} + +#pragma - scene delegate + +- (void)sceneWillEnterForeground:(UIScene *)scene UISCENE_AVAILABLE +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateForegrounding]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneDidBecomeActive:(UIScene *)scene UISCENE_AVAILABLE +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateActive]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneWillResignActive:(UIScene *)scene UISCENE_AVAILABLE +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateDeactivating]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +- (void)sceneDidEnterBackground:(UIScene *)scene UISCENE_AVAILABLE +{ + [KSCrashAppStateTracker.shared _setTransitionState:KSCrashAppTransitionStateBackground]; + __KS_CALLING_DELEGATE__(self, _cmd, scene); +} + +@end + +@interface Proxier : NSObject +@end + +@implementation Proxier + ++ (void)copyMethodsFromClass:(Class)fromClass toClass:(Class)toClass baseClass:(Class)baseClass +{ + unsigned int count = 0; + Method *methods = class_copyMethodList(fromClass, &count); + for (unsigned int i = 0; i < count; i++) { + + SEL name = method_getName(methods[i]); + IMP imp = method_getImplementation(methods[i]); + const char *type = method_getTypeEncoding(methods[i]); + + NSLog(@"Adding %s", sel_getName(name)); + + Method originalMethod = class_getInstanceMethod(baseClass, name); + if (originalMethod) { + gMappings[sel_getName(name)] = originalMethod; + NSLog(@"-> original exists"); + } else { + NSLog(@"-> no original"); + } + + if (!class_addMethod(toClass, name, imp, type)) { + NSLog(@"-> Failed to add %s", sel_getName(name)); + } + } + free(methods); +} + ++ (Class)subclassClass:(Class)klass copyMethodsFromClass:(Class)methodSourceClass +{ + NSString *subclassName = [[[@"__KSCrash__" + stringByAppendingString:NSStringFromClass(klass)] + stringByAppendingString:@"_"] + stringByAppendingString:[NSUUID UUID].UUIDString]; + Class subclass = objc_allocateClassPair(klass, subclassName.UTF8String, 0); + if (!subclass) { + return nil; + } + + [Proxier copyMethodsFromClass:methodSourceClass + toClass:subclass + baseClass:klass]; + + objc_registerClassPair(subclass); + + return subclass; +} + ++ (void)proxyObject:(NSObject *)object withMethodsFromClass:(Class)methodSourceClass +{ + Class subclass = [self subclassClass:object.class copyMethodsFromClass:methodSourceClass]; + Class originalClass = object_setClass(object, subclass); + if (originalClass) { + NSLog(@"[AC] Swizzled '%@' with '%@'", NSStringFromClass(originalClass), NSStringFromClass(subclass)); + } else { + NSLog(@"[AC] Swizzled failed"); + } +} + +@end + +@implementation UIApplication (__KS_CALLING_DELEGATE_TEMPLATE__) + +- (void)__ks_setDelegate:(id)delegate +{ + if (delegate) { + [Proxier proxyObject:delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; + KSCrashAppStateTracker.shared.proxied = YES; + } + [self __ks_setDelegate:delegate]; +} + +@end + +UISCENE_AVAILABLE +@implementation UIScene (__KS_CALLING_DELEGATE_TEMPLATE__) + +- (void)__ks_proxyDelegate +{ + if (self.delegate) { + [Proxier proxyObject:self.delegate withMethodsFromClass:__KS_CALLING_DELEGATE_TEMPLATE__.class]; + KSCrashAppStateTracker.shared.proxied = YES; + } +} + +@end + +#endif diff --git a/Sources/KSCrashRecording/include/KSCrashAppStateTracker.h b/Sources/KSCrashRecording/include/KSCrashAppStateTracker.h index 4eecd3cf1..990963c0a 100644 --- a/Sources/KSCrashRecording/include/KSCrashAppStateTracker.h +++ b/Sources/KSCrashRecording/include/KSCrashAppStateTracker.h @@ -59,6 +59,7 @@ typedef void (^KSCrashAppStateTrackerObserverBlock)(KSCrashAppTransitionState tr - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER; @property (atomic, readonly) KSCrashAppTransitionState transitionState; +@property (atomic, readonly, getter=isTransitionStateComplete) BOOL transitionStateComplete; /** * Adds an observer that implements the _KSCrashAppStateTrackerObserving_ protocol. diff --git a/Sources/KSCrashRecording/include/KSCrashAppTransitionState.h b/Sources/KSCrashRecording/include/KSCrashAppTransitionState.h index baa035b05..422ccd67e 100644 --- a/Sources/KSCrashRecording/include/KSCrashAppTransitionState.h +++ b/Sources/KSCrashRecording/include/KSCrashAppTransitionState.h @@ -38,6 +38,7 @@ enum { KSCrashAppTransitionStateStartup = 0, KSCrashAppTransitionStateStartupPrewarm, KSCrashAppTransitionStateLaunching, + KSCrashAppTransitionStateLaunched, KSCrashAppTransitionStateForegrounding, KSCrashAppTransitionStateActive, KSCrashAppTransitionStateDeactivating, diff --git a/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m b/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m index b88c0baeb..51f2c804b 100644 --- a/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m +++ b/Tests/KSCrashRecordingTests/KSCrashMonitor_Memory_Tests.m @@ -187,6 +187,7 @@ - (void) testTransitionState XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateStartup)); XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateLaunching)); + XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateLaunched)); XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateForegrounding)); XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateActive)); XCTAssertTrue(ksapp_transition_state_is_user_perceptible(KSCrashAppTransitionStateDeactivating)); @@ -234,7 +235,7 @@ - (void)testAppStateTrackerNoPrewarm #if KSCRASH_HAS_UIAPPLICATION [center postNotificationName:UIApplicationDidFinishLaunchingNotification object:nil]; - XCTAssertEqual(tracker.transitionState, KSCrashAppTransitionStateLaunching); + XCTAssertEqual(tracker.transitionState, KSCrashAppTransitionStateLaunched); XCTAssertEqual(tracker.transitionState, state); [center postNotificationName:UIApplicationWillEnterForegroundNotification object:nil]; @@ -252,11 +253,7 @@ - (void)testAppStateTrackerNoPrewarm [center postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil]; XCTAssertEqual(tracker.transitionState, KSCrashAppTransitionStateBackground); XCTAssertEqual(tracker.transitionState, state); - - [center postNotificationName:UIApplicationDidFinishLaunchingNotification object:nil]; - XCTAssertEqual(tracker.transitionState, KSCrashAppTransitionStateLaunching); - XCTAssertEqual(tracker.transitionState, state); - + [center postNotificationName:UIApplicationWillTerminateNotification object:nil]; XCTAssertEqual(tracker.transitionState, KSCrashAppTransitionStateTerminating); XCTAssertEqual(tracker.transitionState, state); From c709d56468bff3e9d4c6fd3810feed495844688e Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sun, 26 May 2024 15:32:56 -0400 Subject: [PATCH 3/3] missing include --- Sources/KSCrashRecording/KSCrashLifecycleHandler.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm b/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm index d68c18720..0a7464e34 100644 --- a/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm +++ b/Sources/KSCrashRecording/KSCrashLifecycleHandler.mm @@ -54,6 +54,7 @@ #import #import +#import #import