Move settings, launcherSettings, GKs to app / flipper-server-core

Summary:
This diff moves a lot of stuff from the client to the server. This diff is fairly large, as a lot of concept closely relate, although some things have split off to the earlier diffs in the stack, or are still to follow (like making intern requests).

This diff primarily moves reading and storing settings and GKs from client to server (both flipper and launcher settings). This means that settings are no longer persisted by Redux (which only exists on client). Most other changes are fallout from that. For now settings are just one big object, although we might need to separate settings that are only make sense in an Electron context. For example launcher settings.

Reviewed By: passy, aigoncharov

Differential Revision: D32498649

fbshipit-source-id: d842faf7a7f03774b621c7656e53a9127afc6192
This commit is contained in:
Michel Weststrate
2021-12-08 04:25:28 -08:00
committed by Facebook GitHub Bot
parent eed19b3a3d
commit bca169df73
71 changed files with 844 additions and 830 deletions

View File

@@ -7,43 +7,22 @@
* @format
*/
import {isTest} from 'flipper-common';
import {FlipperServerConfig} from 'flipper-common';
import {parseFlipperPorts} from './utils/environmentVariables';
export interface FlipperServerConfig {
enableAndroid: boolean;
androidHome: string;
enableIOS: boolean;
idbPath: string;
enablePhysicalIOS: boolean;
validWebSocketOrigins: string[];
staticPath: string;
tempPath: string;
}
// defaultConfig should be used for testing only, and disables by default all features
const testConfig: FlipperServerConfig = {
androidHome: '',
enableAndroid: false,
enableIOS: false,
enablePhysicalIOS: false,
idbPath: '',
validWebSocketOrigins: [],
staticPath: '/static/',
tempPath: '/temp/',
};
let currentConfig: FlipperServerConfig | undefined = undefined;
// just an ugly utility to not need a reference to FlipperServerImpl itself everywhere
export function getFlipperServerConfig(): FlipperServerConfig {
if (!currentConfig) {
if (isTest()) return testConfig;
throw new Error('FlipperServerConfig has not been set');
}
return currentConfig;
}
export function setFlipperServerConfig(config: FlipperServerConfig) {
export function setFlipperServerConfig(
config: FlipperServerConfig | undefined,
) {
currentConfig = config;
}

View File

@@ -24,15 +24,15 @@ import {
FlipperServerCommands,
FlipperServer,
UninitializedClient,
FlipperServerConfig,
} from 'flipper-common';
import {ServerDevice} from './devices/ServerDevice';
import {Base64} from 'js-base64';
import MetroDevice from './devices/metro/MetroDevice';
import {launchEmulator} from './devices/android/AndroidDevice';
import {
FlipperServerConfig,
setFlipperServerConfig,
} from './FlipperServerConfig';
import {setFlipperServerConfig} from './FlipperServerConfig';
import {saveSettings} from './utils/settings';
import {saveLauncherSettings} from './utils/launcherSettings';
/**
* FlipperServer takes care of all incoming device & client connections.
@@ -52,7 +52,7 @@ export class FlipperServerImpl implements FlipperServer {
android: AndroidDeviceManager;
ios: IOSDeviceManager;
constructor(config: FlipperServerConfig, public logger: Logger) {
constructor(public config: FlipperServerConfig, public logger: Logger) {
setFlipperServerConfig(config);
const server = (this.server = new ServerController(this));
this.android = new AndroidDeviceManager(this);
@@ -107,7 +107,7 @@ export class FlipperServerImpl implements FlipperServer {
/**
* Starts listening to parts and watching for devices
*/
async start() {
async connect() {
if (this.state !== 'pending') {
throw new Error('Server already started');
}
@@ -170,6 +170,7 @@ export class FlipperServerImpl implements FlipperServer {
}
private commandHandler: FlipperServerCommands = {
'get-config': async () => this.config,
'device-start-logging': async (serial: string) =>
this.getDevice(serial).startLogging(),
'device-stop-logging': async (serial: string) =>
@@ -223,6 +224,9 @@ export class FlipperServerImpl implements FlipperServer {
'ios-get-simulators': async (bootedOnly) =>
this.ios.getSimulators(bootedOnly),
'ios-launch-simulator': async (udid) => launchSimulator(udid),
'persist-settings': async (settings) => saveSettings(settings),
'persist-launcher-settings': async (settings) =>
saveLauncherSettings(settings),
};
registerDevice(device: ServerDevice) {

View File

@@ -96,7 +96,7 @@ class ServerController extends EventEmitter implements ServerEventsListener {
this.certificateProvider = new CertificateProvider(
this,
this.logger,
getFlipperServerConfig(),
getFlipperServerConfig().settings,
);
this.connectionTracker = new ConnectionTracker(this.logger);
this.secureServer = null;
@@ -244,13 +244,13 @@ class ServerController extends EventEmitter implements ServerEventsListener {
const {os, app, device_id} = clientQuery;
// without these checks, the user might see a connection timeout error instead, which would be much harder to track down
if (os === 'iOS' && !getFlipperServerConfig().enableIOS) {
if (os === 'iOS' && !getFlipperServerConfig().settings.enableIOS) {
console.error(
`Refusing connection from ${app} on ${device_id}, since iOS support is disabled in settings`,
);
return;
}
if (os === 'Android' && !getFlipperServerConfig().enableAndroid) {
if (os === 'Android' && !getFlipperServerConfig().settings.enableAndroid) {
console.error(
`Refusing connection from ${app} on ${device_id}, since Android support is disabled in settings`,
);

View File

@@ -184,7 +184,7 @@ export class AndroidDeviceManager {
async watchAndroidDevices() {
try {
const client = await getAdbClient(getFlipperServerConfig());
const client = await getAdbClient(getFlipperServerConfig().settings);
client
.trackDevices()
.then((tracker) => {

View File

@@ -98,7 +98,7 @@ export function xcrunStartLogListener(udid: string, deviceType: DeviceType) {
function makeTempScreenshotFilePath() {
const imageName = uuid() + '.png';
return path.join(getFlipperServerConfig().tempPath, imageName);
return path.join(getFlipperServerConfig().paths.tempPath, imageName);
}
async function runScreenshotCommand(

View File

@@ -7,13 +7,23 @@
* @format
*/
import {makeIOSBridge} from '../IOSBridge';
import childProcess from 'child_process';
import * as promisifyChildProcess from 'promisify-child-process';
jest.mock('child_process');
jest.mock('promisify-child-process');
import {makeIOSBridge} from '../IOSBridge';
import * as promisifyChildProcess from 'promisify-child-process';
import {setFlipperServerConfig} from '../../../FlipperServerConfig';
import {getRenderHostInstance} from 'flipper-ui-core';
beforeEach(() => {
setFlipperServerConfig(getRenderHostInstance().serverConfig);
});
afterEach(() => {
setFlipperServerConfig(undefined);
});
test('uses xcrun with no idb when xcode is detected', async () => {
const ib = await makeIOSBridge('', true);
@@ -95,10 +105,10 @@ test.unix(
async () => {
const ib = await makeIOSBridge('', true);
ib.screenshot('deadbeef');
await expect(() => ib.screenshot('deadbeef')).rejects.toThrow();
expect(promisifyChildProcess.exec).toHaveBeenCalledWith(
'xcrun simctl io deadbeef screenshot /temp/00000000-0000-0000-0000-000000000000.png',
expect((promisifyChildProcess.exec as any).mock.calls[0][0]).toMatch(
'xcrun simctl io deadbeef screenshot',
);
},
);
@@ -106,17 +116,17 @@ test.unix(
test.unix('uses idb to take screenshots when available', async () => {
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
ib.screenshot('deadbeef');
await expect(() => ib.screenshot('deadbeef')).rejects.toThrow();
expect(promisifyChildProcess.exec).toHaveBeenCalledWith(
'idb screenshot --udid deadbeef /temp/00000000-0000-0000-0000-000000000000.png',
expect((promisifyChildProcess.exec as any).mock.calls[0][0]).toMatch(
'idb screenshot --udid deadbeef ',
);
});
test('uses xcrun to navigate with no idb when xcode is detected', async () => {
const ib = await makeIOSBridge('', true);
ib.navigate('deadbeef', 'fb://dummy');
await ib.navigate('deadbeef', 'fb://dummy');
expect(promisifyChildProcess.exec).toHaveBeenCalledWith(
'xcrun simctl io deadbeef launch url "fb://dummy"',
@@ -126,7 +136,7 @@ test('uses xcrun to navigate with no idb when xcode is detected', async () => {
test('uses idb to navigate when available', async () => {
const ib = await makeIOSBridge('/usr/local/bin/idb', true, async (_) => true);
ib.navigate('deadbeef', 'fb://dummy');
await ib.navigate('deadbeef', 'fb://dummy');
expect(promisifyChildProcess.exec).toHaveBeenCalledWith(
'idb open --udid deadbeef "fb://dummy"',

View File

@@ -11,9 +11,19 @@ import {parseXcodeFromCoreSimPath} from '../iOSDeviceManager';
import {getLogger} from 'flipper-common';
import {IOSBridge} from '../IOSBridge';
import {FlipperServerImpl} from '../../../FlipperServerImpl';
import {getFlipperServerConfig} from '../../../FlipperServerConfig';
import {getRenderHostInstance} from 'flipper-ui-core';
import {
getFlipperServerConfig,
setFlipperServerConfig,
} from '../../../FlipperServerConfig';
const testConfig = getFlipperServerConfig();
beforeEach(() => {
setFlipperServerConfig(getRenderHostInstance().serverConfig);
});
afterEach(() => {
setFlipperServerConfig(undefined);
});
const standardCoresimulatorLog =
'username 1264 0.0 0.1 5989740 41648 ?? Ss 2:23PM 0:12.92 /Applications/Xcode_12.4.0_fb.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/libexec/mobileassetd';
@@ -56,7 +66,10 @@ test('test parseXcodeFromCoreSimPath from standard locations', () => {
});
test('test getAllPromisesForQueryingDevices when xcode detected', () => {
const flipperServer = new FlipperServerImpl(testConfig, getLogger());
const flipperServer = new FlipperServerImpl(
getFlipperServerConfig(),
getLogger(),
);
flipperServer.ios.iosBridge = {} as IOSBridge;
const promises = flipperServer.ios.getAllPromisesForQueryingDevices(
true,
@@ -66,7 +79,10 @@ test('test getAllPromisesForQueryingDevices when xcode detected', () => {
});
test('test getAllPromisesForQueryingDevices when xcode is not detected', () => {
const flipperServer = new FlipperServerImpl(testConfig, getLogger());
const flipperServer = new FlipperServerImpl(
getFlipperServerConfig(),
getLogger(),
);
flipperServer.ios.iosBridge = {} as IOSBridge;
const promises = flipperServer.ios.getAllPromisesForQueryingDevices(
false,
@@ -76,7 +92,10 @@ test('test getAllPromisesForQueryingDevices when xcode is not detected', () => {
});
test('test getAllPromisesForQueryingDevices when xcode and idb are both unavailable', () => {
const flipperServer = new FlipperServerImpl(testConfig, getLogger());
const flipperServer = new FlipperServerImpl(
getFlipperServerConfig(),
getLogger(),
);
flipperServer.ios.iosBridge = {} as IOSBridge;
const promises = flipperServer.ios.getAllPromisesForQueryingDevices(
false,
@@ -86,7 +105,10 @@ test('test getAllPromisesForQueryingDevices when xcode and idb are both unavaila
});
test('test getAllPromisesForQueryingDevices when both idb and xcode are available', () => {
const flipperServer = new FlipperServerImpl(testConfig, getLogger());
const flipperServer = new FlipperServerImpl(
getFlipperServerConfig(),
getLogger(),
);
flipperServer.ios.iosBridge = {} as IOSBridge;
const promises = flipperServer.ios.getAllPromisesForQueryingDevices(
true,

View File

@@ -46,7 +46,7 @@ export class IOSDeviceManager {
private portForwarders: Array<ChildProcess> = [];
private portforwardingClient = path.join(
getFlipperServerConfig().staticPath,
getFlipperServerConfig().paths.staticPath,
'PortForwardingMacApp.app',
'Contents',
'MacOS',
@@ -111,7 +111,7 @@ export class IOSDeviceManager {
isXcodeDetected: boolean,
isIdbAvailable: boolean,
): Array<Promise<any>> {
const config = getFlipperServerConfig();
const config = getFlipperServerConfig().settings;
return [
isIdbAvailable
? getActiveDevices(config.idbPath, config.enablePhysicalIOS).then(
@@ -130,7 +130,7 @@ export class IOSDeviceManager {
}
private async queryDevices(): Promise<any> {
const config = getFlipperServerConfig();
const config = getFlipperServerConfig().settings;
const isXcodeInstalled = await iosUtil.isXcodeDetected();
const isIdbAvailable = await iosUtil.isAvailable(config.idbPath);
console.debug(
@@ -182,21 +182,19 @@ export class IOSDeviceManager {
public async watchIOSDevices() {
// TODO: pull this condition up
if (!getFlipperServerConfig().enableIOS) {
const settings = getFlipperServerConfig().settings;
if (!settings.enableIOS) {
return;
}
try {
const isDetected = await iosUtil.isXcodeDetected();
this.xcodeCommandLineToolsDetected = isDetected;
if (getFlipperServerConfig().enablePhysicalIOS) {
if (settings.enablePhysicalIOS) {
this.startDevicePortForwarders();
}
try {
// Awaiting the promise here to trigger immediate error handling.
this.iosBridge = await makeIOSBridge(
getFlipperServerConfig().idbPath,
isDetected,
);
this.iosBridge = await makeIOSBridge(settings.idbPath, isDetected);
this.queryDevicesForever();
} catch (err) {
// This case is expected if both Xcode and idb are missing.

View File

@@ -0,0 +1,66 @@
/**
* 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
*/
export type GKID = string;
export const TEST_PASSING_GK = 'TEST_PASSING_GK';
export const TEST_FAILING_GK = 'TEST_FAILING_GK';
export type GKMap = {[key: string]: boolean};
const whitelistedGKs: Array<GKID> = [];
export function loadGKs(_username: string, _gks: Array<GKID>): Promise<GKMap> {
return Promise.reject(
new Error('Implement your custom logic for loading GK'),
);
}
export function loadDistilleryGK(
_gk: GKID,
): Promise<{[key: string]: {result: boolean}}> {
return Promise.reject(
new Error('Implement your custom logic for loading GK'),
);
}
export default class GK {
static init() {}
static get(id: GKID): boolean {
if (process.env.NODE_ENV === 'test' && id === TEST_PASSING_GK) {
return true;
}
if (whitelistedGKs.includes(id)) {
return true;
}
return false;
}
static async withWhitelistedGK(
id: GKID,
callback: () => Promise<void> | void,
) {
whitelistedGKs.push(id);
try {
const p = callback();
if (p) {
await p;
}
} finally {
const idx = whitelistedGKs.indexOf(id);
if (idx !== -1) {
whitelistedGKs.splice(idx, 1);
}
}
}
static allGKs(): GKMap {
return {};
}
}

View File

@@ -0,0 +1,14 @@
/**
* 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 {Tristate} from 'flipper-common';
export async function setupPrefetcher(_settings: {
enablePrefetching: Tristate;
}) {}

View File

@@ -8,3 +8,20 @@
*/
export {FlipperServerImpl} from './FlipperServerImpl';
export {loadSettings} from './utils/settings';
export {loadLauncherSettings} from './utils/launcherSettings';
export {loadProcessConfig} from './utils/processConfig';
import GKImplementation from './fb-stubs/GK';
export {setupPrefetcher} from './fb-stubs/Prefetcher';
let loaded = false;
export function getGatekeepers(): Record<string, boolean> {
if (!loaded) {
// this starts fetching gatekeepers, note that they will only be available on next restart!
GKImplementation.init();
loaded = true;
}
return GKImplementation.allGKs();
}

View File

@@ -0,0 +1,40 @@
/**
* 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 {loadProcessConfig} from '../processConfig';
test('config is decoded from env', () => {
const config = loadProcessConfig({
CONFIG: JSON.stringify({
disabledPlugins: ['pluginA', 'pluginB', 'pluginC'],
lastWindowPosition: {x: 4, y: 8, width: 15, height: 16},
launcherMsg: 'wubba lubba dub dub',
screenCapturePath: '/my/screenshot/path',
launcherEnabled: false,
}),
});
expect(config).toEqual({
disabledPlugins: new Set(['pluginA', 'pluginB', 'pluginC']),
lastWindowPosition: {x: 4, y: 8, width: 15, height: 16},
launcherMsg: 'wubba lubba dub dub',
screenCapturePath: '/my/screenshot/path',
launcherEnabled: false,
});
});
test('config is decoded from env with defaults', () => {
expect(loadProcessConfig({CONFIG: '{}'})).toEqual({
disabledPlugins: new Set([]),
lastWindowPosition: undefined,
launcherMsg: undefined,
screenCapturePath: undefined,
launcherEnabled: true,
});
});

View File

@@ -0,0 +1,91 @@
/**
* 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 path from 'path';
import os from 'os';
import xdg from 'xdg-basedir';
import fs from 'fs-extra';
import TOML, {JsonMap} from '@iarna/toml';
import {LauncherSettings, ReleaseChannel} from 'flipper-common';
export function xdgConfigDir() {
return os.platform() === 'darwin'
? path.join(os.homedir(), 'Library', 'Preferences')
: xdg.config || path.join(os.homedir(), '.config');
}
export function launcherConfigDir() {
return path.join(
xdgConfigDir(),
os.platform() == 'darwin' ? 'rs.flipper-launcher' : 'flipper-launcher',
);
}
function getLauncherSettingsFile(): string {
// There is some disagreement among the XDG Base Directory implementations
// whether to use ~/Library/Preferences or ~/.config on MacOS. The Launcher
// expects the former, whereas `xdg-basedir` implements the latter.
return path.resolve(launcherConfigDir(), 'flipper-launcher.toml');
}
const defaultLauncherSettings: LauncherSettings = {
releaseChannel: ReleaseChannel.DEFAULT,
ignoreLocalPin: false,
};
interface FormattedSettings {
ignore_local_pin?: boolean;
release_channel?: ReleaseChannel;
}
function serialize(value: LauncherSettings): string {
const {ignoreLocalPin, releaseChannel, ...rest} = value;
const formattedSettings: FormattedSettings = {
...rest,
ignore_local_pin: ignoreLocalPin,
release_channel: releaseChannel,
};
return TOML.stringify(formattedSettings as JsonMap);
}
function deserialize(content: string): LauncherSettings {
const {ignore_local_pin, release_channel, ...rest} = TOML.parse(
content,
) as FormattedSettings;
return {
...rest,
ignoreLocalPin: !!ignore_local_pin,
releaseChannel: release_channel ?? ReleaseChannel.DEFAULT,
};
}
export async function loadLauncherSettings(): Promise<LauncherSettings> {
const fileName = getLauncherSettingsFile();
try {
const content = (await fs.readFile(fileName)).toString();
return deserialize(content);
} catch (e) {
console.warn(
`Failed to read settings file: "${fileName}". ${e}. Replacing file with default settings.`,
);
await saveLauncherSettings(defaultLauncherSettings);
return defaultLauncherSettings;
}
}
export async function saveLauncherSettings(settings: LauncherSettings) {
const fileName = getLauncherSettingsFile();
const dir = path.dirname(fileName);
const exists = await fs.pathExists(dir);
if (!exists) {
await fs.mkdir(dir, {recursive: true});
}
const content = serialize(settings);
return fs.writeFile(fileName, content);
}

View File

@@ -0,0 +1,22 @@
/**
* 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 {ProcessConfig} from 'flipper-common';
export function loadProcessConfig(env: NodeJS.ProcessEnv): ProcessConfig {
const json = JSON.parse(env.CONFIG || '{}');
return {
disabledPlugins: new Set<string>(json.disabledPlugins || []),
lastWindowPosition: json.lastWindowPosition,
launcherMsg: json.launcherMsg,
screenCapturePath: json.screenCapturePath,
launcherEnabled:
typeof json.launcherEnabled === 'boolean' ? json.launcherEnabled : true,
};
}

View File

@@ -0,0 +1,67 @@
/**
* 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 os from 'os';
import {resolve} from 'path';
import xdg from 'xdg-basedir';
import {Settings, Tristate} from 'flipper-common';
import {readFile, writeFile, access} from 'fs-extra';
export async function loadSettings(): Promise<Settings> {
if (!access(getSettingsFile())) {
return getDefaultSettings();
}
const json = await readFile(getSettingsFile(), {encoding: 'utf8'});
return JSON.parse(json);
}
export async function saveSettings(settings: Settings): Promise<void> {
await writeFile(getSettingsFile(), JSON.stringify(settings, null, 2), {
encoding: 'utf8',
});
}
function getSettingsFile() {
return resolve(
...(xdg.config ? [xdg.config] : [os.homedir(), '.config']),
'flipper',
'settings.json',
);
}
export const DEFAULT_ANDROID_SDK_PATH = getDefaultAndroidSdkPath();
function getDefaultSettings(): Settings {
return {
androidHome: getDefaultAndroidSdkPath(),
enableAndroid: true,
enableIOS: os.platform() === 'darwin',
enablePhysicalIOS: os.platform() === 'darwin',
enablePrefetching: Tristate.Unset,
idbPath: '/usr/local/bin/idb',
reactNative: {
shortcuts: {
enabled: false,
reload: 'Alt+Shift+R',
openDevMenu: 'Alt+Shift+D',
},
},
darkMode: 'light',
showWelcomeAtStartup: true,
suppressPluginErrors: false,
};
}
function getDefaultAndroidSdkPath() {
return os.platform() === 'win32' ? getWindowsSdkPath() : '/opt/android_sdk';
}
function getWindowsSdkPath() {
return `${os.homedir()}\\AppData\\Local\\android\\sdk`;
}