iOS Simulator LayoutTestRelay
[WebKit-https.git] / Tools / LayoutTestRelay / LayoutTestRelay / LTRelayController.m
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "LTRelayController.h"
27 #import "LTPipeRelay.h"
28
29 #import <AppKit/AppKit.h>
30 #import <CoreSimulator/CoreSimulator.h>
31
32 @interface LTRelayController ()
33 @property (readonly, strong) NSFileHandle *standardInput;
34 @property (readonly, strong) NSFileHandle *standardOutput;
35 @property (readonly, strong) NSFileHandle *standardError;
36 @property (readonly, strong) NSString *uniqueAppPath;
37 @property (readonly, strong) NSString *uniqueAppIdentifer;
38 @property (readonly, strong) NSURL *uniqueAppURL;
39 @property (readonly, strong) NSString *originalAppIdentifier;
40 @property (readonly, strong) NSString *originalAppPath;
41 @property (readonly, strong) NSString *identifierSuffix;
42 @property (readonly, strong) NSArray *dumpToolArguments;
43 @property (readonly, strong) NSString *productDir;
44 @property (strong) SimDevice *device;
45 @property (strong) id<LTRelay> relay;
46 @property pid_t pid;
47 @property (strong) dispatch_source_t appDispatchSource;
48 @end
49
50 @implementation LTRelayController
51
52 - (id)initWithDevice:(SimDevice *)device productDir:(NSString *)productDir appPath:(NSString *)appPath identifierSuffix:(NSString *)suffix dumpToolArguments:(NSArray *)arguments
53 {
54     if ((self = [super init])) {
55         _device = device;
56         _productDir = productDir;
57         _originalAppPath = appPath;
58         _originalAppIdentifier = [NSDictionary dictionaryWithContentsOfFile:[_originalAppPath stringByAppendingPathComponent:@"Info.plist"]][(NSString *)kCFBundleIdentifierKey];
59         _identifierSuffix = suffix;
60         _dumpToolArguments = arguments;
61         _standardInput = [NSFileHandle fileHandleWithStandardInput];
62         _standardOutput = [NSFileHandle fileHandleWithStandardOutput];
63         _standardError = [NSFileHandle fileHandleWithStandardError];
64
65         _relay = [[LTPipeRelay alloc] initWithPrefix:[@"/tmp" stringByAppendingPathComponent:[self uniqueAppIdentifier]]];
66         [_relay setRelayDelegate:self];
67     }
68     return self;
69 }
70
71 - (NSString *)uniqueAppPath
72 {
73     return [[self originalAppPath] stringByReplacingOccurrencesOfString:@".app" withString:[NSString stringWithFormat:@"%@.app", [self identifierSuffix]]];
74 }
75
76 - (NSURL *)uniqueAppURL
77 {
78     return [NSURL fileURLWithPath:[self uniqueAppPath]];
79 }
80
81 - (NSString *)uniqueAppIdentifier
82 {
83     return [[self originalAppIdentifier] stringByAppendingString:[self identifierSuffix]];
84 }
85 - (NSString *)processName
86 {
87     return [[[self originalAppIdentifier] componentsSeparatedByString:@"."] lastObject];
88 }
89
90 - (void)readFileHandle:(NSFileHandle *)fileHandle
91 {
92     NSData *data = [fileHandle availableData];
93     uint8_t bytes[[data length]];
94     [data getBytes:bytes length:[data length]];
95     [[[self relay] outputStream] write:[data bytes] maxLength:[data length]];
96 }
97
98
99 - (void)didReceiveStdoutData:(NSData *)data
100 {
101     [[self standardOutput] writeData:data];
102 }
103
104 - (void)didReceiveStderrData:(NSData *)data
105 {
106     [[self standardError] writeData:data];
107 }
108
109 - (void)didDisconnect
110 {
111     [[self standardInput] setReadabilityHandler:nil];
112 }
113
114 - (void)didConnect
115 {
116     [[self standardInput] setReadabilityHandler: ^(NSFileHandle *fileHandle)
117     {
118         [self readFileHandle:fileHandle];
119     }];
120 }
121
122 - (void)didCrashWithMessage:(NSString *)message
123 {
124     [[self relay] disconnect];
125     NSString *crashMessage = [NSString stringWithFormat:@"\nCRASH: %@ %d\n", [self processName], [self pid]];
126
127     if (message)
128         crashMessage = [crashMessage stringByAppendingFormat:@"%@\n", message];
129
130     [[self standardError] writeData:[crashMessage dataUsingEncoding:NSUTF8StringEncoding]];
131     [[self standardError] closeFile];
132     [[self standardOutput] closeFile];
133     exit(EXIT_FAILURE);
134 }
135
136 - (void)launchSimulator
137 {
138     NSString *developerDir = [[[NSProcessInfo processInfo] environment] valueForKey:@"DEVELOPER_DIR"];
139     if (!developerDir) {
140         NSTask *xcodeSelectTask = [[NSTask alloc] init];
141         [xcodeSelectTask setLaunchPath:@"/usr/bin/xcode-select"];
142         [xcodeSelectTask setArguments:@[@"--print-path"]];
143         [xcodeSelectTask setStandardOutput:[NSPipe pipe]];
144
145         NSFileHandle *stdoutFileHandle = [[xcodeSelectTask standardOutput] fileHandleForReading];
146         [xcodeSelectTask launch];
147         [xcodeSelectTask waitUntilExit];
148
149         NSData *data = [stdoutFileHandle readDataToEndOfFile];
150         developerDir = [NSString stringWithUTF8String:[data bytes]];
151     }
152
153     developerDir = [developerDir stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
154
155     if (!developerDir || ![developerDir length]) {
156         NSLog(@"Not able to determine the path to iOS Simulator.app in your active Xcode.app");
157         exit(EXIT_FAILURE);
158     }
159     NSURL *simulatorURL = [NSURL fileURLWithPath:[developerDir stringByAppendingPathComponent:@"Applications/iOS Simulator.app"]];
160
161     NSDictionary *launchConfiguration = @{
162         NSWorkspaceLaunchConfigurationArguments: @[
163             @"-CurrentDeviceUDID", [[[self device] UDID] UUIDString],
164             ]
165     };
166     NSError *error;
167     [[NSWorkspace sharedWorkspace] launchApplicationAtURL:simulatorURL options:NSWorkspaceLaunchDefault configuration:launchConfiguration error:&error];
168
169     if (error) {
170         NSLog(@"Couldn't launch iOS Simulator from %@: %@", [simulatorURL path], [error description]);
171         exit(EXIT_FAILURE);
172     }
173
174     while ([[self device] state] == SimDeviceStateShutdown) {
175         // Wait for device to start booting
176         sleep(1);
177     }
178 }
179
180 - (void)bootDevice
181 {
182     while ([[self device] state] == SimDeviceStateBooting)
183         sleep(1);
184
185     if ([[self device] state] == SimDeviceStateBooted)
186         return;
187
188     NSError *error;
189     [[self device] bootWithOptions:nil error:&error];
190     if (error) {
191         NSLog(@"Unable to boot device: %@", [error description]);
192         exit(EXIT_FAILURE);
193     }
194 }
195
196 - (void)createUniqueApp
197 {
198     NSError *error;
199     NSFileManager *fileManager = [NSFileManager defaultManager];
200     [fileManager removeItemAtPath:[self uniqueAppPath] error:&error];
201     error = nil;
202
203     [fileManager copyItemAtPath:[self originalAppPath] toPath:[self uniqueAppPath] error:&error];
204     if (error) {
205         NSLog(@"Couldn't copy %@ to %@: %@", [self originalAppPath], [self uniqueAppPath], [error description]);
206         exit(EXIT_FAILURE);
207     }
208
209     NSString *infoPlistPath = [[self uniqueAppPath] stringByAppendingPathComponent:@"Info.plist"];
210     NSMutableDictionary *plist = [NSMutableDictionary dictionaryWithContentsOfFile:infoPlistPath];
211     [plist setValue:[self uniqueAppIdentifier] forKey:(NSString *)kCFBundleIdentifierKey];
212     BOOL written = [plist writeToFile:infoPlistPath atomically:YES];
213     if (!written) {
214         NSLog(@"Couldn't write unique app plist at %@", infoPlistPath);
215         exit(EXIT_FAILURE);
216     }
217
218     NSDictionary *installOptions = @{
219         (NSString *)kCFBundleIdentifierKey: [self uniqueAppIdentifier],
220     };
221
222     if ([[self device] applicationIsInstalled:[self uniqueAppIdentifier] type: nil error: &error]) {
223         BOOL uninstalled = [[self device ] uninstallApplication:[self uniqueAppIdentifier] withOptions:nil error:&error];
224         if (!uninstalled) {
225             NSLog(@"Couldn't uninstall %@: %@", [self uniqueAppIdentifier], [error description]);
226             exit(EXIT_FAILURE);
227         }
228     }
229
230     [[self device] installApplication:[self uniqueAppURL] withOptions:installOptions error:&error];
231     if (error) {
232         NSLog(@"Couldn't install %@: %@", [[self uniqueAppURL] path], [error description]);
233         exit(EXIT_FAILURE);
234     }
235 }
236
237 - (void)killApp
238 {
239     dispatch_source_t dispatch = [self appDispatchSource];
240
241     if (dispatch)
242         dispatch_source_cancel(dispatch);
243
244     if ([self pid] > 1) {
245         kill(self.pid, SIGKILL);
246         [self setPid:-1];
247     }
248     [self setAppDispatchSource:nil];
249 }
250
251 - (void)launchApp
252 {
253     NSDictionary *launchOptions = @{
254         kSimDeviceLaunchApplicationEnvironment: @{
255             @"DYLD_LIBRARY_PATH": [self productDir],
256             @"DYLD_FRAMEWORK_PATH": [self productDir],
257         },
258         kSimDeviceLaunchApplicationArguments: [self dumpToolArguments],
259     };
260
261     NSError *error;
262     pid_t pid = [[self device] launchApplicationWithID:[self uniqueAppIdentifier] options:launchOptions error:&error];
263
264     if (pid < 0) {
265         NSLog(@"Couldn't launch unique app instance %@: %@", [self uniqueAppIdentifier], [error description]);
266         exit(EXIT_FAILURE);
267     }
268
269     [self setPid:pid];
270
271     dispatch_queue_t queue = dispatch_get_main_queue();
272     dispatch_source_t dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, [self pid], DISPATCH_PROC_EXIT, queue);
273     [self setAppDispatchSource:dispatchSource];
274     dispatch_source_set_event_handler(dispatchSource, ^{
275         dispatch_source_cancel(dispatchSource);
276         [self didCrashWithMessage:nil];
277     });
278     dispatch_resume(dispatchSource);
279 }
280
281 - (void)start
282 {
283     [self launchSimulator];
284     [self bootDevice];
285     [self createUniqueApp];
286     [[self relay] setup];
287     [self launchApp];
288     [[self relay] connect];
289 }
290
291 @end