Summary:
[interesting] since it shows how Flipper APIs are exposed through sandy. However, the next diff is a much simpler example of that
This diff adds support for adding menu entries for sandy plugin (renamed keyboard actions to menus, as it always creates a menu entry, but not necessarily a keyboard shortcut)
```
client.addMenuEntry(
// custom entry
{
label: 'Reset Selection',
topLevelMenu: 'Edit',
handler: () => {
selectedID.set(null);
},
},
// based on built-in action (sets standard label, shortcut)
{
action: 'createPaste',
handler: () => {
console.log('creating paste');
},
},
);
```
Most of this diff is introducing the concept of FlipperUtils, a set of static Flipper methods (not related to a device or client) that can be used from Sandy. This will for example be used to implement things as `createPaste` as well
Reviewed By: nikoant
Differential Revision: D22766990
fbshipit-source-id: ce90af3b700e6c3d9a779a3bab4673ba356f3933
268 lines
7.9 KiB
TypeScript
268 lines
7.9 KiB
TypeScript
/**
|
|
* 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 {ChildProcess} from 'child_process';
|
|
import {Store} from '../reducers/index';
|
|
import {setXcodeDetected} from '../reducers/application';
|
|
import {Logger} from '../fb-interfaces/Logger';
|
|
import type {DeviceType} from 'flipper-plugin';
|
|
import {promisify} from 'util';
|
|
import path from 'path';
|
|
import child_process from 'child_process';
|
|
const execFile = child_process.execFile;
|
|
import iosUtil from '../utils/iOSContainerUtility';
|
|
import IOSDevice from '../devices/IOSDevice';
|
|
import isProduction from '../utils/isProduction';
|
|
import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
|
|
|
|
type iOSSimulatorDevice = {
|
|
state: 'Booted' | 'Shutdown' | 'Shutting Down';
|
|
availability?: string;
|
|
isAvailable?: 'YES' | 'NO' | true | false;
|
|
name: string;
|
|
udid: string;
|
|
};
|
|
|
|
type IOSDeviceParams = {udid: string; type: DeviceType; name: string};
|
|
|
|
const exec = promisify(child_process.exec);
|
|
|
|
let portForwarders: Array<ChildProcess> = [];
|
|
|
|
function isAvailable(simulator: iOSSimulatorDevice): boolean {
|
|
// For some users "availability" is set, for others it's "isAvailable"
|
|
// It's not clear which key is set, so we are checking both.
|
|
// We've also seen isAvailable return "YES" and true, depending on version.
|
|
return (
|
|
simulator.availability === '(available)' ||
|
|
simulator.isAvailable === 'YES' ||
|
|
simulator.isAvailable === true
|
|
);
|
|
}
|
|
|
|
const portforwardingClient = isProduction()
|
|
? path.resolve(
|
|
__dirname,
|
|
'PortForwardingMacApp.app/Contents/MacOS/PortForwardingMacApp',
|
|
)
|
|
: 'PortForwardingMacApp.app/Contents/MacOS/PortForwardingMacApp';
|
|
|
|
function forwardPort(port: number, multiplexChannelPort: number) {
|
|
return execFile(portforwardingClient, [
|
|
`-portForward=${port}`,
|
|
`-multiplexChannelPort=${multiplexChannelPort}`,
|
|
]);
|
|
}
|
|
|
|
function startDevicePortForwarders(): void {
|
|
if (portForwarders.length > 0) {
|
|
// Only ever start them once.
|
|
return;
|
|
}
|
|
// start port forwarding server for real device connections
|
|
portForwarders = [forwardPort(8089, 8079), forwardPort(8088, 8078)];
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('beforeunload', () => {
|
|
portForwarders.forEach((process) => process.kill());
|
|
});
|
|
}
|
|
|
|
async function queryDevices(store: Store, logger: Logger): Promise<any> {
|
|
return Promise.all([
|
|
checkXcodeVersionMismatch(store),
|
|
getActiveSimulators().then((devices) => {
|
|
processDevices(store, logger, devices, 'emulator');
|
|
}),
|
|
getActiveDevices().then((devices) => {
|
|
processDevices(store, logger, devices, 'physical');
|
|
}),
|
|
]);
|
|
}
|
|
|
|
function processDevices(
|
|
store: Store,
|
|
logger: Logger,
|
|
activeDevices: IOSDeviceParams[],
|
|
type: 'physical' | 'emulator',
|
|
) {
|
|
const {connections} = store.getState();
|
|
const currentDeviceIDs: Set<string> = new Set(
|
|
connections.devices
|
|
.filter(
|
|
(device) =>
|
|
device instanceof IOSDevice &&
|
|
device.deviceType === type &&
|
|
!device.isArchived,
|
|
)
|
|
.map((device) => device.serial),
|
|
);
|
|
|
|
for (const {udid, type, name} of activeDevices) {
|
|
if (currentDeviceIDs.has(udid)) {
|
|
currentDeviceIDs.delete(udid);
|
|
} else {
|
|
logger.track('usage', 'register-device', {
|
|
os: 'iOS',
|
|
type: type,
|
|
name: name,
|
|
serial: udid,
|
|
});
|
|
const iOSDevice = new IOSDevice(udid, type, name);
|
|
iOSDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
|
|
store.dispatch({
|
|
type: 'REGISTER_DEVICE',
|
|
payload: iOSDevice,
|
|
});
|
|
registerDeviceCallbackOnPlugins(
|
|
store,
|
|
store.getState().plugins.devicePlugins,
|
|
store.getState().plugins.clientPlugins,
|
|
iOSDevice,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (currentDeviceIDs.size > 0) {
|
|
currentDeviceIDs.forEach((id) =>
|
|
logger.track('usage', 'unregister-device', {os: 'iOS', serial: id}),
|
|
);
|
|
store.dispatch({
|
|
type: 'UNREGISTER_DEVICES',
|
|
payload: currentDeviceIDs,
|
|
});
|
|
}
|
|
}
|
|
|
|
function getActiveSimulators(): Promise<Array<IOSDeviceParams>> {
|
|
const deviceSetPath = process.env.DEVICE_SET_PATH
|
|
? ['--set', process.env.DEVICE_SET_PATH]
|
|
: [];
|
|
return promisify(execFile)(
|
|
'xcrun',
|
|
['simctl', ...deviceSetPath, 'list', 'devices', '--json'],
|
|
{
|
|
encoding: 'utf8',
|
|
},
|
|
)
|
|
.then(({stdout}) => JSON.parse(stdout).devices)
|
|
.then((simulatorDevices: Array<iOSSimulatorDevice>) => {
|
|
const simulators: Array<iOSSimulatorDevice> = Object.values(
|
|
simulatorDevices,
|
|
).reduce((acc: Array<iOSSimulatorDevice>, cv) => acc.concat(cv), []);
|
|
|
|
return simulators
|
|
.filter(
|
|
(simulator) => simulator.state === 'Booted' && isAvailable(simulator),
|
|
)
|
|
.map((simulator) => {
|
|
return {
|
|
udid: simulator.udid,
|
|
type: 'emulator',
|
|
name: simulator.name,
|
|
} as IOSDeviceParams;
|
|
});
|
|
})
|
|
.catch((_) => []);
|
|
}
|
|
|
|
function getActiveDevices(): Promise<Array<IOSDeviceParams>> {
|
|
return iosUtil.targets().catch((e) => {
|
|
console.error(e.message);
|
|
return [];
|
|
});
|
|
}
|
|
|
|
function queryDevicesForever(store: Store, logger: Logger) {
|
|
return queryDevices(store, logger)
|
|
.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);
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
let xcodeVersionMismatchFound = false;
|
|
async function checkXcodeVersionMismatch(store: Store) {
|
|
if (xcodeVersionMismatchFound) {
|
|
return;
|
|
}
|
|
try {
|
|
let {stdout: xcodeCLIVersion} = await exec('xcode-select -p');
|
|
xcodeCLIVersion = xcodeCLIVersion.trim();
|
|
const {stdout} = await exec('ps aux | grep CoreSimulator');
|
|
for (const line of stdout.split('\n')) {
|
|
const match = line.match(
|
|
/\/Applications\/Xcode[^/]*\.app\/Contents\/Developer/,
|
|
);
|
|
const runningVersion = match && match.length > 0 ? match[0].trim() : null;
|
|
if (runningVersion && runningVersion !== xcodeCLIVersion) {
|
|
const errorMessage = `Xcode version mismatch: Simulator is running from "${runningVersion}" while Xcode CLI is "${xcodeCLIVersion}". Running "xcode-select --switch ${runningVersion}" can fix this.`;
|
|
store.dispatch({
|
|
type: 'SERVER_ERROR',
|
|
payload: {
|
|
message: errorMessage,
|
|
details:
|
|
"You might want to run 'sudo xcode-select -s /Applications/Xcode.app/Contents/Developer'",
|
|
urgent: true,
|
|
},
|
|
});
|
|
// Fire a console.error as well, so that it gets reported to the backend.
|
|
console.error(errorMessage);
|
|
xcodeVersionMismatchFound = true;
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
async function isXcodeDetected(): Promise<boolean> {
|
|
return exec('xcode-select -p')
|
|
.then((_) => true)
|
|
.catch((_) => false);
|
|
}
|
|
|
|
export async function getActiveDevicesAndSimulators(): Promise<
|
|
Array<IOSDevice>
|
|
> {
|
|
const activeDevices: Array<Array<IOSDeviceParams>> = await Promise.all([
|
|
getActiveSimulators(),
|
|
getActiveDevices(),
|
|
]);
|
|
const allDevices = activeDevices[0].concat(activeDevices[1]);
|
|
return allDevices.map((device) => {
|
|
const {udid, type, name} = device;
|
|
return new IOSDevice(udid, type, name);
|
|
});
|
|
}
|
|
|
|
export default (store: Store, logger: Logger) => {
|
|
// monitoring iOS devices only available on MacOS.
|
|
if (process.platform !== 'darwin') {
|
|
return;
|
|
}
|
|
if (!store.getState().settingsState.enableIOS) {
|
|
return;
|
|
}
|
|
isXcodeDetected().then((isDetected) => {
|
|
store.dispatch(setXcodeDetected(isDetected));
|
|
if (isDetected) {
|
|
if (store.getState().settingsState.enablePhysicalIOS) {
|
|
startDevicePortForwarders();
|
|
}
|
|
return queryDevicesForever(store, logger);
|
|
}
|
|
});
|
|
};
|