├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── watch-sim.m /.gitignore: -------------------------------------------------------------------------------- 1 | watch-sim 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eloy Durán 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | 3 | CC = "$(shell xcrun --sdk macosx --find clang)" 4 | SDK = "$(shell xcrun --sdk macosx --show-sdk-path)" 5 | INSTALL = $(shell xcrun --find install) -c 6 | 7 | watch-sim: watch-sim.m 8 | $(CC) -isysroot $(SDK) -ObjC -fobjc-arc -framework Foundation -g -o watch-sim watch-sim.m 9 | 10 | all: watch-sim 11 | 12 | install: all 13 | $(INSTALL) watch-sim $(PREFIX)/bin 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watch-sim 2 | 3 | A command-line WatchKit application launcher for the iOS Simulator. 4 | 5 | This requires an Xcode that supports WatchKit. At the time of writing, it has been successfully 6 | used on: 7 | 8 | * Xcode 6.2 beta 5 9 | * Xcode 6.3 beta 1 10 | 11 | And just as with Xcode, it doesn’t always boot properly the first (few) time(s)… 😭🐼 But will wait to 12 | see if Apple improves that before looking into it myself. 13 | 14 | ## Installation 15 | 16 | ``` 17 | $ git clone https://github.com/alloy/watch-sim.git 18 | $ cd watch-sim 19 | $ make install [PREFIX=/usr/local] 20 | ``` 21 | 22 | 23 | ## Usage 24 | 25 | ``` 26 | $ watch-sim path/to/build/WatchHost.app \ 27 | -display [Compact|Regular] -type [Glance|Notification] \ 28 | -notification-payload [path/to/payload.json] -verbose [YES|NO] \ 29 | -start-suspended [YES|NO] -developer-dir [Xcode.app/Contents/Developer] 30 | ``` 31 | 32 | The launcher does **not** build applications, as the name implies, it is only meant to _launch_ 33 | applications. As such, you will first have to create a build of your application with your favorite 34 | build tool and then point `watch-sim` at it. 35 | 36 | The `WatchHost.app` is your **normal** iPhone application, **not** the SockPuppet application that 37 | gets embedded in your normal iPhone application. 38 | 39 | 40 | #### Options 41 | 42 | --------------------------------------------------------------------------------------------------- 43 | 44 | `-display [Compact|Regular]` 45 | 46 | The Apple Watch device to simulate, which correspond to respectively the 38mm and the 42mm devices. 47 | 48 | This defaults to `Regular`. 49 | 50 | --------------------------------------------------------------------------------------------------- 51 | 52 | `-type [Glance|Notification]` 53 | 54 | The type of WatchKit application interface to simulate. 55 | 56 | This defaults to `nil`, which corresponds to the normal full WatchKit application. 57 | 58 | In the case of a `Notification` application interface, you are required to specify the 59 | `-notification-payload` option. 60 | 61 | --------------------------------------------------------------------------------------------------- 62 | 63 | `-notification-payload [path/to/payload.json]` 64 | 65 | The data payload used to populate a `Notification` WatchKit application interface with. 66 | 67 | An example payload file is generated by Xcode when adding a WatchKit application with Notification 68 | interface to your project. 69 | 70 | This is required when the `-type Notification` option is specified. 71 | 72 | --------------------------------------------------------------------------------------------------- 73 | 74 | `-verbose [YES|NO]` 75 | 76 | Whether or not to log the steps being taken to the console. 77 | 78 | Defaults to `NO`. 79 | 80 | --------------------------------------------------------------------------------------------------- 81 | 82 | `-start-suspended [YES|NO]` 83 | 84 | Whether or not to immediately finish launching the application once the debugger has attached. 85 | 86 | You can use this to set additional breakpoints _before_ the application has a chance to run any of 87 | your own code. 88 | 89 | Defaults to `NO`. 90 | 91 | --------------------------------------------------------------------------------------------------- 92 | 93 | `-developer-dir [Xcode.app/Contents/Developer]` 94 | 95 | The installation location of an Xcode application bundle that provides support for WatchKit 96 | applications. 97 | 98 | Defaults to the `DEVELOPER_DIR` environment variable, if set, or `xcode-select -p`. 99 | 100 | 101 | ## Random 102 | 103 | I would have called this program ‘Nanny’, as in ‘WatchKit’ -> ‘watch kid’ -> ‘nanny’. Would it have 104 | been a stretch? Yeah, I thought as much. Reality, such a major bummer. 105 | -------------------------------------------------------------------------------- /watch-sim.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | 10 | // CoreSimulator 11 | 12 | @interface SimDeviceSet : NSObject 13 | + (instancetype)defaultSet; 14 | - (NSArray *)devices; 15 | @end 16 | 17 | @interface SimDevice : NSObject 18 | - (NSString *)name; 19 | - (BOOL)supportsFeature:(NSString *)feature; 20 | @end 21 | 22 | // DVTFoundation 23 | 24 | @interface DVTFilePath : NSObject 25 | + (instancetype)filePathForPathString:(NSString *)path; 26 | @end 27 | 28 | @interface DVTFuture : NSObject 29 | - (long long)waitUntilFinished; 30 | - (id)error; 31 | @end 32 | 33 | @interface DVTXPCServiceInformation : NSObject // Real superclass: DVTProcessInformation 34 | - (instancetype)initWithServiceName:(NSString *)extensionBundleID 35 | pid:(int)pid 36 | parentPID:(int)parentPID; 37 | - (void)setStartSuspended:(BOOL)flag; 38 | - (void)setFullPath:(NSString *)path; 39 | - (void)setEnvironment:(NSDictionary *)environment; 40 | @end 41 | 42 | @interface DVTPlatform : NSObject 43 | + (BOOL)loadAllPlatformsReturningError:(NSError **)error; 44 | @end 45 | 46 | @interface DVTDevice : NSObject 47 | - (NSString *)nativeArchitecture; 48 | - (void)terminateWatchAppForCompanionIdentifier:(NSString *)ID options:(NSDictionary *)options; 49 | - (void)launchWatchAppForCompanionIdentifier:(NSString *)ID options:(NSDictionary *)options completionblock:(id)block; 50 | @end 51 | 52 | // DTXConnectionServices 53 | 54 | @protocol XCDTMobileIS_XPCDebuggingProcotol; 55 | 56 | @interface DTXChannel : NSObject 57 | // Normally defined as `DTXAllowedRPC`, but because it actually has to be a protocol that itself 58 | // conforms to `DTXAllowedRPC`. I'm defining it as `XCDTMobileIS_XPCDebuggingProcotol` which is what 59 | // `DVTiPhoneSimulator` implements. 60 | // 61 | - (void)setDispatchTarget:(id )target; 62 | @end 63 | 64 | // IDEFoundation 65 | 66 | // There is no option for “interface” launch mode, the key should simply be omitted completely, in 67 | // which case it will be the default way the application is launched. 68 | // 69 | NSString * const kIDEWatchLaunchModeKey = @"IDEWatchLaunchMode"; 70 | NSString * const kIDEWatchLaunchModeGlance = @"IDEWatchLaunchMode-Glance"; 71 | NSString * const kIDEWatchLaunchModeNotification = @"IDEWatchLaunchMode-Notification"; 72 | NSString * const kIDEWatchNotificationPayloadKey = @"IDEWatchNotificationPayload"; 73 | 74 | // IDEiOSSupportCore 75 | 76 | NSString * const kIDEWatchCompanionFeature = @"com.apple.watch.companion"; 77 | 78 | @interface DVTiPhoneSimulator : DVTDevice // Real superclass: DVTAbstractiOSDevice 79 | + (instancetype)simulatorWithDevice:(SimDevice *)device; 80 | - (DVTFuture *)installApplicationAtPath:(DVTFilePath *)path; 81 | - (void)debugXPCServices:(NSArray *)services; 82 | - (DTXChannel *)xpcAttachServiceChannel; 83 | - (SimDevice *)device; 84 | @end 85 | 86 | @protocol DTXAllowedRPC 87 | @end 88 | 89 | @protocol XCDTMobileIS_XPCDebuggingProcotol 90 | - (void)outputReceived:(NSString *)output fromProcess:(int)pid atTime:(unsigned long long)time; 91 | - (void)xpcServiceObserved:(NSString *)observedServiceID 92 | withProcessIdentifier:(int)pid 93 | requestedByProcess:(int)parentPID 94 | options:(NSDictionary *)options; 95 | @end 96 | 97 | @class DTiPhoneSimulatorSession; 98 | 99 | @protocol DTiPhoneSimulatorSessionDelegate 100 | - (void)session:(DTiPhoneSimulatorSession *)session didEndWithError:(NSError *)error; 101 | - (void)session:(DTiPhoneSimulatorSession *)session 102 | didStart:(BOOL)didStart 103 | withError:(NSError *)error; 104 | @end 105 | 106 | // DVTiPhoneSimulatorRemoteClient 107 | 108 | typedef NS_ENUM(NSInteger, DVTiPhoneSimulatorExternalDisplayType) { 109 | DVTiPhoneSimulatorWatchRegularExternalDisplayType = 1, 110 | DVTiPhoneSimulatorWatchCompactExternalDisplayType = 2, 111 | DVTiPhoneSimulatorCarPlayExternalDisplayType = 3 112 | }; 113 | 114 | @interface DTiPhoneSimulatorSessionConfig : NSObject 115 | - (void)setExternalDisplayType:(DVTiPhoneSimulatorExternalDisplayType)type; 116 | - (void)setDevice:(SimDevice *)device; 117 | @end 118 | 119 | @interface DTiPhoneSimulatorSession : NSObject 120 | - (void)setDelegate:(id ) delegate; 121 | - (BOOL)requestStartWithConfig:(DTiPhoneSimulatorSessionConfig *)config 122 | timeout:(double)timeout 123 | error:(NSError **)error; 124 | @end 125 | 126 | // Imported classes 127 | 128 | static Class SimDeviceSetClass = nil; 129 | static Class SimDeviceClass = nil; 130 | static Class DVTFilePathClass = nil; 131 | static Class DVTXPCServiceInformationClass = nil; 132 | static Class DVTPlatformClass = nil; 133 | static Class DVTiPhoneSimulatorClass = nil; 134 | static Class DTiPhoneSimulatorSessionClass = nil; 135 | static Class DTiPhoneSimulatorSessionConfigClass = nil; 136 | static Class DTXChannelClass = nil; 137 | 138 | static void 139 | InitImportedClasses(NSString *developerDir) { 140 | void *CoreSimulator = dlopen([[developerDir stringByAppendingPathComponent:@"Library/PrivateFrameworks/CoreSimulator.framework/CoreSimulator"] UTF8String], RTLD_NOW); 141 | assert(CoreSimulator != NULL); 142 | SimDeviceSetClass = objc_getClass("SimDeviceSet"); 143 | assert(SimDeviceSetClass != nil); 144 | SimDeviceClass = objc_getClass("SimDevice"); 145 | assert(SimDeviceClass != nil); 146 | 147 | void *DVTFoundation = dlopen([[developerDir stringByAppendingPathComponent:@"../SharedFrameworks/DVTFoundation.framework/Versions/A/DVTFoundation"] UTF8String], RTLD_NOW); 148 | assert(DVTFoundation != NULL); 149 | DVTFilePathClass = objc_getClass("DVTFilePath"); 150 | assert(DVTFilePathClass != nil); 151 | DVTXPCServiceInformationClass = objc_getClass("DVTXPCServiceInformation"); 152 | assert(DVTXPCServiceInformationClass != nil); 153 | DVTPlatformClass = objc_getClass("DVTPlatform"); 154 | assert(DVTPlatformClass != nil); 155 | 156 | void *DevToolsCore = dlopen([[developerDir stringByAppendingPathComponent:@"../OtherFrameworks/DevToolsCore.framework/DevToolsCore"] UTF8String], RTLD_NOW); 157 | assert(DevToolsCore != NULL); 158 | void *IDEiOSSupportCore = dlopen([[developerDir stringByAppendingPathComponent:@"../PlugIns/IDEiOSSupportCore.ideplugin/Contents/MacOS/IDEiOSSupportCore"] UTF8String], RTLD_NOW); 159 | assert(IDEiOSSupportCore != NULL); 160 | DVTiPhoneSimulatorClass = objc_getClass("DVTiPhoneSimulator"); 161 | assert(DVTiPhoneSimulatorClass != nil); 162 | 163 | void *DTXConnectionServices = dlopen([[developerDir stringByAppendingPathComponent:@"../SharedFrameworks/DTXConnectionServices.framework/Versions/A/DTXConnectionServices"] UTF8String], RTLD_NOW); 164 | assert(DTXConnectionServices != NULL); 165 | DTXChannelClass = objc_getClass("DTXChannel"); 166 | assert(DTXChannelClass != nil); 167 | 168 | void *DVTiPhoneSimulatorRemoteClient = dlopen([[developerDir stringByAppendingPathComponent:@"../SharedFrameworks/DVTiPhoneSimulatorRemoteClient.framework/Versions/A/DVTiPhoneSimulatorRemoteClient"] UTF8String], RTLD_NOW); 169 | assert(DVTiPhoneSimulatorRemoteClient != NULL); 170 | DTiPhoneSimulatorSessionConfigClass = objc_getClass("DTiPhoneSimulatorSessionConfig"); 171 | assert(DTiPhoneSimulatorSessionConfigClass != nil); 172 | DTiPhoneSimulatorSessionClass = objc_getClass("DTiPhoneSimulatorSession"); 173 | assert(DTiPhoneSimulatorSessionClass != nil); 174 | } 175 | 176 | 177 | // ------------------------------------------------------------------------------------------------- 178 | // 179 | // Our Implementation 180 | // 181 | // ------------------------------------------------------------------------------------------------- 182 | 183 | // The channel listener class has to conform to a protocol that in turn has to conform to the 184 | // `DTXAllowedRPC` protocol. 185 | // 186 | // Verification of this happens in the following order: 187 | // * `-[DTXMessage invokeWithTarget:replyChannel:validator:]` 188 | // * `shouldDispatchSelectorToObject` 189 | // * `__shouldDispatchSelectorToObject_block_invoke_2` 190 | // 191 | 192 | @interface WatchKitLauncher : NSObject 193 | // `launchMode` can be: 194 | // * `nil`: the normal “interface” application is launched. 195 | // * `kIDEWatchLaunchModeGlance`: the “glance” application is launched. 196 | // * `kIDEWatchLaunchModeNotification`: the “notification” application is launched. 197 | // 198 | // `notificationPayload` should be specified if `launchMode` is `kIDEWatchLaunchModeNotification`. 199 | // 200 | @property (strong) NSString *launchMode; 201 | @property (strong) NSDictionary *notificationPayload; 202 | @property (assign) BOOL verbose; 203 | @property (assign) BOOL startSuspended; 204 | @property (assign) DVTiPhoneSimulatorExternalDisplayType externalDisplayType; 205 | @end 206 | 207 | @interface WatchKitLauncher () 208 | @property (readonly) NSBundle *appBundle; 209 | @property (readonly) NSBundle *watchKitExtensionBundle; 210 | @property (readonly) DVTiPhoneSimulator *simulator; 211 | @property (strong) DTiPhoneSimulatorSession *session; 212 | @end 213 | 214 | @implementation WatchKitLauncher 215 | 216 | @synthesize watchKitExtensionBundle = _watchKitExtensionBundle; 217 | @synthesize simulator = _simulator; 218 | 219 | + (instancetype)launcherWithAppBundlePath:(NSString *)appBundlePath; 220 | { 221 | return [[self alloc] initWithAppBundle:[NSBundle bundleWithPath:appBundlePath]]; 222 | } 223 | 224 | - (instancetype)initWithAppBundle:(NSBundle *)appBundle; 225 | { 226 | NSParameterAssert(appBundle); 227 | if ((self = [super init])) { 228 | _appBundle = appBundle; 229 | _externalDisplayType = DVTiPhoneSimulatorWatchRegularExternalDisplayType; 230 | } 231 | return self; 232 | } 233 | 234 | // Launch flow is as follows: 235 | // * ensure simulator is running and with correct device 236 | // * install application 237 | // * actually launch application 238 | // * attach debugger 239 | // 240 | - (void)launch; 241 | { 242 | if (self.verbose) { 243 | printf("-> Launching simulator with device `%s`...\n", [self.simulator.device.name UTF8String]); 244 | } 245 | 246 | NSError *error = nil; 247 | if (![DVTPlatformClass loadAllPlatformsReturningError:&error]) { 248 | fprintf(stderr, "[!] Unable to initialize Dev Tools (%s).\n", [[error description] UTF8String]); 249 | exit(1); 250 | } 251 | 252 | DTiPhoneSimulatorSessionConfig *config = [DTiPhoneSimulatorSessionConfigClass new]; 253 | config.device = self.simulator.device; 254 | config.externalDisplayType = self.externalDisplayType; 255 | self.session = [DTiPhoneSimulatorSessionClass new]; 256 | self.session.delegate = self; 257 | if (![self.session requestStartWithConfig:config timeout:0 error:&error]) { 258 | fprintf(stderr, "[!] Unable to launch the Simulator (%s).\n", [[error description] UTF8String]); 259 | exit(1); 260 | } 261 | } 262 | 263 | // Called from `session:didStart:withError:` once the simulator is running. 264 | // 265 | - (void)continueLaunch; 266 | { 267 | [self installApplication]; 268 | [self actuallyLaunch]; 269 | } 270 | 271 | // Install the application to the `device`. This could be done in any number of ways, including the 272 | // newly available `simctl` tool. But for now this tool replicates the behaviour seen in Xcode when 273 | // launching extensions. 274 | // 275 | - (void)installApplication; 276 | { 277 | if (self.verbose) { 278 | printf("-> Installing `%s`...\n", [self.appBundle.bundlePath UTF8String]); 279 | } 280 | DVTFilePath *appFilePath = [DVTFilePathClass filePathForPathString:self.appBundle.bundlePath]; 281 | DVTFuture *installation = [self.simulator installApplicationAtPath:appFilePath]; 282 | [installation waitUntilFinished]; 283 | if (installation.error != nil) { 284 | fprintf(stderr, "[!] An error occurred while installing the application (%s)\n", 285 | [[installation.error description] UTF8String]); 286 | exit(1); 287 | } 288 | } 289 | 290 | - (void)actuallyLaunch; 291 | { 292 | if (self.verbose) { 293 | printf("-> Launching application...\n"); 294 | } 295 | 296 | DVTXPCServiceInformation *unstartedService = [self watchKitAppInformation]; 297 | [self.simulator debugXPCServices:@[unstartedService]]; 298 | DTXChannel *channel = self.simulator.xpcAttachServiceChannel; 299 | channel.dispatchTarget = self; 300 | 301 | NSString *appBundleID = self.appBundle.bundleIdentifier; 302 | // Reap any existing process 303 | [self.simulator terminateWatchAppForCompanionIdentifier:appBundleID options:@{}]; 304 | // Start new process 305 | [self.simulator launchWatchAppForCompanionIdentifier:appBundleID options:self.launchOptions completionblock:^(id error) { 306 | if (error != nil) { 307 | fprintf(stderr, "[!] An error occurred while launching the application (%s)\n", 308 | [[error description] UTF8String]); 309 | exit(1); 310 | } 311 | }]; 312 | } 313 | 314 | - (void)attachDebuggerToPID:(int)pid; 315 | { 316 | NSString *commands = [NSString stringWithFormat:@"" \ 317 | "process attach -p %d\n" \ 318 | "breakpoint set --name objc_exception_throw\n", pid]; 319 | if (!self.startSuspended) { 320 | commands = [commands stringByAppendingString:@"continue\n"]; 321 | } 322 | char path[PATH_MAX]; 323 | snprintf(path, PATH_MAX, "%s/watch-sim-debugger-commands.XXXXXX", (getenv("TMPDIR") ?: "/tmp")); 324 | assert(mktemp(path) != NULL); 325 | NSError *error = nil; 326 | if (![commands writeToFile:[NSString stringWithUTF8String:path] 327 | atomically:YES 328 | encoding:NSASCIIStringEncoding 329 | error:&error]) { 330 | fprintf(stderr, "[!] Unable to save debugger commands file to `%s` (%s)\n", path, 331 | [[error description] UTF8String]); 332 | exit(1); 333 | } 334 | 335 | if (self.verbose) { 336 | printf("-> Attaching debugger...\n"); 337 | } 338 | char command[1024]; 339 | sprintf(command, "lldb -s %s", path); 340 | int status = system(command); 341 | 342 | if (self.verbose) { 343 | printf("-> Exiting...\n"); 344 | } 345 | 346 | // Reap process. 347 | // TODO exiting immediately afterwards makes reaping not actually work. 348 | NSString *appBundleID = self.appBundle.bundleIdentifier; 349 | [self.simulator terminateWatchAppForCompanionIdentifier:appBundleID options:@{}]; 350 | 351 | // Exit launcher with status from LLDB. 352 | // TODO Is that helpful? 353 | exit(status); 354 | } 355 | 356 | #pragma mark - Accessors 357 | 358 | - (NSBundle *)watchKitExtensionBundle; 359 | { 360 | @synchronized(self) { 361 | if (_watchKitExtensionBundle == nil) { 362 | NSString *pluginsPath = self.appBundle.builtInPlugInsPath; 363 | NSError *error = nil; 364 | NSArray *plugins = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginsPath 365 | error:&error]; 366 | if (error) { 367 | fprintf(stderr, "[!] Unable to read host application’s PlugIns directory (%s).\n", 368 | [[error description] UTF8String]); 369 | exit(1); 370 | } 371 | for (NSString *plugin in plugins) { 372 | if ([[plugin pathExtension] isEqualToString:@"appex"]) { 373 | NSString *extensionPath = [pluginsPath stringByAppendingPathComponent:plugin]; 374 | NSBundle *extensionBundle = [NSBundle bundleWithPath:extensionPath]; 375 | NSDictionary *extensionInfo = extensionBundle.infoDictionary; 376 | NSString *extensionType = extensionInfo[@"NSExtension"][@"NSExtensionPointIdentifier"]; 377 | if ([extensionType isEqualToString:@"com.apple.watchkit"]) { 378 | _watchKitExtensionBundle = extensionBundle; 379 | break; 380 | } 381 | } 382 | } 383 | assert(_watchKitExtensionBundle != nil); 384 | } 385 | } 386 | return _watchKitExtensionBundle; 387 | } 388 | 389 | - (DVTiPhoneSimulator *)simulator; 390 | { 391 | @synchronized(self) { 392 | if (_simulator == nil) { 393 | DVTiPhoneSimulator *simulator = nil; 394 | NSArray *architectures = [self watchKitAppArchitectures]; 395 | for (SimDevice *availableDevice in [[SimDeviceSetClass defaultSet] devices]) { 396 | simulator = [DVTiPhoneSimulatorClass simulatorWithDevice:availableDevice]; 397 | if ([availableDevice supportsFeature:kIDEWatchCompanionFeature] 398 | && [architectures indexOfObject:simulator.nativeArchitecture] != NSNotFound) { 399 | _simulator = simulator; 400 | break; 401 | } 402 | } 403 | if (_simulator == nil) { 404 | fprintf(stderr, "[!] Cannot find any simulator devices, please add devices in " \ 405 | "Xcode -> Window -> Devices.\n"); 406 | exit(1); 407 | } 408 | } 409 | } 410 | return _simulator; 411 | } 412 | 413 | static NSString * 414 | NameOfArchitecture(cpu_type_t cputype, cpu_subtype_t cpusubtype) { 415 | const NXArchInfo *arch_info = NXGetArchInfoFromCpuType(cputype, cpusubtype); 416 | return [NSString stringWithUTF8String:arch_info->name]; 417 | } 418 | 419 | - (NSArray *)watchKitAppArchitectures; 420 | { 421 | FILE *executable = fopen([self.watchKitExtensionBundle.executablePath UTF8String], "rb"); 422 | if (executable == NULL) { 423 | fprintf(stderr, "[!] Unable to open WatchKit executable (%s).\n", strerror(errno)); 424 | exit(1); 425 | } 426 | 427 | // Get at least size of a fat_header and two fat_arch structs (sim only has i386 and x86_64, 428 | // because it's larger than a single mach_header, so we can safely fall back. 429 | size_t buffer_size = sizeof(struct fat_header) + (sizeof(struct fat_arch) * 2); 430 | uint8_t bytes[buffer_size]; 431 | size_t read_bytes = fread((void *)bytes, buffer_size, 1, executable); 432 | fclose(executable); 433 | if (read_bytes == 0) { 434 | fprintf(stderr, "[!] Unable to read WatchKit executable (%s).\n", strerror(errno)); 435 | exit(1); 436 | } 437 | 438 | NSMutableArray *architectures = [NSMutableArray new]; 439 | struct fat_header fheader = *(struct fat_header *)bytes; 440 | if (fheader.magic == FAT_MAGIC || fheader.magic == FAT_CIGAM) { 441 | if (fheader.magic == FAT_CIGAM) { 442 | swap_fat_header(&fheader, NX_LittleEndian); 443 | } 444 | struct fat_arch *archs = (struct fat_arch *)((struct fat_header *)bytes+1); 445 | for (uint32_t i = 0; i < fheader.nfat_arch; i++) { 446 | struct fat_arch arch = archs[i]; 447 | swap_fat_arch(&arch, 1, NX_LittleEndian); 448 | [architectures addObject:NameOfArchitecture(arch.cputype, arch.cpusubtype)]; 449 | } 450 | } else { 451 | struct mach_header mheader = *(struct mach_header *)bytes; 452 | [architectures addObject:NameOfArchitecture(mheader.cputype, mheader.cpusubtype)]; 453 | } 454 | 455 | assert(architectures.count > 0); 456 | if (self.verbose) { 457 | printf("-> Detected architecture(s) of WatchKit executable to be: %s\n", 458 | [[architectures componentsJoinedByString:@", "] UTF8String]); 459 | } 460 | return [architectures copy]; 461 | } 462 | 463 | // TODO Do we maybe need to set all those build paths in the env for dSYM location, or is it just in 464 | // case a framework is loaded and is not inside the host app bundle? 465 | // 466 | - (DVTXPCServiceInformation *)watchKitAppInformation; 467 | { 468 | NSString *name = self.watchKitExtensionBundle.bundleIdentifier; 469 | DVTXPCServiceInformation *app = [[DVTXPCServiceInformationClass alloc] initWithServiceName:name 470 | pid:-1 471 | parentPID:0]; 472 | app.fullPath = self.watchKitExtensionBundle.bundlePath; 473 | app.startSuspended = YES; 474 | app.environment = @{ @"NSUnbufferedIO": @"YES" }; 475 | //app.environment = @{ 476 | //@"NSUnbufferedIO": @"YES", 477 | //@"DYLD_FRAMEWORK_PATH": buildDir, 478 | //@"DYLD_LIBRARY_PATH": buildDir, 479 | //@"__XCODE_BUILT_PRODUCTS_DIR_PATHS": buildDir, 480 | //@"__XPC_DYLD_FRAMEWORK_PATH": buildDir, 481 | //@"__XPC_DYLD_LIBRARY_PATH": buildDir 482 | //}; 483 | return app; 484 | } 485 | 486 | - (NSDictionary *)launchOptions; 487 | { 488 | NSMutableDictionary *options = [NSMutableDictionary new]; 489 | if (self.launchMode) { 490 | options[kIDEWatchLaunchModeKey] = self.launchMode; 491 | if ([self.launchMode isEqualToString:kIDEWatchLaunchModeNotification]) { 492 | NSParameterAssert(self.notificationPayload); 493 | options[kIDEWatchNotificationPayloadKey] = self.notificationPayload; 494 | } 495 | } 496 | return [options copy]; 497 | } 498 | 499 | #pragma mark - DTiPhoneSimulatorSessionDelegate 500 | 501 | - (void)session:(DTiPhoneSimulatorSession *)session didEndWithError:(NSError *)error; 502 | { 503 | if (error != nil) { 504 | fprintf(stderr, "[!] Ended Simulator session (%s).\n", [[error description] UTF8String]); 505 | exit(1); 506 | } 507 | } 508 | 509 | - (void)session:(DTiPhoneSimulatorSession *)session 510 | didStart:(BOOL)didStart 511 | withError:(NSError *)error; 512 | { 513 | if (!didStart) { 514 | fprintf(stderr, "[!] Unable to launch the Simulator (%s).\n", [[error description] UTF8String]); 515 | exit(1); 516 | } 517 | [self continueLaunch]; 518 | } 519 | 520 | #pragma mark - XCDTMobileIS_XPCDebuggingProcotol 521 | 522 | // If our service has started, connect to it with LLDB from the main thread. Do not block the XPC 523 | // queue any further, otherwise we won't get any output messages. 524 | // 525 | - (void)xpcServiceObserved:(NSString *)observedServiceID 526 | withProcessIdentifier:(int)pid 527 | requestedByProcess:(int)parentPID 528 | options:(NSDictionary *)options; 529 | { 530 | if ([observedServiceID isEqualToString:self.watchKitExtensionBundle.bundleIdentifier]) { 531 | if (self.verbose) { 532 | printf("-> Requested XPC service has been observed with PID: %d\n", pid); 533 | } 534 | assert(pid > 0); 535 | dispatch_async(dispatch_get_main_queue(), ^{ 536 | [self attachDebuggerToPID:pid]; 537 | }); 538 | } 539 | } 540 | 541 | // Directly print from the XPC queue this is delivered on so that it's shown while LLDB is running. 542 | // 543 | - (void)outputReceived:(NSString *)output fromProcess:(int)pid atTime:(unsigned long long)time; 544 | { 545 | printf("%s", [output UTF8String]); 546 | } 547 | 548 | @end 549 | 550 | void 551 | print_help_banner(void) { 552 | fprintf(stderr, "Usage: watch-sim path/to/build/WatchHost.app -display [Compact|Regular] " \ 553 | "-type [Glance|Notification] -notification-payload [path/to/payload.json] " \ 554 | "-verbose [YES|NO] -start-suspended [YES|NO] " \ 555 | "-developer-dir [Xcode.app/Contents/Developer]\n"); 556 | } 557 | 558 | int 559 | main(int argc, char **argv) { 560 | NSArray *allArguments = [NSProcessInfo processInfo].arguments; 561 | NSMutableArray *arguments = [NSMutableArray new]; 562 | for (NSInteger i = 1; i < argc; i++) { 563 | NSString *argument = allArguments[i]; 564 | if ([argument hasPrefix:@"-"]) { 565 | // Skip next argument, which is the value for this option. 566 | i++; 567 | } else { 568 | [arguments addObject:argument]; 569 | } 570 | } 571 | 572 | if (arguments.count != 1) { 573 | print_help_banner(); 574 | return 1; 575 | } 576 | NSString *appPath = arguments[0]; 577 | 578 | NSUserDefaults *options = [NSUserDefaults standardUserDefaults]; 579 | BOOL verbose = [options boolForKey:@"verbose"]; 580 | BOOL startSuspended = [options boolForKey:@"start-suspended"]; 581 | 582 | DVTiPhoneSimulatorExternalDisplayType externalDisplayType = 0; 583 | NSString *displayType = [[options valueForKey:@"display"] lowercaseString]; 584 | if (displayType != nil) { 585 | if ([displayType isEqualToString:@"regular"]) { 586 | externalDisplayType = DVTiPhoneSimulatorWatchRegularExternalDisplayType; 587 | } else if ([displayType isEqualToString:@"compact"]) { 588 | externalDisplayType = DVTiPhoneSimulatorWatchCompactExternalDisplayType; 589 | } else { 590 | fprintf(stderr, "[!] Unknown external display type `%s`.\n", [displayType UTF8String]); 591 | print_help_banner(); 592 | return 1; 593 | } 594 | } 595 | 596 | NSString *launchMode = nil; 597 | NSDictionary *notificationPayload = nil; 598 | NSString *appType = [[options valueForKey:@"type"] lowercaseString]; 599 | if (appType != nil) { 600 | if ([appType isEqualToString:@"glance"]) { 601 | launchMode = kIDEWatchLaunchModeGlance; 602 | } else if ([appType isEqualToString:@"notification"]) { 603 | // Get the obligatory notification payload (JSON) data. 604 | launchMode = kIDEWatchLaunchModeNotification; 605 | NSString *payloadFile = [options valueForKey:@"notification-payload"]; 606 | if (payloadFile == nil) { 607 | fprintf(stderr, "[!] A `-notification-payload` is required with `-type Notification`.\n"); 608 | print_help_banner(); 609 | return 1; 610 | } 611 | NSData *payloadData = [NSData dataWithContentsOfFile:payloadFile]; 612 | NSError *error = nil; 613 | notificationPayload = [NSJSONSerialization JSONObjectWithData:payloadData 614 | options:0 615 | error:&error]; 616 | if (error != nil) { 617 | fprintf(stderr, "[!] Unable to load notification payload file `%s` (%s)\n", 618 | [payloadFile UTF8String], [[error description] UTF8String]); 619 | return 1; 620 | } 621 | assert([notificationPayload isKindOfClass:[NSDictionary class]]); 622 | } else { 623 | fprintf(stderr, "[!] Unknown application type `%s`.\n", [appType UTF8String]); 624 | print_help_banner(); 625 | return 1; 626 | } 627 | } 628 | 629 | NSString *developerDir = [options valueForKey:@"developer-dir"]; 630 | if (developerDir == nil) { 631 | char *dir = getenv("DEVELOPER_DIR"); 632 | if (dir == NULL) { 633 | FILE *pipe = popen("/usr/bin/xcode-select -p", "r"); 634 | assert(pipe != NULL); 635 | char buffer[PATH_MAX]; 636 | assert(fgets(buffer, PATH_MAX, pipe) != NULL); 637 | pclose(pipe); 638 | dir = buffer; 639 | } 640 | NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; 641 | developerDir = [[NSString stringWithUTF8String:dir] stringByTrimmingCharactersInSet:whitespace]; 642 | } 643 | 644 | InitImportedClasses(developerDir); 645 | 646 | WatchKitLauncher *launcher = [WatchKitLauncher launcherWithAppBundlePath:appPath]; 647 | launcher.verbose = verbose; 648 | launcher.startSuspended = startSuspended; 649 | launcher.launchMode = launchMode; 650 | launcher.notificationPayload = notificationPayload; 651 | if (externalDisplayType != 0) { 652 | launcher.externalDisplayType = externalDisplayType; 653 | } 654 | [launcher launch]; 655 | 656 | while (1) { 657 | CFRunLoopRun(); 658 | } 659 | 660 | // This should never be reached. 661 | return 1; 662 | } 663 | --------------------------------------------------------------------------------