Skip to content

Commit

Permalink
Simplify occupied port handling in start command (facebook#39078)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#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: 5dce80365449165116d52e68c4de5624cc3a840c
  • Loading branch information
huntie authored and facebook-github-bot committed Aug 22, 2023
1 parent 7a14b08 commit f39483d
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 79 deletions.
43 changes: 4 additions & 39 deletions flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>) => void,
info: (...message: Array<string>) => void,
warn: (...message: Array<string>) => void,
error: (...message: Array<string>) => void,
debug: (...message: Array<string>) => void,
error: (...message: Array<string>) => void,
log: (...message: Array<string>) => void,
setVerbose: (level: boolean) => void,
isVerbose: () => boolean,
disable: () => void,
enable: () => void,
info: (...message: Array<string>) => void,
warn: (...message: Array<string>) => 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<void>,
}>;
Expand Down
72 changes: 32 additions & 40 deletions packages/community-cli-plugin/src/commands/start/runServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand All @@ -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,
});

Expand Down
71 changes: 71 additions & 0 deletions packages/community-cli-plugin/src/utils/isDevServerRunning.js
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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});
});
}

0 comments on commit f39483d

Please sign in to comment.