// // FLEXSystemLogViewController.m // FLEX // // Created by Ryan Olson on 1/19/15. // Copyright (c) 2020 FLEX Team. All rights reserved. // #import "FLEXSystemLogViewController.h" #import "FLEXASLLogController.h" #import "FLEXOSLogController.h" #import "FLEXSystemLogCell.h" #import "FLEXMutableListSection.h" #import "FLEXUtility.h" #import "FLEXColor.h" #import "FLEXResources.h" #import "UIBarButtonItem+FLEX.h" #import "NSUserDefaults+FLEX.h" #import "flex_fishhook.h" #import @interface FLEXSystemLogViewController () @property (nonatomic, readonly) FLEXMutableListSection *logMessages; @property (nonatomic, readonly) id logController; @end static void (*MSHookFunction)(void *symbol, void *replace, void **result); static BOOL FLEXDidHookNSLog = NO; static BOOL FLEXNSLogHookWorks = NO; BOOL (*os_log_shim_enabled)(void *addr) = nil; BOOL (*orig_os_log_shim_enabled)(void *addr) = nil; static BOOL my_os_log_shim_enabled(void *addr) { return NO; } @implementation FLEXSystemLogViewController #pragma mark - Initialization + (void)load { // User must opt-into disabling os_log if (!NSUserDefaults.standardUserDefaults.flex_disableOSLog) { return; } // Thanks to @Ram4096 on GitHub for telling me that // os_log is conditionally enabled by the SDK version void *addr = __builtin_return_address(0); void *libsystem_trace = dlopen("/usr/lib/system/libsystem_trace.dylib", RTLD_LAZY); os_log_shim_enabled = dlsym(libsystem_trace, "os_log_shim_enabled"); if (!os_log_shim_enabled) { return; } FLEXDidHookNSLog = flex_rebind_symbols((struct rebinding[1]) {{ "os_log_shim_enabled", (void *)my_os_log_shim_enabled, (void **)&orig_os_log_shim_enabled }}, 1) == 0; if (FLEXDidHookNSLog && orig_os_log_shim_enabled != nil) { // Check if our rebinding worked FLEXNSLogHookWorks = my_os_log_shim_enabled(addr) == NO; } // So, just because we rebind the lazily loaded symbol for // this function doesn't mean it's even going to be used. // While it seems to be sufficient for the simulator, for // whatever reason it is not sufficient on-device. We need // to actually hook the function with something like Substrate. // Check if we have substrate, and if so use that instead void *handle = dlopen("/usr/lib/libsubstrate.dylib", RTLD_LAZY); if (handle) { MSHookFunction = dlsym(handle, "MSHookFunction"); if (MSHookFunction) { // Set the hook and check if it worked void *unused; MSHookFunction(os_log_shim_enabled, my_os_log_shim_enabled, &unused); FLEXNSLogHookWorks = os_log_shim_enabled(addr) == NO; } } } - (id)init { return [super initWithStyle:UITableViewStylePlain]; } #pragma mark - Overrides - (void)viewDidLoad { [super viewDidLoad]; self.showsSearchBar = YES; self.showSearchBarInitially = NO; __weak __typeof(self) weakSelf = self; id logHandler = ^(NSArray *newMessages) { __strong __typeof(weakSelf) strongSelf = weakSelf; [strongSelf handleUpdateWithNewMessages:newMessages]; }; if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) { _logController = [FLEXOSLogController withUpdateHandler:logHandler]; } else { _logController = [FLEXASLLogController withUpdateHandler:logHandler]; } #if !TARGET_OS_TV self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; #endif self.title = @"Waiting for Logs..."; // Toolbar buttons // UIBarButtonItem *scrollDown = [UIBarButtonItem flex_itemWithImage:FLEXResources.scrollToBottomIcon target:self action:@selector(scrollToLastRow) ]; UIBarButtonItem *settings = [UIBarButtonItem flex_itemWithImage:FLEXResources.gearIcon target:self action:@selector(showLogSettings) ]; [self addToolbarItems:@[scrollDown, settings]]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.logController startMonitoring]; } - (NSArray *)makeSections { __weak __typeof(self) weakSelf = self; _logMessages = [FLEXMutableListSection list:@[] cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) { __strong __typeof(self) strongSelf = weakSelf; if (strongSelf) { cell.logMessage = message; cell.highlightedText = strongSelf.filterText; if (row % 2 == 0) { cell.backgroundColor = FLEXColor.primaryBackgroundColor; } else { cell.backgroundColor = FLEXColor.secondaryBackgroundColor; } } } filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) { NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message]; return [displayedText localizedCaseInsensitiveContainsString:filterText]; } ]; self.logMessages.cellRegistrationMapping = @{ kFLEXSystemLogCellIdentifier : [FLEXSystemLogCell class] }; return @[self.logMessages]; } - (NSArray *)nonemptySections { return @[self.logMessages]; } #pragma mark - Private - (void)handleUpdateWithNewMessages:(NSArray *)newMessages { self.title = [self.class globalsEntryTitle:FLEXGlobalsRowSystemLog]; [self.logMessages mutate:^(NSMutableArray *list) { [list addObjectsFromArray:newMessages]; }]; // "Follow" the log as new messages stream in if we were previously near the bottom. BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0; [self reloadData]; if (wasNearBottom) { [self scrollToLastRow]; } } - (void)scrollToLastRow { NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0]; if (numberOfRows > 0) { NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0]; [self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; } } - (void)showLogSettings { NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; BOOL disableOSLog = defaults.flex_disableOSLog; BOOL persistent = defaults.flex_cacheOSLogMessages; NSString *aslToggle = disableOSLog ? @"Enable os_log (default)" : @"Disable os_log"; NSString *persistence = persistent ? @"Disable persistent logging" : @"Enable persistent logging"; NSString *title = @"System Log Settings"; NSString *body = @"In iOS 10 and up, ASL has been replaced by os_log. " "The os_log API is much more limited. Below, you can opt-into the old behavior " "if you want cleaner, more reliable logs within FLEX, but this will break " "anything that expects os_log to be working, such as Console.app. " "This setting requires the app to restart to take effect. \n\n" "To get as close to the old behavior as possible with os_log enabled, logs must " "be collected manually at launch and stored. This setting has no effect " "on iOS 9 and below, or if os_log is disabled. " "You should only enable persistent logging when you need it."; FLEXOSLogController *logController = (FLEXOSLogController *)self.logController; [FLEXAlert makeAlert:^(FLEXAlert *make) { make.title(title).message(body); make.button(aslToggle).destructiveStyle().handler(^(NSArray *strings) { [defaults flex_toggleBoolForKey:kFLEXDefaultsDisableOSLogForceASLKey]; }); make.button(persistence).handler(^(NSArray *strings) { [defaults flex_toggleBoolForKey:kFLEXDefaultsiOSPersistentOSLogKey]; logController.persistent = !persistent; [logController.messages addObjectsFromArray:self.logMessages.list]; }); make.button(@"Dismiss").cancelStyle(); } showFrom:self]; } #pragma mark - FLEXGlobalsEntry + (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row { return @"⚠️ System Log"; } + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row { return [self new]; } #pragma mark - Table view data source - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { FLEXSystemLogMessage *logMessage = self.logMessages.filteredList[indexPath.row]; return [FLEXSystemLogCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width]; } #pragma mark - Copy on long press - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { return action == @selector(copy:); } - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { if (action == @selector(copy:)) { // We usually only want to copy the log message itself, not any metadata associated with it. #if !TARGET_OS_TV UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText; #endif } } #if FLEX_AT_LEAST_IOS13_SDK #if !TARGET_OS_TV - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) { __weak __typeof__(self) weakSelf = self; return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu *(NSArray *suggestedActions) { UIAction *copy = [UIAction actionWithTitle:@"Copy" image:nil identifier:@"Copy" handler:^(__kindof UIAction *action) { // We usually only want to copy the log message itself, not any metadata associated with it. UIPasteboard.generalPasteboard.string = weakSelf.logMessages.filteredList[indexPath.row].messageText ?: @""; }]; return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]]; }]; } #endif #endif @end