From 8fcb34b9f02bfae130d831b7fd1be74cedc155ea Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Mon, 21 Aug 2023 05:37:16 -0700 Subject: [PATCH] Simplify occupied port handling in start command (#39078) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/39078 Simplifies and hardens behaviour for detecting other processes / dev server instances when running `react-native start`. - New flow: - Exits with error message if port is taken by another process (*no longer suggests next port*). - Exits with info message if port is taken by another instance of this dev server (**unchanged**). - Continues if result unknown. - *(No longer logs dedicated message for another RN server running in a different project root.)* - This now checks if the TCP port is in use before attempting an HTTP fetch. Previous behaviour: [`handlePortUnavailable`](https://github.com/react-native-community/cli/blob/734222118707fff41c71463528e4e0c227b31cc6/packages/cli-tools/src/handlePortUnavailable.ts#L8). This decouples us from some lower-level `react-native-community/cli-tools` utils, which remain reused by the `android` and `ios` commands. Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D48433285 fbshipit-source-id: 03cf450824ebd1e1e750f087c060c2f81e0a6851 --- .../cli-tools_v12.x.x.js | 43 ++--------- .../src/commands/start/runServer.js | 72 +++++++++---------- .../src/utils/isDevServerRunning.js | 71 ++++++++++++++++++ 3 files changed, 107 insertions(+), 79 deletions(-) create mode 100644 packages/community-cli-plugin/src/utils/isDevServerRunning.js diff --git a/flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js b/flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js index a855762f78c9b5..aef9a4fc695978 100644 --- a/flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js +++ b/flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js @@ -18,50 +18,15 @@ declare module '@react-native-community/cli-tools' { constructor(msg: string, originalError?: Error | mixed | string): this; } - declare export function getPidFromPort(port: number): number | null; - - declare export function handlePortUnavailable( - initialPort: number, - projectRoot: string, - initialPackager?: boolean, - ): Promise<{ - port: number, - packager: boolean, - }>; - - declare export function hookStdout(callback: Function): () => void; - - declare export function isPackagerRunning( - packagerPort: string | number | void, - ): Promise< - | { - status: 'running', - root: string, - } - | 'not_running' - | 'unrecognized', - >; - declare export const logger: $ReadOnly<{ - success: (...message: Array) => void, - info: (...message: Array) => void, - warn: (...message: Array) => void, - error: (...message: Array) => void, debug: (...message: Array) => void, + error: (...message: Array) => void, log: (...message: Array) => void, - setVerbose: (level: boolean) => void, - isVerbose: () => boolean, - disable: () => void, - enable: () => void, + info: (...message: Array) => void, + warn: (...message: Array) => void, + ... }>; - declare export function logAlreadyRunningBundler(port: number): void; - - declare export function resolveNodeModuleDir( - root: string, - packageName: string, - ): string; - declare export const version: $ReadOnly<{ logIfUpdateAvailable: (projectRoot: string) => Promise, }>; diff --git a/packages/community-cli-plugin/src/commands/start/runServer.js b/packages/community-cli-plugin/src/commands/start/runServer.js index 482d0a6378dcd8..16d9f9cce20e3e 100644 --- a/packages/community-cli-plugin/src/commands/start/runServer.js +++ b/packages/community-cli-plugin/src/commands/start/runServer.js @@ -23,14 +23,9 @@ import { createDevServerMiddleware, indexPageMiddleware, } from '@react-native-community/cli-server-api'; -import { - isPackagerRunning, - logger, - version, - logAlreadyRunningBundler, - handlePortUnavailable, -} from '@react-native-community/cli-tools'; +import {logger, version} from '@react-native-community/cli-tools'; +import isDevServerRunning from '../../utils/isDevServerRunning'; import loadMetroConfig from '../../utils/loadMetroConfig'; import attachKeyHandlers from './attachKeyHandlers'; @@ -58,41 +53,38 @@ async function runServer( ctx: Config, args: StartCommandArgs, ) { - let port = args.port ?? 8081; - let packager = true; - const packagerStatus = await isPackagerRunning(port); - - if ( - typeof packagerStatus === 'object' && - packagerStatus.status === 'running' - ) { - if (packagerStatus.root === ctx.root) { - packager = false; - logAlreadyRunningBundler(port); - } else { - const result = await handlePortUnavailable(port, ctx.root, packager); - [port, packager] = [result.port, result.packager]; - } - } else if (packagerStatus === 'unrecognized') { - const result = await handlePortUnavailable(port, ctx.root, packager); - [port, packager] = [result.port, result.packager]; - } - - if (packager === false) { - process.exit(); - } - - logger.info(`Starting dev server on port ${chalk.bold(String(port))}`); - const metroConfig = await loadMetroConfig(ctx, { config: args.config, maxWorkers: args.maxWorkers, - port: port, + port: args.port ?? 8081, resetCache: args.resetCache, watchFolders: args.watchFolders, projectRoot: args.projectRoot, sourceExts: args.sourceExts, }); + const host = args.host?.length ? args.host : 'localhost'; + const { + projectRoot, + server: {port}, + watchFolders, + } = metroConfig; + + const serverStatus = await isDevServerRunning(host, port, projectRoot); + + if (serverStatus === 'matched_server_running') { + logger.info( + `A dev server is already running for this project on port ${port}. Exiting.`, + ); + return; + } else if (serverStatus === 'port_taken') { + logger.error( + `Another process is running on port ${port}. Please terminate this ` + + 'process and try again, or use another port with "--port".', + ); + return; + } + + logger.info(`Starting dev server on port ${chalk.bold(String(port))}`); if (args.assetPlugins) { // $FlowIgnore[cannot-write] Assigning to readonly property @@ -107,14 +99,14 @@ async function runServer( messageSocketEndpoint, eventsSocketEndpoint, } = createDevServerMiddleware({ - host: args.host, - port: metroConfig.server.port, - watchFolders: metroConfig.watchFolders, + host, + port, + watchFolders, }); const {middleware, websocketEndpoints} = createDevMiddleware({ - host: args.host?.length ? args.host : 'localhost', - port: metroConfig.server.port, - projectRoot: metroConfig.projectRoot, + host, + port, + projectRoot, logger, }); diff --git a/packages/community-cli-plugin/src/utils/isDevServerRunning.js b/packages/community-cli-plugin/src/utils/isDevServerRunning.js new file mode 100644 index 00000000000000..42239bb18194e0 --- /dev/null +++ b/packages/community-cli-plugin/src/utils/isDevServerRunning.js @@ -0,0 +1,71 @@ +/** + * 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. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import net from 'net'; +import fetch from 'node-fetch'; + +/** + * Determine whether we can run the dev server. + * + * Return values: + * - `not_running`: The port is unoccupied. + * - `matched_server_running`: The port is occupied by another instance of this + * dev server (matching the passed `projectRoot`). + * - `port_taken`: The port is occupied by another process. + * - `unknown`: An error was encountered; attempt server creation anyway. + */ +export default async function isDevServerRunning( + host: string, + port: number, + projectRoot: string, +): Promise< + 'not_running' | 'matched_server_running' | 'port_taken' | 'unknown', +> { + try { + if (!(await isPortOccupied(host, port))) { + return 'not_running'; + } + + const statusResponse = await fetch(`http://localhost:${port}/status`); + const body = await statusResponse.text(); + + return body === 'packager-status:running' && + statusResponse.headers.get('X-React-Native-Project-Root') === projectRoot + ? 'matched_server_running' + : 'port_taken'; + } catch (e) { + return 'unknown'; + } +} + +async function isPortOccupied(host: string, port: number): Promise { + let result = false; + const server = net.createServer(); + + return new Promise((resolve, reject) => { + server.once('error', e => { + server.close(); + if (e.code === 'EADDRINUSE') { + result = true; + } else { + reject(e); + } + }); + server.once('listening', () => { + result = false; + server.close(); + }); + server.once('close', () => { + resolve(result); + }); + server.listen({host, port}); + }); +}