Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optimize the resolve logic of devServer's port and the server running at... log(#7271) #7274

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/vite/src/node/constants.ts
Expand Up @@ -92,3 +92,6 @@ export const DEFAULT_ASSETS_RE = new RegExp(
)

export const DEP_VERSION_RE = /[\?&](v=[\w\.-]+)\b/

export const DEFAULT_IPV4_ADDR = '0.0.0.0'
export const DEFAULT_IPV6_ADDR = '::'
137 changes: 120 additions & 17 deletions packages/vite/src/node/http.ts
@@ -1,4 +1,7 @@
import fs, { promises as fsp } from 'fs'
import * as http from 'http'
import os from 'os'
import type { NetworkInterfaceInfoIPv6 } from 'os'
import path from 'path'
import type {
OutgoingHttpHeaders as HttpServerHeaders,
Expand Down Expand Up @@ -168,6 +171,115 @@ async function getCertificate(cacheDir: string) {
}
}

function getExternalHost() {
const hosts: string[] = []
Object.values(os.networkInterfaces())
.flatMap((nInterface) => nInterface ?? [])
.filter((detail) => detail && detail.address && detail.internal === false)
.map((detail) => {
let address = detail.address
if (address.indexOf('fe80:') === 0) {
// support ipv6 scope
address += '%' + (detail as NetworkInterfaceInfoIPv6).scopeid
}
hosts.push(address)
})
return hosts
}

function createTestServer(port: number, host: string): Promise<string> {
return new Promise(function (resolve, reject) {
const server = http.createServer()
server.listen(port, host, function () {
server.once('close', function () {
resolve(host)
})
server.close()
})
server.on('error', (e: Error & { code?: string }) => {
if (e.code === 'EADDRINUSE') {
reject(host)
} else {
reject(e)
}
})
})
}

function getConflictHosts(host: string | undefined) {
const externalHost = getExternalHost()
const internalHost = ['127.0.0.1', '::1']
const defaultHost = ['0.0.0.0', '::']
let conflictIpList: string[] = []
if (host === undefined || host === '::' || host === '0.0.0.0') {
// User may want to listen on every IPs, we should check every IPs
conflictIpList = [...externalHost, ...internalHost, ...defaultHost]
} else if (host === '127.0.0.1' || host === 'localhost') {
// User may want to listen on 127.0.0.1 and use localhost as the hostname.
// check ::1 cause localhost may parse to ::1 first,::1 is reachable when ::1 or :: is in listening
conflictIpList = ['127.0.0.1', '::1', '::']
} else {
// Only listen on specific address
conflictIpList = [host]
}
return conflictIpList
}

// inspired by https://gist.github.com/eplawless/51afd77bc6e8631f6b5cb117208d5fe0#file-node-is-a-liar-snippet-4-js
async function checkHostsAndPortAvailable(hosts: string[], port: number) {
const servers = hosts.reduce(function (
lastServer: Promise<string> | null,
host
) {
return lastServer
? lastServer.then(function () {
return createTestServer(port, host)
})
: createTestServer(port, host)
},
null)

return servers
}

async function resolvePort(
host: string | undefined,
startPort: number,
logger: Logger,
strictPort?: boolean
) {
const conflictIpList = getConflictHosts(host)
return new Promise((resolve: (port: number) => void, reject) => {
checkHostsAndPortAvailable(conflictIpList, startPort).then(
() => {
resolve(startPort)
},
(conflictHost: string | Error) => {
if (typeof conflictHost !== 'string') {
reject(conflictHost)
return
}
if (strictPort) {
logger.info(`Port ${startPort} is in use by ${conflictHost}`)
reject(conflictHost)
} else {
logger.info(
`Port ${startPort} is in use by ${conflictHost}, trying another one...`
)
resolvePort(host, startPort + 1, logger, strictPort).then(
(port) => {
resolve(port)
},
(e: Error) => {
reject(e)
}
)
}
}
)
})
}

