From fd6cab2a7bd4df883b5a91972bfeebb1088182a4 Mon Sep 17 00:00:00 2001 From: Pascal Hartig Date: Mon, 25 Mar 2019 11:30:12 -0700 Subject: [PATCH] Launcher cycle detection Summary: Flipper Electron delegates to the Launcher if it is found right on startup to fetch the most recent/compatible version of Flipper. The Launcher then opens the downloaded app with a `--no-launcher` option to avoid bouncing back and forth between the Electron app and the Launcher. This depends on the argument processing working unchanged. In the past this has been somewhat difficult to guarantee as this doesn't happen in one place and dev/prod builds have handled arguments different due to Electron weirdness (requiring a `--` passed in, for instance). If anything here goes wrong, we end up in a very nasty scenario where the launcher and the Electron app rapidly open and close, making it nearly impossible for users to escape that vicious cycle. `pkill -f Flipper` being the best option, if you can focus a terminal for long enough. In order to avoid this from ever happening in the future, this introduces a quick check for the last startup is written with a timestamp and if this is less than 5s in the past, we will skip delegating to the Launcher altogether, keeping the current instance running. Reviewed By: jknoxville Differential Revision: D14598136 fbshipit-source-id: b3335ce7ec7dc3e5e014d459db31df4c8a774fc6 --- static/index.js | 37 +++++++++++++++++++------------------ static/launcher.js | 36 +++++++++++++++++++++++++++++++++++- static/package.json | 4 +++- static/yarn.lock | 5 +++++ yarn.lock | 1 + 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/static/index.js b/static/index.js index b9d53e418..6b5e387ef 100644 --- a/static/index.js +++ b/static/index.js @@ -159,24 +159,25 @@ app.on('will-finish-launching', () => { app.on('ready', () => { // If we delegate to the launcher, shut down this instance of the app. - if (delegateToLauncher(argv)) { - app.quit(); - return; - } - - appReady = true; - app.commandLine.appendSwitch('scroll-bounce'); - tryCreateWindow(); - // if in development install the react devtools extension - if (process.env.NODE_ENV === 'development') { - const { - default: installExtension, - REACT_DEVELOPER_TOOLS, - REDUX_DEVTOOLS, - } = require('electron-devtools-installer'); - installExtension(REACT_DEVELOPER_TOOLS.id); - installExtension(REDUX_DEVTOOLS.id); - } + delegateToLauncher(argv).then(hasLauncherInvoked => { + if (hasLauncherInvoked) { + app.quit(); + return; + } + appReady = true; + app.commandLine.appendSwitch('scroll-bounce'); + tryCreateWindow(); + // if in development install the react devtools extension + if (process.env.NODE_ENV === 'development') { + const { + default: installExtension, + REACT_DEVELOPER_TOOLS, + REDUX_DEVTOOLS, + } = require('electron-devtools-installer'); + installExtension(REACT_DEVELOPER_TOOLS.id); + installExtension(REDUX_DEVTOOLS.id); + } + }); }); ipcMain.on('componentDidMount', event => { diff --git a/static/launcher.js b/static/launcher.js index 79980d5c0..512cf6a06 100644 --- a/static/launcher.js +++ b/static/launcher.js @@ -7,7 +7,11 @@ const os = require('os'); const fs = require('fs'); +const path = require('path'); +const promisify = require('util').promisify; const {spawn} = require('child_process'); +const xdg = require('xdg-basedir'); +const mkdirp = require('mkdirp'); const isProduction = () => !/node_modules[\\/]electron[\\/]/.test(process.execPath); @@ -38,12 +42,42 @@ const startLauncher = argv => { } }; +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 promisify(fs.readFile)(filePath), 10); + } catch (e) { + backThen = 0; + } + + const delta = rightNow - backThen; + await promisify(mkdirp)(dir); + await promisify(fs.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. */ -module.exports = function delegateToLauncher(argv) { +module.exports = async function delegateToLauncher(argv) { if (argv.launcher && isProduction() && 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.`, diff --git a/static/package.json b/static/package.json index 0ef4d5ef9..1373844f9 100644 --- a/static/package.json +++ b/static/package.json @@ -15,7 +15,9 @@ "@babel/preset-react": "^7.0.0", "expand-tilde": "^2.0.2", "metro": "^0.49.0", - "recursive-readdir": "2.2.2" + "mkdirp": "^0.5.1", + "recursive-readdir": "2.2.2", + "xdg-basedir": "^3.0.0" }, "devDependencies": {}, "resolutions": { diff --git a/static/yarn.lock b/static/yarn.lock index 73eac3959..45b849a01 100644 --- a/static/yarn.lock +++ b/static/yarn.lock @@ -3182,6 +3182,11 @@ ws@^1.1.0: options ">=0.0.5" ultron "1.0.x" +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + xpipe@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf" diff --git a/yarn.lock b/yarn.lock index 840b00227..88a0ddff2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7558,6 +7558,7 @@ ws@~3.3.1: xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= xml-name-validator@^3.0.0: version "3.0.0"