Skip to content

Commit 24d6eb4

Browse files
huntiefacebook-github-bot
authored andcommitted
Simplify occupied port handling in start command (facebook#39078)
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: 2b0452c6c0b7d6e87b2ed0958a2d1e5da307dbc2
1 parent ec11fdc commit 24d6eb4

File tree

3 files changed

+107
-79
lines changed

3 files changed

+107
-79
lines changed

flow-typed/npm/@react-native-community/cli-tools_v12.x.x.js

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,50 +18,15 @@ declare module '@react-native-community/cli-tools' {
1818
constructor(msg: string, originalError?: Error | mixed | string): this;
1919
}
2020

21-
declare export function getPidFromPort(port: number): number | null;
22-
23-
declare export function handlePortUnavailable(
24-
initialPort: number,
25-
projectRoot: string,
26-
initialPackager?: boolean,
27-
): Promise<{
28-
port: number,
29-
packager: boolean,
30-
}>;
31-
32-
declare export function hookStdout(callback: Function): () => void;
33-
34-
declare export function isPackagerRunning(
35-
packagerPort: string | number | void,
36-
): Promise<
37-
| {
38-
status: 'running',
39-
root: string,
40-
}
41-
| 'not_running'
42-
| 'unrecognized',
43-
>;
44-
4521
declare export const logger: $ReadOnly<{
46-
success: (...message: Array<string>) => void,
47-
info: (...message: Array<string>) => void,
48-
warn: (...message: Array<string>) => void,
49-
error: (...message: Array<string>) => void,
5022
debug: (...message: Array<string>) => void,
23+
error: (...message: Array<string>) => void,
5124
log: (...message: Array<string>) => void,
52-
setVerbose: (level: boolean) => void,
53-
isVerbose: () => boolean,
54-
disable: () => void,
55-
enable: () => void,
25+
info: (...message: Array<string>) => void,
26+
warn: (...message: Array<string>) => void,
27+
...
5628
}>;
5729

58-
declare export function logAlreadyRunningBundler(port: number): void;
59-
60-
declare export function resolveNodeModuleDir(
61-
root: string,
62-
packageName: string,
63-
): string;
64-
6530
declare export const version: $ReadOnly<{
6631
logIfUpdateAvailable: (projectRoot: string) => Promise<void>,
6732
}>;

packages/community-cli-plugin/src/commands/start/runServer.js

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ import {
2323
createDevServerMiddleware,
2424
indexPageMiddleware,
2525
} from '@react-native-community/cli-server-api';
26-
import {
27-
isPackagerRunning,
28-
logger,
29-
version,
30-
logAlreadyRunningBundler,
31-
handlePortUnavailable,
32-
} from '@react-native-community/cli-tools';
26+
import {logger, version} from '@react-native-community/cli-tools';
3327

28+
import isDevServerRunning from '../../utils/isDevServerRunning';
3429
import loadMetroConfig from '../../utils/loadMetroConfig';
3530
import attachKeyHandlers from './attachKeyHandlers';
3631

@@ -58,41 +53,38 @@ async function runServer(
5853
ctx: Config,
5954
args: StartCommandArgs,
6055
) {
61-
let port = args.port ?? 8081;
62-
let packager = true;
63-
const packagerStatus = await isPackagerRunning(port);
64-
65-
if (
66-
typeof packagerStatus === 'object' &&
67-
packagerStatus.status === 'running'
68-
) {
69-
if (packagerStatus.root === ctx.root) {
70-
packager = false;
71-
logAlreadyRunningBundler(port);
72-
} else {
73-
const result = await handlePortUnavailable(port, ctx.root, packager);
74-
[port, packager] = [result.port, result.packager];
75-
}
76-
} else if (packagerStatus === 'unrecognized') {
77-
const result = await handlePortUnavailable(port, ctx.root, packager);
78-
[port, packager] = [result.port, result.packager];
79-
}
80-
81-
if (packager === false) {
82-
process.exit();
83-
}
84-
85-
logger.info(`Starting dev server on port ${chalk.bold(String(port))}`);
86-
8756
const metroConfig = await loadMetroConfig(ctx, {
8857
config: args.config,
8958
maxWorkers: args.maxWorkers,
90-
port: port,
59+
port: args.port ?? 8081,
9160
resetCache: args.resetCache,
9261
watchFolders: args.watchFolders,
9362
projectRoot: args.projectRoot,
9463
sourceExts: args.sourceExts,
9564
});
65+
const host = args.host?.length ? args.host : 'localhost';
66+
const {
67+
projectRoot,
68+
server: {port},
69+
watchFolders,
70+
} = metroConfig;
71+
72+
const serverStatus = await isDevServerRunning(host, port, projectRoot);
73+
74+
if (serverStatus === 'matched_server_running') {
75+
logger.info(
76+
`A dev server is already running for this project on port ${port}. Exiting.`,
77+
);
78+
return;
79+
} else if (serverStatus === 'port_taken') {
80+
logger.error(
81+
`Another process is running on port ${port}. Please terminate this ` +
82+
'process and try again, or use another port with "--port".',
83+
);
84+
return;
85+
}
86+
87+
logger.info(`Starting dev server on port ${chalk.bold(String(port))}`);
9688

9789
if (args.assetPlugins) {
9890
// $FlowIgnore[cannot-write] Assigning to readonly property
@@ -107,14 +99,14 @@ async function runServer(
10799
messageSocketEndpoint,
108100
eventsSocketEndpoint,
109101
} = createDevServerMiddleware({
110-
host: args.host,
111-
port: metroConfig.server.port,
112-
watchFolders: metroConfig.watchFolders,
102+
host,
103+
port,
104+
watchFolders,
113105
});
114106
const {middleware, websocketEndpoints} = createDevMiddleware({
115-
host: args.host?.length ? args.host : 'localhost',
116-
port: metroConfig.server.port,
117-
projectRoot: metroConfig.projectRoot,
107+
host,
108+
port,
109+
projectRoot,
118110
logger,
119111
});
120112

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import net from 'net';
13+
import fetch from 'node-fetch';
14+
15+
/**
16+
* Determine whether we can run the dev server.
17+
*
18+
* Return values:
19+
* - `not_running`: The port is unoccupied.
20+
* - `matched_server_running`: The port is occupied by another instance of this
21+
* dev server (matching the passed `projectRoot`).
22+
* - `port_taken`: The port is occupied by another process.
23+
* - `unknown`: An error was encountered; attempt server creation anyway.
24+
*/
25+
export default async function isDevServerRunning(
26+
host: string,
27+
port: number,
28+
projectRoot: string,
29+
): Promise<
30+
'not_running' | 'matched_server_running' | 'port_taken' | 'unknown',
31+
> {
32+
try {
33+
if (!(await isPortOccupied(host, port))) {
34+
return 'not_running';
35+
}
36+
37+
const statusResponse = await fetch(`http://localhost:${port}/status`);
38+
const body = await statusResponse.text();
39+
40+
return body === 'packager-status:running' &&
41+
statusResponse.headers.get('X-React-Native-Project-Root') === projectRoot
42+
? 'matched_server_running'
43+
: 'port_taken';
44+
} catch (e) {
45+
return 'unknown';
46+
}
47+
}
48+
49+
async function isPortOccupied(host: string, port: number): Promise<boolean> {
50+
let result = false;
51+
const server = net.createServer();
52+
53+
return new Promise((resolve, reject) => {
54+
server.once('error', e => {
55+
server.close();
56+
if (e.code === 'EADDRINUSE') {
57+
result = true;
58+
} else {
59+
reject(e);
60+
}
61+
});
62+
server.once('listening', () => {
63+
result = false;
64+
server.close();
65+
});
66+
server.once('close', () => {
67+
resolve(result);
68+
});
69+
server.listen({host, port});
70+
});
71+
}

0 commit comments

Comments
 (0)