mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-29 12:31:52 -04:00
172 lines
5.5 KiB
TypeScript
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'),
|
|
}
|
|
}
|
|
}
|