Files
pnpm/store/plugin-commands-server/src/start.ts
Khải 006f4c8a81 refactor: type checking perf (#8132)
* refactor: type checking perf

* fix: eslint
2024-05-26 22:13:02 +02:00

172 lines
5.5 KiB
TypeScript

// cspell:ignore diable
import {
close as _close,
closeSync,
open as _open,
promises as fs,
unlinkSync,
write as _write,
} from 'fs'
import { promisify } from 'util'
import path from 'path'
import { packageManager } from '@pnpm/cli-meta'
import { PnpmError } from '@pnpm/error'
import { logger } from '@pnpm/logger'
import { type StoreServerHandle, createServer } from '@pnpm/server'
import {
createNewStoreController,
type CreateStoreControllerOptions,
serverConnectionInfoDir,
} from '@pnpm/store-connection-manager'
import { getStorePath } from '@pnpm/store-path'
import Diable from '@zkochan/diable'
import getPort from 'get-port'
import isWindows from 'is-windows'
import onExit from 'signal-exit'
const storeServerLogger = logger('store-server')
const write = promisify(_write)
const close = promisify(_close)
const open = promisify(_open)
export async function start (
opts: CreateStoreControllerOptions & {
background?: boolean
protocol?: 'auto' | 'tcp' | 'ipc'
port?: number
ignoreStopRequests?: boolean
ignoreUploadRequests?: boolean
}
): Promise<void> {
if (opts.protocol === 'ipc' && opts.port) {
throw new Error('Port cannot be selected when server communicates via IPC')
}
if (opts.background && !Diable.isDaemon()) {
Diable()
}
const storeDir = await getStorePath({
pkgRoot: opts.dir,
storePath: opts.storeDir,
pnpmHomeDir: opts.pnpmHomeDir,
})
const connectionInfoDir = serverConnectionInfoDir(storeDir)
const serverJsonPath = path.join(connectionInfoDir, 'server.json')
await fs.mkdir(connectionInfoDir, { recursive: true })
// Open server.json with exclusive write access to ensure only one process can successfully
// start the server. Note: NFS does not support exclusive writing, but do we really care?
// Source: https://github.com/moxystudio/node-proper-lockfile#user-content-comparison
let fd: number | null
try {
fd = await open(serverJsonPath, 'wx')
} catch (error: any) { // eslint-disable-line
if (error.code !== 'EEXIST') {
throw error
}
throw new PnpmError('SERVER_MANIFEST_LOCKED', `Canceling startup of server (pid ${process.pid}) because another process got exclusive access to server.json`)
}
let server: null | StoreServerHandle = null
onExit(() => {
if (server !== null) {
// Note that server.close returns a Promise, but we cannot wait for it because we may be
// inside the 'exit' even of process.
server.close() // eslint-disable-line @typescript-eslint/no-floating-promises
}
if (fd !== null) {
try {
closeSync(fd)
} catch (error: any) { // eslint-disable-line
storeServerLogger.error(error, 'Got error while closing file descriptor of server.json, but the process is already exiting')
}
}
try {
unlinkSync(serverJsonPath)
} catch (error: any) { // eslint-disable-line
if (error.code !== 'ENOENT') {
storeServerLogger.error(error, 'Got error unlinking server.json, but the process is already exiting')
}
}
})
const store = await createNewStoreController(Object.assign(opts, {
storeDir,
}))
const protocol = opts.protocol ?? (opts.port ? 'tcp' : 'auto')
const serverOptions = await getServerOptions(connectionInfoDir, { protocol, port: opts.port })
const connectionOptions = {
remotePrefix: serverOptions.path != null
? `http://unix:${serverOptions.path}:`
: `http://${serverOptions.hostname!}:${serverOptions.port!}`,
}
server = createServer(store.ctrl, {
...serverOptions,
ignoreStopRequests: opts.ignoreStopRequests,
ignoreUploadRequests: opts.ignoreUploadRequests,
})
// Make sure to populate server.json after the server has started, so clients know that the server is
// listening if a server.json with valid JSON content exists.
const serverJson = {
connectionOptions,
pid: process.pid,
pnpmVersion: packageManager.version,
}
const serverJsonStr = JSON.stringify(serverJson, undefined, 2) // undefined and 2 are for formatting.
const serverJsonBuffer = Buffer.from(serverJsonStr, 'utf8')
// fs.write on NodeJS 4 requires the parameters offset and length to be set:
// https://nodejs.org/docs/latest-v4.x/api/fs.html#fs_fs_write_fd_buffer_offset_length_position_callback
await write(fd, serverJsonBuffer, 0, serverJsonBuffer.byteLength)
const fdForClose = fd
// Set fd to null so we only attempt to close it once.
fd = null
await close(fdForClose)
// Intentionally avoid returning control back to the caller until the server
// exits. This defers cleanup operations that should not run before the server
// finishes.
await server.waitForClose
}
interface ServerOptions {
hostname?: string
port?: number
path?: string
}
async function getServerOptions (
connectionInfoDir: string,
opts: {
protocol: 'auto' | 'tcp' | 'ipc'
port?: number
}
): Promise<ServerOptions> {
switch (opts.protocol) {
case 'tcp':
return getTcpOptions()
case 'ipc':
if (isWindows()) {
throw new Error('IPC protocol is not supported on Windows currently')
}
return getIpcOptions()
case 'auto':
if (isWindows()) {
return getTcpOptions()
}
return getIpcOptions()
default:
throw new Error(`Protocol ${opts.protocol as string} is not supported`)
}
async function getTcpOptions () {
return {
hostname: 'localhost',
port: opts.port || await getPort({ port: 5813 }), // eslint-disable-line
}
}
function getIpcOptions (): ServerOptions {
return {
path: path.join(connectionInfoDir, 'socket'),
}
}
}