/
instruments.js
405 lines (370 loc) · 14 KB
/
instruments.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// Wrapper around Apple's Instruments app
import { spawn } from 'teen_process';
import log from './logger';
import _ from 'lodash';
import { through } from 'through';
import path from 'path';
import { mkdirp, fs, cancellableDelay } from 'appium-support';
import xcode from 'appium-xcode';
import B from 'bluebird';
import { killAllSimulators } from 'appium-ios-simulator';
import { getInstrumentsPath, parseLaunchTimeout, getIwdPath } from './utils';
import { outputStream, errorStream, webSocketAlertStream, dumpStream } from './streams';
import 'colors';
const ERR_NEVER_CHECKED_IN = 'Instruments never checked in';
const ERR_CRASHED_ON_STARTUP = 'Instruments crashed on startup';
const ERR_AMBIGUOUS_DEVICE = 'Instruments Usage Error : Ambiguous device name/identifier';
class Instruments {
// simple factory with sane defaults
static async quickInstruments (opts) {
opts = _.clone(opts);
let xcodeTraceTemplatePath = await xcode.getAutomationTraceTemplatePath();
_.defaults(opts, {
launchTimeout: 60000,
template: xcodeTraceTemplatePath,
withoutDelay: true,
xcodeVersion: '8.1',
webSocket: null,
flakeyRetries: 2
});
return new Instruments(opts);
}
/*
* opts:
* - app
* - termTimeout - defaults to 5000
* - flakeyRetries - defaults to 0
* - udid
* - bootstrap
* - template
* - withoutDelay
* - processArguments
* - simulatorSdkAndDevice
* - tmpDir - defaults to `/tmp/appium-instruments`
* - traceDir
* - launchTimeout - defaults to 90000
* - webSocket
* - instrumentsPath
* - realDevice - true/false, defaults to false
*/
constructor (opts) {
opts = _.cloneDeep(opts);
_.defaults(opts, {
termTimeout: 5000,
tmpDir: '/tmp/appium-instruments',
launchTimeout: 90000,
flakeyRetries: 0,
realDevice: false
});
// config
const props = [
'app', 'termTimeout', 'flakeyRetries', 'udid', 'bootstrap',
'template', 'withoutDelay', 'processArguments', 'realDevice',
'simulatorSdkAndDevice', 'tmpDir', 'traceDir', 'locale', 'language',
];
for (const f of props) {
this[f] = opts[f];
}
this.traceDir = this.traceDir || this.tmpDir;
this.launchTimeout = parseLaunchTimeout(opts.launchTimeout);
// state
this.proc = null;
this.webSocket = opts.webSocket;
this.instrumentsPath = opts.instrumentsPath;
this.launchTries = 0;
this.socketConnectDelays = [];
this.gotFBSOpenApplicationError = false;
this.onShutdown = new B((resolve, reject) => {
this.onShutdownDeferred = {resolve, reject};
});
// avoids UnhandledException
this.onShutdown.catch(() => {}).done(); // eslint-disable-line promise/catch-or-return
}
async configure () {
if (!this.xcodeVersion) {
this.xcodeVersion = await xcode.getVersion(true);
}
if (this.xcodeVersion.versionFloat === 6.0 && this.withoutDelay) {
log.info('In xcode 6.0, instruments-without-delay does not work. ' +
'If using Appium, you can disable instruments-without-delay ' +
'with the --native-instruments-lib server flag');
}
if (this.xcodeVersion.versionString === '5.0.1') {
throw new Error('Xcode 5.0.1 ships with a broken version of ' +
'Instruments. please upgrade to 5.0.2');
}
if (this.xcodeVersion.major > 7) {
throw new Error(`Instruments-based automation was removed in Xcode 8. ` +
`Xcode ${this.xcodeVersion.versionString} is not supported. ` +
`Please try the XCUItest driver.`);
}
if (!this.template) {
this.template = await xcode.getAutomationTraceTemplatePath();
}
if (!this.instrumentsPath) {
this.instrumentsPath = await getInstrumentsPath();
}
}
async launchOnce () {
log.info('Launching instruments');
// prepare temp dir
await fs.rimraf(this.tmpDir);
await mkdirp(this.tmpDir);
await mkdirp(this.traceDir);
this.exitListener = null;
this.proc = await this.spawnInstruments();
this.proc.on('exit', (code, signal) => {
const msg = code !== null ? `code: ${code}` : `signal: ${signal}`;
log.debug(`Instruments exited with ${msg}`);
});
// set up the promise to handle launch
let launchResultPromise = new B((resolve, reject) => {
this.launchResultDeferred = {resolve, reject};
});
// There was a special case for ignoreStartupExit
// but it is not needed anymore, you may just listen for exit.
this.setExitListener(() => {
this.proc = null;
this.launchResultDeferred.reject(new Error(ERR_CRASHED_ON_STARTUP));
});
this.proc.on('error', (err) => { // eslint-disable-line promise/prefer-await-to-callbacks
log.debug(`Error with instruments proc: ${err.message}`);
if (err.message.indexOf('ENOENT') !== -1) {
this.proc = null; // otherwise we'll try to send sigkill
log.error(`Unable to spawn instruments: ${err.message}`);
this.launchResultDeferred.reject(err);
}
});
this.proc.stdout.setEncoding('utf8');
this.proc.stdout.pipe(outputStream()).pipe(dumpStream());
this.proc.stderr.setEncoding('utf8');
let actOnStderr = (output) => {
if (this.launchTimeout.afterSimLaunch && output && output.match(/CLTilesManagerClient: initialize/)) {
this.addSocketConnectTimer(this.launchTimeout.afterSimLaunch, 'afterLaunch', async () => {
await this.killInstruments();
this.launchResultDeferred.reject(new Error(ERR_NEVER_CHECKED_IN));
});
}
let fbsErrStr = '(FBSOpenApplicationErrorDomain error 8.)';
if (output.indexOf(fbsErrStr) !== -1) {
this.gotFBSOpenApplicationError = true;
}
if (output.indexOf(ERR_AMBIGUOUS_DEVICE) !== -1) {
let msg = `${ERR_AMBIGUOUS_DEVICE}: '${this.simulatorSdkAndDevice}'`;
this.launchResultDeferred.reject(new Error(msg));
}
};
this.proc.stderr.pipe(through(function onStderr (output) {
actOnStderr(output);
this.queue(output);
})).pipe(errorStream())
.pipe(webSocketAlertStream(this.webSocket))
.pipe(dumpStream());
// start waiting for instruments to launch successfully
this.addSocketConnectTimer(this.launchTimeout.global, 'global', async () => {
await this.killInstruments();
this.launchResultDeferred.reject(new Error(ERR_NEVER_CHECKED_IN));
});
try {
await launchResultPromise;
} finally {
this.clearSocketConnectTimers();
}
this.setExitListener((code, signal) => {
this.proc = null;
const msg = code !== null ? `code: ${code}` : `signal: ${signal}`;
this.onShutdownDeferred.reject(new Error(`Abnormal exit with ${msg}`));
});
}
async launch () {
await this.configure();
let launchTries = 0;
do {
launchTries++;
log.debug(`Attempting to launch instruments, this is try #${launchTries}`);
try {
await this.launchOnce();
break;
} catch (err) {
log.error(`Error launching instruments: ${err.message}`);
let errIsCatchable = err.message === ERR_NEVER_CHECKED_IN ||
err.message === ERR_CRASHED_ON_STARTUP;
if (!errIsCatchable) {
throw err;
}
if (launchTries <= this.flakeyRetries) {
if (this.gotFBSOpenApplicationError) {
log.debug('Got the FBSOpenApplicationError, not killing the ' +
'sim but leaving it open so the app will launch');
this.gotFBSOpenApplicationError = false; // clear out for next launch
await B.delay(1000);
} else {
if (!this.realDevice) {
await killAllSimulators();
}
await B.delay(5000);
}
} else {
log.errorAndThrow('We exceeded the number of retries allowed for ' +
'instruments to successfully start; failing launch');
}
}
} while (true); // eslint-disable-line no-constant-condition
}
registerLaunch () {
this.launchResultDeferred.resolve();
}
async spawnInstruments () {
let traceDir;
for (let i = 0; ; i++) {
// loop while there are tracedirs to delete
traceDir = path.resolve(this.traceDir, `instrumentscli${i}.trace`);
if (!await fs.exists(traceDir)) break; // eslint-disable-line curly
}
// build up the arguments to use
let args = ['-t', this.template, '-D', traceDir];
if (this.udid) {
// real device, so specify udid
args = args.concat(['-w', this.udid]);
log.debug(`Attempting to run app on real device with UDID '${this.udid}'`);
}
if (!this.udid && this.simulatorSdkAndDevice) {
// sim, so specify the sdk and device
args = args.concat(['-w', this.simulatorSdkAndDevice]);
log.debug(`Attempting to run app on ${this.simulatorSdkAndDevice}`);
}
args = args.concat([this.app]);
if (this.processArguments) {
log.debug(`Attempting to run app with process arguments: ${JSON.stringify(this.processArguments)}`);
// any additional stuff specified by the user
if (_.isString(this.processArguments)) {
if (this.processArguments.indexOf('-e ') === -1) {
log.debug('Plain string process arguments being pushed into arguments');
args.push(this.processArguments);
} else {
log.debug('Environment variables being pushed into arguments');
for (let arg of this.processArguments.split('-e ')) {
arg = arg.trim();
if (arg.length) {
let space = arg.indexOf(' ');
let flag = arg.substring(0, space);
let value = arg.substring(space + 1);
args.push('-e', flag, value);
}
}
}
} else {
// process arguments can also be a hash of flags and values
// {"processArguments": {"flag1": "value1", "flag2": "value2"}}
for (let [flag, value] of _.toPairs(this.processArguments)) {
args.push('-e', flag, value);
}
}
}
args = args.concat(['-e', 'UIASCRIPT', this.bootstrap]);
args = args.concat(['-e', 'UIARESULTSPATH', this.tmpDir]);
if (this.language) {
args = args.concat([`-AppleLanguages (${this.language})`]);
args = args.concat([`-NSLanguages (${this.language})`]);
}
if (this.locale) {
args = args.concat([`-AppleLocale ${this.locale}`]);
}
let env = _.clone(process.env);
if (this.xcodeVersion.major >= 7 && !this.udid) {
// iwd currently does not work with xcode7, setting withoutDelay to false
log.info('On xcode 7.0+, instruments-without-delay does not work, ' +
'skipping instruments-without-delay');
this.withoutDelay = false;
}
let iwdPath = await getIwdPath(this.xcodeVersion.major);
env.CA_DEBUG_TRANSACTIONS = 1;
if (this.withoutDelay && !this.udid) {
// sim, and using i-w-d
env.DYLD_INSERT_LIBRARIES = path.resolve(iwdPath, 'InstrumentsShim.dylib');
env.LIB_PATH = iwdPath;
}
let instrumentsExecArgs = [this.instrumentsPath, ...args];
instrumentsExecArgs = _.map(instrumentsExecArgs, function instrumentArgs (arg) {
if (arg === null) {
throw new Error('A null value was passed as an arg to execute ' +
'instruments on the command line. A letiable is ' +
'probably not getting set. Array of command args: ' +
JSON.stringify(instrumentsExecArgs));
}
// escape any argument that has a space in it
if (_.isString(arg) && arg.indexOf(' ') !== -1) {
return `"${arg}"`;
}
// otherwise just use the argument
return arg;
});
log.debug(`Spawning instruments with command: '${instrumentsExecArgs.join(' ')}'`);
if (this.withoutDelay) {
log.debug('And extra without-delay env: ' + JSON.stringify({
DYLD_INSERT_LIBRARIES: env.DYLD_INSERT_LIBRARIES,
LIB_PATH: env.LIB_PATH
}));
}
log.debug(`And launch timeouts (in ms): ${JSON.stringify(this.launchTimeout)}`);
return await spawn(this.instrumentsPath, args, {env});
}
addSocketConnectTimer (delay, type, doAction) {
let socketConnectDelay = cancellableDelay(delay);
/* eslint-disable */
socketConnectDelay.then(() => {
log.warn(`Instruments socket client never checked in; timing out (${type})`);
return doAction();
}).catch(B.CancellationError, () => {}).done();
/* eslint-enable */
this.socketConnectDelays.push(socketConnectDelay);
}
clearSocketConnectTimers () {
for (let delay of this.socketConnectDelays) {
delay.cancel();
}
this.socketConnectDelays = [];
}
setExitListener (exitListener) {
if (!this.proc) return; // eslint-disable-line curly
if (this.exitListener) {
this.proc.removeListener('exit', this.exitListener);
}
this.exitListener = exitListener;
this.proc.on('exit', exitListener);
}
killInstruments () {
if (!this.proc) return; // eslint-disable-line curly
log.debug(`Kill Instruments process (pid: ${this.proc.pid})`);
return new B(async (resolve) => {
let wasTerminated = false;
// monitoring process termination
let termDelay = cancellableDelay(this.termTimeout);
let termPromise = termDelay.catch(B.CancellationError, () => {});
this.setExitListener(() => {
this.proc = null;
wasTerminated = true;
termDelay.cancel();
resolve();
});
log.debug('Sending SIGTERM');
this.proc.kill('SIGTERM');
await termPromise;
if (!wasTerminated) {
log.warn(`Instruments did not terminate after ${this.termTimeout / 1000} seconds!`);
log.debug('Sending SIGKILL');
this.proc.kill('SIGKILL');
if (_.isFunction(this.exitListener)) {
this.exitListener();
}
}
});
}
/* PROCESS MANAGEMENT */
async shutdown () {
log.debug('Starting shutdown.');
await this.killInstruments();
this.onShutdownDeferred.resolve();
}
}
export default Instruments;