Move first pieces of functionality of Electron

Summary: Started abstraction some Electron specific APIs away, like process id, select directory dialogs etc.

Reviewed By: timur-valiev, aigoncharov

Differential Revision: D31827016

fbshipit-source-id: e835ac9095e63d7ea79dd0eaf7f2918ac8d09994
This commit is contained in:
Michel Weststrate
2021-10-22 09:20:14 -07:00
committed by Facebook GitHub Bot
parent 9b16d0c29a
commit 27549ac5eb
8 changed files with 123 additions and 51 deletions

View File

@@ -0,0 +1,45 @@
/**
* 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
*/
/**
* Utilities provided by the render host, e.g. Electron, the Browser, etc
*/
export interface RenderHost {
readonly processId: number;
readTextFromClipboard(): string | undefined;
selectDirectory?(defaultPath?: string): Promise<string | undefined>;
registerShortcut(shortCut: string, callback: () => void): void;
hasFocus(): boolean;
}
let renderHostInstance: RenderHost | undefined;
export function getRenderHostInstance(): RenderHost {
if (!renderHostInstance) {
throw new Error('setRenderHostInstance was never called');
}
return renderHostInstance;
}
export function setRenderHostInstance(instance: RenderHost) {
renderHostInstance = instance;
}
if (process.env.NODE_ENV === 'test') {
setRenderHostInstance({
processId: -1,
readTextFromClipboard() {
return '';
},
registerShortcut() {},
hasFocus() {
return true;
},
});
}

View File

