Files
orca/config/scripts/native-build/windows/build-windows-process-tree-addon.mjs
Neil 9bd6272fdf refactor(tooling): organize build, CI, and test owners (#38130)
* refactor(tooling): organize test runtime and maintenance owners

* test(tooling): preserve relocated runtime fixture dependency edges

* refactor(tooling): preserve xterm routing owner checks

* ci(tooling): include routing owner in sparse checkout

* test(tooling): preserve copied workflow dependency closures

* fix(tooling): normalize hashed JSON assets in release parity

* fix(tooling): distinguish asset aliases from literal references

* fix(tooling): frame normalized asset reference payloads

* fix(tooling): normalize decoded JSON asset references

* refactor(tooling): organize build, CI and release owners

* fix(tooling): align moved generator and workflow path contracts

* fix(tooling): update generated plugin bundle owner banners

* test(tooling): create relocated mobile verifier fixture directories

* test(tooling): copy headless detector dependency closure

* test(tooling): align native batch policy fixtures with owner path

* fix(tooling): link moved reliability command to historical evidence

* fix(tooling): preserve nested sandbox owners and canonical guide source

* fix(tooling): target release version owners in checkout fixture
2026-11-04 21:08:19 -08:00

496 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Compile patched N-API addon with Node headers for compiler-free relay hosts; arm64 cross-builds from Windows x64.
// Published binaries predate the >1024-process fix, while remote npm rebuild needs Spectre libraries; ship patched output instead.
// Usage: 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 '../node-addons/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']
// Shared pnpm store belongs to every worktree; repair/build a private source copy.
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)
}
}
// Verify all patch hunks: Spectre settings, process-cap removal and nested-layout node_addon_api.gyp paths; partial patches can build and silently truncate enumeration.
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}`
)
})
// Check each repair independently; enum-only success must not hide missing buildNode splat.
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
}
// Repair pnpm’s potentially unpatched CRLF source before node-gyp.
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 && /, '')
// Memory/CPU queries need only QUERY_LIMITED_INFORMATION; unnecessary VM_READ raises 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 })
}
}