| | |
| | import './cpu-profile' |
| | import { getNetworkHost } from '../../lib/get-network-host' |
| |
|
| | if (performance.getEntriesByName('next-start').length === 0) { |
| | performance.mark('next-start') |
| | } |
| | import '../next' |
| | import '../require-hook' |
| |
|
| | import type { IncomingMessage, ServerResponse } from 'http' |
| | import type { SelfSignedCertificate } from '../../lib/mkcert' |
| | import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types' |
| |
|
| | import fs from 'fs' |
| | import v8 from 'v8' |
| | import path from 'path' |
| | import http from 'http' |
| | import https from 'https' |
| | import os from 'os' |
| | import { exec } from 'child_process' |
| | import Watchpack from 'next/dist/compiled/watchpack' |
| | import * as Log from '../../build/output/log' |
| | import setupDebug from 'next/dist/compiled/debug' |
| | import { RESTART_EXIT_CODE } from './utils' |
| | import { formatHostname } from './format-hostname' |
| | import { initialize } from './router-server' |
| | import { |
| | CONFIG_FILES, |
| | PHASE_DEVELOPMENT_SERVER, |
| | } from '../../shared/lib/constants' |
| | import { getEnvInfo, logExperimentalInfo, logStartInfo } from './app-info-log' |
| | import { validateTurboNextConfig } from '../../lib/turbopack-warning' |
| | import { type Span, trace, flushAllTraces } from '../../trace' |
| | import { isIPv6 } from './is-ipv6' |
| | import { AsyncCallbackSet } from './async-callback-set' |
| | import type { NextServer } from '../next' |
| | import { durationToString } from '../../build/duration-to-string' |
| |
|
| | const debug = setupDebug('next:start-server') |
| | let startServerSpan: Span | undefined |
| |
|
| | |
| | |
| | |
| | async function getProcessIdUsingPort(port: number): Promise<string | null> { |
| | const timeoutMs = 250 |
| | const processLookupController = new AbortController() |
| |
|
| | const pidPromise = new Promise<string | null>((resolve) => { |
| | const handleError = (error: Error) => { |
| | debug('Failed to get process ID for port', port, error) |
| | resolve(null) |
| | } |
| |
|
| | try { |
| | |
| | if (process.platform !== 'win32') { |
| | exec( |
| | `lsof -ti:${port} -sTCP:LISTEN`, |
| | { signal: processLookupController.signal }, |
| | (error, stdout) => { |
| | if (error) { |
| | handleError(error) |
| | return |
| | } |
| | |
| | const pid = stdout.trim() |
| | resolve(pid || null) |
| | } |
| | ) |
| | } else { |
| | |
| | exec( |
| | `netstat -ano | findstr /C:":${port} " | findstr LISTENING`, |
| | { signal: processLookupController.signal }, |
| | (error, stdout) => { |
| | if (error) { |
| | handleError(error) |
| | return |
| | } |
| | |
| | const cleanOutput = stdout.replace(/\s+/g, ' ').trim() |
| | if (cleanOutput) { |
| | const lines = cleanOutput.split('\n') |
| | const firstLine = lines[0].trim() |
| | if (firstLine) { |
| | const parts = firstLine.split(' ') |
| | const pid = parts[parts.length - 1] |
| | resolve(pid || null) |
| | } else { |
| | resolve(null) |
| | } |
| | } else { |
| | resolve(null) |
| | } |
| | } |
| | ) |
| | } |
| | } catch (cause) { |
| | handleError( |
| | new Error('Unexpected error during process lookup', { cause }) |
| | ) |
| | } |
| | }) |
| |
|
| | const timeoutId = setTimeout(() => { |
| | processLookupController.abort( |
| | `PID detection timed out after ${timeoutMs}ms for port ${port}.` |
| | ) |
| | }, timeoutMs) |
| |
|
| | pidPromise.finally(() => clearTimeout(timeoutId)) |
| |
|
| | return pidPromise |
| | } |
| |
|
| | export interface StartServerOptions { |
| | dir: string |
| | port: number |
| | isDev: boolean |
| | hostname?: string |
| | allowRetry?: boolean |
| | customServer?: boolean |
| | minimalMode?: boolean |
| | keepAliveTimeout?: number |
| | |
| | selfSignedCertificate?: SelfSignedCertificate |
| | } |
| |
|
| | export async function getRequestHandlers({ |
| | dir, |
| | port, |
| | isDev, |
| | onDevServerCleanup, |
| | server, |
| | hostname, |
| | minimalMode, |
| | keepAliveTimeout, |
| | experimentalHttpsServer, |
| | quiet, |
| | }: { |
| | dir: string |
| | port: number |
| | isDev: boolean |
| | onDevServerCleanup: ((listener: () => Promise<void>) => void) | undefined |
| | server?: import('http').Server |
| | hostname?: string |
| | minimalMode?: boolean |
| | keepAliveTimeout?: number |
| | experimentalHttpsServer?: boolean |
| | quiet?: boolean |
| | }): ReturnType<typeof initialize> { |
| | return initialize({ |
| | dir, |
| | port, |
| | hostname, |
| | onDevServerCleanup, |
| | dev: isDev, |
| | minimalMode, |
| | server, |
| | keepAliveTimeout, |
| | experimentalHttpsServer, |
| | startServerSpan, |
| | quiet, |
| | }) |
| | } |
| |
|
| | export type StartServerResult = { |
| | distDir: string |
| | } |
| |
|
| | export async function startServer( |
| | serverOptions: StartServerOptions |
| | ): Promise<StartServerResult> { |
| | const { |
| | dir, |
| | isDev, |
| | hostname, |
| | minimalMode, |
| | allowRetry, |
| | keepAliveTimeout, |
| | selfSignedCertificate, |
| | } = serverOptions |
| | let { port } = serverOptions |
| |
|
| | process.title = `next-server (v${process.env.__NEXT_VERSION})` |
| | let handlersReady = () => {} |
| | let handlersError = () => {} |
| |
|
| | let handlersPromise: Promise<void> | undefined = new Promise<void>( |
| | (resolve, reject) => { |
| | handlersReady = resolve |
| | handlersError = reject |
| | } |
| | ) |
| | let requestHandler: WorkerRequestHandler = async ( |
| | req: IncomingMessage, |
| | res: ServerResponse |
| | ): Promise<void> => { |
| | if (handlersPromise) { |
| | await handlersPromise |
| | return requestHandler(req, res) |
| | } |
| | throw new Error('Invariant request handler was not setup') |
| | } |
| | let upgradeHandler: WorkerUpgradeHandler = async ( |
| | req, |
| | socket, |
| | head |
| | ): Promise<void> => { |
| | if (handlersPromise) { |
| | await handlersPromise |
| | return upgradeHandler(req, socket, head) |
| | } |
| | throw new Error('Invariant upgrade handler was not setup') |
| | } |
| |
|
| | let nextServer: NextServer | undefined |
| |
|
| | |
| | if (selfSignedCertificate && !isDev) { |
| | throw new Error( |
| | 'Using a self signed certificate is only supported with `next dev`.' |
| | ) |
| | } |
| |
|
| | async function requestListener(req: IncomingMessage, res: ServerResponse) { |
| | try { |
| | if (handlersPromise) { |
| | await handlersPromise |
| | handlersPromise = undefined |
| | } |
| | await requestHandler(req, res) |
| | } catch (err) { |
| | res.statusCode = 500 |
| | res.end('Internal Server Error') |
| | Log.error(`Failed to handle request for ${req.url}`) |
| | console.error(err) |
| | } finally { |
| | if (isDev) { |
| | if ( |
| | v8.getHeapStatistics().used_heap_size > |
| | 0.8 * v8.getHeapStatistics().heap_size_limit |
| | ) { |
| | Log.warn( |
| | `Server is approaching the used memory threshold, restarting...` |
| | ) |
| | trace('server-restart-close-to-memory-threshold', undefined, { |
| | 'memory.heapSizeLimit': String( |
| | v8.getHeapStatistics().heap_size_limit |
| | ), |
| | 'memory.heapUsed': String(v8.getHeapStatistics().used_heap_size), |
| | }).stop() |
| | await flushAllTraces() |
| | process.exit(RESTART_EXIT_CODE) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | const server = selfSignedCertificate |
| | ? https.createServer( |
| | { |
| | key: fs.readFileSync(selfSignedCertificate.key), |
| | cert: fs.readFileSync(selfSignedCertificate.cert), |
| | }, |
| | requestListener |
| | ) |
| | : http.createServer(requestListener) |
| |
|
| | if (keepAliveTimeout) { |
| | server.keepAliveTimeout = keepAliveTimeout |
| | } |
| | server.on('upgrade', async (req, socket, head) => { |
| | try { |
| | await upgradeHandler(req, socket, head) |
| | } catch (err) { |
| | socket.destroy() |
| | Log.error(`Failed to handle request for ${req.url}`) |
| | console.error(err) |
| | } |
| | }) |
| |
|
| | let portRetryCount = 0 |
| | const originalPort = port |
| |
|
| | server.on('error', (err: NodeJS.ErrnoException) => { |
| | if ( |
| | allowRetry && |
| | port && |
| | isDev && |
| | err.code === 'EADDRINUSE' && |
| | portRetryCount < 10 |
| | ) { |
| | port += 1 |
| | portRetryCount += 1 |
| | server.listen(port, hostname) |
| | } else { |
| | Log.error(`Failed to start server`) |
| | console.error(err) |
| | process.exit(1) |
| | } |
| | }) |
| |
|
| | let cleanupListeners = isDev ? new AsyncCallbackSet() : undefined |
| |
|
| | const distDir = await new Promise<string>((resolve) => { |
| | server.on('listening', async () => { |
| | const addr = server.address() |
| | const actualHostname = formatHostname( |
| | typeof addr === 'object' |
| | ? addr?.address || hostname || 'localhost' |
| | : addr |
| | ) |
| | const formattedHostname = |
| | !hostname || actualHostname === '0.0.0.0' |
| | ? 'localhost' |
| | : actualHostname === '[::]' |
| | ? '[::1]' |
| | : formatHostname(hostname) |
| |
|
| | port = typeof addr === 'object' ? addr?.port || port : port |
| |
|
| | if (portRetryCount) { |
| | const pid = await getProcessIdUsingPort(originalPort) |
| | if (pid) { |
| | Log.warn( |
| | `Port ${originalPort} is in use by process ${pid}, using available port ${port} instead.` |
| | ) |
| | } else { |
| | Log.warn( |
| | `Port ${originalPort} is in use by an unknown process, using available port ${port} instead.` |
| | ) |
| | } |
| | } |
| |
|
| | const networkHostname = |
| | hostname ?? getNetworkHost(isIPv6(actualHostname) ? 'IPv6' : 'IPv4') |
| |
|
| | const protocol = selfSignedCertificate ? 'https' : 'http' |
| |
|
| | const networkUrl = networkHostname |
| | ? `${protocol}://${formatHostname(networkHostname)}:${port}` |
| | : null |
| |
|
| | const appUrl = `${protocol}://${formattedHostname}:${port}` |
| |
|
| | |
| | |
| | |
| | process.env.PORT = port + '' |
| |
|
| | process.env.__NEXT_PRIVATE_ORIGIN = appUrl |
| |
|
| | |
| | if (selfSignedCertificate) { |
| | process.env.__NEXT_EXPERIMENTAL_HTTPS = '1' |
| | } |
| |
|
| | |
| | const envInfo = isDev ? getEnvInfo(dir) : undefined |
| |
|
| | |
| | logStartInfo({ |
| | networkUrl, |
| | appUrl, |
| | envInfo, |
| | logBundler: isDev, |
| | }) |
| |
|
| | |
| | |
| | const startTime = parseInt(process.env.NEXT_PRIVATE_START_TIME || '0', 10) |
| | const endTime = Date.now() |
| | const startServerProcessDurationMs = endTime - startTime |
| |
|
| | const formattedStartDuration = durationToString( |
| | startServerProcessDurationMs / 1000 |
| | ) |
| |
|
| | Log.event(`Ready in ${formattedStartDuration}`) |
| |
|
| | try { |
| | let cleanupStarted = false |
| | let closeUpgraded: (() => void) | null = null |
| | const cleanup = () => { |
| | if (cleanupStarted) { |
| | |
| | |
| | |
| | |
| | return |
| | } |
| | cleanupStarted = true |
| | ;(async () => { |
| | debug('start-server process cleanup') |
| |
|
| | |
| | |
| | await new Promise<void>((res) => { |
| | server.close((err) => { |
| | if (err) console.error(err) |
| | res() |
| | }) |
| | if (isDev) { |
| | server.closeAllConnections() |
| | closeUpgraded?.() |
| | } |
| | }) |
| |
|
| | |
| | await Promise.all([ |
| | nextServer?.close().catch(console.error), |
| | cleanupListeners?.runAll().catch(console.error), |
| | ]) |
| |
|
| | |
| | if (isDev) { |
| | try { |
| | const { traceGlobals } = |
| | require('../../trace/shared') as typeof import('../../trace/shared') |
| | const telemetry = traceGlobals.get('telemetry') as |
| | | InstanceType< |
| | typeof import('../../telemetry/storage').Telemetry |
| | > |
| | | undefined |
| | if (telemetry) { |
| | |
| | |
| | |
| | telemetry.flushDetached('dev', dir) |
| | } |
| | } catch (_) { |
| | |
| | } |
| | } |
| |
|
| | debug('start-server process cleanup finished') |
| | process.exit(0) |
| | })() |
| | } |
| |
|
| | |
| | |
| | if (!process.env.NEXT_MANUAL_SIG_HANDLE) { |
| | process.on('SIGINT', cleanup) |
| | process.on('SIGTERM', cleanup) |
| | } |
| |
|
| | |
| | const initResult = await getRequestHandlers({ |
| | dir, |
| | port, |
| | isDev, |
| | onDevServerCleanup: cleanupListeners |
| | ? cleanupListeners.add.bind(cleanupListeners) |
| | : undefined, |
| | server, |
| | hostname, |
| | minimalMode, |
| | keepAliveTimeout, |
| | experimentalHttpsServer: !!selfSignedCertificate, |
| | }) |
| | requestHandler = initResult.requestHandler |
| | upgradeHandler = initResult.upgradeHandler |
| | nextServer = initResult.server |
| | closeUpgraded = initResult.closeUpgraded |
| |
|
| | |
| | if (isDev) { |
| | logExperimentalInfo({ |
| | experimentalFeatures: initResult.experimentalFeatures, |
| | cacheComponents: initResult.cacheComponents, |
| | }) |
| | } |
| |
|
| | handlersReady() |
| |
|
| | if (process.env.TURBOPACK && isDev) { |
| | await validateTurboNextConfig({ |
| | dir: serverOptions.dir, |
| | configPhase: PHASE_DEVELOPMENT_SERVER, |
| | }) |
| | } |
| |
|
| | resolve(initResult.distDir) |
| | } catch (err) { |
| | |
| | handlersError() |
| | console.error(err) |
| | process.exit(1) |
| | } |
| | }) |
| | server.listen(port, hostname) |
| | }) |
| |
|
| | if (isDev) { |
| | function watchConfigFiles( |
| | dirToWatch: string, |
| | onChange: (filename: string) => void |
| | ) { |
| | const wp = new Watchpack() |
| | wp.watch({ |
| | files: CONFIG_FILES.map((file) => path.join(dirToWatch, file)), |
| | }) |
| | wp.on('change', onChange) |
| | } |
| | watchConfigFiles(dir, async (filename) => { |
| | if (process.env.__NEXT_DISABLE_MEMORY_WATCHER) { |
| | Log.info( |
| | `Detected change, manual restart required due to '__NEXT_DISABLE_MEMORY_WATCHER' usage` |
| | ) |
| | return |
| | } |
| |
|
| | Log.warn( |
| | `Found a change in ${path.basename( |
| | filename |
| | )}. Restarting the server to apply the changes...` |
| | ) |
| | process.exit(RESTART_EXIT_CODE) |
| | }) |
| | } |
| |
|
| | return { distDir } |
| | } |
| |
|
| | if (process.env.NEXT_PRIVATE_WORKER && process.send) { |
| | process.addListener('message', async (msg: any) => { |
| | if ( |
| | msg && |
| | typeof msg === 'object' && |
| | msg.nextWorkerOptions && |
| | process.send |
| | ) { |
| | startServerSpan = trace('start-dev-server', undefined, { |
| | cpus: String(os.cpus().length), |
| | platform: os.platform(), |
| | 'memory.freeMem': String(os.freemem()), |
| | 'memory.totalMem': String(os.totalmem()), |
| | 'memory.heapSizeLimit': String(v8.getHeapStatistics().heap_size_limit), |
| | }) |
| | const result = await startServerSpan.traceAsyncFn(() => |
| | startServer(msg.nextWorkerOptions) |
| | ) |
| | const memoryUsage = process.memoryUsage() |
| | startServerSpan.setAttribute('memory.rss', String(memoryUsage.rss)) |
| | startServerSpan.setAttribute( |
| | 'memory.heapTotal', |
| | String(memoryUsage.heapTotal) |
| | ) |
| | startServerSpan.setAttribute( |
| | 'memory.heapUsed', |
| | String(memoryUsage.heapUsed) |
| | ) |
| | process.send({ |
| | nextServerReady: true, |
| | port: process.env.PORT, |
| | distDir: result.distDir, |
| | }) |
| | } |
| | }) |
| | process.send({ nextWorkerReady: true }) |
| | } |
| |
|