Files
orca/config/scripts/build-windows-process-tree-addon.mjs
T
Neil 4fc1855bda build: share one pnpm virtual store across worktrees (off in CI and on Windows) (#30254)
* build: share one pnpm virtual store across worktrees, off under CI

Each worktree carried a full node_modules; the global virtual store links
one slot per package version instead. pnpm keeps an explicit setting on
under CI, so a pnpmfile hook turns it off there: CI packages from,
caches and verifies a project-local node_modules.

* fix(build): keep shared electron and windows-process-tree dirs untouched under GVS

Every worktree runs Electron from the one shared electron package, so the
per-repo dist cache must not swap that dist and a foreign-arch install must
not land in it. The relay addon builder now repairs and compiles a private
copy instead of the shared windows-process-tree dir.

* fix(build): accept CLI runtime imports reached through an in-artifact store link

Under the global virtual store node_modules/<pkg> realpaths into a store
outside the repo. The check still rejects anything not reached through a
node_modules link inside the artifact, such as a stray ~/node_modules.

* docs: explain the global virtual store layout and the ~/node_modules hazard

* test: compare CLI closure paths with POSIX separators so the suite passes on Windows

* build: keep Windows on the normal pnpm layout unless ORCA_PNPM_GLOBAL_VIRTUAL_STORE=1

Store links are true symlinks on Windows, and installs without admin or
Developer Mode are unverified; CI still forces the normal layout first.

* fix(build): close the remaining shared-slot gaps under the global virtual store

- verify-skills-cli-runtime resolves each import from the importer's realpath,
  as Node does, so an artifact-local copy can't mask a ~/node_modules load.
- A concurrent sibling install of the shared Electron dist is kept, not swapped out.
- rebuild:node goes through ensure-native-runtime, which builds a shared
  node-pty beside the slot instead of node-gyp cleaning its build/ dir.

* fix(build): keep pnpm rebuild:node a forced rebuild

It replaced pnpm rebuild node-pty, which always rebuilt; without --force it
only rebuilt a node-pty that failed to load.

* docs: note that the store switches must stay set, since pnpm run relinks

* build: move the pnpmfile under config/ to keep the repo root allowlisted

pnpm-workspace.yaml points pnpm at it; the lockfile checksum is unchanged
because the content is.

* fix(build): enable the global virtual store from the pnpmfile, not the workspace file

pnpm exec derives NODE_PATH from the file setting, so even with the store
forced off under CI every child process got the root node_modules on its
NODE_PATH; a Windows rebuild fixture then resolved the real
@orca/windows-registry it deliberately omits. The hook now sets the store
both ways, and ORCA_PNPM_GLOBAL_VIRTUAL_STORE=0/1 is the one switch.

* test(pty): keep F3's concurrent spawns on the node-pty mock

Two concurrent first import('node-pty') calls race vitest's mock registry
and the loser loads the real addon under the mocked linux platform. It
passed only where a platform-agnostic build/Release happened to exist,
so it failed on every shared-store worktree.

* ci: fail PRs whose packages need a dependency they never declared

A new dependency_isolation job installs with the global virtual store
forced on (ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI), HOME and the store
in an empty temp dir, then runs pnpm tc and the undeclared-dependency test.
Postinstall now warns when a node_modules above the shared store, such as
~/node_modules, would mask the same class of bug locally.

* ci: set the isolation job's HOME and store through GITHUB_ENV

Job-level env cannot read the runner context, which made pr.yml invalid.
2026-10-18 20:36:24 -07:00

525 lines
22 KiB
JavaScript

#!/usr/bin/env node
/**
* Compile `@vscode/windows-process-tree` for a relay host.
*
* The relay is deployed to machines with no compiler, and this addon cannot be
* npm-installed there: it carries a binding.gyp, so npm rebuilds from source and
* the build wants Spectre-mitigated libraries even where MSVC is present. The
* binary inside the published tarball loads, but predates our patch and still
* caps enumeration at 1024 processes -- a busy host then gets a truncated table
* missing its own pid, which reads as "unavailable" only under load.
*
* So we compile it here, from the patched source pnpm already materialized, and
* ship the result as a relay artifact. Windows arm64 cross-compiles from an x64
* runner, so both arches come off one Windows job.
*
* Node headers, not Electron: the relay runs under the host's own `node`. The
* addon is N-API, so one build serves every Node the remote might have.
*
* node config/scripts/build-windows-process-tree-addon.mjs --arch=arm64
*/
import { execFileSync } from 'node:child_process'
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join, relative, resolve } from 'node:path'
import { isSharedPackageDir } from './native-build-entry.cjs'
import { canReusePreparedAddon } from './windows-process-tree-prepared-addon.mjs'
import { ORCAD_WINDOWS_PROCESS_TREE_FILENAME } from '../../src/shared/orcad-artifacts.ts'
import {
ensureWindowsProcessTreeCommandLinePatch,
inspectWindowsProcessTreeAddon,
nodeGypRebuildInvocation,
stageWindowsProcessTreeNodeAddonApiHeaders,
windowsProcessTreeAddonHasBreakawayLauncher,
WINDOWS_PROCESS_TREE_PACKAGE_DIR as INSTALLED_PACKAGE_DIR
} from './windows-process-tree-gyp-rebuild.mjs'
const { PE_MACHINE, describePeMachine, readPeMachine } = createRequire(import.meta.url)(
'./windows-pe-machine.cjs'
)
const ROOT = resolve(import.meta.dirname, '..', '..')
const SUPPORTED_ARCHES = ['x64', 'arm64']
// Why: under pnpm's global virtual store the installed dir is every worktree's, so the source
// repairs and the build below run in a private copy instead.
const SCRATCH_DIR =
existsSync(INSTALLED_PACKAGE_DIR) && isSharedPackageDir(INSTALLED_PACKAGE_DIR, ROOT)
? mkdtempSync(join(tmpdir(), 'orca-windows-process-tree-'))
: null
const PACKAGE_DIR = SCRATCH_DIR === null ? INSTALLED_PACKAGE_DIR : join(SCRATCH_DIR, 'package')
if (SCRATCH_DIR !== null) {
const physicalDir = realpathSync(INSTALLED_PACKAGE_DIR)
cpSync(physicalDir, PACKAGE_DIR, {
recursive: true,
dereference: true,
filter: (path) => relative(physicalDir, path) !== 'build'
})
}
function parseArgs(argv) {
const arch = argv.find((a) => a.startsWith('--arch='))?.slice('--arch='.length) ?? process.arch
const outDir = argv.find((a) => a.startsWith('--out='))?.slice('--out='.length)
if (!SUPPORTED_ARCHES.includes(arch)) {
throw new Error(`--arch must be one of ${SUPPORTED_ARCHES.join(', ')}; got ${arch}`)
}
return {
arch,
outDir: outDir ? resolve(outDir) : join(ROOT, '.build', 'windows-process-tree', arch)
}
}
/**
* Refuse to build unpatched source.
*
* Each hunk fails differently: Spectre dies outright, the 1024-process cap
* succeeds and lies, and `.targets` is cwd-relative so pnpm's nested layout
* makes node-gyp miss node_addon_api.gyp on Windows. Checking the source
* rather than trusting the install is what stops a silently unpatched tree
* from being shipped as if it were patched.
*/
function assertPatchApplied() {
const bindingGyp = readFileSync(join(PACKAGE_DIR, 'binding.gyp'), 'utf8')
if (bindingGyp.includes('SpectreMitigation')) {
throw new Error(
'binding.gyp still requests SpectreMitigation. pnpm did not apply ' +
'config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
if (bindingGyp.includes('node_addon_api.gyp')) {
throw new Error(
'binding.gyp still depends on node_addon_api.gyp. pnpm and node-gyp rewrite that ' +
'project path incorrectly on Windows. ' +
'pnpm did not apply config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
if (!bindingGyp.includes('"include_dirs": ["deps/node-addon-api"]')) {
throw new Error('binding.gyp does not use the staged node-addon-api headers.')
}
const processCc = readFileSync(join(PACKAGE_DIR, 'src', 'process.cc'), 'utf8')
if (processCc.includes('process_count < 1024')) {
throw new Error(
'src/process.cc still caps enumeration at 1024 processes. pnpm did not apply ' +
'config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
if (processCc.includes('OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ')) {
throw new Error(
'src/process.cc still takes PROCESS_VM_READ for memory or CPU counters it never reads ' +
'from the address space. pnpm did not apply ' +
'config/patches/@vscode__windows-process-tree@0.8.0.patch; run pnpm install.'
)
}
// Every string the repair below can write, so a repaired tree cannot be
// declared patched while one of the pieces is silently missing.
const requiredCreationTimeSources = [
['src/process.h', 'CREATIONTIME = 4'],
['src/process.h', 'ULONGLONG creationTimeMs'],
['src/process.cc', 'GetProcessCreationTime(pinfo)'],
['src/process.cc', 'GetProcessTimes(hProcess, &creationTime'],
['src/process_worker.cc', 'object.Set("creationTimeMs"'],
['src/addon.cc', 'exports.Set("supportedProcessDataFlags"'],
['src/addon.cc', 'exports.Set("getProcessCreationTime"'],
['lib/index.js', '["CreationTime"] = 4'],
['lib/index.js', 'exports.supportedProcessDataFlags'],
['lib/index.js', 'exports.getProcessCreationTime'],
['lib/index.js', 'creationTimeMs,'],
['lib/index.ts', 'CreationTime = 4'],
['lib/index.ts', 'export const supportedProcessDataFlags'],
['lib/index.ts', 'export const getProcessCreationTime'],
['lib/index.ts', 'creationTimeMs,'],
['typings/windows-process-tree.d.ts', 'creationTimeMs?: number'],
// A regex because IProcessInfo declares the same field: only the tree node
// is followed by `children`, and that is the one buildNode fills.
['typings/windows-process-tree.d.ts', /creationTimeMs\?: number;\r?\n\s*children:/],
['typings/windows-process-tree.d.ts', 'export const supportedProcessDataFlags'],
['typings/windows-process-tree.d.ts', 'export const getProcessCreationTime']
]
for (const [relativePath, expected] of requiredCreationTimeSources) {
const source = readFileSync(join(PACKAGE_DIR, relativePath), 'utf8')
const present = typeof expected === 'string' ? source.includes(expected) : expected.test(source)
if (!present) {
throw new Error(
`${relativePath} does not contain the process creation-time patch (${expected}). ` +
'Run pnpm install before building the relay addon.'
)
}
}
// Without the launcher a standard-user SSH host cannot start a relay that outlives the session.
const requiredLauncherSources = [
['binding.gyp', '"src/process_launch.cc"'],
['src/addon.cc', 'exports.Set("spawnOutsideJob"'],
['src/process_launch.cc', 'CREATE_BREAKAWAY_FROM_JOB']
]
for (const [relativePath, expected] of requiredLauncherSources) {
const filePath = join(PACKAGE_DIR, relativePath)
if (!existsSync(filePath) || !readFileSync(filePath, 'utf8').includes(expected)) {
throw new Error(
`${relativePath} does not contain the relay launcher patch (${expected}). ` +
'Run pnpm install before building the relay addon.'
)
}
}
}
function repairCreationTimeSources() {
let repaired = false
const rewrite = (relativePath, transform) => {
const filePath = join(PACKAGE_DIR, relativePath)
const source = readFileSync(filePath, 'utf8')
const next = transform(source, source.includes('\r\n') ? '\r\n' : '\n')
if (next !== source) {
writeFileSync(filePath, next)
repaired = true
}
}
rewrite('src/process.h', (source, eol) => {
let next = source
if (!next.includes('ULONGLONG creationTimeMs')) {
next = next.replace(
/ std::string commandLine;\r?\n/,
` std::string commandLine;${eol} ULONGLONG creationTimeMs;${eol}`
)
}
if (!next.includes('CREATIONTIME = 4')) {
next = next.replace(
/ COMMANDLINE = 2\r?\n/,
` COMMANDLINE = 2,${eol} CREATIONTIME = 4${eol}`
)
}
if (!next.includes('void GetProcessCreationTime')) {
next = next.replace(
/void GetProcessMemoryUsage\(ProcessInfo& process_info\);\r?\n/,
`void GetProcessMemoryUsage(ProcessInfo& process_info);${eol}${eol}` +
`void GetProcessCreationTime(ProcessInfo& process_info);${eol}`
)
}
return next
})
rewrite('src/process.cc', (source, eol) => {
let next = source.replace('ProcessInfo pinfo;', 'ProcessInfo pinfo{};')
if (!next.includes('GetProcessCreationTime(pinfo)')) {
next = next.replace(
/( if \(COMMANDLINE & process_data_flags\) \{\r?\n GetProcessCommandLine\(pinfo\);\r?\n \})/,
`$1${eol}${eol} if (CREATIONTIME & process_data_flags) {${eol}` +
` GetProcessCreationTime(pinfo);${eol} }`
)
}
if (!next.includes('void GetProcessCreationTime(ProcessInfo& process_info) {')) {
const producer = [
'void GetProcessCreationTime(ProcessInfo& process_info) {',
' HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_info.pid);',
' if (hProcess == NULL) {',
' return;',
' }',
'',
' FILETIME creationTime, exitTime, kernelTime, userTime;',
' if (GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime)) {',
' ULARGE_INTEGER timestamp;',
' timestamp.LowPart = creationTime.dwLowDateTime;',
' timestamp.HighPart = creationTime.dwHighDateTime;',
' constexpr ULONGLONG WINDOWS_EPOCH_OFFSET_100NS = 116444736000000000ULL;',
' constexpr ULONGLONG HUNDRED_NS_PER_MILLISECOND = 10000ULL;',
' if (timestamp.QuadPart >= WINDOWS_EPOCH_OFFSET_100NS) {',
' process_info.creationTimeMs =',
' (timestamp.QuadPart - WINDOWS_EPOCH_OFFSET_100NS) / HUNDRED_NS_PER_MILLISECOND;',
' }',
' }',
'',
' CloseHandle(hProcess);',
'}',
''
].join(eol)
next = next.replace(
'void GetProcessMemoryUsage',
`${producer}${eol}void GetProcessMemoryUsage`
)
}
return next
})
rewrite('src/process_worker.cc', (source, eol) => {
if (source.includes('object.Set("creationTimeMs"')) {
return source
}
const emission = [
' if ((CREATIONTIME & process_data_flags_) && pinfo.creationTimeMs != 0) {',
' object.Set("creationTimeMs",',
' Napi::Number::New(env, static_cast<double>(pinfo.creationTimeMs)));',
' }',
''
].join(eol)
return source.replace(
' result.Set(i, object);',
`${emission}${eol} result.Set(i, object);`
)
})
rewrite('src/addon.cc', (source, eol) => {
let next = source
if (!next.includes('Napi::Value ReadProcessCreationTime(')) {
const getter = [
'Napi::Value ReadProcessCreationTime(const Napi::CallbackInfo& args) {',
' Napi::Env env(args.Env());',
' if (args.Length() != 1 || !args[0].IsNumber()) {',
' return env.Undefined();',
' }',
' const double pid = args[0].As<Napi::Number>().DoubleValue();',
' if (!(pid >= 1 && pid <= MAXDWORD) || pid != static_cast<DWORD>(pid)) {',
' return env.Undefined();',
' }',
' ProcessInfo pinfo{};',
' pinfo.pid = static_cast<DWORD>(pid);',
' GetProcessCreationTime(pinfo);',
' if (pinfo.creationTimeMs == 0) {',
' return env.Undefined();',
' }',
' return Napi::Number::New(env, static_cast<double>(pinfo.creationTimeMs));',
'}',
''
].join(eol)
next = next.replace('Napi::Object Init(', `${getter}${eol}Napi::Object Init(`)
}
if (!next.includes('exports.Set("getProcessCreationTime"')) {
next = next.replace(
' exports.Set("getProcessList",',
` exports.Set("getProcessCreationTime", Napi::Function::New(env, ReadProcessCreationTime));${eol}` +
' exports.Set("getProcessList",'
)
}
if (next.includes('exports.Set("supportedProcessDataFlags"')) {
return next
}
return next.replace(
/( exports\.Set\("getProcessCpuUsage", Napi::Function::New\(env, GetProcessCpuUsage\)\);\r?\n)/,
`$1 exports.Set("supportedProcessDataFlags",${eol}` +
` Napi::Number::New(env, MEMORY | COMMANDLINE | CREATIONTIME));${eol}`
)
})
// Each piece is guarded on its own: an early-out on the enum alone would let a
// tree with the enum but no buildNode splat pass as repaired.
const NATIVE_CONST =
"const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined;"
for (const relativePath of ['lib/index.ts', 'lib/index.js']) {
const isTs = relativePath.endsWith('.ts')
rewrite(relativePath, (source, eol) => {
let next = source
if (!next.includes('CreationTime')) {
next = isTs
? next.replace(' CommandLine = 2', ` CommandLine = 2,${eol} CreationTime = 4`)
: next.replace(
' ProcessDataFlag[ProcessDataFlag["CommandLine"] = 2] = "CommandLine";',
' ProcessDataFlag[ProcessDataFlag["CommandLine"] = 2] = "CommandLine";' +
`${eol} ProcessDataFlag[ProcessDataFlag["CreationTime"] = 4] = "CreationTime";`
)
}
if (!next.includes('supportedProcessDataFlags')) {
const reExport = isTs
? `/** The flag bits this compiled addon reports; undefined off win32. */${eol}` +
'export const supportedProcessDataFlags: number | undefined = native?.supportedProcessDataFlags;'
: 'exports.supportedProcessDataFlags = native === undefined ? undefined : native.supportedProcessDataFlags;'
next = next.replace(NATIVE_CONST, `${NATIVE_CONST}${eol}${reExport}`)
}
if (!next.includes('getProcessCreationTime')) {
const reExport = isTs
? 'export const getProcessCreationTime: ((pid: number) => number | undefined) | undefined = native?.getProcessCreationTime;'
: 'exports.getProcessCreationTime = native === undefined ? undefined : native.getProcessCreationTime;'
next = next.replace(NATIVE_CONST, `${NATIVE_CONST}${eol}${reExport}`)
}
// buildNode drops any field it does not name, so the destructure and the
// splat have to move together.
next = next.replace(/(memory, commandLine)( \}, children \})/, '$1, creationTimeMs$2')
if (!/\bcreationTimeMs,/.test(next)) {
next = next.replace(
/(\r?\n)(\s*)commandLine,(\r?\n\s*children:)/,
`$1$2commandLine,$1$2creationTimeMs,$3`
)
}
return next
})
}
rewrite('typings/windows-process-tree.d.ts', (source, eol) => {
let next = source
if (!next.includes('CreationTime = 4')) {
next = next.replace(' CommandLine = 2', ` CommandLine = 2,${eol} CreationTime = 4`)
}
if (!next.includes('supportedProcessDataFlags')) {
next = next.replace(
/( CreationTime = 4\r?\n \}\r?\n)/,
`$1${eol} /** The flag bits the compiled addon reports; undefined off win32. */${eol}` +
` export const supportedProcessDataFlags: number | undefined;${eol}`
)
}
if (!next.includes('export const getProcessCreationTime')) {
next = next.replace(
' export const supportedProcessDataFlags: number | undefined;',
' export const supportedProcessDataFlags: number | undefined;' +
`${eol} export const getProcessCreationTime: ((pid: number) => number | undefined) | undefined;`
)
}
if (!next.includes('creationTimeMs?: number')) {
next = next.replace(
/ commandLine\?: string;\r?\n/,
` commandLine?: string;${eol}${eol}` +
` /** Process creation time in Unix milliseconds. */${eol}` +
` creationTimeMs?: number;${eol}`
)
}
// IProcessTreeNode is the second declaration; only it is followed by children.
next = next.replace(
/( commandLine\?: string;\r?\n)( children:)/,
`$1 creationTimeMs?: number;${eol}$2`
)
return next
})
return repaired
}
// pnpm can materialize this CRLF package without applying its patch. Repair the
// load-bearing build settings before node-gyp so the release build stays safe.
function applyWindowsProcessTreeBuildFixes() {
const bindingPath = join(PACKAGE_DIR, 'binding.gyp')
const processPath = join(PACKAGE_DIR, 'src', 'process.cc')
let bindingGyp = readFileSync(bindingPath, 'utf8')
let processCc = readFileSync(processPath, 'utf8')
const originalBinding = bindingGyp
const originalProcess = processCc
for (const dynamicDependency of [
String.raw`<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except`,
String.raw`<!(node -p \"require.resolve('node-addon-api/node_addon_api.gyp')\"):node_addon_api_except`,
'../../node-addon-api/node_addon_api.gyp:node_addon_api_except'
]) {
bindingGyp = bindingGyp.replace(`"${dynamicDependency}",`, '')
}
bindingGyp = bindingGyp.replace(
'"include_dirs": []',
'"include_dirs": ["deps/node-addon-api"],\n "defines": ["NAPI_CPP_EXCEPTIONS", "_HAS_EXCEPTIONS=1"]'
)
if (!bindingGyp.includes('"ExceptionHandling": 1')) {
bindingGyp = bindingGyp.replace(
'"VCCLCompilerTool": {',
'"VCCLCompilerTool": {\n "ExceptionHandling": 1,'
)
}
bindingGyp = bindingGyp.replace(
/\r?\n\s*"msvs_configuration_attributes": \{\s*"SpectreMitigation": "Spectre"\s*\},?/s,
''
)
processCc = processCc.replace(/process_count < 1024 && /, '')
// The memory and CPU readers only ever call GetProcessMemoryInfo/GetProcessTimes,
// which need no more than PROCESS_QUERY_LIMITED_INFORMATION; taking VM_READ is
// what EDR scores.
processCc = processCc.replaceAll(
'OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)',
'OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)'
)
if (bindingGyp !== originalBinding) {
writeFileSync(bindingPath, bindingGyp)
}
if (processCc !== originalProcess) {
writeFileSync(processPath, processCc)
}
const repairedCreationTime = repairCreationTimeSources()
stageWindowsProcessTreeNodeAddonApiHeaders(PACKAGE_DIR, INSTALLED_PACKAGE_DIR)
const repairedCommandLine = ensureWindowsProcessTreeCommandLinePatch(PACKAGE_DIR)
const repaired =
bindingGyp !== originalBinding ||
processCc !== originalProcess ||
repairedCommandLine ||
repairedCreationTime
if (repaired) {
console.warn('[windows-process-tree] Repaired un-applied pnpm patch hunks before build.')
}
return repaired
}
function main() {
const { arch, outDir } = parseArgs(process.argv.slice(2))
if (process.platform !== 'win32') {
throw new Error(
`This addon only builds on Windows; running on ${process.platform}. ` +
'Relay builds elsewhere simply omit it and fall back to the CIM scan.'
)
}
if (!existsSync(PACKAGE_DIR)) {
throw new Error(`${PACKAGE_DIR} is missing. Run pnpm install first.`)
}
const sourceRepaired = applyWindowsProcessTreeBuildFixes()
assertPatchApplied()
const built = join(PACKAGE_DIR, 'build', 'Release', 'windows_process_tree.node')
if (
canReusePreparedAddon({
enabled: process.argv.includes('--reuse-prepared-runtime'),
arch,
addonPath: built,
sourceRepaired
})
) {
console.log(`[windows-process-tree] reusing the prepared ${arch} addon`)
} else {
const gyp = nodeGypRebuildInvocation(arch, PACKAGE_DIR)
console.log(`[windows-process-tree] building ${arch} from ${gyp.cwd}`)
execFileSync(process.execPath, gyp.args, { cwd: gyp.cwd, stdio: 'inherit' })
}
if (!existsSync(built)) {
throw new Error(`node-gyp reported success but ${built} is missing.`)
}
// Why check the artifact and not only the source: the source checks above run
// before node-gyp, and a stale build directory can outlive them.
if (inspectWindowsProcessTreeAddon(built) === 'unpatched') {
throw new Error(
'The built addon still calls ReadProcessMemory, so it did not come from the patched ' +
'command-line reader. A relay would get the primitive MDE scores as credential dumping.'
)
}
const machine = readPeMachine(built)
if (machine !== PE_MACHINE[arch]) {
const cause =
machine === null
? 'A truncated or quarantined build output looks like this; a relay would get a binary no host can load.'
: 'node-gyp ignored --arch; a relay would get a binary its host cannot load.'
throw new Error(
`Built binary is ${describePeMachine(machine)}, expected 0x${PE_MACHINE[arch].toString(16)} for ${arch}. ${cause}`
)
}
if (!windowsProcessTreeAddonHasBreakawayLauncher(built)) {
throw new Error(
'The built addon does not export spawnOutsideJob. A relay would fall back to WMI, ' +
'which refuses to launch it for a standard user.'
)
}
mkdirSync(outDir, { recursive: true })
const staged = join(outDir, ORCAD_WINDOWS_PROCESS_TREE_FILENAME)
copyFileSync(built, staged)
console.log(`[windows-process-tree] ${arch} -> ${staged}`)
}
try {
main()
} catch (error) {
console.error(`[windows-process-tree] ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
} finally {
if (SCRATCH_DIR !== null) {
rmSync(SCRATCH_DIR, { recursive: true, force: true })
}
}