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:
committed by
Facebook GitHub Bot
parent
9b16d0c29a
commit
27549ac5eb
45
desktop/app/src/RenderHost.tsx
Normal file
45
desktop/app/src/RenderHost.tsx
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user