Handle port conflicts during server startup (#5349) * Handle port conflicts during server startup * Fix return type of startHTTPorHTTPS * Update language in getAddressInUseMessage --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -55,6 +55,8 @@ import { router as volcengineRouter } from './endpoints/volcengine.js'; | |||
| 55 | * @typedef {object} ServerStartupResult | 55 | * @typedef {object} ServerStartupResult |
| 56 | * @property {boolean} v6Failed If the server failed to start on IPv6 | 56 | * @property {boolean} v6Failed If the server failed to start on IPv6 |
| 57 | * @property {boolean} v4Failed If the server failed to start on IPv4 | 57 | * @property {boolean} v4Failed If the server failed to start on IPv4 |
| 58 | * @property {unknown} [v6Error] The IPv6 server startup error | ||
| 59 | * @property {unknown} [v4Error] The IPv4 server startup error | ||
| 58 | * @property {boolean} useIPv6 If use IPv6 | 60 | * @property {boolean} useIPv6 If use IPv6 |
| 59 | * @property {boolean} useIPv4 If use IPv4 | 61 | * @property {boolean} useIPv4 If use IPv4 |
| 60 | */ | 62 | */ |
| @@ -207,6 +209,37 @@ export class ServerStartup { | |||
| 207 | } | 209 | } |
| 208 | 210 | ||
| 209 | /** | 211 | /** |
| 212 | * Checks if the error was caused by an occupied port. | ||
| 213 | * @param {unknown} error | ||
| 214 | * @returns {error is NodeJS.ErrnoException} | ||
| 215 | */ | ||
| 216 | #isAddressInUseError(error) { | ||
| 217 | return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EADDRINUSE'; | ||
| 218 | } | ||
| 219 | |||
| 220 | /** | ||
| 221 | * Gets a readable listen address for an IP version. | ||
| 222 | * @param {URL} url The URL to listen on | ||
| 223 | * @param {number} ipVersion The IP version to use | ||
| 224 | * @returns {string} | ||
| 225 | */ | ||
| 226 | #getListenAddress(url, ipVersion) { | ||
| 227 | const host = ipVersion === 6 ? urlHostnameToIPv6(url.hostname) : url.hostname; | ||
| 228 | return `${host}:${Number(url.port || (this.cliArgs.ssl ? 443 : 80))}`; | ||
| 229 | } | ||
| 230 | |||
| 231 | /** | ||
| 232 | * Builds a user-facing error for an occupied port. | ||
| 233 | * @param {URL} url The URL that failed to bind | ||
| 234 | * @param {number} ipVersion The IP version that failed | ||
| 235 | * @returns {string} | ||
| 236 | */ | ||
| 237 | #getAddressInUseMessage(url, ipVersion) { | ||
| 238 | const listenAddress = this.#getListenAddress(url, ipVersion); | ||
| 239 | return `Address ${listenAddress} is already in use. Another SillyTavern instance may already be running. Stop the other process or change "port" in config.yaml.`; | ||
| 240 | } | ||
| 241 | |||
| 242 | /** | ||
| 210 | * Checks if SSL options are valid. If not, it will print an error message and exit the process. | 243 | * Checks if SSL options are valid. If not, it will print an error message and exit the process. |
| 211 | * @returns {void} | 244 | * @returns {void} |
| 212 | */ | 245 | */ |
| @@ -287,11 +320,13 @@ export class ServerStartup { | |||
| 287 | * Starts the server using http or https depending on config | 320 | * Starts the server using http or https depending on config |
| 288 | * @param {boolean} useIPv6 If use IPv6 | 321 | * @param {boolean} useIPv6 If use IPv6 |
| 289 | * @param {boolean} useIPv4 If use IPv4 | 322 | * @param {boolean} useIPv4 If use IPv4 |
| 290 | * @returns {Promise<[boolean, boolean]>} A promise that resolves with an array of booleans indicating if the server failed to start on IPv6 and IPv4, respectively | 323 | * @returns {Promise<[boolean, boolean, unknown, unknown]>} A promise that resolves with an array of booleans indicating if the server failed to start on IPv6 and IPv4, respectively, and the corresponding errors |
| 291 | */ | 324 | */ |
| 292 | async #startHTTPorHTTPS(useIPv6, useIPv4) { | 325 | async #startHTTPorHTTPS(useIPv6, useIPv4) { |
| 293 | let v6Failed = false; | 326 | let v6Failed = false; |
| 294 | let v4Failed = false; | 327 | let v4Failed = false; |
| 328 | let v6Error; | ||
| 329 | let v4Error; | ||
| 295 | 330 | ||
| 296 | const createFunc = this.cliArgs.ssl ? this.#createHttpsServer.bind(this) : this.#createHttpServer.bind(this); | 331 | const createFunc = this.cliArgs.ssl ? this.#createHttpsServer.bind(this) : this.#createHttpServer.bind(this); |
| 297 | 332 | ||
| @@ -300,9 +335,14 @@ export class ServerStartup { | |||
| 300 | await createFunc(this.cliArgs.getIPv6ListenUrl(), 6); | 335 | await createFunc(this.cliArgs.getIPv6ListenUrl(), 6); |
| 301 | } catch (error) { | 336 | } catch (error) { |
| 302 | console.error('Warning: failed to start server on IPv6'); | 337 | console.error('Warning: failed to start server on IPv6'); |
| 303 | console.error(error); | 338 | if (this.#isAddressInUseError(error)) { |
| 339 | console.error(this.#getAddressInUseMessage(this.cliArgs.getIPv6ListenUrl(), 6)); | ||
| 340 | } else { | ||
| 341 | console.error(error); | ||
| 342 | } | ||
| 304 | 343 | ||
| 305 | v6Failed = true; | 344 | v6Failed = true; |
| 345 | v6Error = error; | ||
| 306 | } | 346 | } |
| 307 | } | 347 | } |
| 308 | 348 | ||
| @@ -311,13 +351,18 @@ export class ServerStartup { | |||
| 311 | await createFunc(this.cliArgs.getIPv4ListenUrl(), 4); | 351 | await createFunc(this.cliArgs.getIPv4ListenUrl(), 4); |
| 312 | } catch (error) { | 352 | } catch (error) { |
| 313 | console.error('Warning: failed to start server on IPv4'); | 353 | console.error('Warning: failed to start server on IPv4'); |
| 314 | console.error(error); | 354 | if (this.#isAddressInUseError(error)) { |
| 355 | console.error(this.#getAddressInUseMessage(this.cliArgs.getIPv4ListenUrl(), 4)); | ||
| 356 | } else { | ||
| 357 | console.error(error); | ||
| 358 | } | ||
| 315 | 359 | ||
| 316 | v4Failed = true; | 360 | v4Failed = true; |
| 361 | v4Error = error; | ||
| 317 | } | 362 | } |
| 318 | } | 363 | } |
| 319 | 364 | ||
| 320 | return [v6Failed, v4Failed]; | 365 | return [v6Failed, v4Failed, v6Error, v4Error]; |
| 321 | } | 366 | } |
| 322 | 367 | ||
| 323 | /** | 368 | /** |
| @@ -325,16 +370,25 @@ export class ServerStartup { | |||
| 325 | * @param {ServerStartupResult} result The results of the server startup | 370 | * @param {ServerStartupResult} result The results of the server startup |
| 326 | * @returns {void} | 371 | * @returns {void} |
| 327 | */ | 372 | */ |
| 328 | #handleServerListenFail({ v6Failed, v4Failed, useIPv6, useIPv4 }) { | 373 | #handleServerListenFail({ v6Failed, v4Failed, v6Error, v4Error, useIPv6, useIPv4 }) { |
| 329 | if (v6Failed && !useIPv4) { | 374 | if (v6Failed && !useIPv4) { |
| 375 | if (this.#isAddressInUseError(v6Error)) { | ||
| 376 | this.#fatal('Error: Startup aborted because IPv6 is the only enabled protocol and its listen port is already in use.'); | ||
| 377 | } | ||
| 330 | this.#fatal('Error: Failed to start server on IPv6 and IPv4 disabled'); | 378 | this.#fatal('Error: Failed to start server on IPv6 and IPv4 disabled'); |
| 331 | } | 379 | } |
| 332 | 380 | ||
| 333 | if (v4Failed && !useIPv6) { | 381 | if (v4Failed && !useIPv6) { |
| 382 | if (this.#isAddressInUseError(v4Error)) { | ||
| 383 | this.#fatal('Error: Startup aborted because IPv4 is the only enabled protocol and its listen port is already in use.'); | ||
| 384 | } | ||
| 334 | this.#fatal('Error: Failed to start server on IPv4 and IPv6 disabled'); | 385 | this.#fatal('Error: Failed to start server on IPv4 and IPv6 disabled'); |
| 335 | } | 386 | } |
| 336 | 387 | ||
| 337 | if (v6Failed && v4Failed) { | 388 | if (v6Failed && v4Failed) { |
| 389 | if (this.#isAddressInUseError(v6Error) && this.#isAddressInUseError(v4Error)) { | ||
| 390 | this.#fatal('Error: Failed to start server because the configured IPv6 and IPv4 listen ports are already in use.'); | ||
| 391 | } | ||
| 338 | this.#fatal('Error: Failed to start server on both IPv6 and IPv4'); | 392 | this.#fatal('Error: Failed to start server on both IPv6 and IPv4'); |
| 339 | } | 393 | } |
| 340 | } | 394 | } |
| @@ -388,8 +442,8 @@ export class ServerStartup { | |||
| 388 | process.exit(1); | 442 | process.exit(1); |
| 389 | } | 443 | } |
| 390 | 444 | ||
| 391 | const [v6Failed, v4Failed] = await this.#startHTTPorHTTPS(useIPv6, useIPv4); | 445 | const [v6Failed, v4Failed, v6Error, v4Error] = await this.#startHTTPorHTTPS(useIPv6, useIPv4); |
| 392 | const result = { v6Failed, v4Failed, useIPv6, useIPv4 }; | 446 | const result = { v6Failed, v4Failed, v6Error, v4Error, useIPv6, useIPv4 }; |
| 393 | this.#handleServerListenFail(result); | 447 | this.#handleServerListenFail(result); |
| 394 | return result; | 448 | return result; |
| 395 | } | 449 | } |