export async function httpServerStart(
httpServer: HttpServer,
serverOptions: {
Expand All @@ -180,26 +292,17 @@ export async function httpServerStart(
return new Promise((resolve, reject) => {
let { port, strictPort, host, logger } = serverOptions

const onError = (e: Error & { code?: string }) => {
if (e.code === 'EADDRINUSE') {
if (strictPort) {
httpServer.removeListener('error', onError)
reject(new Error(`Port ${port} is already in use`))
} else {
logger.info(`Port ${port} is in use, trying another one...`)
httpServer.listen(++port, host)
}
} else {
httpServer.removeListener('error', onError)
resolvePort(host, port, logger, strictPort).then((availablePort) => {
const onError = (e: Error & { code?: string }) => {
reject(e)
}
}

httpServer.on('error', onError)
httpServer.on('error', onError)

httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError)
resolve(port)
})
httpServer.listen(availablePort, host, () => {
httpServer.removeListener('error', onError)
resolve(port)
})
}, reject)
})
}
42 changes: 37 additions & 5 deletions packages/vite/src/node/logger.ts
Expand Up @@ -9,6 +9,7 @@ import type { ResolvedConfig } from '.'
import type { CommonServerOptions } from './http'
import type { Hostname } from './utils'
import { resolveHostname } from './utils'
import { DEFAULT_IPV4_ADDR, DEFAULT_IPV6_ADDR } from './constants'

export type LogType = 'error' | 'warn' | 'info'
export type LogLevel = LogType | 'silent'
Expand Down Expand Up @@ -190,12 +191,43 @@ function printServerUrls(
} else {
Object.values(os.networkInterfaces())
.flatMap((nInterface) => nInterface ?? [])
.filter((detail) => detail && detail.address && detail.family === 'IPv4')
.filter((detail) => {
if (!detail || !detail.address) {
return false
}

// Only show ipv6 url when host is ipv6 and host isn't ::
if (detail.family === 'IPv6') {
return (
hostname.host &&
hostname.host.includes(detail.address) &&
hostname.host !== '::'
)
} else {
const isIpv4DefaultAddress = detail.address.includes('127.0.0.1')
if (
hostname.host === undefined ||
hostname.host === DEFAULT_IPV4_ADDR ||
hostname.host === DEFAULT_IPV6_ADDR ||
hostname.host.includes(detail.address) ||
// Use '127.0.0.1' for any other host except '::1' as local url
// here '127.0.0.1' will be replace to hostname.name later
(isIpv4DefaultAddress && hostname.host !== '::1')
) {
return true
}
return false
}
})
.map((detail) => {
const type = detail.address.includes('127.0.0.1')
? 'Local: '
: 'Network: '
const host = detail.address.replace('127.0.0.1', hostname.name)
const type =
detail.address.includes('127.0.0.1') || detail.address.includes('::1')
? 'Local: '
: 'Network: '
let host = detail.address.replace('127.0.0.1', hostname.name)
if (host.includes(':')) {
host = `[${host}]`
}
const url = `${protocol}://${host}:${colors.bold(port)}${base}`
return ` > ${type} ${colors.cyan(url)}`
})
Expand Down
6 changes: 4 additions & 2 deletions packages/vite/src/node/utils.ts
Expand Up @@ -635,8 +635,10 @@ export function resolveHostname(
optionsHost: string | boolean | undefined
): Hostname {
let host: string | undefined
if (optionsHost === undefined || optionsHost === false) {
// Use a secure default
if (!optionsHost) {
// Use a secure default when optionsHost can transfer to false
// to avoid node listen on default_addr. ie: optionsHost is '' or 0 or other false values
// see https://github.com/nodejs/node/blob/v17.7.1/lib/net.js#L906
host = '127.0.0.1'
} else if (optionsHost === true) {
// If passed --host in the CLI without arguments
Expand Down