Start bridge abstraction

Summary: This is just an early start of centralising some ad-hoc logic we've got all over the place right now. Memoised do-we-have-idb calls with concatenated shell invocations. This gives us the opportunity to do a bit of testing, too.

Reviewed By: mweststrate

Differential Revision: D26694863

fbshipit-source-id: cd2b9883f90397802bbaae6030f7cb3881c565c2
This commit is contained in:
Pascal Hartig
2021-03-02 09:51:16 -08:00
committed by Facebook GitHub Bot
parent 43242557aa
commit 390f27a137
4 changed files with 183 additions and 61 deletions

View File

@@ -19,6 +19,7 @@ import path from 'path';
import {promisify} from 'util';
import {exec} from 'child_process';
import {default as promiseTimeout} from '../utils/promiseTimeout';
import {IOSBridge} from '../utils/IOSBridge';
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
@@ -45,11 +46,16 @@ export default class IOSDevice extends BaseDevice {
private recordingProcess?: ChildProcess;
private recordingLocation?: string;
constructor(serial: string, deviceType: DeviceType, title: string) {
constructor(
iOSBridge: IOSBridge,
serial: string,
deviceType: DeviceType,
title: string,
) {
super(serial, deviceType, title, 'iOS');
this.icon = 'mobile';
this.buffer = '';
this.startLogListener();
this.startLogListener(iOSBridge);
}
async screenshot(): Promise<Buffer> {
@@ -81,47 +87,13 @@ export default class IOSDevice extends BaseDevice {
}
}
startLogListener(retries: number = 3) {
startLogListener(iOSBridge: IOSBridge, retries: number = 3) {
if (retries === 0) {
console.warn('Attaching iOS log listener continuously failed.');
return;
}
if (!this.log) {
const deviceSetPath = process.env.DEVICE_SET_PATH
? ['--set', process.env.DEVICE_SET_PATH]
: [];
const extraArgs = [
'--style',
'json',
'--predicate',
'senderImagePath contains "Containers"',
'--debug',
'--info',
];
if (this.deviceType === 'physical') {
this.log = child_process.spawn(
'idb',
['log', '--udid', this.serial, '--', ...extraArgs],
{},
);
} else {
this.log = child_process.spawn(
'xcrun',
[
'simctl',
...deviceSetPath,
'spawn',
this.serial,
'log',
'stream',
...extraArgs,
],
{},
);
}
this.log = iOSBridge.startLogListener(this.serial);
this.log.on('error', (err: Error) => {
console.error('iOS log tailer error', err);
});
@@ -133,22 +105,22 @@ export default class IOSDevice extends BaseDevice {
this.log.on('exit', () => {
this.log = undefined;
});
}
try {
this.log.stdout
.pipe(new StripLogPrefix())
.pipe(JSONStream.parse('*'))
.on('data', (data: RawLogEntry) => {
const entry = IOSDevice.parseLogEntry(data);
this.addLogEntry(entry);
});
} catch (e) {
console.error('Could not parse iOS log stream.', e);
// restart log stream
this.log.kill();
this.log = undefined;
this.startLogListener(retries - 1);
try {
this.log.stdout
.pipe(new StripLogPrefix())
.pipe(JSONStream.parse('*'))
.on('data', (data: RawLogEntry) => {
const entry = IOSDevice.parseLogEntry(data);
this.addLogEntry(entry);
});
} catch (e) {
console.error('Could not parse iOS log stream.', e);
// restart log stream
this.log.kill();
this.log = undefined;
this.startLogListener(iOSBridge, retries - 1);
}
}
}

View File

@@ -22,6 +22,7 @@ import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
import {addErrorNotification} from '../reducers/notifications';
import {getStaticPath} from '../utils/pathUtils';
import {destroyDevice} from '../reducers/connections';
import {IOSBridge, makeIOSBridge} from '../utils/IOSBridge';
type iOSSimulatorDevice = {
state: 'Booted' | 'Shutdown' | 'Shutting Down';
@@ -92,15 +93,19 @@ if (typeof window !== 'undefined') {
});
}
async function queryDevices(store: Store, logger: Logger): Promise<any> {
async function queryDevices(
store: Store,
logger: Logger,
iosBridge: IOSBridge,
): Promise<any> {
return Promise.all([
checkXcodeVersionMismatch(store),
getSimulators(store, true).then((devices) => {
processDevices(store, logger, devices, 'emulator');
processDevices(store, logger, iosBridge, devices, 'emulator');
}),
getActiveDevices(store.getState().settingsState.idbPath).then(
(devices: IOSDeviceParams[]) => {
processDevices(store, logger, devices, 'physical');
processDevices(store, logger, iosBridge, devices, 'physical');
},
),
]);
@@ -109,6 +114,7 @@ async function queryDevices(store: Store, logger: Logger): Promise<any> {
function processDevices(
store: Store,
logger: Logger,
iosBridge: IOSBridge,
activeDevices: IOSDeviceParams[],
type: 'physical' | 'emulator',
) {
@@ -136,7 +142,7 @@ function processDevices(
name: name,
serial: udid,
});
const iOSDevice = new IOSDevice(udid, type, name);
const iOSDevice = new IOSDevice(iosBridge, udid, type, name);
iOSDevice.loadDevicePlugins(
store.getState().plugins.devicePlugins,
store.getState().connections.enabledDevicePlugins,
@@ -225,12 +231,16 @@ function getActiveDevices(idbPath: string): Promise<Array<IOSDeviceParams>> {
});
}
function queryDevicesForever(store: Store, logger: Logger) {
return queryDevices(store, logger)
function queryDevicesForever(
store: Store,
logger: Logger,
iosBridge: IOSBridge,
) {
return queryDevices(store, logger, iosBridge)
.then(() => {
// It's important to schedule the next check AFTER the current one has completed
// to avoid simultaneous queries which can cause multiple user input prompts.
setTimeout(() => queryDevicesForever(store, logger), 3000);
setTimeout(() => queryDevicesForever(store, logger, iosBridge), 3000);
})
.catch((err) => {
console.warn('Failed to continuously query devices:', err);
@@ -285,7 +295,9 @@ export default (store: Store, logger: Logger) => {
if (store.getState().settingsState.enablePhysicalIOS) {
startDevicePortForwarders();
}
return queryDevicesForever(store, logger);
return makeIOSBridge(
store.getState().settingsState.idbPath,
).then((iosBridge) => queryDevicesForever(store, logger, iosBridge));
}
});
};

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import fs from 'fs';
import child_process from 'child_process';
export interface IOSBridge {
startLogListener: (
udid: string,
) => child_process.ChildProcessWithoutNullStreams;
}
async function isAvailable(idbPath: string): Promise<boolean> {
if (!idbPath) {
return false;
}
return fs.promises
.access(idbPath, fs.constants.X_OK)
.then((_) => true)
.catch((_) => false);
}
const LOG_EXTRA_ARGS = [
'--style',
'json',
'--predicate',
'senderImagePath contains "Containers"',
'--debug',
'--info',
];
function idbStartLogListener(
idbPath: string,
udid: string,
): child_process.ChildProcessWithoutNullStreams {
return child_process.spawn(
idbPath,
['log', '--udid', udid, '--', ...LOG_EXTRA_ARGS],
{},
);
}
function xcrunStartLogListener(udid: string) {
const deviceSetPath = process.env.DEVICE_SET_PATH
? ['--set', process.env.DEVICE_SET_PATH]
: [];
return child_process.spawn(
'xcrun',
[
'simctl',
...deviceSetPath,
'spawn',
udid,
'log',
'stream',
...LOG_EXTRA_ARGS,
],
{},
);
}
export async function makeIOSBridge(idbPath: string): Promise<IOSBridge> {
if (await isAvailable(idbPath)) {
return {
startLogListener: idbStartLogListener.bind(null, idbPath),
};
}
return {
startLogListener: xcrunStartLogListener,
};
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {makeIOSBridge} from '../IOSBridge';
import childProcess from 'child_process';
import {mocked} from 'ts-jest/utils';
jest.mock('child_process');
const spawn = mocked(childProcess.spawn);
test('uses xcrun with no idb', async () => {
const ib = await makeIOSBridge('');
ib.startLogListener('deadbeef');
expect(spawn).toHaveBeenCalledWith(
'xcrun',
[
'simctl',
'spawn',
'deadbeef',
'log',
'stream',
'--style',
'json',
'--predicate',
'senderImagePath contains "Containers"',
'--debug',
'--info',
],
{},
);
});
test('uses idb when present', async () => {
const ib = await makeIOSBridge('/usr/local/bin/idb');
ib.startLogListener('deadbeef');
expect(spawn).toHaveBeenCalledWith(
'/usr/local/bin/idb',
[
'log',
'--udid',
'deadbeef',
'--',
'--style',
'json',
'--predicate',
'senderImagePath contains "Containers"',
'--debug',
'--info',
],
{},
);
});