diff --git a/Makefile b/Makefile index bd6beb41..77bf3013 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -IOS_CC = /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/gcc +IOS_CC = xcrun -sdk iphoneos clang +IOS_MIN_OS = 5.1 +IOS_SDK = 6.1 all: demo.app fruitstrap @@ -9,16 +11,16 @@ demo.app: demo Info.plist codesign -f -s "iPhone Developer" --entitlements Entitlements.plist demo.app demo: demo.c - $(IOS_CC) -arch armv7 -isysroot /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk -framework CoreFoundation -o demo demo.c + $(IOS_CC) -isysroot `xcode-select -print-path`/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS$(IOS_SDK).sdk -mios-version-min=$(IOS_MIN_OS) -arch armv7 -framework CoreFoundation -o demo demo.c fruitstrap: fruitstrap.c - gcc -o fruitstrap -framework CoreFoundation -framework MobileDevice -F/System/Library/PrivateFrameworks fruitstrap.c + clang -o fruitstrap -framework CoreFoundation -framework MobileDevice -F/System/Library/PrivateFrameworks fruitstrap.c install: all - ./fruitstrap demo.app + ./fruitstrap -b demo.app debug: all - ./fruitstrap -d demo.app + ./fruitstrap -d -b demo.app clean: - rm -rf *.app demo fruitstrap \ No newline at end of file + rm -rf *.app demo fruitstrap diff --git a/MobileDevice.h b/MobileDevice.h index 1a39b098..437660bd 100644 --- a/MobileDevice.h +++ b/MobileDevice.h @@ -448,6 +448,11 @@ void AMDAddLogFileDescriptor(int fd); //kern_return_t AMDeviceSendMessage(service_conn_t socket, void *unused, CFPropertyListRef plist); //kern_return_t AMDeviceReceiveMessage(service_conn_t socket, CFDictionaryRef options, CFPropertyListRef * result); +typedef int (*am_device_install_application_callback)(CFDictionaryRef, int); + +mach_error_t AMDeviceInstallApplication(service_conn_t socket, CFStringRef path, CFDictionaryRef options, am_device_install_application_callback callback, void *user); +mach_error_t AMDeviceTransferApplication(service_conn_t socket, CFStringRef path, CFDictionaryRef options, am_device_install_application_callback callbackj, void *user); + /* ---------------------------------------------------------------------------- * Semi-private routines * ------------------------------------------------------------------------- */ @@ -486,4 +491,4 @@ typedef unsigned int (*t_performOperation)(struct am_restore_device *rdev, } #endif -#endif \ No newline at end of file +#endif diff --git a/README.md b/README.md index f087557c..ec9af5de 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -## This project is no longer maintained. - fruitstrap ========== Install and debug iPhone apps without using Xcode. Designed to work on unjailbroken devices. ## Requirements -* Mac OS X. Tested on Snow Leopard only. -* You need to have a valid iPhone development certificate installed. +* Mac OS X. Tested on Lion/Mountain Lion. +* You need to have a valid iPhone development certificate installed (or at least a correctly signed iOS app). * Xcode must be installed, along with the SDK for your iOS version. ## Usage -* `fruitstrap [-d] -b [device_id]` +* `fruitstrap [-d/--debug] [-i/--id device_id] -b/--bundle [-a/--args arguments] [-t/--timeout timeout(seconds)] [-u/--unbuffered] [-g/--gdbargs gdb_arguments]` * Optional `-d` flag launches a remote GDB session after the app has been installed. * `` must be an iPhone application bundle, *not* an IPA. -* Optional `device_id`; useful when you have more than one iPhone/iPad connected. +* Optional device id, useful when you have more than one iPhone/iPad connected to your computer +* `` are passed as argv to the running app. +* `` are passed to gdb. ## Demo diff --git a/apple.py b/apple.py new file mode 100755 index 00000000..a39b2855 --- /dev/null +++ b/apple.py @@ -0,0 +1,918 @@ +#!/usr/bin/python +# vim:ts=4 sts=4 sw=4 expandtab + +"""apple.py + +Manages and launches iOS applications on device. + +This is primarily intended to be used to run automated smoke tests of iOS +applications on non-jailbroken iOS devices. + +See "apple.py -h" for usage. + +A typical automated test might execute something like follow, to uninstall any +old versions of the app (-u), install the new one (-m), mount the developer +disk image (-m), run the app (-r), and pass the arguments "--smokeTest" to the +app (-a). + apple.py -b build/Example.app -u -i -m -r -a --smokeTest + +This will display all output from the app to standard out as it is running and +exit with the application's exit code. +""" + +__author__ = 'Cory McWilliams ' +__version__ = "1.0.0" + +__license__ = """ +Copyright (c) 2013 Cory McWilliams + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +__credits__ = """ +Made possible by: +1. fruitstrap from Greg Hughes: + Why didn't Apple just write this and save us all time? +2. idevice-app-runner for demonstrating that you don't need gdb to talk to + debugserver: +3. libimobiledevice for demonstrating how to make penguins talk to fruits: + +""" + +import argparse +import ctypes +import ctypes.macholib.dyld +import ctypes.util +import os +import plistlib +import socket +import subprocess +import sys +import time + +# CoreFoundation.framework + +if sys.platform == 'win32': + CoreFoundation = ctypes.CDLL('CoreFoundation.dll') +else: + CoreFoundation = ctypes.CDLL('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation') + +CFShow = CoreFoundation.CFShow +CFShow.argtypes = [ctypes.c_void_p] +CFShow.restype = None + +CFGetTypeID = CoreFoundation.CFGetTypeID +CFGetTypeID.argtypes = [ctypes.c_void_p] +CFGetTypeID.restype = ctypes.c_ulong + +CFStringRef = ctypes.c_void_p + +CFStringGetTypeID = CoreFoundation.CFStringGetTypeID +CFStringGetTypeID.argtypes = [] +CFStringGetTypeID.restype = ctypes.c_ulong + +CFDictionaryGetTypeID = CoreFoundation.CFDictionaryGetTypeID +CFDictionaryGetTypeID.argtypes = [] +CFDictionaryGetTypeID.restype = ctypes.c_ulong + +CFStringGetLength = CoreFoundation.CFStringGetLength +CFStringGetLength.argtypes = [CFStringRef] +CFStringGetLength.restype = ctypes.c_ulong + +CFCopyDescription = CoreFoundation.CFCopyDescription +CFCopyDescription.argtypes = [ctypes.c_void_p] +CFCopyDescription.restype = CFStringRef + +CFNumberGetValue = CoreFoundation.CFNumberGetValue +CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p] +CFNumberGetValue.restype = ctypes.c_bool + +kCFNumberSInt32Type = 3 + +CFRunLoopRun = CoreFoundation.CFRunLoopRun +CFRunLoopRun.argtypes = [] +CFRunLoopRun.restype = None + +CFRunLoopStop = CoreFoundation.CFRunLoopStop +CFRunLoopStop.argtypes = [ctypes.c_void_p] +CFRunLoopStop.restype = None + +CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent +CFRunLoopGetCurrent.argtype = [] +CFRunLoopGetCurrent.restype = ctypes.c_void_p + +cf_run_loop_timer_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p) + +CFRunLoopTimerCreate = CoreFoundation.CFRunLoopTimerCreate +CFRunLoopTimerCreate.argtypes = [ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.c_uint, ctypes.c_uint, cf_run_loop_timer_callback, ctypes.c_void_p] +CFRunLoopTimerCreate.restype = ctypes.c_void_p + +kCFRunLoopCommonModes = CFStringRef.in_dll(CoreFoundation, 'kCFRunLoopCommonModes') + +CFAbsoluteTimeGetCurrent = CoreFoundation.CFAbsoluteTimeGetCurrent +CFAbsoluteTimeGetCurrent.argtypes = [] +CFAbsoluteTimeGetCurrent.restype = ctypes.c_double + +CFRunLoopAddTimer = CoreFoundation.CFRunLoopAddTimer +CFRunLoopAddTimer.argtypes = [ctypes.c_void_p, ctypes.c_void_p, CFStringRef] +CFRunLoopAddTimer.restype = None + +CFRunLoopRemoveTimer = CoreFoundation.CFRunLoopRemoveTimer +CFRunLoopRemoveTimer.argtypes = [ctypes.c_void_p, ctypes.c_void_p, CFStringRef] +CFRunLoopRemoveTimer.restype = None + +CFDictionaryRef = ctypes.c_void_p + +class CFDictionaryKeyCallBacks(ctypes.Structure): + _fields_ = [ + ('version', ctypes.c_uint), + ('retain', ctypes.c_void_p), + ('release', ctypes.c_void_p), + ('copyDescription', ctypes.c_void_p), + ('equal', ctypes.c_void_p), + ('hash', ctypes.c_void_p), + ] + +class CFDictionaryValueCallBacks(ctypes.Structure): + _fields_ = [ + ('version', ctypes.c_uint), + ('retain', ctypes.c_void_p), + ('release', ctypes.c_void_p), + ('copyDescription', ctypes.c_void_p), + ('equal', ctypes.c_void_p), + ] + +CFDictionaryCreate = CoreFoundation.CFDictionaryCreate +CFDictionaryCreate.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_void_p), ctypes.c_int, ctypes.POINTER(CFDictionaryKeyCallBacks), ctypes.POINTER(CFDictionaryValueCallBacks)] +CFDictionaryCreate.restype = CFDictionaryRef + +CFDictionaryGetValue = CoreFoundation.CFDictionaryGetValue +CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFStringRef] +CFDictionaryGetValue.restype = ctypes.c_void_p + +CFDictionaryGetCount = CoreFoundation.CFDictionaryGetCount +CFDictionaryGetCount.argtypes = [CFDictionaryRef] +CFDictionaryGetCount.restype = ctypes.c_int + +CFDictionaryGetKeysAndValues = CoreFoundation.CFDictionaryGetKeysAndValues +CFDictionaryGetKeysAndValues.argtypes = [CFDictionaryRef, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_void_p)] +CFDictionaryGetKeysAndValues.restype = None + +kCFTypeDictionaryKeyCallBacks = CFDictionaryKeyCallBacks.in_dll(CoreFoundation, 'kCFTypeDictionaryKeyCallBacks') +kCFTypeDictionaryValueCallBacks = CFDictionaryValueCallBacks.in_dll(CoreFoundation, 'kCFTypeDictionaryValueCallBacks') + +CFDataRef = ctypes.c_void_p +CFDataCreate = CoreFoundation.CFDataCreate +CFDataCreate.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int] +CFDataCreate.restype = ctypes.c_void_p + +CFStringCreateWithCString = CoreFoundation.CFStringCreateWithCString +CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint] +CFStringCreateWithCString.restype = CFStringRef + +def CFStr(value): + return CFStringCreateWithCString(None, value, kCFStringEncodingUTF8) + +CFStringGetCStringPtr = CoreFoundation.CFStringGetCStringPtr +CFStringGetCStringPtr.argtypes = [CFStringRef, ctypes.c_uint] +CFStringGetCStringPtr.restype = ctypes.c_char_p + +CFStringGetCString = CoreFoundation.CFStringGetCString +CFStringGetCString.argtypes = [CFStringRef, ctypes.c_char_p, ctypes.c_uint, ctypes.c_uint] +CFStringGetCString.restype = ctypes.c_bool + +kCFStringEncodingUTF8 = 0x08000100 + +def CFStringGetStr(cfstr): + result = None + if cfstr: + result = CFStringGetCStringPtr(cfstr, kCFStringEncodingUTF8) + if not result: + length = CFStringGetLength(cfstr) * 2 + 1 + stringBuffer = ctypes.create_string_buffer(length) + if CFStringGetCString(cfstr, stringBuffer, length, kCFStringEncodingUTF8): + result = stringBuffer.value + else: + raise RuntimeError('Failed to convert string.') + return result + +def CFDictionaryToDict(dictionary): + count = CFDictionaryGetCount(dictionary) + keys = (ctypes.c_void_p * count)() + values = (ctypes.c_void_p * count)() + CFDictionaryGetKeysAndValues(dictionary, keys, values) + keys = [CFToPython(key) for key in keys] + values = [CFToPython(value) for value in values] + return dict(zip(keys, values)) + +def CFToPython(dataRef): + typeId = CFGetTypeID(dataRef) + if typeId == CFStringGetTypeID(): + return CFStringGetStr(dataRef) + elif typeId == CFDictionaryGetTypeID(): + return CFDictionaryToDict(dataRef) + else: + description = CFCopyDescription(dataRef) + return CFStringGetStr(description) + +# MobileDevice.Framework + +if sys.platform == 'win32': + MobileDevice = ctypes.CDLL('MobileDevice.dll') +else: + MobileDevice = ctypes.CDLL('/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice') + +AMDSetLogLevel = MobileDevice.AMDSetLogLevel +AMDSetLogLevel.argtypes = [ctypes.c_int] +AMDSetLogLevel.restype = None + +AMDSetLogLevel(5) + +am_device_p = ctypes.c_void_p + +class am_device_notification(ctypes.Structure): + pass + +class am_device_notification_callback_info(ctypes.Structure): + _fields_ = [ + ('dev', am_device_p), + ('msg', ctypes.c_uint), + ('subscription', ctypes.POINTER(am_device_notification)), + ] + +am_device_notification_callback = ctypes.CFUNCTYPE(None, ctypes.POINTER(am_device_notification_callback_info), ctypes.c_int) + +am_device_notification._fields_ = [ + ('unknown0', ctypes.c_uint), + ('unknown1', ctypes.c_uint), + ('unknown2', ctypes.c_uint), + ('callback', ctypes.c_void_p), + ('cookie', ctypes.c_uint), +] + +am_device_notification_p = ctypes.POINTER(am_device_notification) + +AMDeviceNotificationSubscribe = MobileDevice.AMDeviceNotificationSubscribe +AMDeviceNotificationSubscribe.argtypes = [am_device_notification_callback, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_void_p)] +AMDeviceNotificationSubscribe.restype = ctypes.c_uint + +AMDeviceNotificationUnsubscribe = MobileDevice.AMDeviceNotificationUnsubscribe +AMDeviceNotificationUnsubscribe.argtypes = [ctypes.c_void_p] +AMDeviceNotificationUnsubscribe.restype = ctypes.c_uint + +ADNCI_MSG_CONNECTED = 1 +ADNCI_MSG_DISCONNECTED = 2 +ADNCI_MSG_UNKNOWN = 3 + +AMDeviceCopyValue = MobileDevice.AMDeviceCopyValue +AMDeviceCopyValue.argtypes = [am_device_p, CFStringRef, CFStringRef] +AMDeviceCopyValue.restype = CFStringRef + +AMDeviceGetConnectionID = MobileDevice.AMDeviceGetConnectionID +AMDeviceGetConnectionID.argtypes = [am_device_p] +AMDeviceGetConnectionID.restype = ctypes.c_uint + +AMDeviceCopyDeviceIdentifier = MobileDevice.AMDeviceCopyDeviceIdentifier +AMDeviceCopyDeviceIdentifier.argtypes = [am_device_p] +AMDeviceCopyDeviceIdentifier.restype = CFStringRef + +AMDeviceConnect = MobileDevice.AMDeviceConnect +AMDeviceConnect.argtypes = [am_device_p] +AMDeviceConnect.restype = ctypes.c_uint + +AMDevicePair = MobileDevice.AMDevicePair +AMDevicePair.argtypes = [am_device_p] +AMDevicePair.restype = ctypes.c_uint + +AMDeviceIsPaired = MobileDevice.AMDeviceIsPaired +AMDeviceIsPaired.argtypes = [am_device_p] +AMDeviceIsPaired.restype = ctypes.c_uint + +AMDeviceValidatePairing = MobileDevice.AMDeviceValidatePairing +AMDeviceValidatePairing.argtypes = [am_device_p] +AMDeviceValidatePairing.restype = ctypes.c_uint + +AMDeviceStartSession = MobileDevice.AMDeviceStartSession +AMDeviceStartSession.argtypes = [am_device_p] +AMDeviceStartSession.restype = ctypes.c_uint + +AMDeviceStopSession = MobileDevice.AMDeviceStopSession +AMDeviceStopSession.argtypes = [am_device_p] +AMDeviceStopSession.restype = ctypes.c_uint + +AMDeviceDisconnect = MobileDevice.AMDeviceDisconnect +AMDeviceDisconnect.argtypes = [am_device_p] +AMDeviceDisconnect.restype = ctypes.c_uint + +am_device_mount_image_callback = ctypes.CFUNCTYPE(ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p) + +try: + AMDeviceMountImage = MobileDevice.AMDeviceMountImage + AMDeviceMountImage.argtypes = [am_device_p, CFStringRef, CFDictionaryRef, am_device_mount_image_callback, ctypes.c_void_p] + AMDeviceMountImage.restype = ctypes.c_uint +except AttributeError: + # AMDeviceMountImage is missing on win32. + AMDeviceMountImage = None + +AMDeviceStartService = MobileDevice.AMDeviceStartService +AMDeviceStartService.argtypes = [am_device_p, CFStringRef, ctypes.POINTER(ctypes.c_int), ctypes.c_void_p] +AMDeviceStartService.restype = ctypes.c_uint + +am_device_install_application_callback = ctypes.CFUNCTYPE(ctypes.c_uint, CFDictionaryRef, ctypes.c_void_p) + +AMDeviceTransferApplication = MobileDevice.AMDeviceTransferApplication +AMDeviceTransferApplication.argtypes = [ctypes.c_int, CFStringRef, CFDictionaryRef, am_device_install_application_callback, ctypes.c_void_p] +AMDeviceTransferApplication.restype = ctypes.c_uint + +AMDeviceInstallApplication = MobileDevice.AMDeviceInstallApplication +AMDeviceInstallApplication.argtypes = [ctypes.c_int, CFStringRef, CFDictionaryRef, am_device_install_application_callback, ctypes.c_void_p] +AMDeviceInstallApplication.restype = ctypes.c_uint + +AMDeviceUninstallApplication = MobileDevice.AMDeviceUninstallApplication +AMDeviceUninstallApplication.argtypes = [ctypes.c_int, CFStringRef, CFDictionaryRef, am_device_install_application_callback, ctypes.c_void_p] +AMDeviceUninstallApplication.restype = ctypes.c_uint + +AMDeviceLookupApplications = MobileDevice.AMDeviceLookupApplications +AMDeviceLookupApplications.argtypes = [am_device_p, ctypes.c_uint, ctypes.POINTER(CFDictionaryRef)] +AMDeviceLookupApplications.restype = ctypes.c_uint + +# ws2_32.dll + +if sys.platform == 'win32': + ws2_32 = ctypes.WinDLL('ws2_32.dll') + + socket_close = ws2_32.closesocket + socket_close.argtypes = [ctypes.c_uint] + socket_close.restype = ctypes.c_int + + socket_recv = ws2_32.recv + socket_recv.argtypes = [ctypes.c_uint, ctypes.c_char_p, ctypes.c_int, ctypes.c_int] + socket_recv.restype = ctypes.c_int + + socket_send = ws2_32.send + socket_send.argtypes = [ctypes.c_uint, ctypes.c_char_p, ctypes.c_int, ctypes.c_int] + socket_send.restype = ctypes.c_int + + socket_setsockopt = ws2_32.setsockopt + socket_setsockopt.argtypes = [ctypes.c_uint, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_int] + socket_setsockopt.restype = ctypes.c_int + + SOL_SOCKET = 0xffff + SO_SNDTIMEO = 0x1005 + SO_RCVTIMEO = 0x1006 + + class MockSocket(object): + """ + Python doesn't provide a way to get a socket-like object from a socket + descriptor, so this implements just enough of the interface for what we + need. + """ + + def __init__(self, socketDescriptor): + self._socket = socketDescriptor + + def send(self, data): + return socket_send(self._socket, data, len(data), 0) + + def sendall(self, data): + while data: + result = self.send(data) + if result < 0: + raise RuntimeError('Error sending data: %d' % result) + data = data[result:] + + def recv(self, bytes): + data = ctypes.create_string_buffer(bytes) + result = socket_recv(self._socket, data, bytes, 0) + if result < 0: + raise RuntimeError('Error receiving data: %d' % result) + return data.raw[:result] + + def close(self): + socket_close(self._socket) + self._socket = None + + def settimeout(self, timeout): + ms = int(timeout * 1000) + value = ctypes.c_int(ms) + e = socket_setsockopt(self._socket, SOL_SOCKET, SO_SNDTIMEO, ctypes.byref(value), 4) + if e != 0: + raise RuntimeError('setsockopt returned %d' % e) + e = socket_setsockopt(self._socket, SOL_SOCKET, SO_RCVTIMEO, ctypes.byref(value), 4) + if e != 0: + raise RuntimeError('setsockopt returned %d' % e) + +# Finally, the good stuff. + +class MobileDeviceManager(object): + """ + Presents interesting parts of Apple's MobileDevice framework as a much more + Python-friendly way. + + Usage is generally like this: + mdm = MobileDeviceManager() + mdm.waitForDevice() + mdm.connect() + + # do things with the connected device... + mdm.installApplication('build/MyApp.app') + + mdm.disconnect() + mdm.close() + """ + + def __init__(self): + self._device = None + self._notification = None + + self._transferCallback = am_device_install_application_callback(self._transfer) + self._installCallback = am_device_install_application_callback(self._install) + self._uninstallCallback = am_device_install_application_callback(self._uninstall) + self._timerCallback = cf_run_loop_timer_callback(self._timer) + + def close(self): + if self._device: + self._device = None + if self._notification: + AMDeviceNotificationUnsubscribe(self._notification) + self._notification = None + + def connect(self): + e = AMDeviceConnect(self._device) + if e != 0: + raise RuntimeError('AMDeviceConnect returned %d' % e) + + if not self.isPaired(): + self.pair() + self.validatePairing() + + def disconnect(self): + e = AMDeviceDisconnect(self._device) + if e != 0: + raise RuntimeError('AMDeviceDisconnect returned %d' % e) + + def pair(self): + e = AMDevicePair(self._device) + if e != 0: + raise RuntimeError('AMDevicePair returned %d' % e) + + def isPaired(self): + return AMDeviceIsPaired(self._device) != 0 + + def validatePairing(self): + e = AMDeviceValidatePairing(self._device) + if e != 0: + raise RuntimeError('AMDeviceValidatePairing returned %d' % e) + + def startSession(self): + e = AMDeviceStartSession(self._device) + if e != 0: + raise RuntimeError('AMDeviceStartSession returned %d' % e) + + def stopSession(self): + e = AMDeviceStopSession(self._device) + if e != 0: + raise RuntimeError('AMDeviceStopSession returned %d' % e) + + def waitForDevice(self, timeout=0): + self._notification = ctypes.c_void_p() + self._notificationCallback = am_device_notification_callback(self._deviceNotification) + e = AMDeviceNotificationSubscribe(self._notificationCallback, 0, 0, 0, ctypes.byref(self._notification)) + if e != 0: + raise RuntimeError('AMDeviceNotificationSubscribe returned %d' % e) + + if timeout > 0: + timer = CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent() + timeout, 0, 0, 0, self._timerCallback, None) + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes) + + CFRunLoopRun() + if timeout > 0: + CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes) + return self._device + + def productVersion(self): + self.connect() + try: + return CFStringGetStr(AMDeviceCopyValue(self._device, None, CFStr("ProductVersion"))) + finally: + self.disconnect() + + def buildVersion(self): + self.connect() + try: + return CFStringGetStr(AMDeviceCopyValue(self._device, None, CFStr("BuildVersion"))) + finally: + self.disconnect() + + def connectionId(self): + return AMDeviceGetConnectionID(self._device) + + def deviceId(self): + return CFStringGetStr(AMDeviceCopyDeviceIdentifier(self._device)) + + def mountImage(self, imagePath): + self.connect() + try: + self.startSession() + try: + signature = open(imagePath + '.signature', 'rb').read() + signature = CFDataCreate(None, signature, len(signature)) + items = 2 + + keys = (ctypes.c_void_p * items)(CFStr('ImageSignature'), CFStr('ImageType')) + values = (ctypes.c_void_p * items)(signature, CFStr('Developer')) + + options = CFDictionaryCreate(None, keys, values, items, ctypes.byref(kCFTypeDictionaryKeyCallBacks), ctypes.byref(kCFTypeDictionaryValueCallBacks)) + self._mountCallback = am_device_mount_image_callback(self._mount) + e = AMDeviceMountImage(self._device, CFStr(imagePath), options, self._mountCallback, None) + if e == 0: + return True + elif e == 0xe8000076: + # already mounted + return False + else: + raise RuntimeError('AMDeviceMountImage returned %d' % e) + finally: + self.stopSession() + finally: + self.disconnect() + + def startService(self, service): + self.connect() + try: + self.startSession() + try: + fd = ctypes.c_int() + e = AMDeviceStartService(self._device, CFStr(service), ctypes.byref(fd), None) + if e != 0: + raise RuntimeError('AMDeviceStartService returned %d' % e) + return fd.value + finally: + self.stopSession() + finally: + self.disconnect() + + def bundleId(self, path): + plist = plistlib.readPlist(os.path.join(path, 'Info.plist')) + return plist['CFBundleIdentifier'] + + def bundleExecutable(self, path): + plist = plistlib.readPlist(os.path.join(path, 'Info.plist')) + return plist['CFBundleExecutable'] + + def transferApplication(self, path): + afc = self.startService("com.apple.afc") + try: + e = AMDeviceTransferApplication(afc, CFStr(os.path.abspath(path)), None, self._transferCallback, None) + if e != 0: + raise RuntimeError('AMDeviceTransferApplication returned %d' % e) + finally: + self.stopService(afc) + + def installApplication(self, path): + afc = mdm.startService("com.apple.mobile.installation_proxy") + try: + + items = 1 + keys = (ctypes.c_void_p * items)(CFStr('PackageType')) + values = (ctypes.c_void_p * items)(CFStr('Developer')) + + options = CFDictionaryCreate(None, keys, values, items, ctypes.byref(kCFTypeDictionaryKeyCallBacks), ctypes.byref(kCFTypeDictionaryValueCallBacks)) + + e = AMDeviceInstallApplication(afc, CFStr(path), options, self._installCallback, None) + if e != 0: + raise RuntimeError('AMDeviceInstallApplication returned %d' % e) + finally: + mdm.stopService(afc) + + def uninstallApplication(self, bundleId): + afc = self.startService("com.apple.mobile.installation_proxy") + try: + e = AMDeviceUninstallApplication(afc, CFStr(bundleId), None, self._uninstallCallback, None) + if e != 0: + raise RuntimeError('AMDeviceUninstallApplication returned %d' % e) + finally: + self.stopService(afc) + + items = 1 + + def lookupApplications(self): + self.connect() + try: + self.startSession() + try: + dictionary = CFDictionaryRef() + e = AMDeviceLookupApplications(self._device, 0, ctypes.byref(dictionary)) + if e != 0: + raise RuntimeError('AMDeviceLookupApplications returned %d' % e) + return CFDictionaryToDict(dictionary) + finally: + self.stopSession() + finally: + self.disconnect() + + def lookupApplicationExecutable(self, identifier): + dictionary = self.lookupApplications() + try: + return '%s/%s' % (dictionary[identifier]['Path'], dictionary[identifier]['CFBundleExecutable']) + except KeyError: + raise RuntimeError('%s not found in app list.' % identifier) + + def stopService(self, fd): + if sys.platform == 'win32': + closesocket(fd) + else: + os.close(fd) + + def showStatus(self, action, dictionary): + show = ['[%s]' % action] + + percentComplete = CFDictionaryGetValue(dictionary, CFStr('PercentComplete')) + if percentComplete: + percent = ctypes.c_int() + CFNumberGetValue(percentComplete, kCFNumberSInt32Type, ctypes.byref(percent)) + show.append(str.rjust('%d%%' % percent.value, 4)) + + show.append(CFStringGetStr(CFDictionaryGetValue(dictionary, CFStr('Status')))) + + path = CFDictionaryGetValue(dictionary, CFStr('Path')) + if path: + show.append(CFStringGetStr(path)) + + print ' '.join(show) + + def debugServer(self): + service = self.startService('com.apple.debugserver') + if sys.platform == 'win32': + return MockSocket(service) + else: + return socket.fromfd(service, socket.AF_INET, socket.SOCK_STREAM) + + def _timer(self, timer, info): + CFRunLoopStop(CFRunLoopGetCurrent()) + + def _transfer(self, dictionary, user): + self.showStatus('Transferring', dictionary) + return 0 + + def _install(self, dictionary, user): + self.showStatus('Installing', dictionary) + return 0 + + def _uninstall(self, dictionary, user): + self.showStatus('Uninstalling', dictionary) + return 0 + + def _mount(self, dictionary, user): + self.showStatus('Mounting', dictionary) + return 0 + + def _deviceNotification(self, info, user): + info = info.contents + if info.msg == ADNCI_MSG_CONNECTED: + self._device = ctypes.c_void_p(info.dev) + CFRunLoopStop(CFRunLoopGetCurrent()) + elif info.msg == ADNCI_MSG_DISCONNECTED: + self._device = None + elif info.msg == ADNCI_MSG_UNKNOWN: + # This happens as we're closing. + pass + else: + raise RuntimeError('Unexpected device notification status: %d' % info.msg) + +class DeviceSupportPaths(object): + """ + A small helper for finding various Xcode directories. + + Written from fruitstrap.c, trial and error, and lldb's + PlatformRemoteiOS.cpp: + + """ + def __init__(self, target, productVersion, buildVersion): + self._target = target + self._productVersion = productVersion + self._buildVersion = buildVersion + + self._deviceSupportDirectory = None + self._deviceSupportForOsVersion = None + self._developerDiskImagePath = None + + def deviceSupportDirectory(self): + if not self._deviceSupportDirectory: + self._deviceSupportDirectory = subprocess.check_output(['xcode-select', '-print-path']).strip() + return self._deviceSupportDirectory + + def deviceSupportDirectoryForOsVersion(self): + if not self._deviceSupportForOsVersion: + path = os.path.join(self.deviceSupportDirectory(), 'Platforms', self._target + '.platform', 'DeviceSupport') + + attempts = [os.path.join(path, attempt) for attempt in self.versionPermutations()] + + for attempt in attempts: + if os.path.exists(attempt): + self._deviceSupportForOsVersion = attempt + break + if not self._deviceSupportForOsVersion: + raise RuntimeError('Could not find device support directory for %s %s (%s).' % (self._target, self._productVersion, self._buildVersion)) + return self._deviceSupportForOsVersion + + def versionPermutations(self): + shortProductVersion = '.'.join(self._productVersion.split('.')[:2]) + return [ + '%s (%s)' % (self._productVersion, self._buildVersion), + '%s (%s)' % (shortProductVersion, self._buildVersion), + '%s' % self._productVersion, + '%s' % shortProductVersion, + 'Latest', + ] + + def developerDiskImagePath(self): + if not self._developerDiskImagePath: + path = os.path.join(self.deviceSupportDirectory(), 'Platforms', self._target + '.platform', 'DeviceSupport') + attempts = [os.path.join(path, attempt, 'DeveloperDiskImage.dmg') for attempt in self.versionPermutations()] + for attempt in attempts: + if os.path.exists(attempt): + self._developerDiskImagePath = attempt + break + if not self._developerDiskImagePath: + raise RuntimeError('Could not find developer disk image for %s %s (%s).' % (self._target, self._productVersion, self._buildVersion)) + return self._developerDiskImagePath + +class DebuggerException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return str(self.value) + +class GdbServer(object): + """ + Given a socket connected to a remote debugserver, this speaks just enough + of the GDB Remote Serial Protocol + to launch + an application and display its output. + + Usage: + GdbServer(connectedSocket).run('/path/to/executable', 'arg1', 'arg2') + """ + def __init__(self, connectedSocket): + self._socket = connectedSocket + self.exitCode = None + self._readBuffer = '' + + def read(self): + startIndex = self._readBuffer.find('$') + endIndex = self._readBuffer.find('#', startIndex) + while startIndex == -1 or endIndex == -1 or len(self._readBuffer) < endIndex + 3: + data = self._socket.recv(4096) + if not data: + break + self._readBuffer += data + startIndex = self._readBuffer.find('$') + endIndex = self._readBuffer.find('#', startIndex) + + # Discard any ACKs. We trust we're on a reliable connection. + while self._readBuffer.startswith('+'): + self._readBuffer = self._readBuffer[1:] + + payload = None + startIndex = self._readBuffer.find('$') + endIndex = self._readBuffer.find('#', startIndex) + if startIndex != -1 and endIndex != -1 and len(self._readBuffer) >= endIndex + 3: + payload = self._readBuffer[startIndex + 1:endIndex] + checksum = self._readBuffer[endIndex + 1:endIndex + 3] + if checksum != '00': + calculated = '%02x' % (sum(ord(c) for c in payload) & 255) + if checksum != calculated: + raise RuntimeError('Bad response checksum (%s vs %s).' % (checksum, calculated)) + + self._readBuffer = self._readBuffer[endIndex + 3:] + + return payload + + def send(self, packet): + data = '$%s#%02x' % (packet, sum(ord(c) for c in packet) & 255) + self._socket.sendall(data) + stopReply = [True for command in ['C', 'c', 'S', 's', 'vCont', 'vAttach', 'vRun', 'vStopped', '?'] if packet.startswith(command)] + + if stopReply: + resume = True + while resume: + resume = False + response = self.read() + if response: + if response.startswith('S'): + signal = '0x' + response[1:3] + message = 'Program received signal %s.' % signal + raise DebuggerException(message) + elif response.startswith('T'): + signal = '0x' + response[1:3] + message = 'Program received signal %s.' % signal + for pair in response[4:].split(';'): + message += '\n%s' % pair + raise DebuggerException(message) + elif response.startswith('W'): + self.exitCode = int(response[1:], 16) + print 'Process returned %d.' % self.exitCode + elif response.startswith('X'): + signal = '0x' + response[1:3] + if ';' in response: + response = response.split(';', 1)[1] + raise DebuggerException('Process terminated with signal %s (%s).' % (signal, response)) + elif response.startswith('O'): + print response[1:].decode('hex'), + resume = True + elif response.startswith('F'): + raise RuntimeError('GDB File-I/O Remote Protocol Unimplemented.') + else: + raise RuntimeError('Unexpected response to stop reply packet: ' + response) + else: + response = self.read() + return response + + def run(self, *argv): + self.send('QStartNoAckMode') + self._socket.sendall('+') + self.send('QEnvironmentHexEncoded:') + self.send('QSetDisableASLR:1') + self.send('A' + ','.join('%d,%d,%s' % (len(arg) * 2, i, arg.encode('hex')) for i, arg in enumerate(argv))) + self.send('qLaunchSuccess') + self.send('vCont;c') + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Manage and launch applications on iOS.') + parser.add_argument('-i', '--install', action='store_true', help='install an application') + parser.add_argument('-r', '--run', action='store_true', help='run an application') + parser.add_argument('-u', '--uninstall', action='store_true', help='uninstall an application') + parser.add_argument('-m', '--mount', action='store_true', help='mount developer disk image (must be done at least once to run, not supported on Windows)') + parser.add_argument('-l', '--list-applications', action='store_true', help='list installed applications') + + parser.add_argument('-b', '--bundle', help='path to local app bundle [to install]') + parser.add_argument('-id', '--appid', help='application identifier [to run or uninstall]') + parser.add_argument('-a', '--arguments', nargs=argparse.REMAINDER, help='arguments to pass to application being run') + parser.add_argument('-t', '--timeout', type=float, help='seconds to wait for slow operations before giving up') + + arguments = parser.parse_args() + + if not arguments.install and not arguments.uninstall and not arguments.run and not arguments.list_applications and not arguments.mount: + print 'Nothing to do.' + sys.exit(0) + + mdm = MobileDeviceManager() + print 'Waiting for a device...' + if not mdm.waitForDevice(timeout=arguments.timeout): + print 'Gave up waiting for a device.' + sys.exit(1) + + print 'Connected to device with UDID:', mdm.deviceId() + + if arguments.uninstall: + bundle = arguments.appid or mdm.bundleId(arguments.bundle) + print '\nUninstalling %s...' % bundle + mdm.uninstallApplication(bundle) + + if arguments.install: + print '\nInstalling %s...' % arguments.bundle + mdm.transferApplication(arguments.bundle) + mdm.installApplication(arguments.bundle) + + if arguments.list_applications: + print '\nInstalled applications:' + applications = mdm.lookupApplications() + bundleIdentifiers = applications.keys() + bundleIdentifiers.sort() + for bundleId in bundleIdentifiers: + print bundleId + + if arguments.mount: + ddi = DeviceSupportPaths('iPhoneOS', mdm.productVersion(), mdm.buildVersion()).developerDiskImagePath() + print '\nMounting %s...' % ddi + mdm.mountImage(ddi) + + if arguments.run: + executable = mdm.lookupApplicationExecutable(arguments.appid or mdm.bundleId(arguments.bundle)) + db = mdm.debugServer() + if arguments.timeout > 0: + db.settimeout(arguments.timeout) + debugger = GdbServer(db) + argv = [executable] + if arguments.arguments: + argv += arguments.arguments + print '\nRunning %s...' % ' '.join(argv) + try: + debugger.run(*argv) + except DebuggerException, e: + print e + sys.exit(1) + sys.exit(debugger.exitCode) + mdm.close() diff --git a/fruitstrap.c b/fruitstrap.c index 6693f515..94c236a6 100644 --- a/fruitstrap.c +++ b/fruitstrap.c @@ -4,24 +4,25 @@ #include #include #include +#include #include #include #include +#include #include "MobileDevice.h" #define FDVENDOR_PATH "/tmp/fruitstrap-remote-debugserver" #define PREP_CMDS_PATH "/tmp/fruitstrap-gdb-prep-cmds" -#define GDB_SHELL "/Developer/Platforms/iPhoneOS.platform/Developer/usr/libexec/gdb/gdb-arm-apple-darwin --arch armv7 -q -x " PREP_CMDS_PATH +#define GDB_SHELL "--arch armv7f -x " PREP_CMDS_PATH // approximation of what Xcode does: -#define GDB_PREP_CMDS CFSTR("set mi-show-protections off\n\ +#define GDB_PREP_CMDS "set mi-show-protections off\n\ set auto-raise-load-levels 1\n\ set shlib-path-substitutions /usr \"{ds_path}/Symbols/usr\" /System \"{ds_path}/Symbols/System\" \"{device_container}\" \"{disk_container}\" \"/private{device_container}\" \"{disk_container}\" /Developer \"{ds_path}/Symbols/Developer\"\n\ set remote max-packet-size 1024\n\ - set sharedlibrary check-uuids on\n\ + set sharedlibrary check-uuids off\n\ set env NSUnbufferedIO YES\n\ set minimal-signal-handling 1\n\ - set sharedlibrary load-rules \\\".*\\\" \\\".*\\\" container\n\ set inferior-auto-start-dyld 0\n\ file \"{disk_app}\"\n\ set remote executable-directory {device_app}\n\ @@ -34,9 +35,9 @@ run {args}\n\ set minimal-signal-handling 0\n\ set inferior-auto-start-cfm off\n\ - set sharedLibrary load-rules dyld \".*libobjc.*\" all dyld \".*CoreFoundation.*\" all dyld \".*Foundation.*\" all dyld \".*libSystem.*\" all dyld \".*AppKit.*\" all dyld \".*PBGDBIntrospectionSupport.*\" all dyld \".*/usr/lib/dyld.*\" all dyld \".*CarbonDataFormatters.*\" all dyld \".*libauto.*\" all dyld \".*CFDataFormatters.*\" all dyld \"/System/Library/Frameworks\\\\\\\\|/System/Library/PrivateFrameworks\\\\\\\\|/usr/lib\" extern dyld \".*\" all exec \".*\" all\n\ + set sharedLibrary load-rules dyld \".*\" none exec \".*\" none\n\ sharedlibrary apply-load-rules all\n\ - set inferior-auto-start-dyld 1") + set inferior-auto-start-dyld 1" typedef struct am_device * AMDeviceRef; int AMDeviceSecureTransferPath(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); @@ -44,13 +45,16 @@ int AMDeviceSecureInstallApplication(int zero, AMDeviceRef device, CFURLRef url, int AMDeviceMountImage(AMDeviceRef device, CFStringRef image, CFDictionaryRef options, void *callback, int cbarg); int AMDeviceLookupApplications(AMDeviceRef device, int zero, CFDictionaryRef* result); -bool found_device = false, debug = false, verbose = false; +bool found_device = false, debug = false, verbose = false, unbuffered = false, nostart = false; char *app_path = NULL; char *device_id = NULL; char *args = NULL; +char *gdb_args = ""; int timeout = 0; CFStringRef last_path = NULL; service_conn_t gdbfd; +pid_t parent = 0; +pid_t child = 0; Boolean path_exists(CFTypeRef path) { if (CFGetTypeID(path) == CFStringGetTypeID()) { @@ -65,87 +69,180 @@ Boolean path_exists(CFTypeRef path) { } } -CFStringRef copy_device_support_path(AMDeviceRef device) { - CFStringRef version = AMDeviceCopyValue(device, 0, CFSTR("ProductVersion")); - CFStringRef build = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); - const char* home = getenv("HOME"); - CFStringRef path; - bool found = false; - - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/iOS DeviceSupport/%@ (%@)"), home, version, build); - found = path_exists(path); - - if (!found) - { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Developer/Platforms/iPhoneOS.platform/DeviceSupport/%@ (%@)"), version, build); - found = path_exists(path); +CFStringRef find_path(CFStringRef rootPath, CFStringRef namePattern, CFStringRef expression) { + FILE *fpipe = NULL; + CFStringRef quotedRootPath = rootPath; + if (CFStringGetCharacterAtIndex(rootPath, 0) != '`') { + quotedRootPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("'%@'"), rootPath); } - if (!found) - { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/iOS DeviceSupport/%@"), home, version); - found = path_exists(path); + CFStringRef cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find %@ -name '%@' %@ 2>/dev/null | sort | tail -n 1"), quotedRootPath, namePattern, expression); + if (quotedRootPath != rootPath) { + CFRelease(quotedRootPath); } - if (!found) - { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Developer/Platforms/iPhoneOS.platform/DeviceSupport/%@"), version); - found = path_exists(path); - } - if (!found) + + char command[1024] = { '\0' }; + CFStringGetCString(cf_command, command, sizeof(command), kCFStringEncodingUTF8); + CFRelease(cf_command); + + if (!(fpipe = (FILE *)popen(command, "r"))) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/%@"), version); - found = path_exists(path); + perror("Error encountered while opening pipe"); + exit(EXIT_FAILURE); } - CFRelease(version); - CFRelease(build); + char buffer[256] = { '\0' }; - if (!found) - { - CFRelease(path); - printf("[ !! ] Unable to locate DeviceSupport directory.\n"); - exit(1); + fgets(buffer, sizeof(buffer), fpipe); + pclose(fpipe); + + strtok(buffer, "\n"); + return CFStringCreateWithCString(NULL, buffer, kCFStringEncodingUTF8); +} + +CFStringRef copy_long_shot_disk_image_path() { + return find_path(CFSTR("`xcode-select --print-path`"), CFSTR("DeveloperDiskImage.dmg"), CFSTR("")); +} + +CFStringRef copy_xcode_dev_path() { + static char xcode_dev_path[256] = { '\0' }; + if (strlen(xcode_dev_path) == 0) { + FILE *fpipe = NULL; + char *command = "xcode-select -print-path"; + + if (!(fpipe = (FILE *)popen(command, "r"))) + { + perror("Error encountered while opening pipe"); + exit(EXIT_FAILURE); + } + + char buffer[256] = { '\0' }; + + fgets(buffer, sizeof(buffer), fpipe); + pclose(fpipe); + + strtok(buffer, "\n"); + strcpy(xcode_dev_path, buffer); } + return CFStringCreateWithCString(NULL, xcode_dev_path, kCFStringEncodingUTF8); +} - return path; +const char *get_home() { + const char* home = getenv("HOME"); + if (!home) { + struct passwd *pwd = getpwuid(getuid()); + home = pwd->pw_dir; + } + return home; } -CFStringRef copy_developer_disk_image_path(AMDeviceRef device) { - CFStringRef version = AMDeviceCopyValue(device, 0, CFSTR("ProductVersion")); - CFStringRef build = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); - const char *home = getenv("HOME"); +CFStringRef copy_xcode_path_for(CFStringRef subPath, CFStringRef search) { + CFStringRef xcodeDevPath = copy_xcode_dev_path(); CFStringRef path; bool found = false; + const char* home = get_home(); - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/iOS DeviceSupport/%@ (%@)/DeveloperDiskImage.dmg"), home, version, build); - found = path_exists(path); - + + // Try using xcode-select --print-path if (!found) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Developer/Platforms/iPhoneOS.platform/DeviceSupport/%@ (%@/DeveloperDiskImage.dmg)"), version, build); + path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@/%@"), xcodeDevPath, subPath, search); found = path_exists(path); } + // Try find `xcode-select --print-path` with search as a name pattern if (!found) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/iOS DeviceSupport/@%/DeveloperDiskImage.dmg"), home, version); - found = path_exists(path); + path = find_path(CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), xcodeDevPath, subPath), search, CFSTR("-maxdepth 1")); + found = CFStringGetLength(path) > 0 && path_exists(path); } + // If not look in the default xcode location (xcode-select is sometimes wrong) if (!found) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Developer/Platforms/iPhoneOS.platform/DeviceSupport/@%/DeveloperDiskImage.dmg"), version); + path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Applications/Xcode.app/Contents/Developer/%@&%@"), subPath, search); found = path_exists(path); } + // If not look in the users home directory, Xcode can store device support stuff there if (!found) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/iOS DeviceSupport/Latest/DeveloperDiskImage.dmg"), home); + path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode/%@/%@"), home, subPath, search); found = path_exists(path); } - if (!found) { - path = CFStringCreateWithFormat(NULL, NULL, CFSTR("/Developer/Platforms/iPhoneOS.platform/DeviceSupport/Latest/DeveloperDiskImage.dmg")); - found = path_exists(path); + + CFRelease(xcodeDevPath); + + if (found) { + return path; + } else { + CFRelease(path); + return NULL; } +} +CFMutableArrayRef get_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 version = NULL; + CFStringRef build = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); + CFStringRef path = NULL; + CFMutableArrayRef version_parts = get_device_product_version_parts(device); + + while (CFArrayGetCount(version_parts) > 0) { + version = CFStringCreateByCombiningStrings(NULL, version_parts, CFSTR(".")); + if (path == NULL) { + path = copy_xcode_path_for(CFSTR("iOS DeviceSupport"), CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@)"), version, build)); + } + if (path == NULL) { + path = copy_xcode_path_for(CFSTR("Platforms/iPhoneOS.platform/DeviceSupport"), CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@)"), version, build)); + } + if (path == NULL) { + path = copy_xcode_path_for(CFSTR("Platforms/iPhoneOS.platform/DeviceSupport"), CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (*)"), version, build)); + } + if (path == NULL) { + path = copy_xcode_path_for(CFSTR("Platforms/iPhoneOS.platform/DeviceSupport"), version); + } + if (path == NULL) { + path = copy_xcode_path_for(CFSTR("Platforms/iPhoneOS.platform/DeviceSupport/Latest"), CFSTR("")); + } + CFRelease(version); + if (path != NULL) { + break; + } + CFArrayRemoveValueAtIndex(version_parts, CFArrayGetCount(version_parts) - 1); + } + + CFRelease(version_parts); CFRelease(build); - if (!found) { + if (path == NULL) + { + printf("[ !! ] Unable to locate DeviceSupport directory.\n[ !! ] This probably means you don't have Xcode installed, you will need to launch the app manually and logging output will not be shown!\n"); + exit(1); + } + + return path; +} + +CFStringRef copy_developer_disk_image_path(CFStringRef deviceSupportPath) { + CFStringRef path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), deviceSupportPath, CFSTR("DeveloperDiskImage.dmg")); + if (!path_exists(path)) { CFRelease(path); - printf("[ !! ] Unable to locate DeviceSupport directory containing DeveloperDiskImage.dmg.\n"); + path = NULL; + } + + if (path == NULL) { + // Sometimes Latest seems to be missing in Xcode, in that case use find and hope for the best + path = copy_long_shot_disk_image_path(); + if (CFStringGetLength(path) < 5) { + CFRelease(path); + path = NULL; + } + } + + if (path == NULL) + { + printf("[ !! ] Unable to locate DeveloperDiskImage.dmg.\n[ !! ] This probably means you don't have Xcode installed, you will need to launch the app manually and logging output will not be shown!\n"); exit(1); } @@ -166,18 +263,14 @@ void mount_callback(CFDictionaryRef dict, int arg) { void mount_developer_image(AMDeviceRef device) { CFStringRef ds_path = copy_device_support_path(device); - CFStringRef image_path = copy_developer_disk_image_path(device); + CFStringRef image_path = copy_developer_disk_image_path(ds_path); CFStringRef sig_path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@.signature"), image_path); - CFRelease(ds_path); if (verbose) { - printf("Device support path: "); - fflush(stdout); - CFShow(ds_path); - printf("Developer disk image: "); - fflush(stdout); - CFShow(image_path); + printf("Device support path: %s\n", CFStringGetCStringPtr(ds_path, CFStringGetSystemEncoding())); + printf("Developer disk image: %s\n", CFStringGetCStringPtr(image_path, CFStringGetSystemEncoding())); } + CFRelease(ds_path); FILE* sig = fopen(CFStringGetCStringPtr(sig_path, kCFStringEncodingMacRoman), "rb"); void *sig_buf = malloc(128); @@ -205,7 +298,7 @@ void mount_developer_image(AMDeviceRef device) { CFRelease(options); } -void transfer_callback(CFDictionaryRef dict, int arg) { +mach_error_t transfer_callback(CFDictionaryRef dict, int arg) { int percent; CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); @@ -222,14 +315,17 @@ void transfer_callback(CFDictionaryRef dict, int arg) { } last_path = CFStringCreateCopy(NULL, path); } + + return 0; } -void install_callback(CFDictionaryRef dict, int arg) { +mach_error_t install_callback(CFDictionaryRef dict, int arg) { int percent; CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); printf("[%3d%%] %s\n", (percent / 2) + 50, CFStringGetCStringPtr(status, kCFStringEncodingMacRoman)); + return 0; } void fdvendor_callback(CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) { @@ -298,7 +394,12 @@ CFStringRef copy_disk_app_identifier(CFURLRef disk_app_url) { } void write_gdb_prep_cmds(AMDeviceRef device, CFURLRef disk_app_url) { - CFMutableStringRef cmds = CFStringCreateMutableCopy(NULL, 0, GDB_PREP_CMDS); + CFMutableStringRef cmds = NULL; + if (nostart) { + cmds = CFStringCreateMutableCopy(NULL, 0, CFSTR(GDB_PREP_CMDS)); + } else { + cmds = CFStringCreateMutableCopy(NULL, 0, CFSTR(GDB_PREP_CMDS "\ncontinue\nquit")); + } CFRange range = { 0, CFStringGetLength(cmds) }; CFStringRef ds_path = copy_device_support_path(device); @@ -378,9 +479,60 @@ void start_remote_debug_server(AMDeviceRef device) { CFRunLoopAddSource(CFRunLoopGetMain(), CFSocketCreateRunLoopSource(NULL, fdvendor, 0), 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, len / sizeof(struct kinfo_proc)); + + free(kp); + return 0; +} + +void killed(int signum) { + kill_ptree(parent, SIGTERM); + _exit(0); +} + +void interrupted(int signum) { + if (getpid() == parent) { + kill(child, SIGINT); + } else { + kill_ptree(child, SIGSTOP); + } +} + void gdb_ready_handler(int signum) { - _exit(0); + _exit(0); } void handle_device(AMDeviceRef device) { @@ -413,7 +565,7 @@ void handle_device(AMDeviceRef device) { CFRelease(relative_url); - int afcFd; + service_conn_t afcFd; assert(AMDeviceStartService(device, CFSTR("com.apple.afc"), &afcFd, NULL) == 0); assert(AMDeviceStopSession(device) == 0); assert(AMDeviceDisconnect(device) == 0); @@ -430,7 +582,7 @@ void handle_device(AMDeviceRef device) { assert(AMDeviceValidatePairing(device) == 0); assert(AMDeviceStartSession(device) == 0); - int installFd; + service_conn_t installFd; assert(AMDeviceStartService(device, CFSTR("com.apple.mobile.installation_proxy"), &installFd, NULL) == 0); assert(AMDeviceStopSession(device) == 0); @@ -469,13 +621,30 @@ void handle_device(AMDeviceRef device) { printf("-------------------------\n"); signal(SIGHUP, gdb_ready_handler); + signal(SIGINT, interrupted); + signal(SIGTERM, killed); - pid_t parent = getpid(); + parent = getpid(); int pid = fork(); if (pid == 0) { - system(GDB_SHELL); // launch gdb + child = getpid(); + CFStringRef path = copy_xcode_path_for(CFSTR("Platforms/iPhoneOS.platform/Developer/usr/libexec/gdb"), CFSTR("gdb-arm-apple-darwin")); + if (path == NULL) { + printf("[ !! ] Unable to locate GDB.\n"); + kill(parent, SIGHUP); + exit(1); + } else { + CFStringRef gdb_cmd = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ %@ %s"), path, CFSTR(GDB_SHELL), gdb_args); + + // Convert CFStringRef to char* for system call + const char *char_gdb_cmd = CFStringGetCStringPtr(gdb_cmd, kCFStringEncodingMacRoman); + + system(char_gdb_cmd); // launch gdb + } kill(parent, SIGHUP); // "No. I am your father." _exit(0); + } else { + child = pid; } } @@ -496,7 +665,18 @@ void timeout_callback(CFRunLoopTimerRef timer, void *info) { } void usage(const char* app) { - printf("usage: %s [-d/--debug] [-i/--id device_id] -b/--bundle bundle.app [-a/--args arguments] [-t/--timeout timeout(seconds)]\n", app); + printf( + "Usage: %s [OPTION]...\n" + " -d, --debug launch the app in a debugger after installation\n" + " -i, --id the id of the device to connect to\n" + " -b, --bundle the path to the app bundle to be installed\n" + " -a, --args command line arguments to pass to the app when launching it\n" + " -t, --timeout number of seconds to wait for a device to be connected\n" + " -u, --unbuffered don't buffer stdout\n" + " -g, --gdbargs extra arguments to pass to GDB when starting the debugger\n" + " -n, --nostart do not start the app when debugging\n" + " -v, --verbose enable verbose output\n", + app); } int main(int argc, char *argv[]) { @@ -507,11 +687,14 @@ int main(int argc, char *argv[]) { { "args", required_argument, NULL, 'a' }, { "verbose", no_argument, NULL, 'v' }, { "timeout", required_argument, NULL, 't' }, + { "unbuffered", no_argument, NULL, 'u' }, + { "gdbargs", required_argument, NULL, 'g' }, + { "nostart", no_argument, NULL, 'n' }, { NULL, 0, NULL, 0 }, }; char ch; - while ((ch = getopt_long(argc, argv, "dvi:b:a:t:", longopts, NULL)) != -1) + while ((ch = getopt_long(argc, argv, "dvuni:b:a:t:g:", longopts, NULL)) != -1) { switch (ch) { case 'd': @@ -532,6 +715,15 @@ int main(int argc, char *argv[]) { case 't': timeout = atoi(optarg); break; + case 'u': + unbuffered = 1; + break; + case 'g': + gdb_args = optarg; + break; + case 'n': + nostart = 1; + break; default: usage(argv[0]); return 1; @@ -543,6 +735,11 @@ int main(int argc, char *argv[]) { exit(0); } + if (unbuffered) { + setbuf(stdout, NULL); + setbuf(stderr, NULL); + } + printf("------ Install phase ------\n"); assert(access(app_path, F_OK) == 0);