/** * 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 {enableMapSet} from 'immer'; import { _NuxManagerContext, _createNuxManager, _setGlobalInteractionReporter, _LoggerContext, } from 'flipper-plugin'; import { createFlipperServerWithSocket, FlipperServerState, } from 'flipper-server-client'; import { checkPortInUse, getAuthToken, getEnvironmentInfo, setupPrefetcher, startFlipperServer, startServer, } from 'flipper-server-core'; import { FlipperServer, getLogger, isTest, Logger, setLoggerInstance, wrapRequire, } from 'flipper-common'; import {initializeElectron} from './electron/initializeElectron'; import path from 'path'; import fs from 'fs-extra'; import {ElectronIpcClientRenderer} from './electronIpc'; import {KeytarModule} from 'flipper-server-core/src/utils/keytar'; import {initCompanionEnv} from 'flipper-server-companion'; import ReconnectingWebSocket from 'reconnecting-websocket'; import WS from 'ws'; import {Module} from 'module'; Module.prototype.require = wrapRequire(Module.prototype.require); enableMapSet(); async function getKeytarModule( staticPath: string, ): Promise { let keytar: any = undefined; try { if (process.env.FLIPPER_DISABLE_KEYTAR) { console.log( 'Using keytar in-memory implementation as per FLIPPER_DISABLE_KEYTAR env var.', ); return undefined; } 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('Failed to load keytar:', e); } return keytar; } async function getExternalServer(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 getFlipperServer( electronIpcClient: ElectronIpcClientRenderer, ): Promise { const execPath = process.execPath || (await electronIpcClient.send('getProcess')).execPath; const appPath = await electronIpcClient.send('getPath', 'app'); const staticPath = getStaticPath(appPath); const isProduction = !/node_modules[\\/]electron[\\/]/.test(execPath); const environmentInfo = await getEnvironmentInfo( staticPath, isProduction, false, ); const keytar: KeytarModule | undefined = await getKeytarModule(staticPath); const port = 52342; async function shutdown(): Promise { console.info('[flipper-server] Attempt to shutdown.'); try { const response = await fetch(`http://localhost:${port}/shutdown`); const json = await response.json(); return json?.success; } catch {} return false; } /** * In this case, order matters. First, check if the TCP port is in use. If so, * then shut down the TCP socket. If not, then try the UDS socket. * * UDS doesn't accept arguments in the query string, this effectively creates a different * socket path which then doesn't match the one used by the server. */ if (await checkPortInUse(port)) { console.warn(`[flipper-server] TCP port ${port} is already in use.`); const success = await shutdown(); console.info(`[flipper-server] Shutdown: ${success}`); } console.info('[flipper-server] Not running/listening, start'); const {readyForIncomingConnections} = await startServer( { staticPath, entry: 'index.web.html', port, }, environmentInfo, ); const server = await startFlipperServer( appPath, staticPath, '', false, keytar, 'embedded', environmentInfo, ); const token: string = await getAuthToken(); const searchParams = new URLSearchParams({token}); const TCPconnectionURL = new URL(`ws://localhost:${port}?${searchParams}`); const companionEnv = await initCompanionEnv(server); await server.connect(); await readyForIncomingConnections(server, companionEnv); return getExternalServer(TCPconnectionURL); } async function start() { const logger = createDelegatedLogger(); setLoggerInstance(logger); const electronIpcClient = new ElectronIpcClientRenderer(); const flipperServer: FlipperServer = await getFlipperServer( electronIpcClient, ); const flipperServerConfig = await flipperServer.exec('get-config'); await initializeElectron( flipperServer, flipperServerConfig, electronIpcClient, ); // By turning this in a require, we force the JS that the body of this module (init) has completed (initializeElectron), // before starting the rest of the Flipper process. // This prevent issues where the render host is referred at module initialisation level, // but not set yet, which might happen when using normal imports. // eslint-disable-next-line import/no-commonjs require('flipper-ui-core').startFlipperDesktop(flipperServer); await flipperServer.connect(); // Initialize launcher setupPrefetcher(flipperServerConfig.settings); } start().catch((e) => { console.error('Failed to start Flipper desktop', e); document.getElementById('loading')!.textContent = 'Failed to start Flipper. ' + e; }); function getStaticPath(appPath: string) { let _staticPath = path.resolve(__dirname, '..', '..', 'static'); // fs.existSync used here, as fs-extra doesn't resovle properly in the app.asar /* eslint-disable node/no-sync*/ if (fs.existsSync(_staticPath)) { // True in unit tests return _staticPath; } if (fs.existsSync(appPath)) { _staticPath = path.join(appPath); } if (!fs.existsSync(_staticPath)) { throw new Error('Static path does not exist: ' + _staticPath); } /* eslint-enable node/no-sync*/ return _staticPath; } // getLogger() is not yet created when the electron app starts. // we can't create it here yet, as the real logger is wired up to // the redux store and the rest of the world. So we create a delegating logger // that uses a simple implementation until the real one comes available function createDelegatedLogger(): Logger { const naiveLogger: Logger = { track(...args: [any, any, any?, any?]) { console.warn('(skipper track)', args); }, trackTimeSince(...args: [any, any, any?]) { console.warn('(skipped trackTimeSince)', args); }, debug(...args: any[]) { console.debug(...args); }, error(...args: any[]) { console.error(...args); console.warn('(skipped error reporting)'); }, warn(...args: any[]) { console.warn(...args); console.warn('(skipped error reporting)'); }, info(...args: any[]) { console.info(...args); }, }; // will be overwrittingen later setLoggerInstance(naiveLogger); return { track() { // noop }, trackTimeSince() { // noop }, debug(...args: any[]) { getLogger().debug(...args); }, error(...args: any[]) { getLogger().error(...args); }, warn(...args: any[]) { getLogger().warn(...args); }, info(...args: any[]) { getLogger().info(...args); }, }; }