@@ -7,7 +7,6 @@
* @format * @format
*/ */
import electron from 'electron';
import { import {
FlexColumn, FlexColumn,
styled, styled,
@@ -19,11 +18,8 @@ import {
} from '../../ui'; } from '../../ui';
import React, {useState} from 'react'; import React, {useState} from 'react';
import {promises as fs} from 'fs'; import {promises as fs} from 'fs';
// Used for dialogs.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import path from 'path';
import {theme} from 'flipper-plugin'; import {theme} from 'flipper-plugin';
import {getRenderHostInstance} from '../../RenderHost';
export const ConfigFieldContainer = styled(FlexRow)({ export const ConfigFieldContainer = styled(FlexRow)({
paddingLeft: 10, paddingLeft: 10,
@@ -71,6 +67,7 @@ export function FilePathConfigField(props: {
// Defaults to allowing directories only, this changes to expect regular files. // Defaults to allowing directories only, this changes to expect regular files.
isRegularFile?: boolean; isRegularFile?: boolean;
}) { }) {
const renderHost = getRenderHostInstance();
const [value, setValue] = useState(props.defaultValue); const [value, setValue] = useState(props.defaultValue);
const [isValid, setIsValid] = useState(true); const [isValid, setIsValid] = useState(true);
fs.stat(value) fs.stat(value)
@@ -102,27 +99,28 @@ export function FilePathConfigField(props: {
.catch((_) => setIsValid(false)); .catch((_) => setIsValid(false));
}} }}
/> />
{renderHost.selectDirectory && (
<FlexColumn <FlexColumn
onClick={() => onClick={() => {
remote.dialog renderHost
.showOpenDialog({ .selectDirectory?.()
properties: ['openDirectory', 'showHiddenFiles'], .then((path) => {
defaultPath: path.resolve('/'), if (path) {
})
.then((result: electron.SaveDialogReturnValue) => {
if (result.filePath) {
const path: string = result.filePath.toString();
setValue(path); setValue(path);
props.onChange(path); props.onChange(path);
} }
}) })
}> .catch((e) => {
console.warn('Failed to select dir', e);
});
}}>
<CenteredGlyph <CenteredGlyph
color={theme.primaryColor} color={theme.primaryColor}
name="dots-3-circle" name="dots-3-circle"
variant="outline" variant="outline"
/> />
</FlexColumn> </FlexColumn>
)}
{props.resetValue && ( {props.resetValue && (
<FlexColumn <FlexColumn
title={`Reset to default path ${props.resetValue}`} title={`Reset to default path ${props.resetValue}`}

View File

@@ -8,8 +8,6 @@
*/ */
// Used responsibly. // Used responsibly.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import flipperServer from './flipperServer'; import flipperServer from './flipperServer';
import application from './application'; import application from './application';
import tracking from './tracking'; import tracking from './tracking';
@@ -31,9 +29,6 @@ import {notNull} from '../utils/typeUtils';
export default function (store: Store, logger: Logger): () => Promise<void> { export default function (store: Store, logger: Logger): () => Promise<void> {
// This only runs in development as when the reload // This only runs in development as when the reload
// kicks in it doesn't unregister the shortcuts // kicks in it doesn't unregister the shortcuts
if (process.env.NODE_ENV === 'development') {
remote.globalShortcut.unregisterAll();
}
const dispatchers: Array<Dispatcher> = [ const dispatchers: Array<Dispatcher> = [
application, application,
tracking, tracking,

View File

@@ -7,10 +7,8 @@
* @format * @format
*/ */
// Used to register a shortcut. Don't have an alternative for that.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import {Store} from '../reducers'; import {Store} from '../reducers';
import {getRenderHostInstance} from '../RenderHost';
type ShortcutEventCommand = type ShortcutEventCommand =
| { | {
@@ -21,6 +19,7 @@ type ShortcutEventCommand =
export default (store: Store) => { export default (store: Store) => {
const settings = store.getState().settingsState.reactNative; const settings = store.getState().settingsState.reactNative;
const renderHost = getRenderHostInstance();
if (!settings.shortcuts.enabled) { if (!settings.shortcuts.enabled) {
return; return;
@@ -41,7 +40,7 @@ export default (store: Store) => {
(shortcut: ShortcutEventCommand) => (shortcut: ShortcutEventCommand) =>
shortcut && shortcut &&
shortcut.shortcut && shortcut.shortcut &&
remote.globalShortcut.register(shortcut.shortcut, () => { renderHost.registerShortcut(shortcut.shortcut, () => {
const devices = store const devices = store
.getState() .getState()
.connections.devices.filter( .connections.devices.filter(

View File

@@ -9,7 +9,7 @@
// Used for PID tracking. // Used for PID tracking.
// eslint-disable-next-line flipper/no-electron-remote-imports // eslint-disable-next-line flipper/no-electron-remote-imports
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer} from 'electron';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
@@ -32,6 +32,7 @@ import {getCPUUsage} from 'process';
import {sideEffect} from '../utils/sideEffect'; import {sideEffect} from '../utils/sideEffect';
import {getSelectionInfo} from '../utils/info'; import {getSelectionInfo} from '../utils/info';
import type {SelectionInfo} from '../utils/info'; import type {SelectionInfo} from '../utils/info';
import {getRenderHostInstance} from '../RenderHost';
const TIME_SPENT_EVENT = 'time-spent'; const TIME_SPENT_EVENT = 'time-spent';
@@ -77,6 +78,7 @@ export function emitBytesReceived(plugin: string, bytes: number) {
} }
export default (store: Store, logger: Logger) => { export default (store: Store, logger: Logger) => {
const renderHost = getRenderHostInstance();
sideEffect( sideEffect(
store, store,
{ {
@@ -97,7 +99,7 @@ export default (store: Store, logger: Logger) => {
const oldExitData = loadExitData(); const oldExitData = loadExitData();
if (oldExitData) { if (oldExitData) {
const isReload = remote.process.pid === oldExitData.pid; const isReload = renderHost.processId === oldExitData.pid;
const timeSinceLastStartup = const timeSinceLastStartup =
Date.now() - parseInt(oldExitData.lastSeen, 10); Date.now() - parseInt(oldExitData.lastSeen, 10);
// console.log(isReload ? 'reload' : 'restart', oldExitData); // console.log(isReload ? 'reload' : 'restart', oldExitData);
@@ -373,7 +375,7 @@ export function persistExitData(
? deconstructClientId(state.selectedAppId).app ? deconstructClientId(state.selectedAppId).app
: '', : '',
cleanExit, cleanExit,
pid: remote.process.pid, pid: getRenderHostInstance().processId,
}; };
window.localStorage.setItem( window.localStorage.setItem(
flipperExitDataKey, flipperExitDataKey,

View File

@@ -50,13 +50,15 @@ import {CopyOutlined} from '@ant-design/icons';
import {getVersionString} from './utils/versionString'; import {getVersionString} from './utils/versionString';
import {PersistGate} from 'redux-persist/integration/react'; import {PersistGate} from 'redux-persist/integration/react';
// eslint-disable-next-line flipper/no-electron-remote-imports // eslint-disable-next-line flipper/no-electron-remote-imports
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote, SaveDialogReturnValue} from 'electron';
import { import {
setLoggerInstance, setLoggerInstance,
setUserSessionManagerInstance, setUserSessionManagerInstance,
GK as flipperCommonGK, GK as flipperCommonGK,
} from 'flipper-common'; } from 'flipper-common';
import {internGraphPOSTAPIRequest} from './fb-stubs/user'; import {internGraphPOSTAPIRequest} from './fb-stubs/user';
import {setRenderHostInstance} from './RenderHost';
import {clipboard} from 'electron';
if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') {
// By default Node.JS has its internal certificate storage and doesn't use // By default Node.JS has its internal certificate storage and doesn't use
@@ -187,6 +189,7 @@ function setProcessState(store: Store) {
} }
function init() { function init() {
initializeFlipperForElectron();
// TODO: centralise all those initialisations in a single configuration call // TODO: centralise all those initialisations in a single configuration call
flipperCommonGK.get = (name) => GK.get(name); flipperCommonGK.get = (name) => GK.get(name);
const store = getStore(); const store = getStore();
@@ -262,3 +265,37 @@ const CodeBlock = styled(Input.TextArea)({
...theme.monospace, ...theme.monospace,
color: theme.textColorSecondary, color: theme.textColorSecondary,
}); });
function initializeFlipperForElectron() {
setRenderHostInstance({
processId: remote.process.pid,
readTextFromClipboard() {
return clipboard.readText();
},
selectDirectory(defaultPath = path.resolve('/')) {
return remote.dialog
.showOpenDialog({
properties: ['openDirectory', 'showHiddenFiles'],
defaultPath,
})
.then((result: SaveDialogReturnValue & {filePaths: string[]}) => {
if (result.filePath) {
return result.filePath.toString();
}
// Electron typings seem of here, just in case,
// (can be tested with settings dialog)
// handle both situations
if (result.filePaths) {
return result.filePaths[0];
}
return undefined;
});
},
registerShortcut(shortcut, callback) {
remote.globalShortcut.register(shortcut, callback);
},
hasFocus() {
return remote.getCurrentWindow().isFocused();
},
});
}

View File

@@ -7,10 +7,8 @@
* @format * @format
*/ */
// This is fine, we're using a focus event.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import {v1 as uuidv1} from 'uuid'; import {v1 as uuidv1} from 'uuid';
import {getRenderHostInstance} from '../RenderHost';
import {Actions} from './'; import {Actions} from './';
export type LauncherMsg = { export type LauncherMsg = {
@@ -83,7 +81,7 @@ export const initialState: () => State = () => ({
leftSidebarVisible: true, leftSidebarVisible: true,
rightSidebarVisible: true, rightSidebarVisible: true,
rightSidebarAvailable: false, rightSidebarAvailable: false,
windowIsFocused: remote.getCurrentWindow().isFocused(), windowIsFocused: getRenderHostInstance().hasFocus(),
activeSheet: null, activeSheet: null,
share: null, share: null,
sessionId: uuidv1(), sessionId: uuidv1(),

View File

@@ -8,11 +8,9 @@
*/ */
import {produce} from 'immer'; import {produce} from 'immer';
// Used for focus events which is fine.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import {Actions} from './'; import {Actions} from './';
import {SelectionInfo} from '../utils/info'; import {SelectionInfo} from '../utils/info';
import {getRenderHostInstance} from '../RenderHost';
export type TrackingEvent = export type TrackingEvent =
| { | {
@@ -31,15 +29,15 @@ export type TrackingEvent =
export type State = { export type State = {
timeline: TrackingEvent[]; timeline: TrackingEvent[];
}; };
const INITAL_STATE: State = { const INITAL_STATE: () => State = () => ({
timeline: [ timeline: [
{ {
type: 'TIMELINE_START', type: 'TIMELINE_START',
time: Date.now(), time: Date.now(),
isFocused: remote.getCurrentWindow().isFocused(), isFocused: getRenderHostInstance().hasFocus(),
}, },
], ],
}; });
export type Action = export type Action =
| { | {
@@ -53,7 +51,7 @@ export type Action =
}; };
export default function reducer( export default function reducer(
state: State = INITAL_STATE, state: State = INITAL_STATE(),
action: Actions, action: Actions,
): State { ): State {
if (action.type === 'CLEAR_TIMELINE') { if (action.type === 'CLEAR_TIMELINE') {
@@ -94,7 +92,7 @@ export function clearTimeline(time: number): Action {
type: 'CLEAR_TIMELINE', type: 'CLEAR_TIMELINE',
payload: { payload: {
time, time,
isFocused: remote.getCurrentWindow().isFocused(), isFocused: getRenderHostInstance().hasFocus(),
}, },
}; };
} }