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

@@ -1,44 +0,0 @@
/**
* 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
*/
/* eslint-disable node/no-sync */
import JsonFileStorage from '../jsonFileReduxPersistStorage';
import fs from 'fs';
const validSerializedData = fs
.readFileSync(
'flipper-ui-core/src/utils/__tests__/data/settings-v1-valid.json',
)
.toString()
.replace(/\r\n/g, '\n')
.trim();
const validDeserializedData =
'{"androidHome":"\\"/opt/android_sdk\\"","something":"{\\"else\\":4}","_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}';
const storage = new JsonFileStorage(
'flipper-ui-core/src/utils/__tests__/data/settings-v1-valid.json',
);
test('A valid settings file gets parsed correctly', () => {
return storage
.getItem('anykey')
.then((result) => expect(result).toEqual(validDeserializedData));
});
test('deserialize works as expected', () => {
const deserialized = storage.deserializeValue(validSerializedData);
expect(deserialized).toEqual(validDeserializedData);
});
test('serialize works as expected', () => {
const serialized = storage.serializeValue(validDeserializedData);
expect(serialized).toEqual(validSerializedData);
});

View File

@@ -1,44 +0,0 @@
/**
* 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 {default as config, resetConfigForTesting} from '../processConfig';
afterEach(() => {
resetConfigForTesting();
});
test('config is decoded from env', () => {
process.env.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', () => {
process.env.CONFIG = '{}';
expect(config()).toEqual({
disabledPlugins: new Set([]),
lastWindowPosition: undefined,
launcherMsg: undefined,
screenCapturePath: undefined,
launcherEnabled: true,
});
});

View File

@@ -11,7 +11,6 @@ import {_setFlipperLibImplementation} from 'flipper-plugin';
import type {Logger} from 'flipper-common';
import type {Store} from '../reducers';
import createPaste from '../fb-stubs/createPaste';
import GK from '../fb-stubs/GK';
import type BaseDevice from '../devices/BaseDevice';
import constants from '../fb-stubs/constants';
import {addNotification} from '../reducers/notifications';
@@ -32,9 +31,7 @@ export function initializeFlipperLibImplementation(
store.dispatch(setMenuEntries(entries));
},
createPaste,
GK(gatekeeper: string) {
return GK.get(gatekeeper);
},
GK: renderHost.GK,
selectPlugin(device, client, pluginId, deeplink) {
store.dispatch({
type: 'SELECT_PLUGIN',
@@ -63,8 +60,8 @@ export function initializeFlipperLibImplementation(
importFile: renderHost.importFile,
exportFile: renderHost.exportFile,
paths: {
appPath: renderHost.paths.appPath,
homePath: renderHost.paths.homePath,
appPath: renderHost.serverConfig.paths.appPath,
homePath: renderHost.serverConfig.paths.homePath,
},
});
}

View File

@@ -73,18 +73,14 @@ export function buildLocalIconURL(name: string, size: number, density: number) {
export function buildIconURLSync(name: string, size: number, density: number) {
const icon = getIconPartsFromName(name);
// eslint-disable-next-line prettier/prettier
const url = `https://facebook.com/assets/?name=${
icon.trimmedName
}&variant=${
icon.variant
}&size=${size}&set=facebook_icons&density=${density}x`;
const url = `https://facebook.com/assets/?name=${icon.trimmedName}&variant=${icon.variant}&size=${size}&set=facebook_icons&density=${density}x`;
if (
typeof window !== 'undefined' &&
(!getIconsSync()[name] || !getIconsSync()[name].includes(size))
) {
// From utils/isProduction
const isProduction = !/node_modules[\\/]electron[\\/]/.test(
getRenderHostInstance().paths.execPath,
getRenderHostInstance().serverConfig.paths.execPath,
);
if (!isProduction) {
@@ -108,9 +104,7 @@ export function buildIconURLSync(name: string, size: number, density: number) {
} else {
throw new Error(
// eslint-disable-next-line prettier/prettier
`Trying to use icon '${name}' with size ${size} and density ${density}, however the icon doesn't seem to exists at ${url}: ${
res.status
}`,
`Trying to use icon '${name}' with size ${size} and density ${density}, however the icon doesn't seem to exists at ${url}: ${res.status}`,
);
}
})
@@ -129,7 +123,7 @@ export function getIconURLSync(
name: string,
size: number,
density: number,
basePath: string = getRenderHostInstance().paths.appPath,
basePath: string = getRenderHostInstance().serverConfig.paths.appPath,
) {
if (name.indexOf('/') > -1) {
return name;

View File

@@ -9,13 +9,13 @@
import {PluginDetails} from 'flipper-plugin-lib';
import semver from 'semver';
import GK from '../fb-stubs/GK';
import {getRenderHostInstance} from '../RenderHost';
import {getAppVersion} from './info';
export function isPluginCompatible(plugin: PluginDetails) {
const flipperVersion = getAppVersion();
return (
GK.get('flipper_disable_plugin_compatibility_checks') ||
getRenderHostInstance().GK('flipper_disable_plugin_compatibility_checks') ||
flipperVersion === '0.0.0' ||
!plugin.engines?.flipper ||
semver.lte(plugin.engines?.flipper, flipperVersion)

View File

@@ -1,98 +0,0 @@
/**
* 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 {readFile, pathExists, mkdirp, writeFile} from 'fs-extra';
import path from 'path';
/**
* Redux-persist storage engine for storing state in a human readable JSON file.
*
* Differs from the usual engines in two ways:
* * The key is ignored. This storage will only hold one key, so each setItem() call will overwrite the previous one.
* * Stored files are "human readable". Redux-persist calls storage engines with preserialized values that contain escaped strings inside json.
* This engine re-serializes them by parsing the inner strings to store them as top-level json.
* Transforms haven't been used because they operate before serialization, so all serialized values would still end up as strings.
*/
export default class JsonFileStorage {
filepath: string;
constructor(filepath: string) {
this.filepath = filepath;
}
private parseFile(): Promise<any> {
return readFile(this.filepath)
.then((buffer) => buffer.toString())
.then(this.deserializeValue)
.catch(async (e) => {
console.warn(
`Failed to read settings file: "${this.filepath}". ${e}. Replacing file with default settings.`,
);
await this.writeContents(prettyStringify({}));
return {};
});
}
getItem(_key: string, callback?: (_: any) => any): Promise<any> {
const promise = this.parseFile();
callback && promise.then(callback);
return promise;
}
// Sets a new value and returns the value that was PREVIOUSLY set.
// This mirrors the behaviour of the localForage storage engine.
// Not thread-safe.
setItem(_key: string, value: any, callback?: (_: any) => any): Promise<any> {
const originalValue = this.parseFile();
const writePromise = originalValue.then((_) =>
this.writeContents(this.serializeValue(value)),
);
return Promise.all([originalValue, writePromise]).then(([o, _]) => {
callback && callback(o);
return o;
});
}
removeItem(_key: string, callback?: () => any): Promise<void> {
return this.writeContents(prettyStringify({}))
.then((_) => callback && callback())
.then(() => {});
}
serializeValue(value: string): string {
const reconstructedObject = Object.entries(JSON.parse(value))
.map(([k, v]: [string, unknown]) => [k, JSON.parse(v as string)])
.reduce((acc: {[key: string]: any}, cv) => {
acc[cv[0]] = cv[1];
return acc;
}, {});
return prettyStringify(reconstructedObject);
}
deserializeValue(value: string): string {
const reconstructedObject = Object.entries(JSON.parse(value))
.map(([k, v]: [string, unknown]) => [k, JSON.stringify(v)])
.reduce((acc: {[key: string]: string}, cv) => {
acc[cv[0]] = cv[1];
return acc;
}, {});
return JSON.stringify(reconstructedObject);
}
writeContents(content: string): Promise<void> {
const dir = path.dirname(this.filepath);
return pathExists(dir)
.then((dirExists) => (dirExists ? Promise.resolve() : mkdirp(dir)))
.then(() => writeFile(this.filepath, content));
}
}
function prettyStringify(data: Object) {
return JSON.stringify(data, null, 2);
}

