diff --git a/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx b/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx
index e620e0bc4..48ec33258 100644
--- a/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx
+++ b/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx
@@ -291,10 +291,10 @@ async function waitForLogin(store: Store) {
async function verifyFlipperIsUpToDate(title: string) {
const serverConfig = getRenderHostInstance().serverConfig;
- // If this is not a headless build, do not check for updates.
- if (!serverConfig.environmentInfo.isHeadlessBuild) {
- return;
- }
+ // // If this is not a headless build, do not check for updates.
+ // if (!serverConfig.environmentInfo.isHeadlessBuild) {
+ // return;
+ // }
const config = serverConfig.processConfig;
if (
!isProduction() ||
diff --git a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx
index 8e4023c1c..4327b7bf9 100644
--- a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx
+++ b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx
@@ -103,8 +103,7 @@ export const Navbar = withTrackingScope(function Navbar() {
- {getRenderHostInstance().serverConfig.environmentInfo
- .isHeadlessBuild && }
+
);
diff --git a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx
index b47791cf6..dfdb33568 100644
--- a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx
+++ b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx
@@ -15,32 +15,31 @@ import {
Layout,
Dialog,
_PortalsManager,
- getFlipperLib,
- styled,
} from 'flipper-plugin';
+import {Link, styled} from '../ui';
import {theme} from 'flipper-plugin';
-import {isProduction, Logger} from 'flipper-common';
+import {Logger} from 'flipper-common';
+
import {Navbar} from './Navbar';
import {useStore} from '../utils/useStore';
import {AppInspect} from './appinspect/AppInspect';
import PluginContainer from '../PluginContainer';
import {ContentContainer} from './ContentContainer';
+import {showChangelog} from '../chrome/ChangelogSheet';
import PlatformSelectWizard, {
hasPlatformWizardBeenDone,
} from '../chrome/PlatformSelectWizard';
-import {getVersionString} from '../utils/versionString';
-import config from '../fb-stubs/config';
-import {WelcomeScreenStaticView} from './WelcomeScreen';
-import {isFBEmployee} from '../utils/fbEmployee';
-import {Button, Modal, notification} from 'antd';
-import {getRenderHostInstance} from 'flipper-frontend-core';
-import {WarningOutlined} from '@ant-design/icons';
import PWAInstallationWizard, {
shouldShowPWAInstallationWizard,
} from '../chrome/PWAppInstallationWizard';
-import {showChangelog} from '../chrome/ChangelogSheet';
-import {Link} from '../ui';
+import {getVersionString} from '../utils/versionString';
+import config from '../fb-stubs/config';
+import {WelcomeScreenStaticView} from './WelcomeScreen';
import fbConfig from '../fb-stubs/config';
+import {isFBEmployee} from '../utils/fbEmployee';
+import {notification} from 'antd';
+import isProduction from '../utils/isProduction';
+import {getRenderHostInstance} from 'flipper-frontend-core';
export function SandyApp() {
const logger = useLogger();
@@ -49,17 +48,10 @@ export function SandyApp() {
);
const staticView = useStore((state) => state.connections.staticView);
- const serverConfig = getRenderHostInstance().serverConfig;
-
useEffect(() => {
- let title = `Flipper (${getVersionString()}${
+ document.title = `Flipper (${getVersionString()}${
config.isFBBuild ? '@FB' : ''
})`;
- if (!serverConfig.environmentInfo.isHeadlessBuild) {
- title += ' (Unsupported)';
- }
-
- document.title = title;
registerStartupTime(logger);
@@ -79,55 +71,17 @@ export function SandyApp() {
Dialog.showModal((onHide) => );
}
- if (serverConfig.environmentInfo.isHeadlessBuild) {
- showChangelog(true);
- }
+ showChangelog(true);
// don't warn about logger, even with a new logger we don't want to re-register
// eslint-disable-next-line
}, []);
useEffect(() => {
- isFBEmployee()
- .then((isEmployee) => {
- if (isEmployee) {
- if (!serverConfig.environmentInfo.isHeadlessBuild) {
- Dialog.showModal((onHide) => (
- onHide()}
- width={570}
- title={
- <>
- This Version of Flipper is Unsupported
- >
- }
- footer={
- <>
-
-
- >
- }>
- This version is only meant to be used for React Native
- debugging. It is not maintained and it doesn't receive updates.
- Instead, you should be using the main Flipper version from
- Managed Software Center for all other purposes.
-
- ));
- } else if (fbConfig.warnFBEmployees && isProduction()) {
+ if (fbConfig.warnFBEmployees && isProduction()) {
+ isFBEmployee()
+ .then((isEmployee) => {
+ if (isEmployee) {
notification.warning({
placement: 'bottomLeft',
message: 'Please use Flipper@FB',
@@ -144,11 +98,11 @@ export function SandyApp() {
duration: null,
});
}
- }
- })
- .catch((e) => {
- console.warn('Failed to check if user is employee', e);
- });
+ })
+ .catch((e) => {
+ console.warn('Failed to check if user is employee', e);
+ });
+ }
}, []);
return (
diff --git a/desktop/flipper-ui-core/src/sandy-chrome/WelcomeScreen.tsx b/desktop/flipper-ui-core/src/sandy-chrome/WelcomeScreen.tsx
index eba8a1089..a4ac5a43f 100644
--- a/desktop/flipper-ui-core/src/sandy-chrome/WelcomeScreen.tsx
+++ b/desktop/flipper-ui-core/src/sandy-chrome/WelcomeScreen.tsx
@@ -160,7 +160,7 @@ function WelcomeScreenContent() {
}}
width={125}
height={125}
- src={isHeadlessBuild ? './icon.png' : './icon-rn-only.png'}
+ src={isHeadlessBuild ? './icon.png' : './icon.png'}
preview={false}
/>
Welcome to Flipper
diff --git a/desktop/scripts/build-release.tsx b/desktop/scripts/build-release.tsx
index f8f97ceec..99047dc04 100755
--- a/desktop/scripts/build-release.tsx
+++ b/desktop/scripts/build-release.tsx
@@ -265,7 +265,6 @@ async function buildDist(buildFolder: string) {
},
mac: {
bundleVersion: FIX_RELEASE_VERSION,
- icon: path.resolve(buildFolder, 'icon-rn-only.icns'),
},
win: {
signAndEditExecutable: !isFB,
diff --git a/desktop/static/launcher.tsx b/desktop/static/launcher.tsx
new file mode 100644
index 000000000..abc597f51
--- /dev/null
+++ b/desktop/static/launcher.tsx
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and 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 fs from 'fs';
+import path from 'path';
+import {spawn} from 'child_process';
+import xdg from 'xdg-basedir';
+import mkdirp from 'mkdirp';
+
+const isProduction = () =>
+ !/node_modules[\\/]electron[\\/]/.test(process.execPath);
+
+const isLauncherInstalled = async () => {
+ if (os.type() == 'Darwin') {
+ const receipt = 'com.facebook.flipper.launcher';
+ const plistLocation = '/Applications/Flipper.app/Contents/Info.plist';
+ try {
+ return (
+ (await fs.promises.stat(plistLocation)) &&
+ (await fs.promises.readFile(plistLocation)).indexOf(receipt) > 0
+ );
+ } catch (e) {
+ console.error('Error while reading Info.plist', e);
+ return false;
+ }
+ }
+
+ return false;
+};
+
+const startLauncher = (argv: {file?: string; url?: string}) => {
+ const args = [];
+ if (argv.file) {
+ args.push('--file', argv.file);
+ }
+ if (argv.url) {
+ args.push('--url', argv.url);
+ }
+ if (os.type() == 'Darwin') {
+ spawn('open', ['/Applications/Flipper.app', '--args'].concat(args));
+ }
+};
+
+const checkIsCycle = async () => {
+ const dir = path.join(xdg.cache!, 'flipper');
+ const filePath = path.join(dir, 'last-launcher-run');
+ // This isn't monotonically increasing, so there's a change we get time drift
+ // between the checks, but the worst case here is that we do two roundtrips
+ // before this check works.
+ const rightNow = Date.now();
+
+ let backThen;
+ try {
+ backThen = parseInt((await fs.promises.readFile(filePath)).toString(), 10);
+ } catch (e) {
+ backThen = 0;
+ }
+
+ const delta = rightNow - backThen;
+ await mkdirp(dir);
+ await fs.promises.writeFile(filePath, '' + rightNow);
+
+ // If the last startup was less than 5s ago, something's not okay.
+ return Math.abs(delta) < 5000;
+};
+
+/**
+ * Runs the launcher if required and returns a boolean based on whether
+ * it has. You should shut down this instance of the app in that case.
+ */
+export default async function delegateToLauncher(argv: {
+ launcher: boolean;
+ file?: string;
+ url?: string;
+}) {
+ if (argv.launcher && isProduction() && (await isLauncherInstalled())) {
+ if (await checkIsCycle()) {
+ console.error(
+ 'Launcher cycle detected. Not delegating even though I usually would.',
+ );
+ return false;
+ }
+
+ console.warn('Delegating to Flipper Launcher ...');
+ console.warn(
+ `You can disable this behavior by passing '--no-launcher' at startup.`,
+ );
+ startLauncher(argv);
+ return true;
+ }
+
+ return false;
+}
diff --git a/desktop/static/main.tsx b/desktop/static/main.tsx
index 96d34c4ef..6b872f636 100644
--- a/desktop/static/main.tsx
+++ b/desktop/static/main.tsx
@@ -28,6 +28,7 @@ import fixPath from 'fix-path';
import {exec} from 'child_process';
import setup, {Config, configPath} from './setup';
import isFB from './fb-stubs/isFB';
+import delegateToLauncher from './launcher';
import yargs from 'yargs';
import {promisify} from 'util';
import process from 'process';
@@ -173,50 +174,58 @@ app.on('ready', async () => {
const config = await setup(argv);
processConfig(config);
- appReady = true;
-
- app.commandLine.appendSwitch('scroll-bounce');
- configureSession();
- createWindow(config);
-
- // if in development install the react devtools extension
- if (process.env.NODE_ENV === 'development') {
- const {
- default: installExtension,
- REACT_DEVELOPER_TOOLS,
- } = require('electron-devtools-installer');
- // if set, try to download a newever version of the dev tools
- const forceDownload = process.env.FLIPPER_UPDATE_DEV_TOOLS === 'true';
- if (forceDownload) {
- console.log('Force updating DevTools');
- }
- // React
- // Fix for extension loading (see D27685981)
- // Work around per https://github.com/electron/electron/issues/23662#issuecomment-787420799
- const reactDevToolsPath = `${os.homedir()}/Library/Application Support/Electron/extensions/${
- REACT_DEVELOPER_TOOLS.id
- }`;
- if (await promisify(fs.exists)(reactDevToolsPath)) {
- console.log('Loading React devtools from disk ' + reactDevToolsPath);
- try {
- await session.defaultSession.loadExtension(
- reactDevToolsPath,
- // @ts-ignore only supported (and needed) in Electron 12
- {allowFileAccess: true},
- );
- } catch (e) {
- console.error('Failed to load React devtools from disk: ', e);
+ // If we delegate to the launcher, shut down this instance of the app.
+ delegateToLauncher(argv)
+ .then(async (hasLauncherInvoked: boolean) => {
+ if (hasLauncherInvoked) {
+ app.quit();
+ return;
}
- } else {
- try {
- await installExtension(REACT_DEVELOPER_TOOLS.id, {
- loadExtensionOptions: {allowFileAccess: true, forceDownload},
- });
- } catch (e) {
- console.error('Failed to install React devtools extension', e);
+ appReady = true;
+ app.commandLine.appendSwitch('scroll-bounce');
+ configureSession();
+ createWindow(config);
+
+ // if in development install the react devtools extension
+ if (process.env.NODE_ENV === 'development') {
+ const {
+ default: installExtension,
+ REACT_DEVELOPER_TOOLS,
+ } = require('electron-devtools-installer');
+ // if set, try to download a newever version of the dev tools
+ const forceDownload = process.env.FLIPPER_UPDATE_DEV_TOOLS === 'true';
+ if (forceDownload) {
+ console.log('Force updating DevTools');
+ }
+ // React
+ // Fix for extension loading (see D27685981)
+ // Work around per https://github.com/electron/electron/issues/23662#issuecomment-787420799
+ const reactDevToolsPath = `${os.homedir()}/Library/Application Support/Electron/extensions/${
+ REACT_DEVELOPER_TOOLS.id
+ }`;
+ if (await promisify(fs.exists)(reactDevToolsPath)) {
+ console.log('Loading React devtools from disk ' + reactDevToolsPath);
+ try {
+ await session.defaultSession.loadExtension(
+ reactDevToolsPath,
+ // @ts-ignore only supported (and needed) in Electron 12
+ {allowFileAccess: true},
+ );
+ } catch (e) {
+ console.error('Failed to load React devtools from disk: ', e);
+ }
+ } else {
+ try {
+ await installExtension(REACT_DEVELOPER_TOOLS.id, {
+ loadExtensionOptions: {allowFileAccess: true, forceDownload},
+ });
+ } catch (e) {
+ console.error('Failed to install React devtools extension', e);
+ }
+ }
}
- }
- }
+ })
+ .catch((e: any) => console.error('Error while delegating app launch', e));
});
app.on('web-contents-created', (_event, contents) => {