1import chalk from 'chalk'; 2import freeportAsync from 'freeport-async'; 3 4import { env } from './env'; 5import { CommandError } from './errors'; 6import * as Log from '../log'; 7 8/** Get a free port or assert a CLI command error. */ 9export async function getFreePortAsync(rangeStart: number): Promise<number> { 10 const port = await freeportAsync(rangeStart, { hostnames: [null, 'localhost'] }); 11 if (!port) { 12 throw new CommandError('NO_PORT_FOUND', 'No available port found'); 13 } 14 15 return port; 16} 17 18/** @return `true` if the port can still be used to start the dev server, `false` if the dev server should be skipped, and asserts if the port is now taken. */ 19export async function ensurePortAvailabilityAsync( 20 projectRoot: string, 21 { port }: { port: number } 22): Promise<boolean> { 23 const freePort = await freeportAsync(port, { hostnames: [null] }); 24 // Check if port has become busy during the build. 25 if (freePort === port) { 26 return true; 27 } 28 29 const isBusy = await isBusyPortRunningSameProcessAsync(projectRoot, { port }); 30 if (!isBusy) { 31 throw new CommandError( 32 `Port "${port}" became busy running another process while the app was compiling. Re-run command to use a new port.` 33 ); 34 } 35 36 // Log that the dev server will not be started and that the logs will appear in another window. 37 Log.log( 38 '› The dev server for this app is already running in another window. Logs will appear there.' 39 ); 40 return false; 41} 42 43function isRestrictedPort(port: number) { 44 if (process.platform !== 'win32' && port < 1024) { 45 const isRoot = process.getuid && process.getuid() === 0; 46 return !isRoot; 47 } 48 return false; 49} 50 51async function isBusyPortRunningSameProcessAsync(projectRoot: string, { port }: { port: number }) { 52 const { getRunningProcess } = 53 require('./getRunningProcess') as typeof import('./getRunningProcess'); 54 55 const runningProcess = isRestrictedPort(port) ? null : getRunningProcess(port); 56 if (runningProcess) { 57 if (runningProcess.directory === projectRoot) { 58 return true; 59 } else { 60 return false; 61 } 62 } 63 64 return null; 65} 66 67// TODO(Bacon): Revisit after all start and run code is merged. 68export async function choosePortAsync( 69 projectRoot: string, 70 { 71 defaultPort, 72 host, 73 reuseExistingPort, 74 }: { 75 defaultPort: number; 76 host?: string; 77 reuseExistingPort?: boolean; 78 } 79): Promise<number | null> { 80 try { 81 const port = await freeportAsync(defaultPort, { hostnames: [host ?? null] }); 82 if (port === defaultPort) { 83 return port; 84 } 85 86 const isRestricted = isRestrictedPort(port); 87 88 let message = isRestricted 89 ? `Admin permissions are required to run a server on a port below 1024` 90 : `Port ${chalk.bold(defaultPort)} is`; 91 92 const { getRunningProcess } = 93 require('./getRunningProcess') as typeof import('./getRunningProcess'); 94 const runningProcess = isRestricted ? null : getRunningProcess(defaultPort); 95 96 if (runningProcess) { 97 const pidTag = chalk.gray(`(pid ${runningProcess.pid})`); 98 if (runningProcess.directory === projectRoot) { 99 message += ` running this app in another window`; 100 if (reuseExistingPort) { 101 return null; 102 } 103 } else { 104 message += ` running ${chalk.cyan(runningProcess.command)} in another window`; 105 } 106 message += '\n' + chalk.gray(` ${runningProcess.directory} ${pidTag}`); 107 } else { 108 message += ' being used by another process'; 109 } 110 111 Log.log(`\u203A ${message}`); 112 const { confirmAsync } = require('./prompts') as typeof import('./prompts'); 113 const change = await confirmAsync({ 114 message: `Use port ${port} instead?`, 115 initial: true, 116 }); 117 return change ? port : null; 118 } catch (error: any) { 119 if (error.code === 'ABORTED') { 120 throw error; 121 } else if (error.code === 'NON_INTERACTIVE') { 122 Log.warn(chalk.yellow(error.message)); 123 return null; 124 } 125 throw error; 126 } 127} 128 129// TODO(Bacon): Revisit after all start and run code is merged. 130export async function resolvePortAsync( 131 projectRoot: string, 132 { 133 /** Should opt to reuse a port that is running the same project in another window. */ 134 reuseExistingPort, 135 /** Preferred port. */ 136 defaultPort, 137 /** Backup port for when the default isn't available. */ 138 fallbackPort, 139 }: { 140 reuseExistingPort?: boolean; 141 defaultPort?: string | number; 142 fallbackPort?: number; 143 } = {} 144): Promise<number | null> { 145 let port: number; 146 if (typeof defaultPort === 'string') { 147 port = parseInt(defaultPort, 10); 148 } else if (typeof defaultPort === 'number') { 149 port = defaultPort; 150 } else { 151 port = env.RCT_METRO_PORT || fallbackPort || 8081; 152 } 153 154 // Only check the port when the bundler is running. 155 const resolvedPort = await choosePortAsync(projectRoot, { 156 defaultPort: port, 157 reuseExistingPort, 158 }); 159 if (resolvedPort == null) { 160 Log.log('\u203A Skipping dev server'); 161 // Skip bundling if the port is null 162 } else { 163 // Use the new or resolved port 164 process.env.RCT_METRO_PORT = String(resolvedPort); 165 } 166 167 return resolvedPort; 168} 169