View File

@@ -1,40 +0,0 @@
/**
* 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 {ProcessConfig} from './processConfig';
import {Store} from '../reducers/index';
// 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.
const xdgConfigDir = () =>
os.platform() === 'darwin'
? path.join(os.homedir(), 'Library', 'Preferences')
: xdg.config || path.join(os.homedir(), '.config');
export const launcherConfigDir = () =>
path.join(
xdgConfigDir(),
os.platform() == 'darwin' ? 'rs.flipper-launcher' : 'flipper-launcher',
);
export function initLauncherHooks(config: ProcessConfig, store: Store) {
if (config.launcherMsg) {
store.dispatch({
type: 'LAUNCHER_MSG',
payload: {
severity: 'warning',
message: config.launcherMsg,
},
});
}
}

View File

@@ -1,89 +0,0 @@
/**
* 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-extra';
import path from 'path';
import TOML, {JsonMap} from '@iarna/toml';
import {Storage} from 'redux-persist/es/types';
import {
defaultLauncherSettings,
LauncherSettings,
} from '../reducers/launcherSettings';
import ReleaseChannel from '../ReleaseChannel';
export default class LauncherSettingsStorage implements Storage {
constructor(readonly filepath: string) {}
async getItem(_key: string): Promise<any> {
return await this.parseFile();
}
async setItem(_key: string, value: LauncherSettings): Promise<any> {
const originalValue = await this.parseFile();
await this.writeFile(value);
return originalValue;
}
removeItem(_key: string): Promise<void> {
return this.writeFile(defaultLauncherSettings);
}
private async parseFile(): Promise<LauncherSettings> {
try {
const content = (await fs.readFile(this.filepath)).toString();
return deserialize(content);
} catch (e) {
console.warn(
`Failed to read settings file: "${this.filepath}". ${e}. Replacing file with default settings.`,
);
await this.writeFile(defaultLauncherSettings);
return defaultLauncherSettings;
}
}
private async writeFile(value: LauncherSettings): Promise<void> {
this.ensureDirExists();
const content = serialize(value);
return fs.writeFile(this.filepath, content);
}
private async ensureDirExists(): Promise<void> {
const dir = path.dirname(this.filepath);
const exists = await fs.pathExists(dir);
if (!exists) {
await fs.mkdir(dir, {recursive: true});
}
}
}
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,
};
}

View File

@@ -23,6 +23,9 @@ import {getStaticPath} from '../utils/pathUtils';
export default async function loadDynamicPlugins(): Promise<
InstalledPluginDetails[]
> {
if (process.env.NODE_ENV === 'test') {
return [];
}
if (process.env.FLIPPER_FAST_REFRESH) {
console.log(
'❌ Skipping loading of dynamic plugins because Fast Refresh is enabled. Fast Refresh only works with bundled plugins.',

View File

@@ -0,0 +1,26 @@
/**
* 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 {Settings} from 'flipper-common';
import {getRenderHostInstance} from '../RenderHost';
export function loadTheme(theme: Settings['darkMode']) {
let shouldUseDarkMode = false;
if (theme === 'dark') {
shouldUseDarkMode = true;
} else if (theme === 'light') {
shouldUseDarkMode = false;
} else if (theme === 'system') {
shouldUseDarkMode = getRenderHostInstance().shouldUseDarkColors();
}
(
document.getElementById('flipper-theme-import') as HTMLLinkElement
).href = `themes/${shouldUseDarkMode ? 'dark' : 'light'}.css`;
getRenderHostInstance().sendIpcEvent('setTheme', theme);
}

View File

@@ -14,7 +14,7 @@ import {promisify} from 'util';
import {getRenderHostInstance} from '../RenderHost';
const getPackageJSON = async () => {
const base = getRenderHostInstance().paths.appPath;
const base = getRenderHostInstance().serverConfig.paths.appPath;
const content = await promisify(fs.readFile)(
path.join(base, 'package.json'),
'utf-8',

View File

@@ -20,7 +20,7 @@ export function getStaticPath(
relativePath: string = '.',
{asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false},
) {
const staticDir = getRenderHostInstance().paths.staticPath;
const staticDir = getRenderHostInstance().serverConfig.paths.staticPath;
const absolutePath = path.resolve(staticDir, relativePath);
// Unfortunately, path.resolve, fs.pathExists, fs.read etc do not automatically work with asarUnpacked files.
// All these functions still look for files in "app.asar" even if they are unpacked.

View File

@@ -1,45 +0,0 @@
/**
* 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 {getRenderHostInstance} from '../RenderHost';
export type ProcessConfig = {
disabledPlugins: Set<string>;
lastWindowPosition: {
x: number;
y: number;
width: number;
height: number;
} | null;
screenCapturePath: string | null;
launcherMsg: string | null;
// Controls whether to delegate to the launcher if present.
launcherEnabled: boolean;
};
let configObj: ProcessConfig | null = null;
export default function config(): ProcessConfig {
if (configObj === null) {
const json = JSON.parse(getRenderHostInstance().env.CONFIG || '{}');
configObj = {
disabledPlugins: new Set(json.disabledPlugins || []),
lastWindowPosition: json.lastWindowPosition,
launcherMsg: json.launcherMsg,
screenCapturePath: json.screenCapturePath,
launcherEnabled:
typeof json.launcherEnabled === 'boolean' ? json.launcherEnabled : true,
};
}
return configObj;
}
export function resetConfigForTesting() {
configObj = null;
}

View File

@@ -12,12 +12,12 @@ import path from 'path';
import BaseDevice from '../devices/BaseDevice';
import {reportPlatformFailures} from 'flipper-common';
import expandTilde from 'expand-tilde';
import config from '../utils/processConfig';
import {getRenderHostInstance} from '../RenderHost';
export function getCaptureLocation() {
return expandTilde(
config().screenCapturePath || getRenderHostInstance().paths.desktopPath,
getRenderHostInstance().serverConfig.processConfig.screenCapturePath ||
getRenderHostInstance().serverConfig.paths.desktopPath,
);
}

View File

@@ -10,7 +10,7 @@
import isProduction from '../utils/isProduction';
import {getAppVersion} from './info';
import config from '../fb-stubs/config';
import ReleaseChannel from '../ReleaseChannel';
import {ReleaseChannel} from 'flipper-common';
export function getVersionString() {
return (