fix(windows): restore a CIM fallback for relay hosts with no native binding (#16550)

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
Jinjing
2026-08-25 23:15:37 -07:00
committed by GitHub
co-authored by Neil
parent f72dcb908e
commit e4d95e032d
7 changed files with 415 additions and 3 deletions
@@ -0,0 +1,102 @@
import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { build } from 'esbuild'
import { describe, expect, it } from 'vitest'
import { RELAY_NATIVE_DEPS } from './ssh-relay-deploy'
/**
* The relay is a bundle deployed to a host that has none of Orca's node_modules.
* Every native addon it reaches therefore has to be installed by
* `installNativeDeps`, or the relay silently loses that capability forever.
*
* This exists because #15749 moved the Windows process table onto
* `@vscode/windows-process-tree` without adding it here. The relay tests all
* passed: they inject a fake module through
* `__setWindowsProcessTreeLoaderForTests`, so nothing ever exercised the real
* require that the remote host would fail. Assert against the real graph.
*/
const REPO_ROOT = resolve(import.meta.dirname, '..', '..', '..')
/**
* Native addons the relay imports but deliberately does NOT install, each with
* the reason its absence is safe. Adding an entry is a decision, not a default:
* anything not listed must appear in RELAY_NATIVE_DEPS.
*/
const DEGRADES_WITHOUT_INSTALL: Record<string, string> = {
'@vscode/windows-process-tree':
'Windows-only and ships no prebuilds, so installing it would put a from-source ' +
'node-gyp build (MSVC + SDK + Spectre-mitigated libs) on the critical path of ' +
'every Windows relay deploy — and pnpm patches do not cross SSH, so the remote ' +
'would get the unpatched 1024-process cap anyway. windows-process-table.ts ' +
'falls back to a Get-CimInstance scan when the binding is absent.'
}
/** Addons are the packages npm has to build or unpack a binary for. */
function nativeDependencyNames(): string[] {
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as {
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
}
const declared = new Set([
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.optionalDependencies ?? {})
])
return [...declared].filter((name) => {
const dir = join(REPO_ROOT, 'node_modules', name)
return existsSync(join(dir, 'binding.gyp')) || existsSync(join(dir, 'prebuilds'))
})
}
/**
* Source files reachable from the relay entry.
*
* Why the metafile and not the emitted bundle: the process table resolves its
* addon through `createRequire`, which esbuild cannot see and minification
* rewrites, so the specifier only survives reliably in the sources.
*/
async function relayReachableSources(): Promise<string[]> {
const result = await build({
entryPoints: [join(REPO_ROOT, 'src', 'relay', 'relay.ts')],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
write: false,
metafile: true,
external: ['node-pty', '@parcel/watcher', 'electron'],
define: { 'process.env.NODE_ENV': '"production"' }
})
return Object.keys(result.metafile.inputs).filter((input) => !input.includes('node_modules'))
}
describe('relay native dependency coverage', () => {
it('installs every native addon the relay bundle imports', async () => {
const sources = await relayReachableSources()
const text = sources.map((file) => readFileSync(join(REPO_ROOT, file), 'utf8')).join('\n')
const imported = nativeDependencyNames().filter(
(name) => text.includes(`'${name}'`) || text.includes(`"${name}"`)
)
expect(imported.length).toBeGreaterThan(0)
const uncovered = imported.filter(
(name) => !(name in RELAY_NATIVE_DEPS) && !(name in DEGRADES_WITHOUT_INSTALL)
)
expect(uncovered).toEqual([])
}, 60_000)
it('keeps every installed native dep at the version the app depends on', () => {
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as {
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
}
const declared = { ...pkg.dependencies, ...pkg.optionalDependencies }
for (const [name, version] of Object.entries(RELAY_NATIVE_DEPS)) {
// The relay pins exact versions where the app carries a range, so compare
// the base: a relay on a different version than the app marshals the same
// node-pty/watcher data through a binding nothing else cross-checks.
expect(declared[name]?.replace(/^[\^~]/, ''), `${name} matches the app`).toBe(version)
}
})
})
+4 -1
View File
@@ -697,7 +697,10 @@ function uploadStageNamespaceIfSupported(
const NODE_PTY_VERSION = '1.1.0'
const NODE_PTY_CONSOLE_LIST_PATCH_FILENAME = 'node-pty-1.1.0-console-list-agent-patch.cjs'
const RELAY_NATIVE_DEPS = {
// Exported for the relay-native-dependency-coverage test, which asserts every
// native addon the relay bundle imports is either installed here or explicitly
// declared as degrading without it.
export const RELAY_NATIVE_DEPS = {
'node-pty': NODE_PTY_VERSION,
'@parcel/watcher': '2.5.6'
} as const