/** * 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 './fb-stubs'; import './electronRequire'; import process from 'process'; import chalk from 'chalk'; import path from 'path'; import {attachDevServer} from './attachDevServer'; import {initializeLogger} from './logger'; import fs from 'fs-extra'; import yargs from 'yargs'; import open from 'open'; import os from 'os'; import {initCompanionEnv} from 'flipper-server-companion'; import { checkPortInUse, getEnvironmentInfo, hasAuthToken, startFlipperServer, startServer, tracker, } from 'flipper-server-core'; import {isTest} from 'flipper-common'; import exitHook from 'exit-hook'; import {getAuthToken} from 'flipper-server-core'; import {findInstallation} from './findInstallation'; import ReconnectingWebSocket from 'reconnecting-websocket'; import { createFlipperServerWithSocket, FlipperServerState, } from 'flipper-server-client'; import WS from 'ws'; const argv = yargs .usage('yarn flipper-server [args]') .options({ port: { describe: 'TCP port to serve on', type: 'number', default: 52342, }, bundler: { describe: 'Serve the UI bundle from source. This option only works for source checkouts', type: 'boolean', default: false, }, open: { describe: 'Open Flipper in the default browser after starting', type: 'boolean', default: true, }, failFast: { describe: 'Exit the process immediately if the server cannot start, for example due to an incorrect configuration.', type: 'boolean', default: false, }, settingsString: { describe: `override the existing defaults settings of flipper (settings.json file) e.g "{"androidHome":"/usr/local/bin","enableAndroid":true}"`, type: 'string', default: '', }, launcherSettings: { describe: 'Open Flipper with the configuration stored in .config folder for the launcher', type: 'boolean', default: true, }, tcp: { describe: 'Open a TCP port (--no-tcp can be specified as to use unix-domain-socket exclusively)', type: 'boolean', default: true, }, replace: { describe: 'Replaces any running instance, if any.', type: 'boolean', default: true, }, }) .version('DEV') .help() .parse(process.argv.slice(1)); console.log( `[flipper-server] Starting flipper server with ${ argv.bundler ? 'UI bundle from source' : 'pre-bundled UI' }`, ); /** * When running as a standlone app not run from the terminal, the process itself * doesn't inherit the user's terminal PATH environment variable. * The PATH, when NOT launched from terminal is `/usr/bin:/bin:/usr/sbin:/sbin` * which is missing `/usr/local/bin`. */ if (os.platform() !== 'win32') { process.env.PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'; } const rootPath = argv.bundler ? path.resolve(__dirname, '..', '..') : path.resolve(__dirname, '..'); // in pre packaged versions of the server, static is copied inside the package const staticPath = path.join(rootPath, 'static'); async function connectToRunningServer(url: URL) { console.info(`[flipper-server] Obtain connection to existing server.`); console.info(`[flipper-server] URL: ${url}`); const options = { WebSocket: class WSWithUnixDomainSocketSupport extends WS { constructor(url: string, protocols: string | string[]) { // Flipper exports could be large, and we snd them over the wire // Setting this limit fairly high (1GB) to allow any reasonable Flipper export to be loaded super(url, protocols, {maxPayload: 1024 * 1024 * 1024}); } }, }; const socket = new ReconnectingWebSocket(url.toString(), [], options); const server = await createFlipperServerWithSocket( socket as WebSocket, (_state: FlipperServerState) => {}, ); return server; } async function shutdown() { console.info('[flipper-server] Attempt to shutdown.'); let token: string | undefined; if (await hasAuthToken()) { token = await getAuthToken(); } const searchParams = new URLSearchParams(token ? {token} : {}); const url = new URL(`ws://localhost:${argv.port}?${searchParams}`); const server = await connectToRunningServer(url); await server.exec('shutdown').catch(() => { /** shutdown will ultimately make this request fail, ignore error. */ console.info('[flipper-server] Shutdown may have succeeded'); }); server.close(); } async function start() { console.info('[flipper-server][bootstrap] Booting up'); const t0 = performance.now(); const enhanceLogger = await initializeLogger(staticPath); const t1 = performance.now(); const loggerInitializedMS = t1 - t0; console.info( `[flipper-server][bootstrap] Logger initialised (${loggerInitializedMS} ms)`, ); let keytar: any = undefined; try { if (!isTest()) { const keytarPath = path.join( staticPath, 'native-modules', `keytar-${process.platform}-${process.arch}.node`, ); if (!(await fs.pathExists(keytarPath))) { throw new Error( `Keytar binary does not exist for platform ${process.platform}-${process.arch}`, ); } keytar = electronRequire(keytarPath); } } catch (e) { console.error('[flipper-server] Failed to load keytar:', e); } const t2 = performance.now(); const keytarLoadedMS = t2 - t1; console.info( `[flipper-server][bootstrap] Keytar loaded (${keytarLoadedMS} ms)`, ); const isProduction = process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test'; const environmentInfo = await getEnvironmentInfo( rootPath, isProduction, true, ); if (await checkPortInUse(argv.port)) { console.warn(`[flipper-server] Port ${argv.port} is already in use`); if (!argv.replace) { console.info(`[flipper-server] Not replacing existing instance, exiting`); return; } await shutdown(); } const t3 = performance.now(); const runningInstanceShutdownMS = t3 - t2; console.info( `[flipper-server][bootstrap] Check for running instances completed (${runningInstanceShutdownMS} ms)`, ); const {app, server, socket, readyForIncomingConnections} = await startServer({ staticPath, entry: `index.web${argv.bundler ? '.dev' : ''}.html`, port: argv.port, tcp: argv.tcp, }); const t4 = performance.now(); const httpServerStartedMS = t4 - t3; console.info( `[flipper-server][bootstrap] HTTP server started (${httpServerStartedMS} ms)`, ); const flipperServer = await startFlipperServer( rootPath, staticPath, argv.settingsString, argv.launcherSettings, keytar, 'external', environmentInfo, ); const t5 = performance.now(); const serverCreatedMS = t5 - t4; console.info( `[flipper-server][bootstrap] FlipperServer created (${serverCreatedMS} ms)`, ); exitHook(async () => { await flipperServer.close(); }); enhanceLogger((logEntry) => { flipperServer.emit('server-log', logEntry); }); const companionEnv = await initCompanionEnv(flipperServer); const t6 = performance.now(); const companionEnvironmentInitializedMS = t6 - t5; console.info( `[flipper-server][bootstrap] Companion environment initialised (${companionEnvironmentInitializedMS} ms)`, ); if (argv.failFast) { flipperServer.on('server-state', ({state}) => { if (state === 'error') { console.error( '[flipper-server] state changed to error, process will exit.', ); process.exit(1); } }); } await flipperServer.connect(); const t7 = performance.now(); const appServerStartedMS = t7 - t6; console.info( `[flipper-server][bootstrap] Ready for app connections (${appServerStartedMS} ms)`, ); if (argv.bundler) { await attachDevServer(app, server, socket, rootPath); } const t8 = performance.now(); const developmentServerAttachedMS = t8 - t7; console.info( `[flipper-server][bootstrap] Development server attached (${developmentServerAttachedMS} ms)`, ); readyForIncomingConnections(flipperServer, companionEnv); const t9 = performance.now(); const serverStartedMS = t9 - t8; console.info( `[flipper-server][bootstrap] Listening at port ${chalk.green( argv.port, )} (${serverStartedMS} ms)`, ); tracker.track('server-bootstrap-performance', { loggerInitializedMS, keytarLoadedMS, runningInstanceShutdownMS, httpServerStartedMS, serverCreatedMS, companionEnvironmentInitializedMS, appServerStartedMS, developmentServerAttachedMS, serverStartedMS, }); } async function launch() { if (!argv.tcp) { return; } let token: string | undefined; if (await hasAuthToken()) { token = await getAuthToken(); } const searchParams = new URLSearchParams({token: token ?? ''}); const url = new URL(`http://localhost:${argv.port}?${searchParams}`); console.log('Go to: ' + chalk.green(chalk.bold(url))); if (!argv.open) { return; } const openInBrowser = () => { open(url.toString(), {app: {name: open.apps.chrome}}); }; if (argv.bundler) { openInBrowser(); } else { const path = await findInstallation(); if (path) { open(path); } else { openInBrowser(); } } } process.on('uncaughtException', (error) => { console.error( '[flipper-server] uncaught exception, process will exit.', error, ); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.warn( '[flipper-server] unhandled rejection for:', promise, 'reason:', reason, ); }); start() .then(launch) .catch((e) => { console.error(chalk.red('Server startup error: '), e); process.exit(1); });