Files
Xen-Orchestra-xen-orchestra…/@xen-orchestra/vmware-explorer/_nbdkit.mjs
Florent BEAUCHAMP 3bf50c38ee fix(vmware-explorer): fixes omnibus (#10363)
This branch is a reliability overhaul of the ESXi/vSphere source side of V2V, plus a scope reduction of the package. The SOAP plumbing is rewritten:

    VimClient wraps @vates/node-vsphere-soap in a promise API with one emitter per call (no more cross-talk between concurrent calls, no listener leak) and a pTimeout;
    vim25 faults are now parsed (_parseFault.mjs) so callers get a real fault type instead of task execution failed;
    the library no longer throws from async callbacks (which killed xo-server) and its beforeExit logout hook is per-client instead of removeAllListeners.
    Request specs are built in explicit schema order (soap/specs.mjs) with xsi:type, and property values are normalized (soap/normalize.mjs) so search() and the new #retrieveProperty share one shape.
    search() now paginates, cancels the retrieval and destroys the container view.
    Datastore HTTP downloads get session-cookie reuse, retry with jittered backoff, a headers-only timeout, and range-honouring validation.
    Task waiting polls with backoff and per-operation timeouts (6 h for snapshot consolidation).
    nbdkit servers are memoized by a key that includes the export options, the readiness wait races the process death instead of sleeping 2 s
    the password file is 0600 and unlinked
    the thumbprint is read over TLS in-process instead of two openssl spawns.
    The old VHD-over-datastore readers (VhdEsxi*, Datastore*, openDeltaVmdkAsVhd) are deleted, hence the majo, rleaving the vddk/NBD path with a COWD grain-directory parser as data-map fallback.
    ~1.7k lines of unit tests are added.
2026-09-11 17:36:38 +02:00

110 lines
3.0 KiB
JavaScript

import { connect, createServer } from 'node:net'
import { setTimeout as delay } from 'node:timers/promises'
/**
* Command line of an nbdkit server exporting one disk of a VM through the vddk plugin.
*
* @param {object} params
* @returns {Array<string>}
*/
export function formatNbdkitArgs({
compression,
diskPath,
host,
libdir,
passFile,
port,
singleLink,
threads,
thumbprint,
user,
vmId,
}) {
return [
'-r', // readonly
'-v',
'-f',
'--exit-with-parent', // implies -f , ensure we don't leave orphans
`--threads=${threads}`,
`--port=${port}`,
'vddk', // the vddk plugin
`compression=${compression}`,
`thumbprint=${thumbprint}`,
`server=${host}`,
`user=${user}`,
`password=+${passFile}`,
`libdir=${libdir}`,
`vm=moref=${vmId}`,
// `single-link` restricts the export to the top delta. An empty argument used to be passed
// when it is disabled: nbdkit tolerates it, but it sits in the slot of the magic positional
// parameter of the plugin, right before the disk to export
...(singleLink ? ['single-link=true'] : []),
diskPath,
]
}
/**
* Reserves a port by letting the system pick a free one.
*
* The port is released before being handed out, so the caller must be able to cope with a
* concurrent process taking it in the meantime.
*
* @returns {Promise<number>}
*/
export function findFreePort() {
return new Promise((resolve, reject) => {
const server = createServer()
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
const { port } = server.address()
server.close(error => (error != null ? reject(error) : resolve(port)))
})
})
}
const tryConnect = (port, host) =>
new Promise((resolve, reject) => {
const socket = connect({ port, host })
socket.once('connect', () => {
socket.destroy()
resolve()
})
socket.once('error', error => {
socket.destroy()
reject(error)
})
})
/**
* Waits until something listens on a port.
*
* nbdkit binds its port once its plugin is configured, so this is what tells that the server is
* usable — the previous implementation waited a fixed two seconds, which was both too long for a
* fast host and too short for a slow one.
*
* @param {number} port
* @param {object} [options]
* @param {string} [options.host]
* @param {number} [options.probeDelay] - in ms, between two attempts
* @param {AbortSignal} [options.signal]
* @param {number} [options.timeout] - in ms
* @returns {Promise<void>}
*/
export async function waitForPort(port, { host = '127.0.0.1', probeDelay = 50, signal, timeout = 30e3 } = {}) {
const start = Date.now()
for (;;) {
signal?.throwIfAborted()
try {
return await tryConnect(port, host)
} catch (error) {
if (Date.now() - start >= timeout) {
const wrapped = new Error(`nothing is listening on ${host}:${port} after ${timeout}ms`)
wrapped.code = 'NBDKIT_NOT_LISTENING'
wrapped.cause = error
throw wrapped
}
}
await delay(probeDelay, undefined, { signal })
}
}