/* UIKit Tools - command-line utilities for UIKit * Copyright (C) 2018-2019 Sam Bingner * Copyright (C) 2008-2012 Jay Freeman (saurik) * Portions Copyright (C) 2019 Sam Bingner */ /* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* Modified BSD License {{{ */ /* * Redistribution and use in source and binary * forms, with or without modification, are permitted * provided that the following conditions are met: * * 1. Redistributions of source code must retain the * above copyright notice, this list of conditions * and the following disclaimer. * 2. Redistributions in binary form must reproduce the * above copyright notice, this list of conditions * and the following disclaimer in the documentation * and/or other materials provided with the * distribution. * 3. The name of the author may not be used to endorse * or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* }}} */ #import #import "NSTask.h" #import #import #import #import #import #import #import #import #import #import #include #include #include "csstore.hpp" @interface PBSSystemService : NSObject +(id)sharedInstance; -(void)relaunchBackboardd; @end @interface PBSSystemServiceConnection : NSObject +(id)sharedConnection; -(id)systemServiceProxy; @end @interface NSMutableArray (Cydia) - (void) addInfoDictionary:(NSDictionary *)info; @end @implementation NSMutableArray (Cydia) - (void) addInfoDictionary:(NSDictionary *)info { [self addObject:info]; } - (NSArray *) allInfoDictionaries { return self; } @end @interface NSMutableDictionary (Cydia) - (void) addInfoDictionary:(NSDictionary *)info; @end @implementation NSMutableDictionary (Cydia) - (void) addInfoDictionary:(NSDictionary *)info { NSString *bundle = [info objectForKey:@"CFBundleIdentifier"]; [self setObject:info forKey:bundle]; } - (NSArray *) allInfoDictionaries { return [self allValues]; } @end @interface LSApplicationProxy : NSObject - (NSString*) applicationIdentifier; - (NSURL*) bundleURL; - (NSDate*) registeredDate; @end @interface LSApplicationWorkspace : NSObject + (id) defaultWorkspace; - (BOOL) registerApplication:(id)application; - (BOOL) unregisterApplication:(id)application; - (BOOL) invalidateIconCache:(id)bundle; - (BOOL) registerApplicationDictionary:(id)application; - (BOOL) installApplication:(id)application withOptions:(id)options; - (BOOL) _LSPrivateRebuildApplicationDatabasesForSystemApps:(BOOL)system internal:(BOOL)internal user:(BOOL)user; - (NSArray*) allApplications; @end @interface MCMAppDataContainer +(id)containerWithIdentifier:(NSString*)identifier createIfNecessary:(bool)create existed:(bool*)existed error:(NSError*)error; -(NSURL*)url; @end @interface FBSSystemService +(id)sharedService; -(void)sendActions:(NSSet*)actions withResult:(id)result; @end typedef enum { None = 0, RestartRenderServer = (1 << 0), // also relaunch backboardd SnapshotTransition = (1 << 1), FadeToBlackTransition = (1 << 2), } SBSRelaunchActionStyle; @interface SBSRelaunchAction +(id)actionWithReason:(id)reason options:(int64_t)options targetURL:(NSURL*)url; @end static int verbose=0; static int standard_uicache(void); static Class $MCMPluginKitPluginDataContainer; static Class $MCMAppDataContainer; static Class $LSApplicationWorkspace; LSApplicationWorkspace *workspace=nil; extern char **environ; int my_system(const char *cmd) { pid_t pid; char *argv[] = {"sh", "-c", (char*)cmd, NULL}; int status; fprintf(stderr, "Run command: %s\n", cmd); status = posix_spawn(&pid, "/bin/sh", NULL, NULL, argv, environ); if (status == 0) { printf("Child pid: %i\n", pid); if (waitpid(pid, &status, 0) != -1) { printf("Child exited with status %i\n", status); } else { perror("waitpid"); } } else { printf("posix_spawn: %s\n", strerror(status)); } return status; } NSString *getAppPath(NSString *path) { path = [path stringByResolvingSymlinksInPath]; if (![path hasPrefix:@"/Applications/"]) { fprintf(stderr, "Error: Path must be within /Applications/\n"); return nil; } return [NSString pathWithComponents:[[path pathComponents] subarrayWithRange:NSMakeRange(0, 3)]]; } bool appIsRegistered(NSString *path) { @autoreleasepool { path = getAppPath(path); if (!path) return false; for (LSApplicationProxy *app in [workspace allApplications]) { if ([path isEqualToString:[[app bundleURL] path]]) return true; } return false; } } bool unregisterPath(NSString *path) { @autoreleasepool { if (verbose) fprintf(stderr, "Unregistering %s\n", path.lastPathComponent.UTF8String); path = getAppPath(path); if (!path) return false; if (appIsRegistered(path) && ![workspace unregisterApplication:[NSURL fileURLWithPath:path]]) { fprintf(stderr, "Error: unregisterApplication failed for %s\n", path.lastPathComponent.UTF8String); return false; } } return true; } // Credit to coolstar for finding how to do this and not sharing with the community thereby forcing me to figure out how to do the same thing. bool registerPath(NSString *path) { if (!path) { if (verbose) fprintf(stderr, "registerPath called with no path\n"); return false; } NSString *realPath = getAppPath(path); if (!realPath) { if (verbose) fprintf(stderr, "unable to determine path for %s\n", path.UTF8String); return false; } NSDictionary *infoDictionary = [NSDictionary dictionaryWithContentsOfFile: [realPath stringByAppendingPathComponent:@"Info.plist"]]; NSString *bundleID = [infoDictionary objectForKey:@"CFBundleIdentifier"]; if (bundleID) { NSFileManager *fm = [NSFileManager defaultManager]; if ([infoDictionary objectForKey:@"CFBundleExecutable"]) { NSString *executable = [realPath stringByAppendingPathComponent:[infoDictionary objectForKey:@"CFBundleExecutable"]]; if (![fm fileExistsAtPath:executable]) { fprintf(stderr, "Error: CFBundleExecutable defined but missing for %s - this is a fatal error. Aborting.\n", realPath.lastPathComponent.UTF8String); return false; } } NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"System", @"ApplicationType", @YES, @"BundleNameIsLocalized", bundleID, @"CFBundleIdentifier", @NO, @"CompatibilityState", @NO, @"IsDeletable", realPath, @"Path", [NSMutableDictionary dictionary], @"_LSBundlePlugins", nil]; id appContainer = [$MCMAppDataContainer containerWithIdentifier:bundleID createIfNecessary:YES existed:NULL error:nil]; NSString *appContainerPath = [[appContainer url] path]; if (appContainerPath) { dict[@"Container"] = appContainerPath; } NSString *pluginsPath = [realPath stringByAppendingPathComponent:@"PlugIns"]; for (NSString *plugin in [fm contentsOfDirectoryAtPath:pluginsPath error:nil]) { NSString *pluginPath = [pluginsPath stringByAppendingPathComponent:plugin]; NSString *pluginInfoPlistPath = [pluginPath stringByAppendingPathComponent:@"Info.plist"]; NSString *pluginBundleIdentifier = [[NSDictionary dictionaryWithContentsOfFile:pluginInfoPlistPath] objectForKey:@"CFBundleIdentifier"]; if (pluginBundleIdentifier) { id pluginContainer = [$MCMPluginKitPluginDataContainer containerWithIdentifier:pluginBundleIdentifier createIfNecessary:YES existed:NULL error:nil]; NSURL *pluginContainerURL = [pluginContainer url]; NSString *pluginContainerPath = [pluginContainerURL path]; dict[@"_LSBundlePlugins"][pluginBundleIdentifier] = @{ @"ApplicationType": @"PluginKitPlugin", @"BundleNameIsLocalized": @YES, @"CFBundleIdentifier": pluginBundleIdentifier, @"CompatibilityState": @NO, @"Container": pluginContainerPath, @"Path": pluginPath, @"PluginOwnerBundleID": bundleID }; } } if (![[$LSApplicationWorkspace defaultWorkspace] registerApplicationDictionary:dict]) { fprintf(stderr, "Error: registerApplicationDictionary failed for %s\n", path.lastPathComponent.UTF8String); return false; } } else { return unregisterPath(realPath); } return true; } void usage(void) { fprintf(stderr, "Usage: %s [-hrv] [[-p | -u] /Applications/App.app]]\n", getprogname()); exit(EXIT_FAILURE); } pid_t launch_get_job_pid(const char * job) { launch_data_t resp; launch_data_t msg; msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY); if (msg == NULL) { return -1; } launch_data_dict_insert(msg, launch_data_new_string(job), LAUNCH_KEY_GETJOB); resp = launch_msg(msg); launch_data_free(msg); if (resp == NULL) { return -1; } if (launch_data_get_type(resp) != LAUNCH_DATA_DICTIONARY) return -1; launch_data_t pid_data = launch_data_dict_lookup(resp, "PID"); if (launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) return -1; pid_t pid = (pid_t)launch_data_get_integer(pid_data); launch_data_free(resp); return pid; } int standard_uicache(void) { @autoreleasepool { if (kCFCoreFoundationVersionNumber > 1000 && // this API is on iOS 7 but invaliding the icon cache is harder there [workspace respondsToSelector:@selector(_LSPrivateRebuildApplicationDatabasesForSystemApps:internal:user:)]) { if (![workspace _LSPrivateRebuildApplicationDatabasesForSystemApps:YES internal:YES user:NO]) fprintf(stderr, "failed to rebuild application databases"); return 0; } bool respring(false); NSString *home(NSHomeDirectory()); NSString *path([NSString stringWithFormat:@"%@/Library/Caches/com.apple.mobile.installation.plist", home]); my_system("killall -SIGSTOP SpringBoard"); sleep(1); @try { DeleteCSStores([home UTF8String]); my_system("killall lsd"); if ([workspace respondsToSelector:@selector(invalidateIconCache:)]) while (![workspace invalidateIconCache:nil]) sleep(1); if (NSMutableDictionary *cache = [NSMutableDictionary dictionaryWithContentsOfFile:path]) { NSFileManager *manager = [NSFileManager defaultManager]; NSError *error = nil; NSMutableDictionary *bundles([NSMutableDictionary dictionaryWithCapacity:16]); id after = [cache objectForKey:@"System"]; if (after == nil) { error: fprintf(stderr, "%s\n", error == nil ? strerror(errno) : [[error localizedDescription] UTF8String]); goto cached; } id before([[after copy] autorelease]); [after removeAllObjects]; NSArray *cached([cache objectForKey:@"InfoPlistCachedKeys"]); NSMutableSet *removed([NSMutableSet set]); for (NSDictionary *info in [before allInfoDictionaries]) if (NSString *path = [info objectForKey:@"Path"]) [removed addObject:path]; if (NSArray *apps = [manager contentsOfDirectoryAtPath:@"/Applications" error:&error]) { for (NSString *app in apps) if ([app hasSuffix:@".app"]) { NSString *path = [@"/Applications" stringByAppendingPathComponent:app]; NSString *plist = [path stringByAppendingPathComponent:@"Info.plist"]; if (NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:plist]) { if (NSString *identifier = [info objectForKey:@"CFBundleIdentifier"]) { [bundles setObject:path forKey:identifier]; [removed removeObject:path]; if (cached != nil) { NSMutableDictionary *merged([before objectForKey:identifier]); if (merged == nil) merged = [NSMutableDictionary dictionary]; else merged = [[merged mutableCopy] autorelease]; for (NSString *key in cached) if (NSObject *value = [info objectForKey:key]) [merged setObject:value forKey:key]; else [merged removeObjectForKey:key]; info = merged; } [info setObject:path forKey:@"Path"]; [info setObject:@"System" forKey:@"ApplicationType"]; [after addInfoDictionary:info]; } else fprintf(stderr, "%s missing CFBundleIdentifier", [app UTF8String]); } } } else goto error; [cache writeToFile:path atomically:YES]; if (workspace != nil) { if ([workspace respondsToSelector:@selector(invalidateIconCache:)]) { for (NSString *identifier in bundles) [workspace invalidateIconCache:identifier]; } else { for (NSString *identifier in bundles) { NSString *path([bundles objectForKey:identifier]); [workspace unregisterApplication:[NSURL fileURLWithPath:path]]; } } for (NSString *identifier in bundles) { NSString *path([bundles objectForKey:identifier]); if (kCFCoreFoundationVersionNumber >= 800) [workspace registerApplicationDictionary:[after objectForKey:identifier]]; else [workspace registerApplication:[NSURL fileURLWithPath:path]]; } for (NSString *path in removed) [workspace unregisterApplication:[NSURL fileURLWithPath:path]]; } } else fprintf(stderr, "cannot open cache file. incorrect user?\n"); cached: if (respring || kCFCoreFoundationVersionNumber >= 550.32) { unlink([[NSString stringWithFormat:@"%@/Library/Caches/com.apple.springboard-imagecache-icons", home] UTF8String]); unlink([[NSString stringWithFormat:@"%@/Library/Caches/com.apple.springboard-imagecache-icons.plist", home] UTF8String]); unlink([[NSString stringWithFormat:@"%@/Library/Caches/com.apple.springboard-imagecache-smallicons", home] UTF8String]); unlink([[NSString stringWithFormat:@"%@/Library/Caches/com.apple.springboard-imagecache-smallicons.plist", home] UTF8String]); my_system([[NSString stringWithFormat:@"rm -rf %@/Library/Caches/SpringBoardIconCache", home] UTF8String]); my_system([[NSString stringWithFormat:@"rm -rf %@/Library/Caches/SpringBoardIconCache-small", home] UTF8String]); my_system([[NSString stringWithFormat:@"rm -rf %@/Library/Caches/com.apple.IconsCache", home] UTF8String]); } my_system("killall installd"); } @finally { my_system("killall -SIGCONT SpringBoard"); } notify_post("com.apple.mobile.application_installed"); return 0; } } pid_t pidOfCydia(void) { launch_data_t request = launch_data_new_string(LAUNCH_KEY_GETJOBS); launch_data_t response = launch_msg(request); launch_data_free(request); __block pid_t pid=-1; if (response == NULL || launch_data_get_type(response) != LAUNCH_DATA_DICTIONARY) return -1; xpc_dictionary_apply((xpc_object_t)response, ^bool(const char *key, xpc_object_t value) { if (xpc_get_type(value) == XPC_TYPE_DICTIONARY) { const char *program = xpc_dictionary_get_string(value, "Program"); if (program && strcmp(program, "/Applications/Cydia.app/Cydia") == 0) { pid = (pid_t)xpc_dictionary_get_int64(value, "PID"); if (verbose) fprintf(stderr, "Found Cydia running with PID: %d\n", pid); return false; } } return true; }); if (pid>0) { return pid; } return -1; } int optimized_uicache(void) { __block int rv=0; NSMutableDictionary *registered = [NSMutableDictionary new]; static void (^readHandler)(NSFileHandle*) = ^(NSFileHandle *fh) { NSData *output = [fh readDataToEndOfFile]; if (output.length==0) return; const char *found_path = (const char *)[output bytes]; NSArray *found = [@(found_path) pathComponents]; if (found.count >= 3) { NSString *appPath = [@"/Applications" stringByAppendingPathComponent:found[2]]; @synchronized (registered) { if (registered[appPath]) return; if (verbose) fprintf(stderr, "Updating %s\n", appPath.lastPathComponent.UTF8String); registered[appPath] = @YES; } pid_t cydia_pid; if ([found[2] isEqualToString:@"Cydia.app"] && (cydia_pid = pidOfCydia()) > 0) { // We are in cydia and trying to refresh it - this will kill it. Let's schedule it for later. if (verbose) fprintf(stderr, "Waiting to refresh Cydia...\n"); pid_t pid = fork(); if (pid == 0) { setpgrp(); signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); fclose(stdin); freopen("/dev/null", "a", stderr); freopen("/dev/null", "a", stdout); pid = fork(); if (pid == 0) { while (kill(cydia_pid, 0)==0) { sleep(1); } const char *uicache = (*_NSGetArgv())[0]; execl(uicache, uicache, "-vvvvvvv", NULL); fprintf(stderr, "Unable to exec\n"); fflush(stderr); exit(-1); } exit(0); } else if (pid > 0) { int stat; waitpid(pid, &stat, 0); return; } else { fprintf(stderr, "Unable to fork\n"); } } if (!registerPath(appPath)) rv++; } }; NSFileManager *fm = [NSFileManager defaultManager]; NSMutableDictionary *apps = [NSMutableDictionary new]; NSMutableArray *cleanup = [NSMutableArray new]; NSMutableArray *finds = [NSMutableArray new]; if (verbose>1) fprintf(stderr, "Enumerating apps\n"); for (LSApplicationProxy *app in [workspace allApplications]) { NSString *path = [[app bundleURL] path]; if (![path hasPrefix:@"/Applications/"]) continue; if (verbose>1) fprintf(stderr, "Checking %s\n", path.lastPathComponent.UTF8String); NSDate *lastRegistered = [app registeredDate]; if ([fm fileExistsAtPath:path]) { // Check for updated components NSTask *find = [NSTask new]; [find setLaunchPath:@"/usr/bin/find"]; [find setStandardOutput:[NSPipe pipe]]; NSString *stampPath = [NSString stringWithFormat:@"/var/tmp/uicache.stamp.%@", app.applicationIdentifier]; [fm createFileAtPath:stampPath contents:nil attributes:@{NSFileModificationDate: lastRegistered}]; [cleanup addObject:stampPath]; [find setArguments:@[ path, @"-newer", stampPath, @"-print0", @"-quit"]]; [finds addObject:find]; [find launch]; apps[path.lastPathComponent] = app; } else { if (verbose) fprintf(stderr, "De-registering removed app: %s\n", path.lastPathComponent.UTF8String); @synchronized (registered) { if (registered[path]) continue; registered[path] = @YES; } if (!unregisterPath(path)) rv++; } } for (NSString* existing in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/Applications" error:nil]) { NSString *path = [@"/Applications" stringByAppendingPathComponent:existing]; if (apps[existing] || registered[path] || ![existing hasSuffix:@".app"]) continue; if (verbose) fprintf(stderr, "Registering new app: %s\n", existing.UTF8String); @synchronized (registered) { registered[path] = @YES; } if (!registerPath(path)) rv++; } for (NSTask *find in finds) { if (verbose>2) fprintf(stderr, "waiting for find %s\n", [find.arguments componentsJoinedByString:@" "].UTF8String); readHandler([find.standardOutput fileHandleForReading]); [find waitUntilExit]; } for (NSString *path in cleanup) { [fm removeItemAtPath:path error:nil]; } return rv; } int main(int argc, const char *argv[]) { if (getuid() == 0) { // Be mobile if (setuid(501)) { fprintf(stderr, "Error: unable to become mobile"); return -1; } } dlopen("/System/Library/PrivateFrameworks/MobileContainerManager.framework/MobileContainerManager", RTLD_LAZY); dlopen("/System/Library/PrivateFrameworks/FrontBoardServices.framework/FrontBoardServices", RTLD_LAZY); dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY); Class $SBSRelaunchAction = objc_getClass("SBSRelaunchAction"); Class $FBSSystemService = objc_getClass("FBSSystemService"); $MCMPluginKitPluginDataContainer = objc_getClass("MCMPluginKitPluginDataContainer"); $MCMAppDataContainer = objc_getClass("MCMAppDataContainer"); $LSApplicationWorkspace = objc_getClass("LSApplicationWorkspace"); workspace = [$LSApplicationWorkspace defaultWorkspace]; static int rv=0; @autoreleasepool { bool respring=false, do_all=false; void (*jb_oneshot_entitle_now)(pid_t a, uint64_t b); void *libjb = dlopen("/usr/lib/libjailbreak.dylib", RTLD_LAZY); if (libjb) { dlerror(); jb_oneshot_entitle_now = (void (*)(pid_t, uint64_t))dlsym(libjb, "jb_oneshot_entitle_now"); if (!dlerror()) jb_oneshot_entitle_now(getpid(), 2); } NSMutableDictionary *paths = [NSMutableDictionary new]; NSMutableDictionary *unregister_paths = [NSMutableDictionary new]; static struct option long_options[] = { {"all", no_argument, 0, 'a'}, {"help", no_argument, 0, 'h'}, {"path", required_argument, 0, 'p'}, {"unregister", required_argument, 0, 'u'}, {"respring", no_argument, 0, 'r'}, {"verbose", no_argument, 0, 'v'}, {0, 0, 0, 0} }; int option_index = 0; char ch; bool have_path = false; while ((ch = getopt_long(argc, (char *const *)argv, "ap:ru:vh?", long_options, &option_index)) != -1) { switch (ch) { case 'a': do_all = true; break; case 'h': usage(); break; case 'p': paths[@(optarg)] = @YES; have_path = true; break; case 'r': respring = true; break; case 'u': unregister_paths[@(optarg)] = @YES; have_path = true; break; case 'v': verbose++; break; default: break; } } if (do_all || !$MCMPluginKitPluginDataContainer || !$MCMAppDataContainer || !$LSApplicationWorkspace) { rv = standard_uicache(); } else if (have_path) { for (NSString *path in [paths allKeys]) { if (verbose) fprintf(stderr, "Refreshing %s\n", path.UTF8String); if (!registerPath(path)) rv++; } for (NSString *path in [unregister_paths allKeys]) { if (!unregisterPath(path)) rv++; } } else { rv += optimized_uicache(); } if ( respring ) { [[[PBSSystemServiceConnection sharedConnection] systemServiceProxy] relaunchBackboardd]; pid_t sb_pid = launch_get_job_pid("com.apple.SpringBoard"); if ($SBSRelaunchAction && $FBSSystemService) { id action = [$SBSRelaunchAction actionWithReason:@"respring" options:RestartRenderServer targetURL:nil]; id sharedService = [$FBSSystemService sharedService]; [sharedService sendActions:[NSSet setWithObject:action] withResult:nil]; for (int i=0; i<100; i++) { if (kill(sb_pid, 0)) { break; } usleep(1000); } } else { my_system("launchctl stop com.apple.PineBoard"); my_system("launchctl stop com.apple.backboardd"); } } } // @autoreleasepool return rv; }