OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #import <Foundation/Foundation.h> | |
6 #include <asl.h> | |
7 #include <libgen.h> | |
8 #include <stdarg.h> | |
9 #include <stdio.h> | |
10 | |
11 // An executable (iossim) that runs an app in the iOS Simulator. | |
12 // Run 'iossim -h' for usage information. | |
13 // | |
14 // For best results, the iOS Simulator application should not be running when | |
15 // iossim is invoked. | |
16 // | |
17 // Headers for the iPhoneSimulatorRemoteClient framework used in this tool are | |
18 // generated by class-dump, via GYP. | |
19 // (class-dump is available at http://www.codethecode.com/projects/class-dump/) | |
20 // | |
21 // However, there are some forward declarations required to get things to | |
22 // compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced | |
23 // by the iPhoneSimulatorRemoteClient framework, but not defined in the object | |
24 // file, so it must be defined here before importing the generated | |
25 // iPhoneSimulatorRemoteClient.h file. | |
26 | |
27 @class DTiPhoneSimulatorApplicationSpecifier; | |
28 @class DTiPhoneSimulatorSession; | |
29 @class DTiPhoneSimulatorSessionConfig; | |
30 @class DTiPhoneSimulatorSystemRoot; | |
31 | |
32 @protocol DTiPhoneSimulatorSessionDelegate | |
33 - (void)session:(DTiPhoneSimulatorSession*)session | |
34 didEndWithError:(NSError*)error; | |
35 - (void)session:(DTiPhoneSimulatorSession*)session | |
36 didStart:(BOOL)started | |
37 withError:(NSError*)error; | |
38 @end | |
39 | |
40 #import "iPhoneSimulatorRemoteClient.h" | |
41 | |
42 // An undocumented system log key included in messages from launchd. The value | |
43 // is the PID of the process the message is about (as opposed to launchd's PID). | |
44 #define ASL_KEY_REF_PID "RefPID" | |
45 | |
46 namespace { | |
47 | |
48 // Name of environment variables that control the user's home directory in the | |
49 // simulator. | |
50 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; | |
51 const char* const kHomeEnvVariable = "HOME"; | |
52 | |
53 // Device family codes for iPhone and iPad. | |
54 const int kIPhoneFamily = 1; | |
55 const int kIPadFamily = 2; | |
56 | |
57 // Max number of seconds to wait for the simulator session to start. | |
58 // This timeout must allow time to start up iOS Simulator, install the app | |
59 // and perform any other black magic that is encoded in the | |
60 // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up | |
61 // time is only a couple seconds but machine load, disk caches, etc., can all | |
62 // affect startup time in the wild so the timeout needs to be fairly generous. | |
63 // If this timeout occurs iossim will likely exit with non-zero status; the | |
64 // exception being if the app is invoked and completes execution before the | |
65 // session is started (this case is handled in session:didStart:withError). | |
66 const NSTimeInterval kSessionStartTimeout = 30; // seconds | |
67 | |
68 const char* gToolName = "iossim"; | |
69 | |
70 void LogError(NSString* format, ...) { | |
71 va_list list; | |
72 va_start(list, format); | |
73 | |
74 NSString* message = | |
75 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; | |
76 | |
77 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); | |
78 fflush(stderr); | |
79 | |
80 va_end(list); | |
81 } | |
82 | |
83 void LogWarning(NSString* format, ...) { | |
84 va_list list; | |
85 va_start(list, format); | |
86 | |
87 NSString* message = | |
88 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; | |
89 | |
90 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); | |
91 fflush(stderr); | |
92 | |
93 va_end(list); | |
94 } | |
95 | |
96 } // namespace | |
97 | |
98 // A delegate that is called when the simulated app is started or ended in the | |
99 // simulator. | |
100 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> { | |
101 @private | |
102 NSString* stdioPath_; // weak | |
103 NSThread* outputThread_; | |
104 BOOL appRunning_; | |
105 } | |
106 @end | |
107 | |
108 // An implementation that copies the simulated app's stdio to stdout of this | |
109 // executable. While it would be nice to get stdout and stderr independently | |
110 // from iOS Simulator, issues like I/O buffering and interleaved output | |
111 // between iOS Simulator and the app would cause iossim to display things out | |
112 // of order here. Printing all output to a single file keeps the order correct. | |
113 // Instances of this classe should be initialized with the location of the | |
114 // simulated app's output file. When the simulated app starts, a thread is | |
115 // started which handles copying data from the simulated app's output file to | |
116 // the stdout of this executable. | |
117 @implementation SimulatorDelegate | |
118 | |
119 // Specifies the file locations of the simulated app's stdout and stderr. | |
120 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath { | |
121 self = [super init]; | |
122 if (self) | |
123 stdioPath_ = [stdioPath copy]; | |
124 | |
125 return self; | |
126 } | |
127 | |
128 - (void)dealloc { | |
129 [stdioPath_ release]; | |
130 [super dealloc]; | |
131 } | |
132 | |
133 // Reads data from the simulated app's output and writes it to stdout. This | |
134 // method blocks, so it should be called in a separate thread. The iOS | |
135 // Simulator takes a file path for the simulated app's stdout and stderr, but | |
136 // this path isn't always available (e.g. when the stdout is Xcode's build | |
137 // window). As a workaround, iossim creates a temp file to hold output, which | |
138 // this method reads and copies to stdout. | |
139 - (void)tailOutput { | |
140 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; | |
141 | |
142 // Copy data to stdout/stderr while the app is running. | |
143 NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; | |
144 NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; | |
145 while (appRunning_) { | |
146 NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; | |
147 [standardOutput writeData:[simio readDataToEndOfFile]]; | |
148 [NSThread sleepForTimeInterval:0.1]; // seconds | |
149 [innerPool drain]; | |
150 } | |
151 | |
152 // Once the app is no longer running, copy any data that was written during | |
153 // the last sleep cycle. | |
154 [standardOutput writeData:[simio readDataToEndOfFile]]; | |
155 | |
156 [pool drain]; | |
157 } | |
158 | |
159 - (void)session:(DTiPhoneSimulatorSession*)session | |
160 didStart:(BOOL)started | |
161 withError:(NSError*)error { | |
162 if (!started) { | |
163 // If the test executes very quickly (<30ms), the SimulatorDelegate may not | |
164 // get the initial session:started:withError: message indicating successful | |
165 // startup of the simulated app. Instead the delegate will get a | |
166 // session:started:withError: message after the timeout has elapsed. To | |
167 // account for this case, check if the simulated app's stdio file was | |
168 // ever created and if it exists dump it to stdout and return success. | |
169 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
170 if ([fileManager fileExistsAtPath:stdioPath_ isDirectory:NO]) { | |
171 appRunning_ = NO; | |
172 [self tailOutput]; | |
173 // Note that exiting in this state leaves a process running | |
174 // (i.e. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will | |
stuartmorgan
2012/07/26 19:47:06
s/i.e./e.g.,/
lliabraa
2012/07/27 13:08:58
Done.
| |
175 // prevent future simulator sessions from being started for 30 seconds | |
176 // unless the iOS Simulator application is killed altogether. | |
177 [self session:session didEndWithError:nil]; | |
178 | |
179 // session:didEndWithError should not return (because it exits) so | |
180 // the execution path should never get here. | |
181 exit(EXIT_FAILURE); | |
182 } | |
183 | |
184 LogError(@"Simulator failed to start: %@", [error localizedDescription]); | |
185 exit(EXIT_FAILURE); | |
186 } | |
187 | |
188 // Start a thread to write contents of outputPath to stdout. | |
189 appRunning_ = YES; | |
190 outputThread_ = [[NSThread alloc] initWithTarget:self | |
191 selector:@selector(tailOutput) | |
192 object:nil]; | |
193 [outputThread_ start]; | |
194 } | |
195 | |
196 - (void)session:(DTiPhoneSimulatorSession*)session | |
197 didEndWithError:(NSError*)error { | |
198 appRunning_ = NO; | |
199 // Wait for the output thread to finish copying data to stdout. | |
200 if (outputThread_) { | |
201 while (![outputThread_ isFinished]) { | |
202 [NSThread sleepForTimeInterval:0.1]; // seconds | |
203 } | |
204 [outputThread_ release]; | |
205 outputThread_ = nil; | |
206 } | |
207 | |
208 if (error) { | |
209 LogError(@"Simulator ended with error: %@", [error localizedDescription]); | |
210 exit(EXIT_FAILURE); | |
211 } | |
212 | |
213 // Check if the simulated app exited abnormally by looking for system log | |
214 // messages from launchd that refer to the simulated app's PID. Limit query | |
215 // to messages in the last minute since PIDs are cyclical. | |
216 aslmsg query = asl_new(ASL_TYPE_QUERY); | |
217 asl_set_query(query, ASL_KEY_SENDER, "launchd", | |
218 ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING); | |
219 asl_set_query(query, ASL_KEY_REF_PID, | |
220 [[[session simulatedApplicationPID] stringValue] UTF8String], | |
221 ASL_QUERY_OP_EQUAL); | |
222 asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL); | |
223 | |
224 // Log any messages found. | |
225 aslresponse response = asl_search(NULL, query); | |
226 BOOL entryFound = NO; | |
227 aslmsg entry; | |
228 while ((entry = aslresponse_next(response)) != NULL) { | |
229 entryFound = YES; | |
230 LogWarning(@"Console message: %s", asl_get(entry, ASL_KEY_MSG)); | |
231 } | |
232 | |
233 // launchd only sends messages if the process crashed or exits with a | |
234 // non-zero status, so if the query returned any results iossim should exit | |
235 // with non-zero status. | |
236 if (entryFound) { | |
237 LogError(@"Simulated app crashed or exited with non-zero status"); | |
238 exit(EXIT_FAILURE); | |
239 } | |
240 exit(EXIT_SUCCESS); | |
241 } | |
242 @end | |
243 | |
244 namespace { | |
245 | |
246 // Converts the given app path to an application spec, which requires an | |
247 // absolute path. | |
248 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { | |
249 if (![appPath isAbsolutePath]) { | |
250 NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; | |
251 appPath = [cwd stringByAppendingPathComponent:appPath]; | |
252 } | |
253 appPath = [appPath stringByStandardizingPath]; | |
254 return [DTiPhoneSimulatorApplicationSpecifier | |
255 specifierWithApplicationPath:appPath]; | |
256 } | |
257 | |
258 // Returns the system root for the given SDK version. If sdkVersion is nil, the | |
259 // default system root is returned. Will return nil if the sdkVersion is not | |
260 // valid. | |
261 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { | |
262 DTiPhoneSimulatorSystemRoot* systemRoot = | |
263 [DTiPhoneSimulatorSystemRoot defaultRoot]; | |
264 if (sdkVersion) | |
265 systemRoot = [DTiPhoneSimulatorSystemRoot rootWithSDKVersion:sdkVersion]; | |
266 | |
267 return systemRoot; | |
268 } | |
269 | |
270 // Builds a config object for starting the specified app. | |
271 DTiPhoneSimulatorSessionConfig* BuildSessionConfig( | |
272 DTiPhoneSimulatorApplicationSpecifier* appSpec, | |
273 DTiPhoneSimulatorSystemRoot* systemRoot, | |
274 NSString* stdoutPath, | |
275 NSString* stderrPath, | |
276 NSArray* appArgs, | |
277 NSNumber* deviceFamily) { | |
278 DTiPhoneSimulatorSessionConfig* sessionConfig = | |
279 [[[DTiPhoneSimulatorSessionConfig alloc] init] autorelease]; | |
280 sessionConfig.applicationToSimulateOnStart = appSpec; | |
281 sessionConfig.simulatedSystemRoot = systemRoot; | |
282 sessionConfig.localizedClientName = @"chromium"; | |
283 sessionConfig.simulatedApplicationStdErrPath = stderrPath; | |
284 sessionConfig.simulatedApplicationStdOutPath = stdoutPath; | |
285 sessionConfig.simulatedApplicationLaunchArgs = appArgs; | |
286 // TODO(lliabraa): Add support for providing environment variables/values | |
287 // for the simulated app's environment. The environment can be set using: | |
288 // sessionConfig.simulatedApplicationLaunchEnvironment = | |
289 // [NSDictionary dictionary]; | |
290 sessionConfig.simulatedDeviceFamily = deviceFamily; | |
291 return sessionConfig; | |
292 } | |
293 | |
294 // Builds a simulator session that will use the given delegate. | |
295 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { | |
296 DTiPhoneSimulatorSession* session = | |
297 [[[DTiPhoneSimulatorSession alloc] init] autorelease]; | |
298 session.delegate = delegate; | |
299 return session; | |
300 } | |
301 | |
302 // Creates a temporary directory with a unique name based on the provided | |
303 // template. The template should not contain any path separators and be suffixed | |
304 // with X's, which will be substituted with a unique alphanumeric string (see | |
305 // 'man mkdtemp' for details). The directory will be created as a subdirectory | |
306 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', | |
307 // this method would return something like '/path/to/tempdir/test-3n2'. | |
308 // | |
309 // Returns the absolute path of the newly-created directory, or nill if unable | |
310 // to create a unique directory. | |
311 NSString* CreateTempDirectory(NSString* dirNameTemplate) { | |
312 NSString* fullPathTemplate = | |
313 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; | |
314 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String])); | |
315 if (fullPath == NULL) | |
316 return nil; | |
317 | |
318 return [NSString stringWithUTF8String:fullPath]; | |
319 } | |
320 | |
321 // Creates the necessary directory structure under the given user home directory | |
322 // path. | |
323 // Returns YES if successful, NO if unable to create the directories. | |
324 BOOL CreateHomeDirSubDirs(NSString* userHomePath) { | |
325 NSFileManager* fileManager = [NSFileManager defaultManager]; | |
326 | |
327 // Create user home and subdirectories. | |
328 NSArray* subDirsToCreate = [NSArray arrayWithObjects: | |
329 @"Documents", | |
330 @"Library/Caches", | |
331 nil]; | |
332 for (NSString* subDir in subDirsToCreate) { | |
333 NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; | |
334 NSError* error; | |
335 if (![fileManager createDirectoryAtPath:path | |
336 withIntermediateDirectories:YES | |
337 attributes:nil | |
338 error:&error]) { | |
339 LogError(@"Unable to create directory: %@. Error: %@", | |
340 path, [error localizedDescription]); | |
341 return NO; | |
342 } | |
343 } | |
344 | |
345 return YES; | |
346 } | |
347 | |
348 // Creates the necessary directory structure under the given user home directory | |
349 // path, then sets the path in the appropriate environment variable. | |
350 // Returns YES if successful, NO if unable to create or initialize the given | |
351 // directory. | |
352 BOOL InitializeSimulatorUserHome(NSString* userHomePath) { | |
353 if (!CreateHomeDirSubDirs(userHomePath)) | |
354 return NO; | |
355 | |
356 // Update the environment to use the specified directory as the user home | |
357 // directory. | |
358 // Note: the third param of setenv specifies whether or not to overwrite the | |
359 // variable's value if it has already been set. | |
360 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || | |
361 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { | |
362 LogError(@"Unable to set environment variables for home directory."); | |
363 return NO; | |
364 } | |
365 | |
366 return YES; | |
367 } | |
368 | |
369 // Prints the usage information to stderr. | |
370 void PrintUsage() { | |
371 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " | |
372 "<appPath> [<appArgs>]\n" | |
373 " where <appPath> is the path to the .app directory and appArgs are any" | |
374 " arguments to send the simulated app.\n" | |
375 "\n" | |
376 "Options:\n" | |
377 " -d Specifies the device (either 'iPhone' or 'iPad')." | |
378 " Defaults to 'iPhone'.\n" | |
379 " -s Specifies the SDK version to use (e.g '4.3')." | |
380 " Will use system default if not specified.\n" | |
381 " -u Specifies a user home directory for the simulator." | |
382 " Will create a new directory if not specified.\n"); | |
383 } | |
384 | |
385 } // namespace | |
386 | |
387 int main(int argc, char* const argv[]) { | |
388 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; | |
389 | |
390 char* toolName = basename(argv[0]); | |
391 if (toolName != NULL) | |
392 gToolName = toolName; | |
393 | |
394 NSString* appPath = nil; | |
395 NSString* appName = nil; | |
396 NSString* sdkVersion = nil; | |
397 NSString* deviceName = @"iPhone"; | |
398 NSString* simHomePath = nil; | |
399 NSMutableArray* appArgs = [NSMutableArray array]; | |
400 | |
401 // Parse the optional arguments | |
402 int c; | |
403 while ((c = getopt(argc, argv, "hs:d:u:")) != -1) { | |
404 switch (c) { | |
405 case 's': | |
406 sdkVersion = [NSString stringWithUTF8String:optarg]; | |
407 break; | |
408 case 'd': | |
409 deviceName = [NSString stringWithUTF8String:optarg]; | |
410 break; | |
411 case 'u': | |
412 simHomePath = [[NSFileManager defaultManager] | |
413 stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; | |
414 break; | |
415 case 'h': | |
416 PrintUsage(); | |
417 exit(EXIT_SUCCESS); | |
418 default: | |
419 PrintUsage(); | |
420 exit(EXIT_FAILURE); | |
421 } | |
422 } | |
423 | |
424 // There should be at least one arg left, specifying the app path. Any | |
425 // additional args are passed as arguments to the app. | |
426 if (optind < argc) { | |
427 appPath = [[NSFileManager defaultManager] | |
428 stringWithFileSystemRepresentation:argv[optind] | |
429 length:strlen(argv[optind])]; | |
430 appName = [appPath lastPathComponent]; | |
431 while (++optind < argc) { | |
432 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; | |
433 } | |
434 } else { | |
435 LogError(@"Unable to parse command line arguments."); | |
436 PrintUsage(); | |
437 exit(EXIT_FAILURE); | |
438 } | |
439 | |
440 // Make sure the app path provided is legit. | |
441 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); | |
442 if (!appSpec) { | |
443 LogError(@"Invalid app path: %@", appPath); | |
444 exit(EXIT_FAILURE); | |
445 } | |
446 | |
447 // Make sure the SDK path provided is legit (or nil). | |
448 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); | |
449 if (!systemRoot) { | |
450 LogError(@"Invalid SDK version: %@", sdkVersion); | |
451 exit(EXIT_FAILURE); | |
452 } | |
453 | |
454 // Get the paths for stdout and stderr so the simulated app's output will show | |
455 // up in the caller's stdout/stderr. | |
456 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); | |
457 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; | |
458 | |
459 // Make sure the device name is legit. | |
460 NSNumber* deviceFamily = nil; | |
461 if (!deviceName || | |
462 [@"iPhone" caseInsensitiveCompare:deviceName] == NSOrderedSame) { | |
463 deviceFamily = [NSNumber numberWithInt:kIPhoneFamily]; | |
464 } else if ([@"iPad" caseInsensitiveCompare:deviceName] == NSOrderedSame) { | |
465 deviceFamily = [NSNumber numberWithInt:kIPadFamily]; | |
466 } else { | |
467 LogError(@"Invalid device name: %@", deviceName); | |
468 exit(EXIT_FAILURE); | |
469 } | |
470 | |
471 // Set up the user home directory for the simulator | |
472 if (!simHomePath) { | |
473 NSString* dirNameTemplate = | |
474 [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName]; | |
475 simHomePath = CreateTempDirectory(dirNameTemplate); | |
476 if (!simHomePath) { | |
477 LogError(@"Unable to create unique directory for template %@", | |
478 dirNameTemplate); | |
479 exit(EXIT_FAILURE); | |
480 } | |
481 } | |
482 if (!InitializeSimulatorUserHome(simHomePath)) { | |
483 LogError(@"Unable to initialize home directory for simulator: %@", | |
484 simHomePath); | |
485 exit(EXIT_FAILURE); | |
486 } | |
487 | |
488 // Create the config and simulator session. | |
489 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, | |
490 systemRoot, | |
491 stdioPath, | |
492 stdioPath, | |
493 appArgs, | |
494 deviceFamily); | |
495 SimulatorDelegate* delegate = | |
496 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath] autorelease]; | |
497 DTiPhoneSimulatorSession* session = BuildSession(delegate); | |
498 | |
499 // Start the simulator session. | |
500 NSError* error; | |
501 BOOL started = [session requestStartWithConfig:config | |
502 timeout:kSessionStartTimeout | |
503 error:&error]; | |
504 | |
505 // Spin the runtime indefinitely. When the delegate gets the message that the | |
506 // app has quit it will exit this program. | |
507 if (started) | |
508 [[NSRunLoop mainRunLoop] run]; | |
509 else | |
510 LogError(@"Simulator failed to start: %@", [error localizedDescription]); | |
511 | |
512 // Note that this code is only executed if the simulator fails to start | |
513 // because once the main run loop is started, only the delegate calling | |
514 // exit() will end the program. | |
515 [pool drain]; | |
516 return EXIT_FAILURE; | |
517 } | |
OLD | NEW |