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:
committed by
Facebook GitHub Bot
parent
43242557aa
commit
390f27a137
@@ -19,6 +19,7 @@ import path from 'path';
|
|||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {exec} from 'child_process';
|
import {exec} from 'child_process';
|
||||||
import {default as promiseTimeout} from '../utils/promiseTimeout';
|
import {default as promiseTimeout} from '../utils/promiseTimeout';
|
||||||
|
import {IOSBridge} from '../utils/IOSBridge';
|
||||||
|
|
||||||
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
|
type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault';
|
||||||
|
|
||||||
@@ -45,11 +46,16 @@ export default class IOSDevice extends BaseDevice {
|
|||||||
private recordingProcess?: ChildProcess;
|
private recordingProcess?: ChildProcess;
|
||||||
private recordingLocation?: string;
|
private recordingLocation?: string;
|
||||||
|
|
||||||
constructor(serial: string, deviceType: DeviceType, title: string) {
|
constructor(
|
||||||
|
iOSBridge: IOSBridge,
|
||||||
|
serial: string,
|
||||||
|
deviceType: DeviceType,
|
||||||
|
title: string,
|
||||||
|
) {
|
||||||
super(serial, deviceType, title, 'iOS');
|
super(serial, deviceType, title, 'iOS');
|
||||||
this.icon = 'mobile';
|
this.icon = 'mobile';
|
||||||
this.buffer = '';
|
this.buffer = '';
|
||||||
this.startLogListener();
|
this.startLogListener(iOSBridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
async screenshot(): Promise<Buffer> {
|
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) {
|
if (retries === 0) {
|
||||||
console.warn('Attaching iOS log listener continuously failed.');
|
console.warn('Attaching iOS log listener continuously failed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.log) {
|
if (!this.log) {
|
||||||
const deviceSetPath = process.env.DEVICE_SET_PATH
|
this.log = iOSBridge.startLogListener(this.serial);
|
||||||
? ['--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.on('error', (err: Error) => {
|
this.log.on('error', (err: Error) => {
|
||||||
console.error('iOS log tailer error', err);
|
console.error('iOS log tailer error', err);
|
||||||
});
|
});
|
||||||
@@ -133,22 +105,22 @@ export default class IOSDevice extends BaseDevice {
|
|||||||
this.log.on('exit', () => {
|
this.log.on('exit', () => {
|
||||||
this.log = undefined;
|
this.log = undefined;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.log.stdout
|
this.log.stdout
|
||||||
.pipe(new StripLogPrefix())
|
.pipe(new StripLogPrefix())
|
||||||
.pipe(JSONStream.parse('*'))
|
.pipe(JSONStream.parse('*'))
|
||||||
.on('data', (data: RawLogEntry) => {
|
.on('data', (data: RawLogEntry) => {
|
||||||
const entry = IOSDevice.parseLogEntry(data);
|
const entry = IOSDevice.parseLogEntry(data);
|
||||||
this.addLogEntry(entry);
|
this.addLogEntry(entry);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Could not parse iOS log stream.', e);
|
console.error('Could not parse iOS log stream.', e);
|
||||||
// restart log stream
|
// restart log stream
|
||||||
this.log.kill();
|
this.log.kill();
|
||||||
this.log = undefined;
|
this.log = undefined;
|
||||||
this.startLogListener(retries - 1);
|
this.startLogListener(iOSBridge, retries - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
|
|||||||
import {addErrorNotification} from '../reducers/notifications';
|
import {addErrorNotification} from '../reducers/notifications';
|
||||||
import {getStaticPath} from '../utils/pathUtils';
|
import {getStaticPath} from '../utils/pathUtils';
|
||||||
import {destroyDevice} from '../reducers/connections';
|
import {destroyDevice} from '../reducers/connections';
|
||||||
|
import {IOSBridge, makeIOSBridge} from '../utils/IOSBridge';
|
||||||
|
|
||||||
type iOSSimulatorDevice = {
|
type iOSSimulatorDevice = {
|
||||||
state: 'Booted' | 'Shutdown' | 'Shutting Down';
|
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([
|
return Promise.all([
|
||||||
checkXcodeVersionMismatch(store),
|
checkXcodeVersionMismatch(store),
|
||||||
getSimulators(store, true).then((devices) => {
|
getSimulators(store, true).then((devices) => {
|
||||||
processDevices(store, logger, devices, 'emulator');
|
processDevices(store, logger, iosBridge, devices, 'emulator');
|
||||||
}),
|
}),
|
||||||
getActiveDevices(store.getState().settingsState.idbPath).then(
|
getActiveDevices(store.getState().settingsState.idbPath).then(
|
||||||
(devices: IOSDeviceParams[]) => {
|
(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(
|
function processDevices(
|
||||||
store: Store,
|
store: Store,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
iosBridge: IOSBridge,
|
||||||
activeDevices: IOSDeviceParams[],
|
activeDevices: IOSDeviceParams[],
|
||||||
type: 'physical' | 'emulator',
|
type: 'physical' | 'emulator',
|
||||||
) {
|
) {
|
||||||
@@ -136,7 +142,7 @@ function processDevices(
|
|||||||
name: name,
|
name: name,
|
||||||
serial: udid,
|
serial: udid,
|
||||||
});
|
});
|
||||||
const iOSDevice = new IOSDevice(udid, type, name);
|
const iOSDevice = new IOSDevice(iosBridge, udid, type, name);
|
||||||
iOSDevice.loadDevicePlugins(
|
iOSDevice.loadDevicePlugins(
|
||||||
store.getState().plugins.devicePlugins,
|
store.getState().plugins.devicePlugins,
|
||||||
store.getState().connections.enabledDevicePlugins,
|
store.getState().connections.enabledDevicePlugins,
|
||||||
@@ -225,12 +231,16 @@ function getActiveDevices(idbPath: string): Promise<Array<IOSDeviceParams>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryDevicesForever(store: Store, logger: Logger) {
|
function queryDevicesForever(
|
||||||
return queryDevices(store, logger)
|
store: Store,
|
||||||
|
logger: Logger,
|
||||||
|
iosBridge: IOSBridge,
|
||||||
|
) {
|
||||||
|
return queryDevices(store, logger, iosBridge)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// It's important to schedule the next check AFTER the current one has completed
|
// 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.
|
// to avoid simultaneous queries which can cause multiple user input prompts.
|
||||||
setTimeout(() => queryDevicesForever(store, logger), 3000);
|
setTimeout(() => queryDevicesForever(store, logger, iosBridge), 3000);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.warn('Failed to continuously query devices:', err);
|
console.warn('Failed to continuously query devices:', err);
|
||||||
@@ -285,7 +295,9 @@ export default (store: Store, logger: Logger) => {
|
|||||||
if (store.getState().settingsState.enablePhysicalIOS) {
|
if (store.getState().settingsState.enablePhysicalIOS) {
|
||||||
startDevicePortForwarders();
|
startDevicePortForwarders();
|
||||||
}
|
}
|
||||||
return queryDevicesForever(store, logger);
|
return makeIOSBridge(
|
||||||
|
store.getState().settingsState.idbPath,
|
||||||
|
).then((iosBridge) => queryDevicesForever(store, logger, iosBridge));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
78
desktop/app/src/utils/IOSBridge.tsx
Normal file
78
desktop/app/src/utils/IOSBridge.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
60
desktop/app/src/utils/__tests__/IOSBridge.node.tsx
Normal file
60
desktop/app/src/utils/__tests__/IOSBridge.node.tsx
Normal 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',
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user