Skip to content

Commit

Permalink
feat: optimize the resolve logic of devServer's port and the server r…
Browse files Browse the repository at this point in the history
…unning at... log(vitejs#7271)
  • Loading branch information
libmw committed Mar 11, 2022
1 parent dddda1e commit c329081
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 24 deletions.
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

0 comments on commit c329081

Please sign in to comment.