| //TODO: don't copy/mount DeveloperDiskImage.dmg if it's already done - Xcode checks this somehow |
| |
| #import <CoreFoundation/CoreFoundation.h> |
| #import <Foundation/Foundation.h> |
| #include <unistd.h> |
| #include <sys/mman.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/un.h> |
| #include <sys/sysctl.h> |
| #include <stdio.h> |
| #include <signal.h> |
| #include <getopt.h> |
| #include <pwd.h> |
| #include <dlfcn.h> |
| #include <time.h> |
| |
| #include <netinet/in.h> |
| #include <netinet/tcp.h> |
| |
| #include "MobileDevice.h" |
| #import "errors.h" |
| #import "device_db.h" |
| |
| #define PREP_CMDS_PATH @"/tmp/%@/fruitstrap-lldb-prep-cmds-" |
| #define LLDB_SHELL @"PATH=/usr/bin /usr/bin/lldb -s %@" |
| /* |
| * Startup script passed to lldb. |
| * To see how xcode interacts with lldb, put this into .lldbinit: |
| * log enable -v -f /Users/vargaz/lldb.log lldb all |
| * log enable -v -f /Users/vargaz/gdb-remote.log gdb-remote all |
| */ |
| #define LLDB_PREP_CMDS CFSTR("\ |
| platform select remote-'{platform}' --sysroot '{symbols_path}'\n\ |
| target create \"{disk_app}\"\n\ |
| script fruitstrap_device_app=\"{device_app}\"\n\ |
| script fruitstrap_connect_url=\"connect://127.0.0.1:{device_port}\"\n\ |
| script fruitstrap_output_path=\"{output_path}\"\n\ |
| script fruitstrap_error_path=\"{error_path}\"\n\ |
| target modules search-paths add {modules_search_paths_pairs}\n\ |
| command script import \"{python_file_path}\"\n\ |
| command script add -f {python_command}.connect_command connect\n\ |
| command script add -s asynchronous -f {python_command}.run_command run\n\ |
| command script add -s asynchronous -f {python_command}.autoexit_command autoexit\n\ |
| command script add -s asynchronous -f {python_command}.safequit_command safequit\n\ |
| connect\n\ |
| ") |
| |
| const char* lldb_prep_no_cmds = ""; |
| |
| const char* lldb_prep_interactive_cmds = "\ |
| run\n\ |
| "; |
| |
| const char* lldb_prep_noninteractive_justlaunch_cmds = "\ |
| run\n\ |
| safequit\n\ |
| "; |
| |
| const char* lldb_prep_noninteractive_cmds = "\ |
| run\n\ |
| autoexit\n\ |
| "; |
| |
| NSMutableString * custom_commands = nil; |
| |
| /* |
| * Some things do not seem to work when using the normal commands like process connect/launch, so we invoke them |
| * through the python interface. Also, Launch () doesn't seem to work when ran from init_module (), so we add |
| * a command which can be used by the user to run it. |
| */ |
| NSString* LLDB_FRUITSTRAP_MODULE = @ |
| #include "lldb.py.h" |
| ; |
| |
| const char* output_path = NULL; |
| const char* error_path = NULL; |
| |
| typedef struct am_device * AMDeviceRef; |
| mach_error_t AMDeviceSecureStartService(AMDeviceRef device, CFStringRef service_name, unsigned int *unknown, ServiceConnRef * handle); |
| mach_error_t AMDeviceCreateHouseArrestService(AMDeviceRef device, CFStringRef identifier, CFDictionaryRef options, AFCConnectionRef * handle); |
| CFSocketNativeHandle AMDServiceConnectionGetSocket(ServiceConnRef con); |
| void AMDServiceConnectionInvalidate(ServiceConnRef con); |
| |
| bool AMDeviceIsAtLeastVersionOnPlatform(AMDeviceRef device, CFDictionaryRef vers); |
| int AMDeviceSecureTransferPath(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); |
| int AMDeviceSecureInstallApplication(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); |
| int AMDeviceSecureInstallApplicationBundle(AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); |
| int AMDeviceMountImage(AMDeviceRef device, CFStringRef image, CFDictionaryRef options, void *callback, int cbarg); |
| mach_error_t AMDeviceLookupApplications(AMDeviceRef device, CFDictionaryRef options, CFDictionaryRef *result); |
| int AMDeviceGetInterfaceType(AMDeviceRef device); |
| AMDeviceRef AMDeviceCopyPairedCompanion(AMDeviceRef device); |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| unsigned int AMDeviceCopyDeveloperModeStatus(AMDeviceRef device, uint32_t *error_code); |
| #endif |
| |
| int AMDServiceConnectionSend(ServiceConnRef con, const void * data, size_t size); |
| int AMDServiceConnectionReceive(ServiceConnRef con, void * data, size_t size); |
| uint64_t AMDServiceConnectionReceiveMessage(ServiceConnRef con, CFPropertyListRef message, CFPropertyListFormat *format); |
| uint64_t AMDServiceConnectionSendMessage(ServiceConnRef con, CFPropertyListRef message, CFPropertyListFormat format); |
| CFArrayRef AMDeviceCopyProvisioningProfiles(AMDeviceRef device); |
| int AMDeviceInstallProvisioningProfile(AMDeviceRef device, void *profile); |
| int AMDeviceRemoveProvisioningProfile(AMDeviceRef device, CFStringRef uuid); |
| CFStringRef MISProfileGetValue(void *profile, CFStringRef key); |
| CFDictionaryRef MISProfileCopyPayload(void *profile); |
| void *MISProfileCreateWithData(int zero, CFDataRef data); |
| int MISProfileWriteToFile(void *profile, CFStringRef dest_path); |
| |
| bool found_device = false, debug = false, verbose = false, unbuffered = false, nostart = false, debugserver_only = false, detect_only = false, install = true, uninstall = false, no_wifi = false; |
| bool faster_path_search = false; |
| bool command_only = false; |
| char *command = NULL; |
| char const*target_filename = NULL; |
| char const*upload_pathname = NULL; |
| char *bundle_id = NULL; |
| NSMutableArray *keys = NULL; |
| bool interactive = true; |
| bool justlaunch = false; |
| bool file_system = false; |
| bool non_recursively = false; |
| char *app_path = NULL; |
| char *app_deltas = NULL; |
| char *device_id = NULL; |
| char *args = NULL; |
| char *envs = NULL; |
| char *list_root = NULL; |
| const char * custom_script_path = NULL; |
| char *symbols_download_directory = NULL; |
| char *profile_uuid = NULL; |
| char *profile_path = NULL; |
| int _timeout = 0; |
| int _detectDeadlockTimeout = 0; |
| bool _json_output = false; |
| NSMutableArray *_file_meta_info = nil; |
| int port = 0; // 0 means "dynamically assigned" |
| ServiceConnRef dbgServiceConnection = NULL; |
| pid_t parent = 0; |
| // PID of child process running lldb |
| pid_t child = 0; |
| // Signal sent from child to parent process when LLDB finishes. |
| const int SIGLLDB = SIGUSR1; |
| NSString* tmpUUID; |
| struct am_device_notification *notify; |
| CFRunLoopSourceRef lldb_socket_runloop; |
| CFRunLoopSourceRef server_socket_runloop; |
| CFRunLoopSourceRef fdvendor_runloop; |
| |
| uint32_t symbols_file_paths_command = 0x30303030; |
| uint32_t symbols_download_file_command = 0x01000000; |
| CFStringRef symbols_service_name = CFSTR("com.apple.dt.fetchsymbols"); |
| const int symbols_logging_interval_ms = 250; |
| |
| const size_t sizeof_uint32_t = sizeof(uint32_t); |
| |
| // Error codes we report on different failures, so scripts can distinguish between user app exit |
| // codes and our exit codes. For non app errors we use codes in reserved 128-255 range. |
| const int exitcode_timeout = 252; |
| const int exitcode_error = 253; |
| const int exitcode_app_crash = 254; |
| |
| // Checks for MobileDevice.framework errors, tries to print them and exits. |
| #define check_error(call) \ |
| do { \ |
| unsigned int err = (unsigned int)call; \ |
| if (err != 0) \ |
| { \ |
| const char* msg = get_error_message(err); \ |
| NSString *description = msg ? [NSString stringWithUTF8String:msg] : @"unknown."; \ |
| NSLogJSON(@{@"Event": @"Error", @"Code": @(err), @"Status": description}); \ |
| on_error(@"Error 0x%x: %@ " #call, err, description); \ |
| } \ |
| } while (false); |
| |
| // Checks for MobileDevice.framework errors and tries to print them. |
| #define log_error(call) \ |
| do { \ |
| unsigned int err = (unsigned int)call; \ |
| if (err != 0) \ |
| { \ |
| const char* msg = get_error_message(err); \ |
| NSString *description = msg ? [NSString stringWithUTF8String:msg] : @"unknown."; \ |
| NSLogJSON(@{@"Event": @"Error", @"Code": @(err), @"Status": description}); \ |
| log_on_error(@"Error 0x%x: %@ " #call, err, description); \ |
| } \ |
| } while (false); |
| |
| |
| |
| void disable_ssl(ServiceConnRef con) |
| { |
| // MobileDevice links with SSL, so function will be available; |
| typedef void (*SSL_free_t)(void*); |
| static SSL_free_t SSL_free = NULL; |
| if (SSL_free == NULL) |
| { |
| SSL_free = (SSL_free_t)dlsym(RTLD_DEFAULT, "SSL_free"); |
| } |
| |
| SSL_free(con->sslContext); |
| con->sslContext = NULL; |
| } |
| |
| void log_on_error(NSString* format, ...) |
| { |
| va_list valist; |
| va_start(valist, format); |
| NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; |
| va_end(valist); |
| |
| if (!_json_output) { |
| NSLog(@"[ !! ] %@", str); |
| } |
| } |
| |
| |
| void on_error(NSString* format, ...) |
| { |
| va_list valist; |
| va_start(valist, format); |
| NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; |
| va_end(valist); |
| |
| if (!_json_output) { |
| NSLog(@"[ !! ] %@", str); |
| } |
| |
| exit(exitcode_error); |
| } |
| |
| // Print error message getting last errno and exit |
| void on_sys_error(NSString* format, ...) { |
| const char* errstr = strerror(errno); |
| |
| va_list valist; |
| va_start(valist, format); |
| NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; |
| va_end(valist); |
| |
| on_error(@"%@ : %@", str, [NSString stringWithUTF8String:errstr]); |
| } |
| |
| void __NSLogOut(NSString* format, va_list valist) { |
| NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; |
| [[str stringByAppendingString:@"\n"] writeToFile:@"/dev/stdout" atomically:NO encoding:NSUTF8StringEncoding error:nil]; |
| } |
| |
| void NSLogOut(NSString* format, ...) { |
| if (!_json_output) { |
| va_list valist; |
| va_start(valist, format); |
| __NSLogOut(format, valist); |
| va_end(valist); |
| } |
| } |
| |
| void NSLogVerbose(NSString* format, ...) { |
| if (verbose && !_json_output) { |
| va_list valist; |
| va_start(valist, format); |
| __NSLogOut(format, valist); |
| va_end(valist); |
| } |
| } |
| |
| void NSLogJSON(NSDictionary* jsonDict) { |
| if (_json_output) { |
| NSError *error; |
| NSData *data = [NSJSONSerialization dataWithJSONObject:jsonDict |
| options:NSJSONWritingPrettyPrinted |
| error:&error]; |
| if (data) { |
| NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; |
| [jsonString writeToFile:@"/dev/stdout" atomically:NO encoding:NSUTF8StringEncoding error:nil]; |
| [jsonString release]; |
| } else { |
| [@"{\"JSONError\": \"JSON error\"}" writeToFile:@"/dev/stdout" atomically:NO encoding:NSUTF8StringEncoding error:nil]; |
| } |
| } |
| } |
| |
| uint64_t get_current_time_in_milliseconds() { |
| return clock_gettime_nsec_np(CLOCK_REALTIME) / (1000 * 1000); |
| } |
| |
| BOOL mkdirp(NSString* path) { |
| NSError* error = nil; |
| BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:&error]; |
| return success; |
| } |
| |
| Boolean path_exists(CFTypeRef path) { |
| if (CFGetTypeID(path) == CFStringGetTypeID()) { |
| CFURLRef url = CFURLCreateWithFileSystemPath(NULL, path, kCFURLPOSIXPathStyle, true); |
| Boolean result = CFURLResourceIsReachable(url, NULL); |
| CFRelease(url); |
| return result; |
| } else if (CFGetTypeID(path) == CFURLGetTypeID()) { |
| return CFURLResourceIsReachable(path, NULL); |
| } else { |
| return false; |
| } |
| } |
| |
| CFStringRef copy_find_path(CFStringRef rootPath, CFStringRef namePattern) { |
| FILE *fpipe = NULL; |
| CFStringRef cf_command; |
| |
| if( !path_exists(rootPath) ) |
| return NULL; |
| |
| if (faster_path_search) { |
| CFIndex maxdepth = 1; |
| CFArrayRef findPathSlash = CFStringCreateArrayWithFindResults(NULL, namePattern, CFSTR("/"), CFRangeMake(0, CFStringGetLength(namePattern)), 0); |
| if (findPathSlash != NULL) { |
| maxdepth = CFArrayGetCount(findPathSlash) + 1; |
| CFRelease(findPathSlash); |
| } |
| |
| cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find '%@' -path '%@/%@' -maxdepth %ld 2>/dev/null | sort | tail -n 1"), rootPath, rootPath, namePattern, maxdepth); |
| } |
| else { |
| if (CFStringFind(namePattern, CFSTR("*"), 0).location == kCFNotFound) { |
| //No wildcards. Let's speed up the search |
| CFStringRef path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), rootPath, namePattern); |
| |
| if( path_exists(path) ) |
| return path; |
| |
| CFRelease(path); |
| return NULL; |
| } |
| |
| if (CFStringFind(namePattern, CFSTR("/"), 0).location == kCFNotFound) { |
| cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find '%@' -name '%@' -maxdepth 1 2>/dev/null | sort | tail -n 1"), rootPath, namePattern); |
| } else { |
| cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find '%@' -path '%@/%@' 2>/dev/null | sort | tail -n 1"), rootPath, rootPath, namePattern); |
| } |
| } |
| |
| char command[1024] = { '\0' }; |
| CFStringGetCString(cf_command, command, sizeof(command), kCFStringEncodingUTF8); |
| CFRelease(cf_command); |
| |
| if (!(fpipe = (FILE *)popen(command, "r"))) |
| on_sys_error(@"Error encountered while opening pipe"); |
| |
| char buffer[256] = { '\0' }; |
| |
| fgets(buffer, sizeof(buffer), fpipe); |
| pclose(fpipe); |
| |
| strtok(buffer, "\n"); |
| |
| CFStringRef path = CFStringCreateWithCString(NULL, buffer, kCFStringEncodingUTF8); |
| |
| if( CFStringGetLength(path) > 0 && path_exists(path) ) |
| return path; |
| |
| CFRelease(path); |
| return NULL; |
| } |
| |
| CFStringRef copy_xcode_dev_path(void) { |
| static char xcode_dev_path[256] = { '\0' }; |
| if (strlen(xcode_dev_path) == 0) { |
| const char* env_dev_path = getenv("DEVELOPER_DIR"); |
| |
| if (env_dev_path && strlen(env_dev_path) > 0) { |
| strcpy(xcode_dev_path, env_dev_path); |
| // DEVELOPER_DIR should refer to Xcode.app/Contents/Developer, but |
| // xcode-select and friends have an extension to fix the path, if it points to Xcode.app/. |
| static char dev_subdir[256] = { '\0' }; |
| strcat(strcat(dev_subdir, env_dev_path), "/Contents/Developer"); |
| struct stat sb; |
| if (stat(dev_subdir, &sb) == 0) |
| { |
| strcpy(xcode_dev_path, dev_subdir); |
| } |
| } else { |
| FILE *fpipe = NULL; |
| char *command = "xcode-select -print-path"; |
| |
| if (!(fpipe = (FILE *)popen(command, "r"))) |
| on_sys_error(@"Error encountered while opening pipe"); |
| |
| char buffer[256] = { '\0' }; |
| |
| fgets(buffer, sizeof(buffer), fpipe); |
| pclose(fpipe); |
| |
| strtok(buffer, "\n"); |
| strcpy(xcode_dev_path, buffer); |
| } |
| NSLogVerbose(@"Found Xcode developer dir %s", xcode_dev_path); |
| } |
| return CFStringCreateWithCString(NULL, xcode_dev_path, kCFStringEncodingUTF8); |
| } |
| |
| const char *get_home(void) { |
| const char* home = getenv("HOME"); |
| if (!home) { |
| struct passwd *pwd = getpwuid(getuid()); |
| home = pwd->pw_dir; |
| } |
| return home; |
| } |
| |
| CFStringRef copy_xcode_path_for_impl(CFStringRef rootPath, CFStringRef subPath, CFStringRef search) { |
| CFStringRef searchPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), rootPath, subPath ); |
| CFStringRef res = copy_find_path(searchPath, search); |
| CFRelease(searchPath); |
| return res; |
| } |
| |
| CFStringRef copy_xcode_path_for(CFStringRef subPath, CFStringRef search) { |
| CFStringRef xcodeDevPath = copy_xcode_dev_path(); |
| CFStringRef defaultXcodeDevPath = CFSTR("/Applications/Xcode.app/Contents/Developer"); |
| CFStringRef path = NULL; |
| const char* home = get_home(); |
| |
| // Try using xcode-select --print-path |
| path = copy_xcode_path_for_impl(xcodeDevPath, subPath, search); |
| |
| // If not look in the default xcode location (xcode-select is sometimes wrong) |
| if (path == NULL && CFStringCompare(xcodeDevPath, defaultXcodeDevPath, 0) != kCFCompareEqualTo ) |
| path = copy_xcode_path_for_impl(defaultXcodeDevPath, subPath, search); |
| |
| // If not look in the users home directory, Xcode can store device support stuff there |
| if (path == NULL) { |
| CFRelease(xcodeDevPath); |
| xcodeDevPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode"), home ); |
| path = copy_xcode_path_for_impl(xcodeDevPath, subPath, search); |
| } |
| |
| CFRelease(xcodeDevPath); |
| |
| return path; |
| } |
| |
| device_desc get_device_desc(CFStringRef model) { |
| if (model != NULL) { |
| size_t sz = sizeof(device_db) / sizeof(device_desc); |
| for (size_t i = 0; i < sz; i ++) { |
| if (CFStringCompare(model, device_db[i].model, kCFCompareNonliteral | kCFCompareCaseInsensitive) == kCFCompareEqualTo) { |
| return device_db[i]; |
| } |
| } |
| } |
| |
| device_desc res = device_db[UNKNOWN_DEVICE_IDX]; |
| |
| res.model = model; |
| res.name = model; |
| |
| return res; |
| } |
| |
| bool is_usb_device(const AMDeviceRef device) { |
| return AMDeviceGetInterfaceType(device) == 1; |
| } |
| |
| void connect_and_start_session(AMDeviceRef device) { |
| AMDeviceConnect(device); |
| assert(AMDeviceIsPaired(device)); |
| check_error(AMDeviceValidatePairing(device)); |
| check_error(AMDeviceStartSession(device)); |
| } |
| |
| CFStringRef get_device_full_name(const AMDeviceRef device) { |
| CFStringRef full_name = NULL, |
| device_udid = AMDeviceCopyDeviceIdentifier(device), |
| device_name = NULL, |
| model_name = NULL, |
| sdk_name = NULL, |
| arch_name = NULL, |
| product_version = NULL, |
| build_version = NULL; |
| |
| AMDeviceConnect(device); |
| |
| device_name = AMDeviceCopyValue(device, 0, CFSTR("DeviceName")); |
| |
| // Please ensure that device is connected or the name will be unknown |
| CFStringRef model = AMDeviceCopyValue(device, 0, CFSTR("HardwareModel")); |
| device_desc dev; |
| if (model != NULL) { |
| dev = get_device_desc(model); |
| } else { |
| dev= device_db[UNKNOWN_DEVICE_IDX]; |
| model = dev.model; |
| } |
| model_name = dev.name; |
| sdk_name = dev.sdk; |
| arch_name = dev.arch; |
| product_version = AMDeviceCopyValue(device, 0, CFSTR("ProductVersion")); |
| build_version = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); |
| |
| NSLogVerbose(@"Hardware Model: %@", model); |
| NSLogVerbose(@"Device Name: %@", device_name); |
| NSLogVerbose(@"Model Name: %@", model_name); |
| NSLogVerbose(@"SDK Name: %@", sdk_name); |
| NSLogVerbose(@"Architecture Name: %@", arch_name); |
| NSLogVerbose(@"Product Version: %@", product_version); |
| NSLogVerbose(@"Build Version: %@", build_version); |
| if (build_version == 0) |
| build_version = CFStringCreateWithCString(NULL, "", kCFStringEncodingUTF8); |
| |
| if (device_name != NULL) { |
| full_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@, %@, %@, %@, %@, %@) a.k.a. '%@'"), device_udid, model, model_name, sdk_name, arch_name, product_version, build_version, device_name); |
| } else { |
| full_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@, %@, %@, %@, %@, %@)"), device_udid, model, model_name, sdk_name, arch_name, product_version, build_version); |
| } |
| |
| AMDeviceDisconnect(device); |
| |
| if(device_udid != NULL) |
| CFRelease(device_udid); |
| if(device_name != NULL) |
| CFRelease(device_name); |
| if(model != NULL) |
| CFRelease(model); |
| if(model_name != NULL && model_name != model) |
| CFRelease(model_name); |
| if(product_version) |
| CFRelease(product_version); |
| if(build_version) |
| CFRelease(build_version); |
| |
| return CFAutorelease(full_name); |
| } |
| |
| NSDictionary* get_device_json_dict(const AMDeviceRef device) { |
| NSMutableDictionary *json_dict = [NSMutableDictionary new]; |
| is_usb_device(device) ? AMDeviceConnect(device) : connect_and_start_session(device); |
| |
| CFStringRef device_udid = AMDeviceCopyDeviceIdentifier(device); |
| if (device_udid) { |
| [json_dict setValue:(__bridge NSString *)device_udid forKey:@"DeviceIdentifier"]; |
| CFRelease(device_udid); |
| } |
| |
| CFStringRef device_hardware_model = AMDeviceCopyValue(device, 0, CFSTR("HardwareModel")); |
| if (device_hardware_model) { |
| [json_dict setValue:(NSString*)device_hardware_model forKey:@"HardwareModel"]; |
| size_t device_db_length = sizeof(device_db) / sizeof(device_desc); |
| for (size_t i = 0; i < device_db_length; i ++) { |
| if (CFStringCompare(device_hardware_model, device_db[i].model, kCFCompareNonliteral | kCFCompareCaseInsensitive) == kCFCompareEqualTo) { |
| device_desc dev = device_db[i]; |
| [json_dict setValue:(__bridge NSString *)dev.name forKey:@"modelName"]; |
| [json_dict setValue:(__bridge NSString *)dev.sdk forKey:@"modelSDK"]; |
| [json_dict setValue:(__bridge NSString *)dev.arch forKey:@"modelArch"]; |
| break; |
| } |
| } |
| CFRelease(device_hardware_model); |
| } |
| |
| for (NSString *deviceValue in @[@"DeviceName", |
| @"BuildVersion", |
| @"DeviceClass", |
| @"ProductType", |
| @"ProductVersion"]) { |
| CFStringRef cf_value = AMDeviceCopyValue(device, 0, (__bridge CFStringRef)deviceValue); |
| if (cf_value) { |
| [json_dict setValue:(__bridge NSString *)cf_value forKey:deviceValue]; |
| CFRelease(cf_value); |
| } |
| } |
| |
| AMDeviceDisconnect(device); |
| |
| return CFAutorelease(json_dict); |
| } |
| |
| int get_companion_interface_type(AMDeviceRef device) |
| { |
| assert(AMDeviceGetInterfaceType(device) == 3); |
| AMDeviceRef companion = AMDeviceCopyPairedCompanion(device); |
| int type = AMDeviceGetInterfaceType(companion); |
| AMDeviceRelease(companion); |
| return type; |
| } |
| |
| CFStringRef get_device_interface_name(const AMDeviceRef device) { |
| // AMDeviceGetInterfaceType(device) 0=Unknown, 1 = Direct/USB, 2 = Indirect/WIFI, 3 = Companion proxy |
| switch(AMDeviceGetInterfaceType(device)) { |
| case 1: |
| return CFSTR("USB"); |
| case 2: |
| return CFSTR("WIFI"); |
| case 3: |
| { |
| if (get_companion_interface_type(device) == 1) |
| { |
| return CFSTR("USB Companion proxy"); |
| } |
| else |
| { |
| return CFSTR("WIFI Companion proxy"); |
| } |
| } |
| default: |
| return CFSTR("Unknown Connection"); |
| } |
| } |
| |
| CFMutableArrayRef copy_device_product_version_parts(AMDeviceRef device) { |
| CFStringRef version = AMDeviceCopyValue(device, 0, CFSTR("ProductVersion")); |
| CFArrayRef parts = CFStringCreateArrayBySeparatingStrings(NULL, version, CFSTR(".")); |
| CFMutableArrayRef result = CFArrayCreateMutableCopy(NULL, CFArrayGetCount(parts), parts); |
| CFRelease(version); |
| CFRelease(parts); |
| return result; |
| } |
| |
| CFStringRef copy_device_support_path(AMDeviceRef device, CFStringRef suffix) { |
| time_t startTime, endTime; |
| time( &startTime ); |
| |
| CFStringRef version = NULL; |
| CFStringRef build = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); |
| CFStringRef deviceClass = AMDeviceCopyValue(device, 0, CFSTR("DeviceClass")); |
| CFStringRef deviceModel = AMDeviceCopyValue(device, 0, CFSTR("HardwareModel")); |
| CFStringRef productType = AMDeviceCopyValue(device, 0, CFSTR("ProductType")); |
| CFStringRef deviceArch = NULL; |
| CFStringRef path = NULL; |
| |
| device_desc dev; |
| if (deviceModel != NULL) { |
| dev = get_device_desc(deviceModel); |
| deviceArch = dev.arch; |
| } |
| |
| CFMutableArrayRef version_parts = copy_device_product_version_parts(device); |
| |
| NSLogVerbose(@"Device Class: %@", deviceClass); |
| NSLogVerbose(@"build: %@", build); |
| |
| CFStringRef deviceClassPath[2]; |
| |
| if (CFStringCompare(CFSTR("AppleTV"), deviceClass, 0) == kCFCompareEqualTo) { |
| deviceClassPath[0] = CFSTR("Platforms/AppleTVOS.platform/DeviceSupport"); |
| deviceClassPath[1] = CFSTR("tvOS DeviceSupport"); |
| } |
| else if (CFStringCompare(CFSTR("Watch"), deviceClass, 0) == kCFCompareEqualTo) { |
| deviceClassPath[0] = CFSTR("Platforms/WatchOS.platform/DeviceSupport"); |
| deviceClassPath[1] = CFSTR("watchOS DeviceSupport"); |
| } |
| else { |
| deviceClassPath[0] = CFSTR("Platforms/iPhoneOS.platform/DeviceSupport"); |
| deviceClassPath[1] = CFSTR("iOS DeviceSupport"); |
| } |
| |
| CFMutableArrayRef string_allocations = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); |
| while (CFArrayGetCount(version_parts) > 0) { |
| version = CFStringCreateByCombiningStrings(NULL, version_parts, CFSTR(".")); |
| NSLogVerbose(@"version: %@", version); |
| |
| for( int i = 0; i < 2; ++i ) { |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@) %@/%@"), version, build, deviceArch, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@)/%@"), version, build, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (*)/%@"), version, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), version, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@.*/%@"), version, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ %@ (%@)/%@"), productType, version, build, suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| } |
| |
| CFRelease(version); |
| if (path != NULL) { |
| break; |
| } |
| |
| // Not all iOS versions have a dedicated developer disk image. Xcode 13.4.1 supports |
| // iOS up to 15.5 but does not include developer disk images for 15.1 or 15.3 |
| // despite being able to deploy to them. For this reason, this logic looks for previous |
| // minor versions if it doesn't find an exact match. In the case where the disk image |
| // from a previous minor version is not compatible, deployment will fail with |
| // kAMDInvalidServiceError. |
| CFStringRef previous_minor_version = NULL; |
| if (CFEqual(CFSTR("DeveloperDiskImage.dmg"), suffix) && |
| CFArrayGetCount(version_parts) == 2) { |
| int minor_version = CFStringGetIntValue(CFArrayGetValueAtIndex(version_parts, 1)); |
| if (minor_version > 0) { |
| previous_minor_version = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, |
| CFSTR("%d"), minor_version - 1); |
| CFArrayAppendValue(string_allocations, previous_minor_version); |
| } |
| } |
| CFArrayRemoveValueAtIndex(version_parts, CFArrayGetCount(version_parts) - 1); |
| if (previous_minor_version) { |
| CFArrayAppendValue(version_parts, previous_minor_version); |
| } |
| } |
| |
| for (int i = 0; i < CFArrayGetCount(string_allocations); i++) { |
| CFRelease(CFArrayGetValueAtIndex(string_allocations, i)); |
| } |
| CFRelease(string_allocations); |
| |
| for( int i = 0; i < 2; ++i ) { |
| if (path == NULL) { |
| CFStringRef search = CFStringCreateWithFormat(NULL, NULL, CFSTR("Latest/%@"), suffix); |
| path = copy_xcode_path_for(deviceClassPath[i], search); |
| CFRelease(search); |
| } |
| } |
| |
| CFRelease(version_parts); |
| CFRelease(build); |
| CFRelease(deviceClass); |
| CFRelease(productType); |
| if (deviceModel != NULL) { |
| CFRelease(deviceModel); |
| } |
| if (path == NULL) { |
| NSString *msg = [NSString stringWithFormat:@"Unable to locate DeviceSupport directory with suffix '%@'. This probably means you don't have Xcode installed, you will need to launch the app manually and logging output will not be shown!", suffix]; |
| NSLogJSON(@{ |
| @"Event": @"DeviceSupportError", |
| @"Status": msg, |
| }); |
| on_error(msg); |
| } |
| |
| time( &endTime ); |
| NSLogVerbose(@"DeviceSupport directory '%@' was located. It took %.2f seconds", path, difftime(endTime,startTime)); |
| |
| return path; |
| } |
| |
| void mount_callback(CFDictionaryRef dict, int arg) { |
| CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); |
| |
| if (CFEqual(status, CFSTR("LookingUpImage"))) { |
| NSLogOut(@"[ 0%%] Looking up developer disk image"); |
| } else if (CFEqual(status, CFSTR("CopyingImage"))) { |
| NSLogOut(@"[ 30%%] Copying DeveloperDiskImage.dmg to device"); |
| } else if (CFEqual(status, CFSTR("MountingImage"))) { |
| NSLogOut(@"[ 90%%] Mounting developer disk image"); |
| } |
| } |
| |
| void mount_developer_image(AMDeviceRef device) { |
| CFStringRef image_path = copy_device_support_path(device, CFSTR("DeveloperDiskImage.dmg")); |
| CFStringRef sig_path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@.signature"), image_path); |
| |
| NSLogVerbose(@"Developer disk image: %@", image_path); |
| |
| FILE* sig = fopen(CFStringGetCStringPtr(sig_path, kCFStringEncodingMacRoman), "rb"); |
| size_t buf_size = 128; |
| void *sig_buf = malloc(buf_size); |
| size_t bytes_read = fread(sig_buf, 1, buf_size, sig); |
| if (bytes_read != buf_size) { |
| on_sys_error(@"fread read %d bytes but expected %d bytes.", bytes_read, buf_size); |
| } |
| fclose(sig); |
| CFDataRef sig_data = CFDataCreateWithBytesNoCopy(NULL, sig_buf, buf_size, NULL); |
| CFRelease(sig_path); |
| |
| CFTypeRef keys[] = { CFSTR("ImageSignature"), CFSTR("ImageType") }; |
| CFTypeRef values[] = { sig_data, CFSTR("Developer") }; |
| CFDictionaryRef options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| CFRelease(sig_data); |
| |
| unsigned int result = (unsigned int)AMDeviceMountImage(device, image_path, options, &mount_callback, 0); |
| if (result == 0) { |
| NSLogOut(@"[ 95%%] Developer disk image mounted successfully"); |
| } else if (result == 0xe8000076 /* already mounted */) { |
| NSLogOut(@"[ 95%%] Developer disk image already mounted"); |
| } else { |
| if (result != 0) { |
| const char* msg = get_error_message(result); |
| NSString *description = @"unknown."; |
| if (msg) { |
| description = [NSString stringWithUTF8String:msg]; |
| NSLogOut(@"Error: %@", description); |
| } |
| NSLogJSON(@{@"Event": @"Error", |
| @"Code": @(result), |
| @"Status": description}); |
| } |
| |
| on_error(@"Unable to mount developer disk image. (%x)", result); |
| } |
| |
| CFStringRef symbols_path = copy_device_support_path(device, CFSTR("Symbols")); |
| if (symbols_path != NULL) |
| { |
| NSLogOut(@"Symbol Path: %@", symbols_path); |
| NSLogJSON(@{@"Event": @"MountDeveloperImage", |
| @"SymbolsPath": (__bridge NSString *)symbols_path |
| }); |
| CFRelease(symbols_path); |
| } |
| |
| CFRelease(image_path); |
| CFRelease(options); |
| } |
| |
| mach_error_t transfer_callback(CFDictionaryRef dict, int arg) { |
| if (CFDictionaryGetValue(dict, CFSTR("Error"))) { |
| return 0; |
| } |
| int percent; |
| CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); |
| CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); |
| |
| if (CFEqual(status, CFSTR("CopyingFile"))) { |
| static CFStringRef last_path = NULL; |
| static int last_overall_percent = -1; |
| |
| CFStringRef path = CFDictionaryGetValue(dict, CFSTR("Path")); |
| int overall_percent = percent / 2; |
| |
| if ((last_path == NULL || !CFEqual(path, last_path) || last_overall_percent != overall_percent) && !CFStringHasSuffix(path, CFSTR(".ipa"))) { |
| NSLogOut(@"[%3d%%] Copying %@ to device", overall_percent, path); |
| NSLogJSON(@{@"Event": @"BundleCopy", |
| @"OverallPercent": @(overall_percent), |
| @"Percent": @(percent), |
| @"Path": (__bridge NSString *)path |
| }); |
| } |
| |
| last_overall_percent = overall_percent; |
| |
| if (last_path != NULL) { |
| CFRelease(last_path); |
| } |
| last_path = CFStringCreateCopy(NULL, path); |
| } |
| |
| return 0; |
| } |
| |
| mach_error_t install_callback(CFDictionaryRef dict, int arg) { |
| if (CFDictionaryGetValue(dict, CFSTR("Error"))) { |
| return 0; |
| } |
| int percent; |
| CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); |
| CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); |
| |
| int overall_percent = (percent / 2) + 50; |
| NSLogOut(@"[%3d%%] %@", overall_percent, status); |
| NSLogJSON(@{@"Event": @"BundleInstall", |
| @"OverallPercent": @(overall_percent), |
| @"Percent": @(percent), |
| @"Status": (__bridge NSString *)status |
| }); |
| return 0; |
| } |
| |
| // During standard installation transferring and installation takes place |
| // in distinct function that can be passed distinct callbacks. Incremental |
| // installation performs both transfer and installation in a single function so |
| // use this callback to determine which step is occuring and call the proper |
| // callback. |
| mach_error_t incremental_install_callback(CFDictionaryRef dict, int arg) { |
| if (CFDictionaryGetValue(dict, CFSTR("Error"))) { |
| return 0; |
| } |
| CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); |
| if (CFEqual(status, CFSTR("TransferringPackage"))) { |
| int percent; |
| CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); |
| int overall_percent = (percent / 2); |
| NSLogOut(@"[%3d%%] %@", overall_percent, status); |
| NSLogJSON(@{@"Event": @"TransferringPackage", |
| @"OverallPercent": @(overall_percent), |
| }); |
| return 0; |
| } else if (CFEqual(status, CFSTR("CopyingFile"))) { |
| return transfer_callback(dict, arg); |
| } else { |
| return install_callback(dict, arg); |
| } |
| } |
| |
| CFURLRef copy_device_app_url(AMDeviceRef device, CFStringRef identifier) { |
| CFDictionaryRef result = nil; |
| |
| NSArray *a = [NSArray arrayWithObjects: |
| @"CFBundleIdentifier", // absolute must |
| @"ApplicationDSID", |
| @"ApplicationType", |
| @"CFBundleExecutable", |
| @"CFBundleDisplayName", |
| @"CFBundleIconFile", |
| @"CFBundleName", |
| @"CFBundleShortVersionString", |
| @"CFBundleSupportedPlatforms", |
| @"CFBundleURLTypes", |
| @"CodeInfoIdentifier", |
| @"Container", |
| @"Entitlements", |
| @"HasSettingsBundle", |
| @"IsUpgradeable", |
| @"MinimumOSVersion", |
| @"Path", |
| @"SignerIdentity", |
| @"UIDeviceFamily", |
| @"UIFileSharingEnabled", |
| @"UIStatusBarHidden", |
| @"UISupportedInterfaceOrientations", |
| nil]; |
| |
| NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; |
| CFDictionaryRef options = (CFDictionaryRef)optionsDict; |
| |
| check_error(AMDeviceLookupApplications(device, options, &result)); |
| |
| CFDictionaryRef app_dict = CFDictionaryGetValue(result, identifier); |
| assert(app_dict != NULL); |
| |
| CFStringRef app_path = CFDictionaryGetValue(app_dict, CFSTR("Path")); |
| assert(app_path != NULL); |
| |
| CFURLRef url = CFURLCreateWithFileSystemPath(NULL, app_path, kCFURLPOSIXPathStyle, true); |
| CFRelease(result); |
| return url; |
| } |
| |
| CFStringRef copy_disk_app_identifier(CFURLRef disk_app_url) { |
| CFURLRef plist_url = CFURLCreateCopyAppendingPathComponent(NULL, disk_app_url, CFSTR("Info.plist"), false); |
| CFReadStreamRef plist_stream = CFReadStreamCreateWithFile(NULL, plist_url); |
| if (!CFReadStreamOpen(plist_stream)) { |
| on_error(@"Cannot read Info.plist file: %@", plist_url); |
| } |
| |
| CFPropertyListRef plist = CFPropertyListCreateWithStream(NULL, plist_stream, 0, kCFPropertyListImmutable, NULL, NULL); |
| CFStringRef bundle_identifier = CFRetain(CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier"))); |
| CFReadStreamClose(plist_stream); |
| |
| CFRelease(plist_url); |
| CFRelease(plist_stream); |
| CFRelease(plist); |
| |
| return bundle_identifier; |
| } |
| |
| CFStringRef copy_modules_search_paths_pairs(CFStringRef symbols_path, CFStringRef disk_container, CFStringRef device_container_private, CFStringRef device_container_noprivate ) |
| { |
| CFMutableStringRef res = CFStringCreateMutable(kCFAllocatorDefault, 0); |
| CFStringAppendFormat(res, NULL, CFSTR("/usr \"%@/usr\""), symbols_path); |
| CFStringAppendFormat(res, NULL, CFSTR(" /System \"%@/System\""), symbols_path); |
| CFStringAppendFormat(res, NULL, CFSTR(" \"%@\" \"%@\""), device_container_private, disk_container); |
| CFStringAppendFormat(res, NULL, CFSTR(" \"%@\" \"%@\""), device_container_noprivate, disk_container); |
| CFStringAppendFormat(res, NULL, CFSTR(" /Developer \"%@/Developer\""), symbols_path); |
| |
| return res; |
| } |
| |
| CFStringRef get_device_platform(AMDeviceRef device) |
| { |
| CFStringRef deviceClass = AMDeviceCopyValue(device, 0, CFSTR("DeviceClass")); |
| CFStringRef platform; |
| if (CFStringCompare(CFSTR("AppleTV"), deviceClass, 0) == kCFCompareEqualTo) { |
| platform = CFSTR("tvos"); |
| } |
| else if (CFStringCompare(CFSTR("Watch"), deviceClass, 0) == kCFCompareEqualTo) { |
| platform = CFSTR("watchos"); |
| } |
| else { |
| platform = CFSTR("ios"); |
| } |
| CFRelease(deviceClass); |
| return platform; |
| } |
| |
| void write_lldb_prep_cmds(AMDeviceRef device, CFURLRef disk_app_url) { |
| CFStringRef symbols_path = copy_device_support_path(device, CFSTR("Symbols")); |
| CFMutableStringRef cmds = CFStringCreateMutableCopy(NULL, 0, LLDB_PREP_CMDS); |
| CFRange range = { 0, CFStringGetLength(cmds) }; |
| |
| CFStringFindAndReplace(cmds, CFSTR("{platform}"), get_device_platform(device), range, 0); |
| range.length = CFStringGetLength(cmds); |
| |
| CFStringFindAndReplace(cmds, CFSTR("{symbols_path}"), symbols_path, range, 0); |
| range.length = CFStringGetLength(cmds); |
| |
| CFMutableStringRef pmodule = CFStringCreateMutableCopy(NULL, 0, (CFStringRef)LLDB_FRUITSTRAP_MODULE); |
| |
| CFRange rangeLLDB = { 0, CFStringGetLength(pmodule) }; |
| |
| CFStringRef exitcode_app_crash_str = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), exitcode_app_crash); |
| CFStringFindAndReplace(pmodule, CFSTR("{exitcode_app_crash}"), exitcode_app_crash_str, rangeLLDB, 0); |
| CFRelease(exitcode_app_crash_str); |
| rangeLLDB.length = CFStringGetLength(pmodule); |
| |
| CFStringRef detect_deadlock_timeout_str = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), _detectDeadlockTimeout); |
| CFStringFindAndReplace(pmodule, CFSTR("{detect_deadlock_timeout}"), detect_deadlock_timeout_str, rangeLLDB, 0); |
| CFRelease(detect_deadlock_timeout_str); |
| rangeLLDB.length = CFStringGetLength(pmodule); |
| |
| if (args) { |
| CFStringRef cf_args = CFStringCreateWithCString(NULL, args, kCFStringEncodingUTF8); |
| CFStringFindAndReplace(cmds, CFSTR("{args}"), cf_args, range, 0); |
| rangeLLDB.length = CFStringGetLength(pmodule); |
| CFStringFindAndReplace(pmodule, CFSTR("{args}"), cf_args, rangeLLDB, 0); |
| |
| //printf("write_lldb_prep_cmds:args: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), |
| // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); |
| CFRelease(cf_args); |
| } else { |
| CFStringFindAndReplace(cmds, CFSTR("{args}"), CFSTR(""), range, 0); |
| CFStringFindAndReplace(pmodule, CFSTR("{args}"), CFSTR(""), rangeLLDB, 0); |
| //printf("write_lldb_prep_cmds: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), |
| // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); |
| } |
| |
| if (envs) { |
| CFStringRef cf_envs = CFStringCreateWithCString(NULL, envs, kCFStringEncodingUTF8); |
| CFStringFindAndReplace(cmds, CFSTR("{envs}"), cf_envs, range, 0); |
| rangeLLDB.length = CFStringGetLength(pmodule); |
| CFStringFindAndReplace(pmodule, CFSTR("{envs}"), cf_envs, rangeLLDB, 0); |
| |
| //printf("write_lldb_prep_cmds:envs: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), |
| // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); |
| CFRelease(cf_envs); |
| } else { |
| CFStringFindAndReplace(cmds, CFSTR("{envs}"), CFSTR(""), range, 0); |
| CFStringFindAndReplace(pmodule, CFSTR("{envs}"), CFSTR(""), rangeLLDB, 0); |
| //printf("write_lldb_prep_cmds: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), |
| // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); |
| } |
| range.length = CFStringGetLength(cmds); |
| |
| CFStringRef bundle_identifier = copy_disk_app_identifier(disk_app_url); |
| CFURLRef device_app_url = copy_device_app_url(device, bundle_identifier); |
| CFRelease(bundle_identifier); |
| CFStringRef device_app_path = CFURLCopyFileSystemPath(device_app_url, kCFURLPOSIXPathStyle); |
| CFStringFindAndReplace(cmds, CFSTR("{device_app}"), device_app_path, range, 0); |
| CFRelease(device_app_path); |
| range.length = CFStringGetLength(cmds); |
| |
| CFStringRef disk_app_path = CFURLCopyFileSystemPath(disk_app_url, kCFURLPOSIXPathStyle); |
| CFStringFindAndReplace(cmds, CFSTR("{disk_app}"), disk_app_path, range, 0); |
| CFRelease(disk_app_path); |
| range.length = CFStringGetLength(cmds); |
| |
| CFStringRef device_port = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), port); |
| CFStringFindAndReplace(cmds, CFSTR("{device_port}"), device_port, range, 0); |
| CFRelease(device_port); |
| range.length = CFStringGetLength(cmds); |
| |
| if (output_path) { |
| CFStringRef output_path_str = CFStringCreateWithCString(NULL, output_path, kCFStringEncodingUTF8); |
| CFStringFindAndReplace(cmds, CFSTR("{output_path}"), output_path_str, range, 0); |
| CFRelease(output_path_str); |
| } else { |
| CFStringFindAndReplace(cmds, CFSTR("{output_path}"), CFSTR(""), range, 0); |
| } |
| range.length = CFStringGetLength(cmds); |
| if (error_path) { |
| CFStringRef error_path_str = CFStringCreateWithCString(NULL, error_path, kCFStringEncodingUTF8); |
| CFStringFindAndReplace(cmds, CFSTR("{error_path}"), error_path_str, range, 0); |
| CFRelease(error_path_str); |
| } else { |
| CFStringFindAndReplace(cmds, CFSTR("{error_path}"), CFSTR(""), range, 0); |
| } |
| range.length = CFStringGetLength(cmds); |
| |
| CFURLRef device_container_url = CFURLCreateCopyDeletingLastPathComponent(NULL, device_app_url); |
| CFRelease(device_app_url); |
| CFStringRef device_container_path = CFURLCopyFileSystemPath(device_container_url, kCFURLPOSIXPathStyle); |
| CFRelease(device_container_url); |
| CFMutableStringRef dcp_noprivate = CFStringCreateMutableCopy(NULL, 0, device_container_path); |
| range.length = CFStringGetLength(dcp_noprivate); |
| CFStringFindAndReplace(dcp_noprivate, CFSTR("/private/var/"), CFSTR("/var/"), range, 0); |
| range.length = CFStringGetLength(cmds); |
| CFStringFindAndReplace(cmds, CFSTR("{device_container}"), dcp_noprivate, range, 0); |
| range.length = CFStringGetLength(cmds); |
| |
| CFURLRef disk_container_url = CFURLCreateCopyDeletingLastPathComponent(NULL, disk_app_url); |
| CFStringRef disk_container_path = CFURLCopyFileSystemPath(disk_container_url, kCFURLPOSIXPathStyle); |
| CFRelease(disk_container_url); |
| CFStringFindAndReplace(cmds, CFSTR("{disk_container}"), disk_container_path, range, 0); |
| range.length = CFStringGetLength(cmds); |
| |
| CFStringRef search_paths_pairs = copy_modules_search_paths_pairs(symbols_path, disk_container_path, device_container_path, dcp_noprivate); |
| CFRelease(symbols_path); |
| CFRelease(device_container_path); |
| CFRelease(dcp_noprivate); |
| CFRelease(disk_container_path); |
| CFStringFindAndReplace(cmds, CFSTR("{modules_search_paths_pairs}"), search_paths_pairs, range, 0); |
| range.length = CFStringGetLength(cmds); |
| CFRelease(search_paths_pairs); |
| |
| NSString* python_file_path = [NSString stringWithFormat:@"/tmp/%@/fruitstrap_", tmpUUID]; |
| mkdirp(python_file_path); |
| |
| NSString* python_command = @"fruitstrap_"; |
| if(device_id != NULL) { |
| python_file_path = [python_file_path stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; |
| python_command = [python_command stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; |
| } |
| python_file_path = [python_file_path stringByAppendingString:@".py"]; |
| |
| CFStringFindAndReplace(cmds, CFSTR("{python_command}"), (CFStringRef)python_command, range, 0); |
| range.length = CFStringGetLength(cmds); |
| CFStringFindAndReplace(cmds, CFSTR("{python_file_path}"), (CFStringRef)python_file_path, range, 0); |
| range.length = CFStringGetLength(cmds); |
| |
| CFDataRef cmds_data = CFStringCreateExternalRepresentation(NULL, cmds, kCFStringEncodingUTF8, 0); |
| NSString* prep_cmds_path = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; |
| if(device_id != NULL) { |
| prep_cmds_path = [prep_cmds_path stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; |
| } |
| FILE *out = fopen([prep_cmds_path UTF8String], "w"); |
| fwrite(CFDataGetBytePtr(cmds_data), CFDataGetLength(cmds_data), 1, out); |
| CFRelease(cmds_data); |
| // Write additional commands based on mode we're running in |
| const char* extra_cmds; |
| if (!interactive) |
| { |
| if (justlaunch) |
| extra_cmds = lldb_prep_noninteractive_justlaunch_cmds; |
| else |
| extra_cmds = lldb_prep_noninteractive_cmds; |
| } |
| else if (nostart) |
| extra_cmds = lldb_prep_no_cmds; |
| else |
| extra_cmds = lldb_prep_interactive_cmds; |
| fwrite(extra_cmds, strlen(extra_cmds), 1, out); |
| if (custom_commands != nil) |
| { |
| const char * cmds = [custom_commands UTF8String]; |
| fwrite(cmds, 1, strlen(cmds), out); |
| } |
| fclose(out); |
| |
| |
| out = fopen([python_file_path UTF8String], "w"); |
| CFDataRef pmodule_data = CFStringCreateExternalRepresentation(NULL, pmodule, kCFStringEncodingUTF8, 0); |
| fwrite(CFDataGetBytePtr(pmodule_data), CFDataGetLength(pmodule_data), 1, out); |
| CFRelease(pmodule_data); |
| |
| if (custom_script_path) |
| { |
| FILE * fh = fopen(custom_script_path, "r"); |
| if (fh == NULL) |
| { |
| on_error(@"Failed to open %s", custom_script_path); |
| } |
| fwrite("\n", 1, 1, out); |
| char buffer[0x1000]; |
| size_t bytesRead; |
| while ((bytesRead = fread(buffer, 1, sizeof(buffer), fh)) > 0) |
| { |
| fwrite(buffer, 1, bytesRead, out); |
| } |
| fclose(fh); |
| } |
| |
| fclose(out); |
| |
| CFRelease(cmds); |
| CFRelease(pmodule); |
| } |
| |
| CFSocketRef server_socket; |
| CFSocketRef lldb_socket; |
| CFWriteStreamRef serverWriteStream = NULL; |
| CFWriteStreamRef lldbWriteStream = NULL; |
| |
| int kill_ptree(pid_t root, int signum); |
| void |
| server_callback (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) |
| { |
| char buffer[0x1000]; |
| int bytesRead = AMDServiceConnectionReceive(dbgServiceConnection, buffer, sizeof(buffer)); |
| if (bytesRead == 0) |
| { |
| // close the socket on which we've got end-of-file, the server_socket. |
| CFSocketInvalidate(s); |
| CFRelease(s); |
| return; |
| } |
| write(CFSocketGetNative (lldb_socket), buffer, bytesRead); |
| while (bytesRead == sizeof(buffer)) |
| { |
| bytesRead = AMDServiceConnectionReceive(dbgServiceConnection, buffer, sizeof(buffer)); |
| if (bytesRead > 0) |
| { |
| write(CFSocketGetNative (lldb_socket), buffer, bytesRead); |
| } |
| } |
| } |
| |
| void lldb_callback(CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) |
| { |
| //printf ("lldb: %s\n", CFDataGetBytePtr (data)); |
| |
| if (CFDataGetLength (data) == 0) { |
| // close the socket on which we've got end-of-file, the lldb_socket. |
| CFSocketInvalidate(s); |
| CFRelease(s); |
| return; |
| } |
| int __unused sent = AMDServiceConnectionSend(dbgServiceConnection, CFDataGetBytePtr(data), CFDataGetLength (data)); |
| assert (CFDataGetLength (data) == sent); |
| } |
| |
| void fdvendor_callback(CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) { |
| CFSocketNativeHandle socket = (CFSocketNativeHandle)(*((CFSocketNativeHandle *)data)); |
| |
| assert (callbackType == kCFSocketAcceptCallBack); |
| //PRINT ("callback!\n"); |
| |
| lldb_socket = CFSocketCreateWithNative(NULL, socket, kCFSocketDataCallBack, &lldb_callback, NULL); |
| int flag = 1; |
| int res = setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(flag)); |
| if (res == -1) { |
| on_sys_error(@"Setting socket option failed."); |
| } |
| if (lldb_socket_runloop) { |
| CFRelease(lldb_socket_runloop); |
| } |
| lldb_socket_runloop = CFSocketCreateRunLoopSource(NULL, lldb_socket, 0); |
| CFRunLoopAddSource(CFRunLoopGetMain(), lldb_socket_runloop, kCFRunLoopCommonModes); |
| |
| CFSocketInvalidate(s); |
| CFRelease(s); |
| } |
| |
| void start_remote_debug_server(AMDeviceRef device) { |
| |
| dbgServiceConnection = NULL; |
| CFStringRef serviceName = CFSTR("com.apple.debugserver"); |
| CFStringRef keys[] = { CFSTR("MinIPhoneVersion"), CFSTR("MinAppleTVVersion"), CFSTR("MinWatchVersion") }; |
| CFStringRef values[] = { CFSTR("14.0"), CFSTR("14.0"), CFSTR("7.0") }; // Not sure about older watchOS versions |
| CFDictionaryRef version = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| |
| bool useSecureProxy = AMDeviceIsAtLeastVersionOnPlatform(device, version); |
| if (useSecureProxy) |
| { |
| serviceName = CFSTR("com.apple.debugserver.DVTSecureSocketProxy"); |
| } |
| |
| int start_err = AMDeviceSecureStartService(device, serviceName, NULL, &dbgServiceConnection); |
| if (start_err != 0) |
| { |
| // After we mount the image, iOS needs to scan the image to register new services. |
| // If we ask to start the service before it is found by ios, we will get 0xe8000022. |
| // In other cases, it's been observed, that device may loose connection here (0xe800002d). |
| // Luckly, we can just restart the connection and continue. |
| // In other cases we just error out. |
| NSLogOut(@"Failed to start debugserver: %x %s", start_err, get_error_message(start_err)); |
| switch(start_err) |
| { |
| case 0xe8000022: |
| NSLogOut(@"Waiting for the device to scan mounted image"); |
| sleep(1); |
| break; |
| case 0x800002d: |
| NSLogOut(@"Reconnecting to device"); |
| // We dont call AMDeviceStopSession as we cannot send any messages anymore |
| check_error(AMDeviceDisconnect(device)); |
| connect_and_start_session(device); |
| break; |
| default: |
| check_error(start_err); |
| } |
| check_error(AMDeviceSecureStartService(device, serviceName, NULL, &dbgServiceConnection)); |
| } |
| assert(dbgServiceConnection != NULL); |
| |
| if (!useSecureProxy) |
| { |
| disable_ssl(dbgServiceConnection); |
| } |
| |
| /* |
| * The debugserver connection is through a fd handle, while lldb requires a host/port to connect, so create an intermediate |
| * socket to transfer data. |
| */ |
| server_socket = CFSocketCreateWithNative (NULL, AMDServiceConnectionGetSocket(dbgServiceConnection), kCFSocketReadCallBack, &server_callback, NULL); |
| if (server_socket_runloop) { |
| CFRelease(server_socket_runloop); |
| } |
| server_socket_runloop = CFSocketCreateRunLoopSource(NULL, server_socket, 0); |
| CFRunLoopAddSource(CFRunLoopGetMain(), server_socket_runloop, kCFRunLoopCommonModes); |
| |
| struct sockaddr_in addr4; |
| memset(&addr4, 0, sizeof(addr4)); |
| addr4.sin_len = sizeof(addr4); |
| addr4.sin_family = AF_INET; |
| addr4.sin_port = htons(port); |
| addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| |
| CFSocketRef fdvendor = CFSocketCreate(NULL, PF_INET, 0, 0, kCFSocketAcceptCallBack, &fdvendor_callback, NULL); |
| |
| if (port) { |
| int yes = 1; |
| setsockopt(CFSocketGetNative(fdvendor), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); |
| } |
| |
| CFDataRef address_data = CFDataCreate(NULL, (const UInt8 *)&addr4, sizeof(addr4)); |
| |
| CFSocketSetAddress(fdvendor, address_data); |
| CFRelease(address_data); |
| socklen_t addrlen = sizeof(addr4); |
| int res = getsockname(CFSocketGetNative(fdvendor),(struct sockaddr *)&addr4,&addrlen); |
| if (res == -1) { |
| on_sys_error(@"Getting socket name failed."); |
| } |
| port = ntohs(addr4.sin_port); |
| |
| if (fdvendor_runloop) { |
| CFRelease(fdvendor_runloop); |
| } |
| fdvendor_runloop = CFSocketCreateRunLoopSource(NULL, fdvendor, 0); |
| CFRunLoopAddSource(CFRunLoopGetMain(), fdvendor_runloop, kCFRunLoopCommonModes); |
| } |
| |
| void kill_ptree_inner(pid_t root, int signum, struct kinfo_proc *kp, int kp_len) { |
| int i; |
| for (i = 0; i < kp_len; i++) { |
| if (kp[i].kp_eproc.e_ppid == root) { |
| kill_ptree_inner(kp[i].kp_proc.p_pid, signum, kp, kp_len); |
| } |
| } |
| if (root != getpid()) { |
| kill(root, signum); |
| } |
| } |
| |
| int kill_ptree(pid_t root, int signum) { |
| int mib[3]; |
| size_t len; |
| mib[0] = CTL_KERN; |
| mib[1] = KERN_PROC; |
| mib[2] = KERN_PROC_ALL; |
| if (sysctl(mib, 3, NULL, &len, NULL, 0) == -1) { |
| return -1; |
| } |
| |
| struct kinfo_proc *kp = calloc(1, len); |
| if (!kp) { |
| return -1; |
| } |
| |
| if (sysctl(mib, 3, kp, &len, NULL, 0) == -1) { |
| free(kp); |
| return -1; |
| } |
| |
| kill_ptree_inner(root, signum, kp, (int)(len / sizeof(struct kinfo_proc))); |
| |
| free(kp); |
| return 0; |
| } |
| |
| void killed(int signum) { |
| // SIGKILL needed to kill lldb, probably a better way to do this. |
| kill(0, SIGKILL); |
| _exit(0); |
| } |
| |
| void lldb_finished_handler(int signum) |
| { |
| int status = 0; |
| if (waitpid(child, &status, 0) == -1) |
| perror("waitpid failed"); |
| _exit(WEXITSTATUS(status)); |
| } |
| |
| pid_t bring_process_to_foreground(void) { |
| pid_t fgpid = tcgetpgrp(STDIN_FILENO); |
| if (setpgid(0, 0) == -1) |
| perror("setpgid failed"); |
| |
| signal(SIGTTOU, SIG_IGN); |
| if (tcsetpgrp(STDIN_FILENO, getpid()) == -1) |
| perror("tcsetpgrp failed"); |
| signal(SIGTTOU, SIG_DFL); |
| return fgpid; |
| } |
| |
| void setup_dummy_pipe_on_stdin(int pfd[2]) { |
| if (pipe(pfd) == -1) |
| perror("pipe failed"); |
| if (dup2(pfd[0], STDIN_FILENO) == -1) |
| perror("dup2 failed"); |
| } |
| |
| void setup_lldb(AMDeviceRef device, CFURLRef url) { |
| CFStringRef device_full_name = get_device_full_name(device), |
| device_interface_name = get_device_interface_name(device); |
| |
| connect_and_start_session(device); |
| CFBooleanRef is_password_protected = AMDeviceCopyValue(device, 0, CFSTR("PasswordProtected")); |
| NSLogJSON(@{@"Event": @"PasswordProtectedStatus", |
| @"Status": @(CFBooleanGetValue(is_password_protected)), |
| }); |
| CFRelease(is_password_protected); |
| |
| NSLogOut(@"------ Debug phase ------"); |
| |
| NSLogOut(@"Starting debug of %@ connected through %@...", device_full_name, device_interface_name); |
| |
| mount_developer_image(device); // put debugserver on the device |
| start_remote_debug_server(device); // start debugserver |
| if (!debugserver_only) |
| write_lldb_prep_cmds(device, url); // dump the necessary lldb commands into a file |
| if (url != NULL) |
| CFRelease(url); |
| |
| NSLogOut(@"[100%%] Connecting to remote debug server"); |
| NSLogOut(@"-------------------------"); |
| |
| setpgid(getpid(), 0); |
| signal(SIGHUP, killed); |
| signal(SIGINT, killed); |
| signal(SIGTERM, killed); |
| // Need this before fork to avoid race conditions. For child process we remove this right after fork. |
| signal(SIGLLDB, lldb_finished_handler); |
| |
| parent = getpid(); |
| } |
| |
| void launch_debugger(AMDeviceRef device, CFURLRef url) { |
| setup_lldb(device, url); |
| int pid = fork(); |
| if (pid == 0) { |
| signal(SIGHUP, SIG_DFL); |
| signal(SIGLLDB, SIG_DFL); |
| child = getpid(); |
| pid_t oldfgpid = 0; |
| int pfd[2] = {-1, -1}; |
| if (isatty(STDIN_FILENO)) |
| // If we are running on a terminal, then we need to bring process to foreground for input |
| // to work correctly on lldb's end. |
| oldfgpid = bring_process_to_foreground(); |
| else |
| // If lldb is running in a non terminal environment, then it freaks out spamming "^D" and |
| // "quit". It seems this is caused by read() on stdin returning EOF in lldb. To hack around |
| // this we setup a dummy pipe on stdin, so read() would block expecting "user's" input. |
| setup_dummy_pipe_on_stdin(pfd); |
| |
| NSString* lldb_shell; |
| NSString* prep_cmds = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; |
| lldb_shell = [NSString stringWithFormat:LLDB_SHELL, prep_cmds]; |
| |
| if(device_id != NULL) { |
| lldb_shell = [lldb_shell stringByAppendingString: [[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; |
| } |
| |
| int status = system([lldb_shell UTF8String]); // launch lldb |
| if (status == -1) |
| perror("failed launching lldb"); |
| |
| close(pfd[0]); |
| close(pfd[1]); |
| |
| // Notify parent we're exiting |
| kill(parent, SIGLLDB); |
| |
| if (oldfgpid) { |
| tcsetpgrp(STDIN_FILENO, oldfgpid); |
| } |
| // Pass lldb exit code |
| _exit(WEXITSTATUS(status)); |
| } else if (pid > 0) { |
| child = pid; |
| } else { |
| on_sys_error(@"Fork failed"); |
| } |
| } |
| |
| void launch_debugger_and_exit(AMDeviceRef device, CFURLRef url) { |
| setup_lldb(device,url); |
| int pfd[2] = {-1, -1}; |
| if (pipe(pfd) == -1) |
| perror("Pipe failed"); |
| int pid = fork(); |
| if (pid == 0) { |
| signal(SIGHUP, SIG_DFL); |
| signal(SIGLLDB, SIG_DFL); |
| child = getpid(); |
| |
| if (dup2(pfd[0],STDIN_FILENO) == -1) |
| perror("dup2 failed"); |
| |
| |
| NSString* prep_cmds = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; |
| NSString* lldb_shell = [NSString stringWithFormat:LLDB_SHELL, prep_cmds]; |
| if(device_id != NULL) { |
| lldb_shell = [lldb_shell stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; |
| } |
| |
| int status = system([lldb_shell UTF8String]); // launch lldb |
| if (status == -1) |
| perror("failed launching lldb"); |
| |
| close(pfd[0]); |
| |
| // Notify parent we're exiting |
| kill(parent, SIGLLDB); |
| // Pass lldb exit code |
| _exit(WEXITSTATUS(status)); |
| } else if (pid > 0) { |
| child = pid; |
| NSLogVerbose(@"Waiting for child [Child: %d][Parent: %d]\n", child, parent); |
| } else { |
| on_sys_error(@"Fork failed"); |
| } |
| } |
| |
| void launch_debugserver_only(AMDeviceRef device, CFURLRef url) |
| { |
| if (url != NULL) |
| CFRetain(url); |
| setup_lldb(device,url); |
| |
| CFStringRef device_app_path = NULL; |
| if (url != NULL) { |
| CFStringRef bundle_identifier = copy_disk_app_identifier(url); |
| CFURLRef device_app_url = copy_device_app_url(device, bundle_identifier); |
| CFRelease(bundle_identifier); |
| device_app_path = CFURLCopyFileSystemPath(device_app_url, kCFURLPOSIXPathStyle); |
| CFRelease(device_app_url); |
| CFRelease(url); |
| } |
| |
| NSLogOut(@"debugserver port: %d", port); |
| if (device_app_path == NULL) { |
| NSLogJSON(@{@"Event": @"DebugServerLaunched", |
| @"Port": @(port), |
| }); |
| } else { |
| NSLogOut(@"App path: %@", device_app_path); |
| NSLogJSON(@{@"Event": @"DebugServerLaunched", |
| @"Port": @(port), |
| @"Path": (__bridge NSString *)device_app_path |
| }); |
| CFRelease(device_app_path); |
| } |
| } |
| |
| CFStringRef copy_bundle_id(CFURLRef app_url) |
| { |
| if (app_url == NULL) |
| return NULL; |
| |
| CFURLRef url = CFURLCreateCopyAppendingPathComponent(NULL, app_url, CFSTR("Info.plist"), false); |
| |
| if (url == NULL) |
| return NULL; |
| |
| CFReadStreamRef stream = CFReadStreamCreateWithFile(NULL, url); |
| CFRelease(url); |
| |
| if (stream == NULL) |
| return NULL; |
| |
| CFPropertyListRef plist = NULL; |
| if (CFReadStreamOpen(stream) == TRUE) { |
| plist = CFPropertyListCreateWithStream(NULL, stream, 0, |
| kCFPropertyListImmutable, NULL, NULL); |
| } |
| CFReadStreamClose(stream); |
| CFRelease(stream); |
| |
| if (plist == NULL) |
| return NULL; |
| |
| const void *value = CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier")); |
| CFStringRef bundle_id = NULL; |
| if (value != NULL) |
| bundle_id = CFRetain(value); |
| |
| CFRelease(plist); |
| return bundle_id; |
| } |
| |
| typedef enum { READ_DIR_FILE, READ_DIR_FIFO, READ_DIR_BEFORE_DIR, READ_DIR_AFTER_DIR } read_dir_cb_reason; |
| |
| void read_dir(AFCConnectionRef afc_conn_p, const char* dir, |
| void(*callback)(AFCConnectionRef conn, const char *dir, read_dir_cb_reason reason)) |
| { |
| char *dir_ent; |
| |
| afc_dictionary* afc_dict_p; |
| char *key, *val; |
| int not_dir = 0; |
| bool is_fifo = 0; |
| |
| unsigned int code = AFCFileInfoOpen(afc_conn_p, dir, &afc_dict_p); |
| if (code != 0) { |
| // there was a problem reading or opening the file to get info on it, abort |
| return; |
| } |
| |
| long long mtime = -1; |
| long long birthtime = -1; |
| long size = -1; |
| long blocks = -1; |
| long nlink = -1; |
| NSString * ifmt = nil; |
| while((AFCKeyValueRead(afc_dict_p,&key,&val) == 0) && key && val) { |
| if (strcmp(key,"st_ifmt")==0) { |
| not_dir = strcmp(val,"S_IFDIR"); |
| is_fifo = !strcmp(val, "S_IFIFO"); |
| if (_json_output) { |
| ifmt = [NSString stringWithUTF8String:val]; |
| } else { |
| break; |
| } |
| } else if (strcmp(key, "st_size") == 0) { |
| size = atol(val); |
| } else if (strcmp(key, "st_mtime") == 0) { |
| mtime = atoll(val); |
| } else if (strcmp(key, "st_birthtime") == 0) { |
| birthtime = atoll(val); |
| } else if (strcmp(key, "st_nlink") == 0) { |
| nlink = atol(val); |
| } else if (strcmp(key, "st_blocks") == 0) { |
| blocks = atol(val); |
| } |
| } |
| AFCKeyValueClose(afc_dict_p); |
| |
| if (_json_output) { |
| if (_file_meta_info == nil) { |
| _file_meta_info = [[NSMutableArray alloc] init]; |
| } |
| [_file_meta_info addObject: @{@"full_path": [NSString stringWithUTF8String:dir], |
| @"st_ifmt": ifmt, |
| @"st_nlink": @(nlink), |
| @"st_size": @(size), |
| @"st_blocks": @(blocks), |
| @"st_mtime": @(mtime), |
| @"st_birthtime": @(birthtime)}]; |
| } |
| |
| if (not_dir) { |
| if (callback) (*callback)(afc_conn_p, dir, is_fifo ? READ_DIR_FIFO : READ_DIR_FILE); |
| return; |
| } |
| |
| afc_directory* afc_dir_p; |
| afc_error_t err = AFCDirectoryOpen(afc_conn_p, dir, &afc_dir_p); |
| |
| if (err != 0) { |
| // Couldn't open dir - was probably a file |
| return; |
| } |
| |
| // Call the callback on the directory before processing its |
| // contents. This is used by copy file callback, which needs to |
| // create the directory on the host before attempting to copy |
| // files into it. |
| if (callback) (*callback)(afc_conn_p, dir, READ_DIR_BEFORE_DIR); |
| |
| while(true) { |
| err = AFCDirectoryRead(afc_conn_p, afc_dir_p, &dir_ent); |
| |
| if (err != 0 || !dir_ent) |
| break; |
| |
| if (strcmp(dir_ent, ".") == 0 || strcmp(dir_ent, "..") == 0) |
| continue; |
| |
| char* dir_joined = malloc(strlen(dir) + strlen(dir_ent) + 2); |
| strcpy(dir_joined, dir); |
| if (dir_joined[strlen(dir)-1] != '/') |
| strcat(dir_joined, "/"); |
| strcat(dir_joined, dir_ent); |
| if (!(non_recursively && strcmp(list_root, dir) != 0)) { |
| read_dir(afc_conn_p, dir_joined, callback); |
| } |
| free(dir_joined); |
| } |
| |
| AFCDirectoryClose(afc_conn_p, afc_dir_p); |
| |
| // Call the callback on the directory after processing its |
| // contents. This is used by the rmtree callback because it needs |
| // to delete the directory's contents before the directory itself |
| if (callback) (*callback)(afc_conn_p, dir, READ_DIR_AFTER_DIR); |
| } |
| |
| AFCConnectionRef start_afc_service(AMDeviceRef device) { |
| AMDeviceConnect(device); |
| assert(AMDeviceIsPaired(device)); |
| check_error(AMDeviceValidatePairing(device)); |
| check_error(AMDeviceStartSession(device)); |
| |
| AFCConnectionRef conn = NULL; |
| ServiceConnRef serviceConn = NULL; |
| |
| if (AMDeviceStartService(device, AMSVC_AFC, &serviceConn, 0) != MDERR_OK) { |
| on_error(@"Unable to start file service!"); |
| } |
| if (AFCConnectionOpen(serviceConn, 0, &conn) != MDERR_OK) { |
| on_error(@"Unable to open connection!"); |
| } |
| |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| return conn; |
| } |
| |
| // Used to send files to app-specific sandbox (Documents dir) |
| AFCConnectionRef start_house_arrest_service(AMDeviceRef device) { |
| connect_and_start_session(device); |
| |
| AFCConnectionRef conn = NULL; |
| |
| if (bundle_id == NULL) { |
| on_error(@"Bundle id is not specified"); |
| } |
| |
| CFStringRef cf_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); |
| CFStringRef keys[1]; |
| keys[0] = CFSTR("Command"); |
| CFStringRef values[1]; |
| values[0] = CFSTR("VendDocuments"); |
| CFDictionaryRef command = CFDictionaryCreate(kCFAllocatorDefault, |
| (void*)keys, |
| (void*)values, |
| 1, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks); |
| if (AMDeviceCreateHouseArrestService(device, cf_bundle_id, 0, &conn) != 0 && |
| AMDeviceCreateHouseArrestService(device, cf_bundle_id, command, &conn) != 0) { |
| on_error(@"Unable to find bundle with id: %@", [NSString stringWithUTF8String:bundle_id]); |
| } |
| |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| CFRelease(cf_bundle_id); |
| |
| return conn; |
| } |
| |
| // Uses realpath() to resolve any symlinks in a path. Returns the resolved |
| // path or the original path if an error occurs. This allocates memory for the |
| // resolved path and the caller is responsible for freeing it. |
| char *resolve_path(char *path) |
| { |
| char buffer[PATH_MAX]; |
| // Use the original path if realpath() fails, otherwise use resolved value. |
| char *resolved_path = realpath(path, buffer) == NULL ? path : buffer; |
| char *new_path = malloc(strlen(resolved_path) + 1); |
| strcpy(new_path, resolved_path); |
| return new_path; |
| } |
| |
| char const* get_filename_from_path(char const* path) |
| { |
| char const*ptr = path + strlen(path); |
| while (ptr > path) |
| { |
| if (*ptr == '/') |
| break; |
| --ptr; |
| } |
| if (ptr+1 >= path+strlen(path)) |
| return NULL; |
| if (ptr == path) |
| return ptr; |
| return ptr+1; |
| } |
| |
| void* read_file_to_memory(char const * path, size_t* file_size) |
| { |
| struct stat buf; |
| int err = stat(path, &buf); |
| if (err < 0) |
| { |
| return NULL; |
| } |
| |
| *file_size = buf.st_size; |
| FILE* fd = fopen(path, "r"); |
| char* content = malloc(*file_size); |
| if (*file_size != 0 && fread(content, *file_size, 1, fd) != 1) |
| { |
| fclose(fd); |
| return NULL; |
| } |
| fclose(fd); |
| return content; |
| } |
| |
| void list_files_callback(AFCConnectionRef conn, const char *name, read_dir_cb_reason reason) |
| { |
| if (reason == READ_DIR_FILE || reason == READ_DIR_FIFO) { |
| NSLogOut(@"%@", [NSString stringWithUTF8String:name]); |
| } else if (reason == READ_DIR_BEFORE_DIR) { |
| NSLogOut(@"%@/", [NSString stringWithUTF8String:name]); |
| } |
| } |
| |
| void list_files(AMDeviceRef device) |
| { |
| AFCConnectionRef afc_conn_p; |
| if (file_system) { |
| afc_conn_p = start_afc_service(device); |
| } else { |
| afc_conn_p = start_house_arrest_service(device); |
| } |
| assert(afc_conn_p); |
| if (_json_output) { |
| read_dir(afc_conn_p, list_root?list_root:"/", NULL); |
| NSLogJSON(@{@"Event": @"FileListed", |
| @"Files": _file_meta_info}); |
| } else { |
| read_dir(afc_conn_p, list_root?list_root:"/", list_files_callback); |
| } |
| |
| check_error(AFCConnectionClose(afc_conn_p)); |
| } |
| |
| int app_exists(AMDeviceRef device) |
| { |
| if (bundle_id == NULL) { |
| NSLogOut(@"Bundle id is not specified."); |
| return 1; |
| } |
| AMDeviceConnect(device); |
| assert(AMDeviceIsPaired(device)); |
| check_error(AMDeviceValidatePairing(device)); |
| check_error(AMDeviceStartSession(device)); |
| |
| CFStringRef cf_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); |
| |
| NSArray *a = [NSArray arrayWithObjects:@"CFBundleIdentifier", nil]; |
| NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; |
| CFDictionaryRef options = (CFDictionaryRef)optionsDict; |
| CFDictionaryRef result = nil; |
| check_error(AMDeviceLookupApplications(device, options, &result)); |
| |
| bool appExists = CFDictionaryContainsKey(result, cf_bundle_id); |
| NSLogOut(@"%@", appExists ? @"true" : @"false"); |
| CFRelease(cf_bundle_id); |
| |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| if (appExists) |
| return 0; |
| return -1; |
| } |
| |
| void get_battery_level(AMDeviceRef device) |
| { |
| |
| AMDeviceConnect(device); |
| assert(AMDeviceIsPaired(device)); |
| check_error(AMDeviceValidatePairing(device)); |
| check_error(AMDeviceStartSession(device)); |
| |
| CFStringRef result = AMDeviceCopyValue(device, (void*)@"com.apple.mobile.battery", (__bridge CFStringRef)@"BatteryCurrentCapacity"); |
| NSLogOut(@"BatteryCurrentCapacity:%@",result); |
| CFRelease(result); |
| |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| } |
| |
| void replace_dict_date_with_absolute_time(CFMutableDictionaryRef dict, CFStringRef key) { |
| CFDateRef date = CFDictionaryGetValue(dict, key); |
| CFAbsoluteTime absolute_date = CFDateGetAbsoluteTime(date); |
| CFNumberRef absolute_date_ref = CFNumberCreate(NULL, kCFNumberDoubleType, &absolute_date); |
| CFDictionaryReplaceValue(dict, key, absolute_date_ref); |
| CFRelease(absolute_date_ref); |
| } |
| |
| void list_provisioning_profiles(AMDeviceRef device) { |
| connect_and_start_session(device); |
| CFArrayRef device_provisioning_profiles = AMDeviceCopyProvisioningProfiles(device); |
| |
| CFIndex provisioning_profiles_count = CFArrayGetCount(device_provisioning_profiles); |
| CFMutableArrayRef serializable_provisioning_profiles = |
| CFArrayCreateMutable(NULL, provisioning_profiles_count, &kCFTypeArrayCallBacks); |
| |
| for (CFIndex i = 0; i < provisioning_profiles_count; i++) { |
| void *device_provisioning_profile = |
| (void *)CFArrayGetValueAtIndex(device_provisioning_profiles, i); |
| CFMutableDictionaryRef serializable_provisioning_profile; |
| |
| if (verbose) { |
| // Verbose output; We selectively omit keys from profile. |
| CFDictionaryRef immutable_profile_dict = |
| MISProfileCopyPayload(device_provisioning_profile); |
| serializable_provisioning_profile = |
| CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, immutable_profile_dict); |
| CFRelease(immutable_profile_dict); |
| |
| // Remove binary values from the output since they aren't readable and add a whole lot |
| // of noise to the output. |
| CFDictionaryRemoveValue(serializable_provisioning_profile, |
| CFSTR("DER-Encoded-Profile")); |
| CFDictionaryRemoveValue(serializable_provisioning_profile, |
| CFSTR("DeveloperCertificates")); |
| } else { |
| // Normal output; We selectively include keys from profile. |
| CFStringRef keys[] = {CFSTR("Name"), CFSTR("UUID"), CFSTR("ExpirationDate")}; |
| CFIndex size = sizeof(keys) / sizeof(CFStringRef); |
| serializable_provisioning_profile = |
| CFDictionaryCreateMutable(kCFAllocatorDefault, size, &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks); |
| for (CFIndex i = 0; i < size; i++) { |
| CFStringRef key = keys[i]; |
| CFStringRef value = MISProfileGetValue(device_provisioning_profile, key); |
| CFDictionaryAddValue(serializable_provisioning_profile, key, value); |
| } |
| } |
| |
| if (_json_output) { |
| // JSON output can't have CFDate objects so convert dates into CFAbsoluteTime's. |
| replace_dict_date_with_absolute_time(serializable_provisioning_profile, |
| CFSTR("ExpirationDate")); |
| replace_dict_date_with_absolute_time(serializable_provisioning_profile, |
| CFSTR("CreationDate")); |
| } |
| |
| CFArrayAppendValue(serializable_provisioning_profiles, serializable_provisioning_profile); |
| CFRelease(serializable_provisioning_profile); |
| } |
| CFRelease(device_provisioning_profiles); |
| |
| if (_json_output) { |
| NSLogJSON(@{ |
| @"Event" : @"ListProvisioningProfiles", |
| @"Profiles" : (NSArray *)serializable_provisioning_profiles |
| }); |
| } else { |
| NSLogOut(@"%@", serializable_provisioning_profiles); |
| } |
| CFRelease(serializable_provisioning_profiles); |
| } |
| |
| void install_provisioning_profile(AMDeviceRef device) { |
| if (!profile_path) { |
| on_error(@"no path to provisioning profile specified"); |
| } |
| |
| size_t file_size = 0; |
| void *file_content = read_file_to_memory(profile_path, &file_size); |
| CFDataRef profile_data = CFDataCreate(NULL, file_content, file_size); |
| void *profile = MISProfileCreateWithData(0, profile_data); |
| connect_and_start_session(device); |
| check_error(AMDeviceInstallProvisioningProfile(device, profile)); |
| |
| free(file_content); |
| CFRelease(profile_data); |
| CFRelease(profile); |
| } |
| |
| void uninstall_provisioning_profile(AMDeviceRef device) { |
| if (!profile_uuid) { |
| on_error(@"no profile UUID specified via --profile-uuid"); |
| } |
| |
| CFStringRef uuid = CFStringCreateWithCString(NULL, profile_uuid, kCFStringEncodingUTF8); |
| connect_and_start_session(device); |
| check_error(AMDeviceRemoveProvisioningProfile(device, uuid)); |
| CFRelease(uuid); |
| } |
| |
| void download_provisioning_profile(AMDeviceRef device) { |
| if (!profile_uuid) { |
| on_error(@"no profile UUID specified via --profile-uuid"); |
| } else if (!profile_path) { |
| on_error(@"no download path specified"); |
| } |
| |
| connect_and_start_session(device); |
| CFArrayRef device_provisioning_profiles = AMDeviceCopyProvisioningProfiles(device); |
| CFIndex provisioning_profiles_count = CFArrayGetCount(device_provisioning_profiles); |
| CFStringRef uuid = CFStringCreateWithCString(NULL, profile_uuid, kCFStringEncodingUTF8); |
| bool found_matching_uuid = false; |
| |
| for (CFIndex i = 0; i < provisioning_profiles_count; i++) { |
| void *profile = (void *)CFArrayGetValueAtIndex(device_provisioning_profiles, i); |
| CFStringRef other_uuid = MISProfileGetValue(profile, CFSTR("UUID")); |
| found_matching_uuid = CFStringCompare(uuid, other_uuid, 0) == kCFCompareEqualTo; |
| |
| if (found_matching_uuid) { |
| NSLogVerbose(@"Writing %@ to %s", MISProfileGetValue(profile, CFSTR("Name")), |
| profile_path); |
| CFStringRef dst_path = |
| CFStringCreateWithCString(NULL, profile_path, kCFStringEncodingUTF8); |
| check_error(MISProfileWriteToFile(profile, dst_path)); |
| CFRelease(dst_path); |
| break; |
| } |
| } |
| |
| CFRelease(uuid); |
| CFRelease(device_provisioning_profiles); |
| if (!found_matching_uuid) { |
| on_error(@"Did not find provisioning profile with UUID %x on device", profile_uuid); |
| } |
| } |
| |
| void list_bundle_id(AMDeviceRef device) |
| { |
| connect_and_start_session(device); |
| NSMutableArray *a = [NSMutableArray arrayWithObjects: |
| @"CFBundleIdentifier", |
| @"CFBundleName", |
| @"CFBundleDisplayName", |
| @"CFBundleVersion", |
| @"CFBundleShortVersionString", nil]; |
| if (keys) { |
| for (NSString * key in keys) { |
| [a addObjectsFromArray:[key componentsSeparatedByCharactersInSet: |
| [NSCharacterSet characterSetWithCharactersInString:@",&"]]]; |
| } |
| } |
| NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; |
| CFDictionaryRef options = (CFDictionaryRef)optionsDict; |
| CFDictionaryRef result = nil; |
| check_error(AMDeviceLookupApplications(device, options, &result)); |
| |
| if (_json_output) { |
| NSLogJSON(@{@"Event": @"ListBundleId", |
| @"Apps": (NSDictionary *)result}); |
| } else { |
| CFIndex count; |
| count = CFDictionaryGetCount(result); |
| const void *keys[count]; |
| CFDictionaryGetKeysAndValues(result, keys, NULL); |
| for(int i = 0; i < count; ++i) { |
| NSLogOut(@"%@", (CFStringRef)keys[i]); |
| } |
| } |
| |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| } |
| |
| void copy_file_callback(AFCConnectionRef afc_conn_p, const char *name, read_dir_cb_reason reason) |
| { |
| const char *local_name=name; |
| |
| if (*local_name=='/') local_name++; |
| |
| if (*local_name=='\0') return; |
| |
| if (reason == READ_DIR_FILE || reason == READ_DIR_FIFO) { |
| NSLogOut(@"%@", [NSString stringWithUTF8String:name]); |
| afc_file_ref fref; |
| int err = AFCFileRefOpen(afc_conn_p,name,1,&fref); |
| |
| if (err) { |
| fprintf(stderr,"AFCFileRefOpen(\"%s\") failed: %d\n",name,err); |
| return; |
| } |
| |
| FILE *fp = fopen(local_name,"w"); |
| |
| if (fp==NULL) { |
| fprintf(stderr,"fopen(\"%s\",\"w\") failer: %s\n",local_name,strerror(errno)); |
| AFCFileRefClose(afc_conn_p,fref); |
| return; |
| } |
| |
| char buf[4096]; |
| size_t sz=sizeof(buf); |
| |
| while (AFCFileRefRead(afc_conn_p,fref,buf,&sz)==0 && sz) { |
| fwrite(buf,sz,1,fp); |
| sz = sizeof(buf); |
| } |
| |
| AFCFileRefClose(afc_conn_p,fref); |
| fclose(fp); |
| |
| } else if (reason == READ_DIR_BEFORE_DIR) { |
| NSLogOut(@"%@/", [NSString stringWithUTF8String:name]); |
| if (mkdir(local_name,0777) && errno!=EEXIST) { |
| fprintf(stderr,"mkdir(\"%s\") failed: %s\n",local_name,strerror(errno)); |
| } |
| } |
| } |
| |
| |
| void download_tree(AMDeviceRef device) |
| { |
| AFCConnectionRef afc_conn_p; |
| if (file_system) { |
| afc_conn_p = start_afc_service(device); |
| } else { |
| afc_conn_p = start_house_arrest_service(device); |
| } |
| |
| assert(afc_conn_p); |
| char *dirname = NULL; |
| |
| list_root = list_root? list_root : "/"; |
| target_filename = target_filename? target_filename : "."; |
| |
| NSString* targetPath = [NSString pathWithComponents:@[ @(target_filename), @(list_root)] ]; |
| mkdirp([targetPath stringByDeletingLastPathComponent]); |
| |
| do { |
| if (target_filename) { |
| dirname = strdup(target_filename); |
| mkdirp(@(dirname)); |
| if (mkdir(dirname,0777) && errno!=EEXIST) { |
| fprintf(stderr,"mkdir(\"%s\") failed: %s\n",dirname,strerror(errno)); |
| break; |
| } |
| if (chdir(dirname)) { |
| fprintf(stderr,"chdir(\"%s\") failed: %s\n",dirname,strerror(errno)); |
| break; |
| } |
| } |
| read_dir(afc_conn_p, list_root, copy_file_callback); |
| } while(0); |
| |
| if (dirname) free(dirname); |
| if (afc_conn_p) AFCConnectionClose(afc_conn_p); |
| } |
| |
| void upload_dir(AMDeviceRef device, AFCConnectionRef afc_conn_p, NSString* source, NSString* destination); |
| void upload_single_file(AMDeviceRef device, AFCConnectionRef afc_conn_p, NSString* sourcePath, NSString* destinationPath); |
| |
| void upload_file(AMDeviceRef device) |
| { |
| AFCConnectionRef afc_conn_p; |
| if (file_system) { |
| afc_conn_p = start_afc_service(device); |
| } else { |
| afc_conn_p = start_house_arrest_service(device); |
| } |
| assert(afc_conn_p); |
| |
| if (!target_filename) |
| { |
| target_filename = get_filename_from_path(upload_pathname); |
| } |
| |
| NSString* sourcePath = [NSString stringWithUTF8String: upload_pathname]; |
| NSString* destinationPath = [NSString stringWithUTF8String: target_filename]; |
| |
| BOOL isDir; |
| bool exists = [[NSFileManager defaultManager] fileExistsAtPath: sourcePath isDirectory: &isDir]; |
| if (!exists) |
| { |
| on_error(@"Could not find file: %s", upload_pathname); |
| } |
| else if (isDir) |
| { |
| upload_dir(device, afc_conn_p, sourcePath, destinationPath); |
| } |
| else |
| { |
| upload_single_file(device, afc_conn_p, sourcePath, destinationPath); |
| } |
| check_error(AFCConnectionClose(afc_conn_p)); |
| } |
| |
| void upload_single_file(AMDeviceRef device, AFCConnectionRef afc_conn_p, NSString* sourcePath, NSString* destinationPath) { |
| |
| afc_file_ref file_ref; |
| |
| size_t file_size; |
| void* file_content = read_file_to_memory([sourcePath fileSystemRepresentation], &file_size); |
| |
| if (!file_content) |
| { |
| on_error(@"Could not open file: %@", sourcePath); |
| } |
| |
| // Make sure the directory was created |
| { |
| NSString *dirpath = [destinationPath stringByDeletingLastPathComponent]; |
| check_error(AFCDirectoryCreate(afc_conn_p, [dirpath fileSystemRepresentation])); |
| } |
| |
| NSLogVerbose(@"%@", destinationPath); |
| NSLogJSON(@{@"Event": @"UploadFile", |
| @"Destination": destinationPath |
| }); |
| |
| int ret = AFCFileRefOpen(afc_conn_p, [destinationPath fileSystemRepresentation], 3, &file_ref); |
| if (ret == 0x000a) { |
| on_error(@"Cannot write to %@. Permission error.", destinationPath); |
| } |
| if (ret == 0x0009) { |
| on_error(@"Target %@ is a directory.", destinationPath); |
| } |
| assert(ret == 0); |
| check_error(AFCFileRefWrite(afc_conn_p, file_ref, file_content, file_size)); |
| check_error(AFCFileRefClose(afc_conn_p, file_ref)); |
| |
| free(file_content); |
| } |
| |
| void upload_dir(AMDeviceRef device, AFCConnectionRef afc_conn_p, NSString* source, NSString* destination) |
| { |
| check_error(AFCDirectoryCreate(afc_conn_p, [destination fileSystemRepresentation])); |
| for (NSString* item in [[NSFileManager defaultManager] contentsOfDirectoryAtPath: source error: nil]) |
| { |
| NSString* sourcePath = [source stringByAppendingPathComponent: item]; |
| NSString* destinationPath = [destination stringByAppendingPathComponent: item]; |
| BOOL isDir; |
| [[NSFileManager defaultManager] fileExistsAtPath: sourcePath isDirectory: &isDir]; |
| if (isDir) |
| { |
| NSString *dirDestinationPath = [destinationPath stringByAppendingString:@"/"]; |
| NSLogVerbose(@"%@", dirDestinationPath); |
| NSLogJSON(@{@"Event": @"UploadDir", |
| @"Destination": dirDestinationPath |
| }); |
| upload_dir(device, afc_conn_p, sourcePath, destinationPath); |
| } |
| else |
| { |
| upload_single_file(device, afc_conn_p, sourcePath, destinationPath); |
| } |
| } |
| } |
| |
| void make_directory(AMDeviceRef device) { |
| AFCConnectionRef afc_conn_p; |
| if (file_system) { |
| afc_conn_p = start_afc_service(device); |
| } else { |
| afc_conn_p = start_house_arrest_service(device); |
| } |
| assert(afc_conn_p); |
| check_error(AFCDirectoryCreate(afc_conn_p, target_filename)); |
| check_error(AFCConnectionClose(afc_conn_p)); |
| } |
| |
| void remove_path(AMDeviceRef device) { |
| AFCConnectionRef afc_conn_p; |
| if (file_system) { |
| afc_conn_p = start_afc_service(device); |
| } else { |
| afc_conn_p = start_house_arrest_service(device); |
| } |
| assert(afc_conn_p); |
| check_error(AFCRemovePath(afc_conn_p, target_filename)); |
| check_error(AFCConnectionClose(afc_conn_p)); |
| } |
| |
| // Handles the READ_DIR_AFTER_DIR callback so that we delete the contents of the |
| // directory before the directory itself |
| void rmtree_callback(AFCConnectionRef conn, const char *name, read_dir_cb_reason reason) |
| { |
| if (reason == READ_DIR_FILE || reason == READ_DIR_AFTER_DIR) { |
| NSLogVerbose(@"Deleting %s", name); |
| log_error(AFCRemovePath(conn, name)); |
| } else if (reason == READ_DIR_FIFO) { |
| NSLogVerbose(@"Skipping %s", name); |
| } |
| } |
| |
| void rmtree(AMDeviceRef device) { |
| AFCConnectionRef afc_conn_p = start_house_arrest_service(device); |
| assert(afc_conn_p); |
| read_dir(afc_conn_p, target_filename, rmtree_callback); |
| check_error(AFCConnectionClose(afc_conn_p)); |
| } |
| |
| void uninstall_app(AMDeviceRef device) { |
| CFRetain(device); // don't know if this is necessary? |
| |
| NSLogOut(@"------ Uninstall phase ------"); |
| |
| //Do we already have the bundle_id passed in via the command line? if so, use it. |
| CFStringRef cf_uninstall_bundle_id = NULL; |
| if (bundle_id != NULL) |
| { |
| cf_uninstall_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); |
| } else { |
| on_error(@"Error: you need to pass in the bundle id, (i.e. --bundle_id com.my.app)"); |
| } |
| |
| if (cf_uninstall_bundle_id == NULL) { |
| on_error(@"Error: Unable to get bundle id from user command or package %@.\nUninstall failed.", [NSString stringWithUTF8String:app_path]); |
| } else { |
| connect_and_start_session(device); |
| |
| int code = AMDeviceSecureUninstallApplication(0, device, cf_uninstall_bundle_id, 0, NULL, 0); |
| if (code == 0) { |
| NSLogOut(@"[ OK ] Uninstalled package with bundle id %@", cf_uninstall_bundle_id); |
| } else { |
| on_error(@"[ ERROR ] Could not uninstall package with bundle id %@", cf_uninstall_bundle_id); |
| } |
| CFRelease(cf_uninstall_bundle_id); |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| } |
| } |
| |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| void check_developer_mode(AMDeviceRef device) { |
| unsigned int error_code = 0; |
| bool is_enabled = AMDeviceCopyDeveloperModeStatus(device, &error_code); |
| |
| if (error_code) { |
| const char *mobdev_error = get_error_message(error_code); |
| NSString *error_description = mobdev_error ? [NSString stringWithUTF8String:mobdev_error] : @"unknown."; |
| if (_json_output) { |
| NSLogJSON(@{ |
| @"Event": @"DeveloperMode", |
| @"IsEnabled": @(is_enabled), |
| @"Code": @(error_code), |
| @"Status": error_description, |
| }); |
| } else { |
| NSLogOut(@"Encountered error checking developer mode status: %@", error_description); |
| } |
| } else { |
| if (_json_output) { |
| NSLogJSON(@{@"Event": @"DeveloperMode", @"IsEnabled": @(is_enabled)}); |
| } else { |
| NSLogOut(@"Developer mode is%s enabled.", is_enabled ? "" : " not"); |
| } |
| } |
| } |
| #endif |
| |
| void start_symbols_service_with_command(AMDeviceRef device, uint32_t command) { |
| connect_and_start_session(device); |
| check_error(AMDeviceSecureStartService(device, symbols_service_name, |
| NULL, &dbgServiceConnection)); |
| |
| uint32_t bytes_sent = AMDServiceConnectionSend(dbgServiceConnection, &command, |
| sizeof_uint32_t); |
| if (bytes_sent != sizeof_uint32_t) { |
| on_error(@"Sent %d bytes but was expecting %d.", bytes_sent, sizeof_uint32_t); |
| } |
| |
| uint32_t response; |
| uint32_t bytes_read = AMDServiceConnectionReceive(dbgServiceConnection, |
| &response, sizeof_uint32_t); |
| if (bytes_read != sizeof_uint32_t) { |
| on_error(@"Read %d bytes but was expecting %d.", bytes_read, sizeof_uint32_t); |
| } else if (response != command) { |
| on_error(@"Failed to get confirmation response for: %s", command); |
| } |
| } |
| |
| CFArrayRef get_dyld_file_paths(AMDeviceRef device) { |
| start_symbols_service_with_command(device, symbols_file_paths_command); |
| |
| CFPropertyListFormat format; |
| CFDictionaryRef dict = NULL; |
| uint64_t bytes_read = |
| AMDServiceConnectionReceiveMessage(dbgServiceConnection, &dict, &format); |
| if (bytes_read == -1) { |
| on_error(@"Received %d bytes after succesfully starting command %d.", bytes_read, |
| symbols_file_paths_command); |
| } |
| AMDeviceStopSession(device); |
| AMDeviceDisconnect(device); |
| |
| CFStringRef files_key = CFSTR("files"); |
| if (!CFDictionaryContainsKey(dict, files_key)) { |
| on_error(@"Incoming messasge did not contain key '%@', %@", files_key, dict); |
| } |
| return CFDictionaryGetValue(dict, files_key); |
| } |
| |
| void write_dyld_file(CFStringRef dest, uint64_t file_size) { |
| // Prepare the destination file by mapping it into memory. |
| int fd = open(CFStringGetCStringPtr(dest, kCFStringEncodingUTF8), |
| O_RDWR | O_CREAT, 0644); |
| if (fd == -1) { |
| on_sys_error(@"Failed to open %@.", dest); |
| } |
| if (lseek(fd, file_size - 1, SEEK_SET) == -1) { |
| on_sys_error(@"Failed to lseek to last byte."); |
| } |
| if (write(fd, "", 1) == -1) { |
| on_sys_error(@"Failed to write to last byte."); |
| } |
| void *map = mmap(NULL, file_size, PROT_WRITE, MAP_SHARED, fd, 0); |
| if (map == MAP_FAILED) { |
| on_sys_error(@"Failed to mmap %@.", dest); |
| } |
| close(fd); |
| |
| // Read the file content packet by packet until we've copied the entire file |
| // to disk. |
| uint64_t total_bytes_read = 0; |
| uint64_t last_time = |
| get_current_time_in_milliseconds() / symbols_logging_interval_ms; |
| while (total_bytes_read < file_size) { |
| uint64_t bytes_remaining = file_size - total_bytes_read; |
| // This fails for some reason if we try to download more than |
| // INT_MAX bytes at a time. |
| uint64_t bytes_to_download = MIN(bytes_remaining, INT_MAX - 1); |
| uint64_t bytes_read = AMDServiceConnectionReceive( |
| dbgServiceConnection, map + total_bytes_read, bytes_to_download); |
| total_bytes_read += bytes_read; |
| |
| uint64_t current_time = |
| get_current_time_in_milliseconds() / symbols_logging_interval_ms; |
| // We can process several packets per second which would result |
| // in spamming output so only log if any of the following are |
| // true: |
| // - Running in verbose mode. |
| // - It's been at least a quarter second since the last log. |
| // - We finished processing the last packet. |
| if (verbose || last_time != current_time || total_bytes_read == file_size) { |
| last_time = current_time; |
| int percent = (double)total_bytes_read / file_size * 100; |
| NSLogOut(@"%llu/%llu (%d%%)", total_bytes_read, file_size, percent); |
| NSLogJSON(@{@"Event": @"DyldCacheDownloadProgress", |
| @"BytesRead": @(total_bytes_read), |
| @"Percent": @(percent), |
| }); |
| } |
| } |
| |
| munmap(map, file_size); |
| } |
| |
| CFStringRef download_dyld_file(AMDeviceRef device, uint32_t dyld_index, |
| CFStringRef filepath) { |
| start_symbols_service_with_command(device, symbols_download_file_command); |
| |
| uint32_t index = CFSwapInt32HostToBig(dyld_index); |
| uint64_t bytes_sent = |
| AMDServiceConnectionSend(dbgServiceConnection, &index, sizeof_uint32_t); |
| if (bytes_sent != sizeof_uint32_t) { |
| on_error(@"Sent %d bytes but was expecting %d.", bytes_sent, sizeof_uint32_t); |
| } |
| |
| uint64_t file_size = 0; |
| uint64_t bytes_read = AMDServiceConnectionReceive(dbgServiceConnection, |
| &file_size, sizeof(uint64_t)); |
| if (bytes_read != sizeof(uint64_t)) { |
| on_error(@"Read %d bytes but was expecting %d.", bytes_read, sizeof(uint64_t)); |
| } |
| file_size = CFSwapInt64BigToHost(file_size); |
| |
| CFStringRef download_path = CFStringCreateWithFormat( |
| NULL, NULL, CFSTR("%s%@"), symbols_download_directory, filepath); |
| mkdirp( |
| ((__bridge NSString *)download_path).stringByDeletingLastPathComponent); |
| NSLogOut(@"Downloading %@ to %@.", filepath, download_path); |
| NSLogJSON(@{@"Event": @"DyldCacheDownload", |
| @"Source": (__bridge NSString *)filepath, |
| @"Destination": (__bridge NSString *)download_path, |
| @"Size": @(file_size), |
| }); |
| |
| write_dyld_file(download_path, file_size); |
| |
| AMDeviceStopSession(device); |
| AMDeviceDisconnect(device); |
| return download_path; |
| } |
| |
| CFStringRef create_dsc_bundle_path_for_device(AMDeviceRef device) { |
| CFStringRef xcode_dev_path = copy_xcode_dev_path(); |
| |
| is_usb_device(device) ? AMDeviceConnect(device) : connect_and_start_session(device); |
| CFStringRef device_class = AMDeviceCopyValue(device, 0, CFSTR("DeviceClass")); |
| AMDeviceDisconnect(device); |
| if (!device_class) { |
| on_error(@"Failed to determine device class"); |
| } |
| |
| CFStringRef platform_name; |
| if (CFStringCompare(CFSTR("AppleTV"), device_class, 0) == kCFCompareEqualTo) { |
| platform_name = CFSTR("AppleTVOS"); |
| } else if (CFStringCompare(CFSTR("Watch"), device_class, 0) == |
| kCFCompareEqualTo) { |
| platform_name = CFSTR("WatchOS"); |
| } else { |
| platform_name = CFSTR("iPhoneOS"); |
| } |
| |
| return CFStringCreateWithFormat( |
| NULL, NULL, |
| CFSTR("%@/Platforms/%@.platform/usr/lib/dsc_extractor.bundle"), |
| xcode_dev_path, platform_name); |
| } |
| |
| typedef int (*extractor_proc)(const char *shared_cache_file_path, const char *extraction_root_path, |
| void (^progress)(unsigned current, unsigned total)); |
| |
| void dyld_shared_cache_extract_dylibs(CFStringRef dsc_extractor_bundle_path, |
| CFStringRef shared_cache_file_path, |
| const char *extraction_root_path) { |
| const char *dsc_extractor_bundle_path_ptr = |
| CFStringGetCStringPtr(dsc_extractor_bundle_path, kCFStringEncodingUTF8); |
| void *handle = dlopen(dsc_extractor_bundle_path_ptr, RTLD_LAZY); |
| if (handle == NULL) { |
| on_error(@"%s could not be loaded", dsc_extractor_bundle_path); |
| } |
| |
| extractor_proc proc = (extractor_proc)dlsym( |
| handle, "dyld_shared_cache_extract_dylibs_progress"); |
| if (proc == NULL) { |
| on_error( |
| @"%s did not have dyld_shared_cache_extract_dylibs_progress symbol", |
| dsc_extractor_bundle_path); |
| } |
| |
| const char *shared_cache_file_path_ptr = |
| CFStringGetCStringPtr(shared_cache_file_path, kCFStringEncodingUTF8); |
| |
| NSLogJSON(@{@"Event": @"DyldCacheExtract", |
| @"Source": (__bridge NSString *)shared_cache_file_path, |
| @"Destination": @(extraction_root_path), |
| }); |
| |
| __block uint64_t last_time = |
| get_current_time_in_milliseconds() / symbols_logging_interval_ms; |
| __block unsigned files_extracted = 0; |
| __block unsigned files_total = 0; |
| int result = |
| (*proc)(shared_cache_file_path_ptr, extraction_root_path, |
| ^(unsigned c, unsigned total) { |
| uint64_t current_time = |
| get_current_time_in_milliseconds() / symbols_logging_interval_ms; |
| if (!verbose && last_time == current_time) return; |
| |
| last_time = current_time; |
| files_extracted = c; |
| files_total = total; |
| |
| int percent = (double)c / total * 100; |
| NSLogOut(@"%d/%d (%d%%)", c, total, percent); |
| NSLogJSON(@{@"Event": @"DyldCacheExtractProgress", |
| @"Extracted": @(c), |
| @"Total": @(total), |
| @"Percent": @(percent), |
| }); |
| }); |
| if (result == 0) { |
| NSLogOut(@"Finished extracting %s.", shared_cache_file_path_ptr); |
| files_extracted = files_total; |
| } else { |
| NSLogOut(@"Failed to extract %s, exit code %d.", shared_cache_file_path_ptr, result); |
| } |
| int percent = (double)files_extracted / files_total * 100; |
| NSLogJSON(@{@"Event": @"DyldCacheExtractProgress", |
| @"Code": @(result), |
| @"Extracted": @(files_extracted), |
| @"Total": @(files_total), |
| @"Percent": @(percent), |
| }); |
| } |
| |
| void download_device_symbols(AMDeviceRef device) { |
| dbgServiceConnection = NULL; |
| CFArrayRef files = get_dyld_file_paths(device); |
| CFIndex files_count = CFArrayGetCount(files); |
| NSLogOut(@"Downloading symbols files: %@", files); |
| NSLogJSON(@{@"Event": @"SymbolsDownload", |
| @"Files": (__bridge NSArray *)files, |
| }); |
| CFStringRef dsc_extractor_bundle = create_dsc_bundle_path_for_device(device); |
| |
| for (uint32_t i = 0; i < files_count; ++i) { |
| CFStringRef filepath = (CFStringRef)CFArrayGetValueAtIndex(files, i); |
| CFStringRef download_path = download_dyld_file(device, i, filepath); |
| dyld_shared_cache_extract_dylibs(dsc_extractor_bundle, download_path, |
| symbols_download_directory); |
| CFRelease(download_path); |
| } |
| |
| CFRelease(dsc_extractor_bundle); |
| } |
| |
| void handle_device(AMDeviceRef device) { |
| NSLogVerbose(@"Already found device? %d", found_device); |
| |
| CFStringRef device_full_name = get_device_full_name(device), |
| device_interface_name = get_device_interface_name(device); |
| |
| if (detect_only) { |
| if (_json_output) { |
| NSLogJSON(@{@"Event": @"DeviceDetected", |
| @"Interface": (__bridge NSString *)device_interface_name, |
| @"Device": get_device_json_dict(device) |
| }); |
| } else { |
| NSLogOut(@"[....] Found %@ connected through %@.", device_full_name, device_interface_name); |
| } |
| found_device = true; |
| return; |
| } |
| if (found_device) |
| { |
| NSLogVerbose(@"Skipping %@.", device_full_name); |
| return; |
| } |
| CFStringRef found_device_id = CFAutorelease(AMDeviceCopyDeviceIdentifier(device)); |
| if (device_id != NULL) { |
| CFStringRef deviceCFSTR = CFAutorelease(CFStringCreateWithCString(NULL, device_id, kCFStringEncodingUTF8)); |
| if (CFStringCompare(deviceCFSTR, found_device_id, kCFCompareCaseInsensitive) == kCFCompareEqualTo) { |
| found_device = true; |
| } else { |
| NSLogVerbose(@"Skipping %@.", device_full_name); |
| return; |
| } |
| } else { |
| // Use the first device we find if a device_id wasn't specified. |
| device_id = strdup(CFStringGetCStringPtr(found_device_id, kCFStringEncodingUTF8)); |
| found_device = true; |
| } |
| |
| NSLogOut(@"[....] Using %@.", device_full_name); |
| |
| if (command_only) { |
| if (strcmp("list", command) == 0) { |
| list_files(device); |
| } else if (strcmp("upload", command) == 0) { |
| upload_file(device); |
| } else if (strcmp("download", command) == 0) { |
| download_tree(device); |
| } else if (strcmp("mkdir", command) == 0) { |
| make_directory(device); |
| } else if (strcmp("rm", command) == 0) { |
| remove_path(device); |
| } else if (strcmp("rmtree", command) == 0) { |
| rmtree(device); |
| } else if (strcmp("exists", command) == 0) { |
| exit(app_exists(device)); |
| } else if (strcmp("uninstall_only", command) == 0) { |
| uninstall_app(device); |
| } else if (strcmp("list_bundle_id", command) == 0) { |
| list_bundle_id(device); |
| } else if (strcmp("get_battery_level", command) == 0) { |
| get_battery_level(device); |
| } else if (strcmp("symbols", command) == 0) { |
| download_device_symbols(device); |
| } else if (strcmp("list_profiles", command) == 0) { |
| list_provisioning_profiles(device); |
| } else if (strcmp("install_profile", command) == 0) { |
| install_provisioning_profile(device); |
| } else if (strcmp("uninstall_profile", command) == 0) { |
| uninstall_provisioning_profile(device); |
| } else if (strcmp("download_profile", command) == 0) { |
| download_provisioning_profile(device); |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| } else if (strcmp("check_developer_mode", command) == 0) { |
| check_developer_mode(device); |
| #endif |
| } |
| exit(0); |
| } |
| |
| if (debugserver_only && app_path == NULL) { |
| launch_debugserver_only(device, NULL); |
| return; |
| } |
| |
| CFRetain(device); // don't know if this is necessary? |
| |
| CFStringRef path = CFStringCreateWithCString(NULL, app_path, kCFStringEncodingUTF8); |
| CFURLRef relative_url = CFURLCreateWithFileSystemPath(NULL, path, kCFURLPOSIXPathStyle, false); |
| CFURLRef url = CFURLCopyAbsoluteURL(relative_url); |
| |
| CFRelease(relative_url); |
| |
| if (uninstall) { |
| NSLogOut(@"------ Uninstall phase ------"); |
| |
| //Do we already have the bundle_id passed in via the command line? if so, use it. |
| CFStringRef cf_uninstall_bundle_id = NULL; |
| if (bundle_id != NULL) |
| { |
| cf_uninstall_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); |
| } else { |
| cf_uninstall_bundle_id = copy_bundle_id(url); |
| } |
| |
| if (cf_uninstall_bundle_id == NULL) { |
| on_error(@"Error: Unable to get bundle id from user command or package %@.\nUninstall failed.", [NSString stringWithUTF8String:app_path]); |
| } else { |
| connect_and_start_session(device); |
| |
| int code = AMDeviceSecureUninstallApplication(0, device, cf_uninstall_bundle_id, 0, NULL, 0); |
| if (code == 0) { |
| NSLogOut(@"[ OK ] Uninstalled package with bundle id %@", cf_uninstall_bundle_id); |
| } else { |
| on_error(@"[ ERROR ] Could not uninstall package with bundle id %@", cf_uninstall_bundle_id); |
| } |
| CFRelease(cf_uninstall_bundle_id); |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| } |
| } |
| |
| if(install) { |
| NSLogOut(@"------ Install phase ------"); |
| NSLogOut(@"[ 0%%] Found %@ connected through %@, beginning install", device_full_name, device_interface_name); |
| |
| CFStringRef install_bundle_id = NULL; |
| if (bundle_id != NULL) { |
| install_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); |
| } else { |
| CFStringRef extracted_bundle_id = copy_bundle_id(url); |
| if (extracted_bundle_id == NULL) { |
| on_error(@"[ ERROR] Could not determine bundle id."); |
| } |
| install_bundle_id = extracted_bundle_id; |
| } |
| |
| CFDictionaryRef options; |
| if (app_deltas == NULL) { // standard install |
| CFStringRef keys[] = { CFSTR("PackageType") }; |
| CFStringRef values[] = { CFSTR("Developer") }; |
| options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| check_error(AMDeviceSecureTransferPath(0, device, url, options, transfer_callback, 0)); |
| |
| connect_and_start_session(device); |
| check_error(AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0)); |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| } else { // incremental install |
| CFStringRef deltas_path = |
| CFStringCreateWithCString(NULL, app_deltas, kCFStringEncodingUTF8); |
| CFURLRef deltas_relative_url = |
| CFURLCreateWithFileSystemPath(NULL, deltas_path, kCFURLPOSIXPathStyle, false); |
| CFURLRef app_deltas_url = CFURLCopyAbsoluteURL(deltas_relative_url); |
| CFStringRef prefer_wifi = no_wifi ? CFSTR("0") : CFSTR("1"); |
| |
| // These values were determined by inspecting Xcode 11.1 logs with the Console app. |
| CFStringRef keys[] = { |
| CFSTR("CFBundleIdentifier"), |
| CFSTR("CloseOnInvalidate"), |
| CFSTR("InvalidateOnDetach"), |
| CFSTR("IsUserInitiated"), |
| CFSTR("PackageType"), |
| CFSTR("PreferWifi"), |
| CFSTR("ShadowParentKey"), |
| }; |
| CFStringRef values[] = { |
| install_bundle_id, |
| CFSTR("1"), |
| CFSTR("1"), |
| CFSTR("1"), |
| CFSTR("Developer"), |
| prefer_wifi, |
| (CFStringRef)app_deltas_url, |
| }; |
| |
| CFIndex size = sizeof(keys)/sizeof(CFStringRef); |
| options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, size, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| |
| // Incremental installs should be done without a session started because of timeouts. |
| check_error(AMDeviceSecureInstallApplicationBundle(device, url, options, incremental_install_callback, 0)); |
| CFRelease(deltas_path); |
| CFRelease(deltas_relative_url); |
| CFRelease(app_deltas_url); |
| free(app_deltas); |
| app_deltas = NULL; |
| } |
| |
| CFRelease(options); |
| |
| connect_and_start_session(device); |
| CFURLRef device_app_url = copy_device_app_url(device, install_bundle_id); |
| check_error(AMDeviceStopSession(device)); |
| check_error(AMDeviceDisconnect(device)); |
| CFStringRef device_app_path = CFURLCopyFileSystemPath(device_app_url, kCFURLPOSIXPathStyle); |
| |
| NSLogOut(@"[100%%] Installed package %@", [NSString stringWithUTF8String:app_path]); |
| NSLogVerbose(@"App path: %@", device_app_path); |
| NSLogJSON(@{@"Event": @"BundleInstall", |
| @"OverallPercent": @(100), |
| @"Percent": @(100), |
| @"Status": @"Complete", |
| @"Path": (__bridge NSString *)device_app_path |
| }); |
| |
| CFRelease(device_app_url); |
| CFRelease(install_bundle_id); |
| CFRelease(device_app_path); |
| } |
| CFRelease(path); |
| |
| if (!debug) |
| exit(0); // no debug phase |
| |
| if (justlaunch) { |
| launch_debugger_and_exit(device, url); |
| } else if (debugserver_only) { |
| launch_debugserver_only(device, url); |
| } else { |
| launch_debugger(device, url); |
| } |
| } |
| |
| void device_callback(struct am_device_notification_callback_info *info, void *arg) { |
| switch (info->msg) { |
| case ADNCI_MSG_CONNECTED: |
| { |
| int itype = AMDeviceGetInterfaceType(info->dev); |
| if (no_wifi && (itype == 2 || ( itype == 3 && get_companion_interface_type(info->dev) == 2))) |
| { |
| NSLogVerbose(@"Skipping wifi device (type: %d)", itype); |
| } |
| else |
| { |
| NSLogVerbose(@"Handling device type: %d", itype); |
| handle_device(info->dev); |
| } |
| break; |
| } |
| case ADNCI_MSG_DISCONNECTED: |
| { |
| CFStringRef device_interface_name = get_device_interface_name(info->dev); |
| CFStringRef device_uuid = AMDeviceCopyDeviceIdentifier(info->dev); |
| NSLogVerbose(@"[....] Disconnected %@ from %@.", device_uuid, device_interface_name); |
| if (detect_only && _json_output) { |
| NSLogJSON(@{@"Event": @"DeviceDisconnected", |
| @"Interface": (__bridge NSString *)device_interface_name, |
| @"Device": get_device_json_dict(info->dev) |
| }); |
| } |
| CFRelease(device_uuid); |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| void timeout_callback(CFRunLoopTimerRef timer, void *info) { |
| if (found_device && (!detect_only)) { |
| // Don't need to exit in the justlaunch mode |
| if (justlaunch) |
| return; |
| |
| // App running for too long |
| NSLogOut(@"[ !! ] App is running for too long"); |
| exit(exitcode_timeout); |
| return; |
| } else if ((!found_device) && (!detect_only)) { |
| on_error(@"Timed out waiting for device."); |
| } |
| else |
| { |
| // Device detection timeout |
| if (!debug) { |
| NSLogOut(@"[....] No more devices found."); |
| } |
| |
| if (detect_only && !found_device) { |
| exit(exitcode_error); |
| return; |
| } else { |
| int mypid = getpid(); |
| if ((parent != 0) && (parent == mypid) && (child != 0)) |
| { |
| NSLogVerbose(@"Timeout. Killing child (%d) tree.", child); |
| kill_ptree(child, SIGHUP); |
| } |
| } |
| exit(0); |
| } |
| } |
| |
| void usage(const char* app) { |
| NSLog( |
| @"Usage: %@ [OPTION]...\n" |
| @" -d, --debug launch the app in lldb after installation\n" |
| @" -i, --id <device_id> the id of the device to connect to\n" |
| @" -c, --detect list all connected devices\n" |
| @" -b, --bundle <bundle.app> the path to the app bundle to be installed\n" |
| @" -a, --args <args> command line arguments to pass to the app when launching it\n" |
| @" -s, --envs <envs> environment variables, space separated key-value pairs, to pass to the app when launching it\n" |
| @" -t, --timeout <timeout> number of seconds to wait for a device to be connected\n" |
| @" -u, --unbuffered don't buffer stdout\n" |
| @" -n, --nostart do not start the app when debugging\n" |
| @" -N, --nolldb start debugserver only. do not run lldb. Can not be used with args or envs options\n" |
| @" -I, --noninteractive start in non interactive mode (quit when app crashes or exits)\n" |
| @" -L, --justlaunch just launch the app and exit lldb\n" |
| @" -v, --verbose enable verbose output\n" |
| @" -m, --noinstall directly start debugging without app install (-d not required)\n" |
| @" -A, --app_deltas incremental install. must specify a directory to store app deltas to determine what needs to be installed\n" |
| @" -p, --port <number> port used for device, default: dynamic\n" |
| @" -r, --uninstall uninstall the app before install (do not use with -m; app cache and data are cleared) \n" |
| @" -9, --uninstall_only uninstall the app ONLY. Use only with -1 <bundle_id> \n" |
| @" -1, --bundle_id <bundle id> specify bundle id for list and upload\n" |
| @" -l, --list[=<dir>] list all app files or the specified directory\n" |
| @" -o, --upload <file> upload file\n" |
| @" -w, --download[=<path>] download app tree or the specified file/directory\n" |
| @" -2, --to <target pathname> use together with up/download file/tree. specify target\n" |
| @" -D, --mkdir <dir> make directory on device\n" |
| @" -R, --rm <path> remove file or directory on device (directories must be empty)\n" |
| @" -X, --rmtree <path> remove directory and all contained files recursively on device\n" |
| @" -V, --version print the executable version \n" |
| @" -e, --exists check if the app with given bundle_id is installed or not \n" |
| @" -B, --list_bundle_id list bundle_id \n" |
| @" -W, --no-wifi ignore wifi devices\n" |
| @" -C, --get_battery_level get battery current capacity \n" |
| @" -O, --output <file> write stdout to this file\n" |
| @" -E, --error_output <file> write stderr to this file\n" |
| @" --detect_deadlocks <sec> start printing backtraces for all threads periodically after specific amount of seconds\n" |
| @" -f, --file_system specify file system for mkdir / list / upload / download / rm\n" |
| @" -F, --non-recursively specify non-recursively walk directory\n" |
| @" -S, --symbols download OS symbols. must specify a directory to store the downloaded symbols\n" |
| @" -j, --json format output as JSON\n" |
| @" -k, --key keys for the properties of the bundle. Joined by ',' and used only with -B <list_bundle_id> and -j <json> \n" |
| @" --custom-script <script> path to custom python script to execute in lldb\n" |
| @" --custom-command <command> specify additional lldb commands to execute\n" |
| @" --faster-path-search use alternative logic to find the device support paths faster\n" |
| @" -P, --list_profiles list all provisioning profiles on device\n" |
| @" --profile-uuid <uuid> the UUID of the provisioning profile to target, use with other profile commands\n" |
| @" --profile-download <path> download a provisioning profile (requires --profile-uuid)\n" |
| @" --profile-install <file> install a provisioning profile\n" |
| @" --profile-uninstall uninstall a provisioning profile (requires --profile-uuid <UUID>)\n" |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| @" --check-developer-mode checks whether the given device has developer mode enabled (requires Xcode 14 or newer)\n", |
| #else |
| , |
| #endif |
| [NSString stringWithUTF8String:app]); |
| } |
| |
| void show_version(void) { |
| NSLogOut(@"%@", @ |
| #include "version.h" |
| ); |
| } |
| |
| int main(int argc, char *argv[]) { |
| |
| // create a UUID for tmp purposes |
| CFUUIDRef uuid = CFUUIDCreate(NULL); |
| CFStringRef str = CFUUIDCreateString(NULL, uuid); |
| CFRelease(uuid); |
| tmpUUID = [(NSString*)str autorelease]; |
| |
| static struct option longopts[] = { |
| { "debug", no_argument, NULL, 'd' }, |
| { "id", required_argument, NULL, 'i' }, |
| { "bundle", required_argument, NULL, 'b' }, |
| { "args", required_argument, NULL, 'a' }, |
| { "envs", required_argument, NULL, 's' }, |
| { "verbose", no_argument, NULL, 'v' }, |
| { "timeout", required_argument, NULL, 't' }, |
| { "unbuffered", no_argument, NULL, 'u' }, |
| { "nostart", no_argument, NULL, 'n' }, |
| { "nolldb", no_argument, NULL, 'N' }, |
| { "noninteractive", no_argument, NULL, 'I' }, |
| { "justlaunch", no_argument, NULL, 'L' }, |
| { "detect", no_argument, NULL, 'c' }, |
| { "version", no_argument, NULL, 'V' }, |
| { "noinstall", no_argument, NULL, 'm' }, |
| { "port", required_argument, NULL, 'p' }, |
| { "uninstall", no_argument, NULL, 'r' }, |
| { "uninstall_only", no_argument, NULL, '9'}, |
| { "list", optional_argument, NULL, 'l' }, |
| { "bundle_id", required_argument, NULL, '1'}, |
| { "upload", required_argument, NULL, 'o'}, |
| { "download", optional_argument, NULL, 'w'}, |
| { "to", required_argument, NULL, '2'}, |
| { "mkdir", required_argument, NULL, 'D'}, |
| { "rm", required_argument, NULL, 'R'}, |
| { "rmtree",required_argument, NULL, 'X'}, |
| { "exists", no_argument, NULL, 'e'}, |
| { "list_bundle_id", no_argument, NULL, 'B'}, |
| { "no-wifi", no_argument, NULL, 'W'}, |
| { "get_battery_level", no_argument, NULL, 'C'}, |
| { "output", required_argument, NULL, 'O' }, |
| { "error_output", required_argument, NULL, 'E' }, |
| { "detect_deadlocks", required_argument, NULL, 1000 }, |
| { "json", no_argument, NULL, 'j'}, |
| { "app_deltas", required_argument, NULL, 'A'}, |
| { "file_system", no_argument, NULL, 'f'}, |
| { "non-recursively", no_argument, NULL, 'F'}, |
| { "key", optional_argument, NULL, 'k' }, |
| { "symbols", required_argument, NULL, 'S' }, |
| { "list_profiles", no_argument, NULL, 'P' }, |
| { "custom-script", required_argument, NULL, 1001}, |
| { "custom-command", required_argument, NULL, 1002}, |
| { "faster-path-search", no_argument, NULL, 1003}, |
| { "profile-install", required_argument, NULL, 1004}, |
| { "profile-uninstall", no_argument, NULL, 1005}, |
| { "profile-download", required_argument, NULL, 1006}, |
| { "profile-uuid", required_argument, NULL, 1007}, |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| { "check-developer-mode", no_argument, NULL, 1008}, |
| #endif |
| { NULL, 0, NULL, 0 }, |
| }; |
| int ch; |
| |
| while ((ch = getopt_long(argc, argv, "VmcdvunrILefFD:R:X:i:b:a:t:p:1:2:o:l:w:9BWjNs:OE:CA:k:S:P", longopts, NULL)) != -1) |
| { |
| switch (ch) { |
| case 'm': |
| install = false; |
| debug = true; |
| break; |
| case 'd': |
| debug = true; |
| break; |
| case 'i': |
| device_id = optarg; |
| break; |
| case 'b': |
| app_path = optarg; |
| break; |
| case 'a': |
| args = optarg; |
| break; |
| case 's': |
| envs = optarg; |
| break; |
| case 'S': |
| symbols_download_directory = optarg; |
| command = "symbols"; |
| command_only = true; |
| break; |
| case 'v': |
| verbose = true; |
| break; |
| case 't': |
| _timeout = atoi(optarg); |
| break; |
| case 'u': |
| unbuffered = true; |
| break; |
| case 'n': |
| nostart = true; |
| break; |
| case 'N': |
| debugserver_only = true; |
| debug = true; |
| break; |
| case 'I': |
| interactive = false; |
| debug = true; |
| break; |
| case 'L': |
| interactive = false; |
| justlaunch = true; |
| debug = true; |
| break; |
| case 'c': |
| detect_only = true; |
| debug = true; |
| break; |
| case 'V': |
| show_version(); |
| return 0; |
| case 'p': |
| port = atoi(optarg); |
| break; |
| case 'r': |
| uninstall = true; |
| break; |
| case '9': |
| command_only = true; |
| command = "uninstall_only"; |
| break; |
| case '1': |
| bundle_id = optarg; |
| break; |
| case '2': |
| target_filename = optarg; |
| break; |
| case 'o': |
| command_only = true; |
| upload_pathname = optarg; |
| command = "upload"; |
| break; |
| case 'l': |
| command_only = true; |
| command = "list"; |
| list_root = optarg; |
| break; |
| case 'w': |
| command_only = true; |
| command = "download"; |
| list_root = optarg; |
| break; |
| case 'D': |
| command_only = true; |
| target_filename = optarg; |
| command = "mkdir"; |
| break; |
| case 'R': |
| command_only = true; |
| target_filename = optarg; |
| command = "rm"; |
| break; |
| case 'X': |
| command_only = true; |
| target_filename = optarg; |
| command = "rmtree"; |
| break; |
| case 'e': |
| command_only = true; |
| command = "exists"; |
| break; |
| case 'B': |
| command_only = true; |
| command = "list_bundle_id"; |
| break; |
| case 'W': |
| no_wifi = true; |
| break; |
| case 'C': |
| command_only = true; |
| command = "get_battery_level"; |
| break; |
| case 'O': |
| output_path = optarg; |
| break; |
| case 'E': |
| error_path = optarg; |
| break; |
| case 1000: |
| _detectDeadlockTimeout = atoi(optarg); |
| break; |
| case 'j': |
| _json_output = true; |
| break; |
| case 'A': |
| app_deltas = resolve_path(optarg); |
| break; |
| case 'f': |
| file_system = true; |
| break; |
| case 'F': |
| non_recursively = true; |
| break; |
| case 1001: |
| custom_script_path = optarg; |
| break; |
| case 1002: |
| if (custom_commands == nil) |
| { |
| custom_commands = [[NSMutableString alloc] init]; |
| } |
| [custom_commands appendFormat:@"%s\n", optarg]; |
| break; |
| case 1003: |
| faster_path_search = true; |
| break; |
| case 1004: |
| command_only = true; |
| command = "install_profile"; |
| profile_path = optarg; |
| break; |
| case 1005: |
| command_only = true; |
| command = "uninstall_profile"; |
| break; |
| case 1006: |
| command_only = true; |
| command = "download_profile"; |
| profile_path = optarg; |
| break; |
| case 1007: |
| profile_uuid = optarg; |
| break; |
| #if defined(IOS_DEPLOY_FEATURE_DEVELOPER_MODE) |
| case 1008: |
| command_only = true; |
| command = "check_developer_mode"; |
| break; |
| #endif |
| case 'P': |
| command_only = true; |
| command = "list_profiles"; |
| break; |
| case 'k': |
| if (!keys) keys = [[NSMutableArray alloc] init]; |
| [keys addObject: [NSString stringWithUTF8String:optarg]]; |
| break; |
| default: |
| usage(argv[0]); |
| return exitcode_error; |
| } |
| } |
| |
| if (debugserver_only && (args || envs)) { |
| usage(argv[0]); |
| on_error(@"The --args and --envs options can not be combined with --nolldb."); |
| } |
| |
| if (!app_path && !detect_only && !debugserver_only && !command_only) { |
| usage(argv[0]); |
| on_error(@"One of -[b|c|o|l|w|D|N|R|X|e|B|C|S|9] is required to proceed!"); |
| } |
| |
| if (unbuffered) { |
| setbuf(stdout, NULL); |
| setbuf(stderr, NULL); |
| } |
| |
| if (detect_only && _timeout == 0) { |
| _timeout = 5; |
| } |
| |
| if (app_path) { |
| if (access(app_path, F_OK) != 0) { |
| on_sys_error(@"Can't access app path '%@'", [NSString stringWithUTF8String:app_path]); |
| } |
| } |
| |
| AMDSetLogLevel(5); // otherwise syslog gets flooded with crap |
| if (_timeout > 0) |
| { |
| CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent() + _timeout, 0, 0, 0, timeout_callback, NULL); |
| CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes); |
| NSLogOut(@"[....] Waiting up to %d seconds for iOS device to be connected", _timeout); |
| } |
| else |
| { |
| NSLogOut(@"[....] Waiting for iOS device to be connected"); |
| } |
| |
| |
| CFStringRef keys[] = { |
| CFSTR("NotificationOptionSearchForPairedDevices"), |
| }; |
| const void* values[] = { |
| kCFBooleanTrue, |
| }; |
| CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void**)keys, values, 1, &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| |
| AMDeviceNotificationSubscribeWithOptions(&device_callback, 0, 0, NULL, ¬ify, options); |
| CFRunLoopRun(); |
| } |