diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index 84e32cd5b..dd07f3889 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -583,6 +583,7 @@ export class FlipperServerImpl implements FlipperServer { return uploadRes; }, shutdown: async () => { + // Do not use processExit helper. We want to server immediatelly quit when this call is triggerred process.exit(0); }, 'is-logged-in': async () => { diff --git a/desktop/flipper-server-core/src/index.tsx b/desktop/flipper-server-core/src/index.tsx index df488c048..344ee7099 100644 --- a/desktop/flipper-server-core/src/index.tsx +++ b/desktop/flipper-server-core/src/index.tsx @@ -13,6 +13,7 @@ export * from './tracker'; export {loadLauncherSettings} from './utils/launcherSettings'; export {loadProcessConfig} from './utils/processConfig'; export {getEnvironmentInfo} from './utils/environmentInfo'; +export {processExit, setProcessExitRoutine} from './utils/processExit'; export {getGatekeepers} from './gk'; export {setupPrefetcher} from './fb-stubs/Prefetcher'; export * from './server/attachSocketServer'; diff --git a/desktop/flipper-server-core/src/server/attachSocketServer.tsx b/desktop/flipper-server-core/src/server/attachSocketServer.tsx index f66b0aaa9..7a56d498b 100644 --- a/desktop/flipper-server-core/src/server/attachSocketServer.tsx +++ b/desktop/flipper-server-core/src/server/attachSocketServer.tsx @@ -29,6 +29,7 @@ import {URLSearchParams} from 'url'; import {tracker} from '../tracker'; import {getFlipperServerConfig} from '../FlipperServerConfig'; import {performance} from 'perf_hooks'; +import {processExit} from '../utils/processExit'; const safe = (f: () => void) => { try { @@ -266,7 +267,7 @@ export function attachSocketServer( console.info( '[flipper-server] Shutdown as no clients are currently connected', ); - process.exit(0); + processExit(0); } }, FIVE_HOURS); } diff --git a/desktop/flipper-server-core/src/server/startServer.tsx b/desktop/flipper-server-core/src/server/startServer.tsx index 413508674..daeb1a7ee 100644 --- a/desktop/flipper-server-core/src/server/startServer.tsx +++ b/desktop/flipper-server-core/src/server/startServer.tsx @@ -27,6 +27,7 @@ import {EnvironmentInfo, isProduction} from 'flipper-common'; import {GRAPH_SECRET} from '../fb-stubs/constants'; import {sessionId} from '../sessionId'; import {UIPreference, openUI} from '../utils/openUI'; +import {processExit} from '../utils/processExit'; type Config = { port: number; @@ -123,7 +124,7 @@ export async function startServer( console.error( `[flipper-server] Unable to become ready within ${timeoutSeconds} seconds, exit`, ); - process.exit(1); + processExit(1); } }, timeoutSeconds * 1000); @@ -192,6 +193,7 @@ async function startHTTPServer( res.json({success: true}); // Just exit the process, this will trigger the shutdown hooks. + // Do not use prcoessExit util as we want the serve to shutdown immediately process.exit(0); }); @@ -226,7 +228,7 @@ async function startHTTPServer( `[flipper-server] Unable to listen at port: ${config.port}, is already in use`, ); tracker.track('server-socket-already-in-use', {}); - process.exit(1); + processExit(1); } }); diff --git a/desktop/flipper-server-core/src/server/utilities.tsx b/desktop/flipper-server-core/src/server/utilities.tsx index 46764bbd5..b842a2c08 100644 --- a/desktop/flipper-server-core/src/server/utilities.tsx +++ b/desktop/flipper-server-core/src/server/utilities.tsx @@ -50,7 +50,9 @@ export async function checkServerRunning( port: number, ): Promise { try { - const response = await fetch(`http://localhost:${port}/info`); + const response = await fetch(`http://localhost:${port}/info`, { + timeout: 1000, + }); if (response.status >= 200 && response.status < 300) { const environmentInfo: EnvironmentInfo = await response.json(); return environmentInfo.appVersion; @@ -74,7 +76,9 @@ export async function checkServerRunning( */ export async function shutdownRunningInstance(port: number): Promise { try { - const response = await fetch(`http://localhost:${port}/shutdown`); + const response = await fetch(`http://localhost:${port}/shutdown`, { + timeout: 1000, + }); if (response.status >= 200 && response.status < 300) { const json = await response.json(); console.info( diff --git a/desktop/flipper-server-core/src/utils/processExit.tsx b/desktop/flipper-server-core/src/utils/processExit.tsx new file mode 100644 index 000000000..a002709de --- /dev/null +++ b/desktop/flipper-server-core/src/utils/processExit.tsx @@ -0,0 +1,44 @@ +/** + * 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 + */ + +const onBeforeExitFns: (() => void | Promise)[] = []; +export const setProcessExitRoutine = ( + onBeforeExit: () => void | Promise, +) => { + onBeforeExitFns.push(onBeforeExit); +}; + +const resIsPromise = (res: void | Promise): res is Promise => + res instanceof Promise; +export const processExit = async (code: number) => { + console.debug('processExit', code); + + // eslint-disable-next-line promise/catch-or-return + await Promise.all( + onBeforeExitFns.map(async (fn) => { + try { + const res = fn(); + if (resIsPromise(res)) { + return res.catch((e) => { + console.error('Process exit routine failed', e); + }); + } + } catch (e) { + console.error('Process exit routine failed', e); + } + }), + ).finally(() => { + process.exit(code); + }); + + setTimeout(() => { + console.error('Process exit routines timed out'); + process.exit(code); + }, 5000); +}; diff --git a/desktop/flipper-server/src/index.tsx b/desktop/flipper-server/src/index.tsx index b30f56a7b..5ab044506 100644 --- a/desktop/flipper-server/src/index.tsx +++ b/desktop/flipper-server/src/index.tsx @@ -30,6 +30,7 @@ import { startFlipperServer, startServer, tracker, + processExit, } from 'flipper-server-core'; import {addLogTailer, isProduction, isTest, LoggerFormat} from 'flipper-common'; import exitHook from 'exit-hook'; @@ -265,7 +266,7 @@ async function start() { console.error( '[flipper-server] state changed to error, process will exit.', ); - process.exit(1); + processExit(1); } }); } @@ -335,7 +336,7 @@ process.on('uncaughtException', (error) => { error, ); reportBrowserConnection(false); - process.exit(1); + processExit(1); }); process.on('unhandledRejection', (reason, promise) => { @@ -347,8 +348,15 @@ process.on('unhandledRejection', (reason, promise) => { ); }); -start().catch((e) => { - console.error(chalk.red('Server startup error: '), e); - reportBrowserConnection(false); - process.exit(1); -}); +// Node.js process never waits for all promises to settle and exits as soon as there is not pending timers or open sockets or tasks in teh macroqueue +const runtimeTimeout = setTimeout(() => {}, Number.MAX_SAFE_INTEGER); +// eslint-disable-next-line promise/catch-or-return +start() + .catch((e) => { + console.error(chalk.red('Server startup error: '), e); + reportBrowserConnection(false); + return processExit(1); + }) + .finally(() => { + clearTimeout(runtimeTimeout); + }); diff --git a/desktop/flipper-server/src/logger.tsx b/desktop/flipper-server/src/logger.tsx index 484e964a7..497e7d4c5 100644 --- a/desktop/flipper-server/src/logger.tsx +++ b/desktop/flipper-server/src/logger.tsx @@ -21,7 +21,10 @@ import fsRotator from 'file-stream-rotator'; import {ensureFile} from 'fs-extra'; import {access} from 'fs/promises'; import {constants} from 'fs'; -import {initializeLogger as initLogger} from 'flipper-server-core'; +import { + initializeLogger as initLogger, + setProcessExitRoutine, +} from 'flipper-server-core'; export const loggerOutputFile = 'flipper-server-log.out'; @@ -64,4 +67,14 @@ export async function initializeLogger( logStream?.write(`${name}: \n${stack}\n`); } }); + + const finalizeLogger = async () => { + const logStreamToEnd = logStream; + // Prevent future writes + logStream = undefined; + await new Promise((resolve) => { + logStreamToEnd?.end(resolve); + }); + }; + setProcessExitRoutine(finalizeLogger); }