mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
fix(windows): make the compiled addon prove its own CreationTime support
CI caught the real defect: the win32 guard test read
isWindowsProcessStartTimeAvailable() as true and then found 0 rows
carrying creationTimeMs. Unlike node-pty, this package publishes a
prebuilt .node at the same build/Release path node-gyp writes to, so
pnpm patches the source tree and leaves that binary alone. A host then
holds a patched lib/index.js -- ProcessDataFlag.CreationTime and all --
over a binary that ignores flag 4, and neither a load check nor a path
check can see the difference.
So the binary now says so itself: addon.cc exports
supportedProcessDataFlags, lib/index.js re-exports it, and
- windows-process-tree-creation-time.cjs asserts it during install,
which is what forces a from-source rebuild. It is shared by the Node
probe in ensure-native-runtime.mjs and the Electron probe in
rebuild-native-deps.mjs, exactly as node-pty-job-ownership.cjs is --
the Electron half matters because that probe decides onlyModules, so
without it the packaged app would ship the stale prebuilt.
- isWindowsProcessStartTimeAvailable() gates on the reported bit, not
the enum. Believing the enum is worse than reporting false: the
descendant snapshot returns null forever and the exit proof latches
unverifiable while structured chat believes it has a reaper.
rebuildNodeRuntimeModules could not actually have rebuilt this package:
the patched binding.gyp includes deps/node-addon-api, which the tarball
does not ship, and node-gyp must run from the physical dir.
Also closes the relay repair path's divergence: repairCreationTimeSources
wrote the C++ but not the buildNode splat or the tree-node typing, and
assertPatchApplied checked neither, so a repaired tree passed as patched
with buildProcessTree silently dropping the field.
The guard test is unchanged.
This commit is contained in:
@@ -1,28 +1,30 @@
|
||||
diff --git a/binding.gyp b/binding.gyp
|
||||
index 855bd4b86f0a3c18c7594212c0e42b6e35bc4001..33774e7ae296f0de39dd94156673c9e773638bf4 100644
|
||||
index 855bd4b86f0a3c18c7594212c0e42b6e35bc4001..0bb2af7923b6e6f1f0da40cae8067304cd1fea14 100644
|
||||
--- a/binding.gyp
|
||||
+++ b/binding.gyp
|
||||
@@ -3,7 +3,6 @@
|
||||
{
|
||||
"target_name": "windows_process_tree",
|
||||
"dependencies": [
|
||||
- "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
|
||||
],
|
||||
"conditions": [
|
||||
['OS=="win"', {
|
||||
@@ -15,12 +14,11 @@
|
||||
"src/process_commandline.cc"
|
||||
],
|
||||
- "include_dirs": [],
|
||||
{
|
||||
"target_name": "windows_process_tree",
|
||||
"dependencies": [
|
||||
- "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
|
||||
],
|
||||
"conditions": [
|
||||
['OS=="win"', {
|
||||
@@ -14,13 +13,12 @@
|
||||
"src/process_worker.cc",
|
||||
"src/process_commandline.cc"
|
||||
],
|
||||
- "include_dirs": [],
|
||||
+ "include_dirs": ["deps/node-addon-api"],
|
||||
+ "defines": ["NAPI_CPP_EXCEPTIONS", "_HAS_EXCEPTIONS=1"],
|
||||
"libraries": [ 'psapi.lib' ],
|
||||
- "msvs_configuration_attributes": {
|
||||
- "SpectreMitigation": "Spectre"
|
||||
- },
|
||||
"msvs_settings": {
|
||||
"VCCLCompilerTool": {
|
||||
"libraries": [ 'psapi.lib' ],
|
||||
- "msvs_configuration_attributes": {
|
||||
- "SpectreMitigation": "Spectre"
|
||||
- },
|
||||
"msvs_settings": {
|
||||
"VCCLCompilerTool": {
|
||||
+ "ExceptionHandling": 1,
|
||||
<<<<<<< HEAD
|
||||
"AdditionalOptions": [
|
||||
"/guard:cf",
|
||||
"/sdl",
|
||||
@@ -194,6 +196,128 @@ index 3eea92077c4d1d433119361d5c432881859131e9..738775f6fcdfb676054386fe34c03803
|
||||
+ // kernel keeps, not the address space -- and acquiring it is what EDR scores.
|
||||
+ hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
||||
+
|
||||
=======
|
||||
"AdditionalOptions": [
|
||||
"/guard:cf",
|
||||
"/sdl",
|
||||
diff --git a/lib/index.js b/lib/index.js
|
||||
index 9747a7402600cd252859144d32580ed45c8c93f7..8b1f467f2c18289f75419d30a43d6fa3a516c4cc 100644
|
||||
--- a/lib/index.js
|
||||
+++ b/lib/index.js
|
||||
@@ -7,11 +7,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getAllProcesses = exports.getProcessTree = exports.getProcessCpuUsage = exports.getProcessList = exports.filterProcessList = exports.buildProcessTree = exports.ProcessDataFlag = void 0;
|
||||
const util_1 = require("util");
|
||||
const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined;
|
||||
+exports.supportedProcessDataFlags = native === undefined ? undefined : native.supportedProcessDataFlags;
|
||||
var ProcessDataFlag;
|
||||
(function (ProcessDataFlag) {
|
||||
ProcessDataFlag[ProcessDataFlag["None"] = 0] = "None";
|
||||
ProcessDataFlag[ProcessDataFlag["Memory"] = 1] = "Memory";
|
||||
ProcessDataFlag[ProcessDataFlag["CommandLine"] = 2] = "CommandLine";
|
||||
+ ProcessDataFlag[ProcessDataFlag["CreationTime"] = 4] = "CreationTime";
|
||||
})(ProcessDataFlag = exports.ProcessDataFlag || (exports.ProcessDataFlag = {}));
|
||||
// requestInProgress is used for any function that uses CreateToolhelp32Snapshot, as multiple calls
|
||||
// to this cannot be done at the same time.
|
||||
@@ -66,11 +68,12 @@ function buildProcessTree(rootPid, processList, maxDepth = MAX_FILTER_DEPTH) {
|
||||
// • the properties are inlined/splatted
|
||||
// • the 'ppid' field is omitted
|
||||
// • the depth of the tree is limited by `maxDepth`
|
||||
- const buildNode = ({ info: { pid, name, memory, commandLine }, children }, depth) => ({
|
||||
+ const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }, depth) => ({
|
||||
pid,
|
||||
name,
|
||||
memory,
|
||||
commandLine,
|
||||
+ creationTimeMs,
|
||||
children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [],
|
||||
});
|
||||
return buildNode(root, maxDepth);
|
||||
diff --git a/lib/index.ts b/lib/index.ts
|
||||
index f9aa005d9ced9e42885b8a976de5eb5bd61899ee..270967c1d1192b37cb6b095457bf52b213f88e8d 100644
|
||||
--- a/lib/index.ts
|
||||
+++ b/lib/index.ts
|
||||
@@ -6,12 +6,15 @@
|
||||
import { promisify } from 'util';
|
||||
|
||||
const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined;
|
||||
+/** The flag bits this compiled addon reports; undefined off win32. */
|
||||
+export const supportedProcessDataFlags: number | undefined = native?.supportedProcessDataFlags;
|
||||
import { IProcessInfo, IProcessTreeNode, IProcessCpuInfo } from '@vscode/windows-process-tree';
|
||||
|
||||
export enum ProcessDataFlag {
|
||||
None = 0,
|
||||
Memory = 1,
|
||||
- CommandLine = 2
|
||||
+ CommandLine = 2,
|
||||
+ CreationTime = 4
|
||||
}
|
||||
|
||||
type RequestCallback = (processList: IProcessInfo[]) => void;
|
||||
@@ -81,11 +84,12 @@ export function buildProcessTree(rootPid: number, processList: Iterable<IProcess
|
||||
// • the properties are inlined/splatted
|
||||
// • the 'ppid' field is omitted
|
||||
// • the depth of the tree is limited by `maxDepth`
|
||||
- const buildNode = ({ info: { pid, name, memory, commandLine }, children }: IProcessInfoNode, depth: number): IProcessTreeNode => ({
|
||||
+ const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }: IProcessInfoNode, depth: number): IProcessTreeNode => ({
|
||||
pid,
|
||||
name,
|
||||
memory,
|
||||
commandLine,
|
||||
+ creationTimeMs,
|
||||
children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [],
|
||||
});
|
||||
|
||||
diff --git a/src/addon.cc b/src/addon.cc
|
||||
index 9214aff281251e797a70ecb9f6e0b52932a0503f..722edd42ddb4740296bfc47582a181bd6d00c464 100644
|
||||
--- a/src/addon.cc
|
||||
+++ b/src/addon.cc
|
||||
@@ -53,6 +53,10 @@ void GetProcessCpuUsage(const Napi::CallbackInfo& args) {
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||||
exports.Set("getProcessList", Napi::Function::New(env, GetProcessList));
|
||||
exports.Set("getProcessCpuUsage", Napi::Function::New(env, GetProcessCpuUsage));
|
||||
+ // Lets a caller prove THIS BINARY understands CREATIONTIME. The JS enum is
|
||||
+ // patched source and says nothing about what the .node was compiled from.
|
||||
+ exports.Set("supportedProcessDataFlags",
|
||||
+ Napi::Number::New(env, MEMORY | COMMANDLINE | CREATIONTIME));
|
||||
return exports;
|
||||
}
|
||||
|
||||
diff --git a/src/process.cc b/src/process.cc
|
||||
index 3eea92077c4d1d433119361d5c432881859131e9..c69442e2917b8474dbabaa1aa5b2c5ce746a56e7 100644
|
||||
--- a/src/process.cc
|
||||
+++ b/src/process.cc
|
||||
@@ -21,7 +21,7 @@ uint32_t GetRawProcessList(std::vector<ProcessInfo>& process_info,
|
||||
if (Process32First(snapshot_handle, &process_entry)) {
|
||||
do {
|
||||
if (process_entry.th32ProcessID != 0) {
|
||||
- ProcessInfo pinfo;
|
||||
+ ProcessInfo pinfo{};
|
||||
pinfo.pid = process_entry.th32ProcessID;
|
||||
pinfo.ppid = process_entry.th32ParentProcessID;
|
||||
|
||||
@@ -33,17 +33,43 @@ uint32_t GetRawProcessList(std::vector<ProcessInfo>& process_info,
|
||||
GetProcessCommandLine(pinfo);
|
||||
}
|
||||
|
||||
+ if (CREATIONTIME & process_data_flags) {
|
||||
+ GetProcessCreationTime(pinfo);
|
||||
+ }
|
||||
+
|
||||
strcpy(pinfo.name, process_entry.szExeFile);
|
||||
process_info.push_back(std::move(pinfo));
|
||||
process_count++;
|
||||
}
|
||||
- } while (process_count < 1024 && Process32Next(snapshot_handle, &process_entry));
|
||||
+ } while (Process32Next(snapshot_handle, &process_entry));
|
||||
}
|
||||
|
||||
CloseHandle(snapshot_handle);
|
||||
return process_count;
|
||||
}
|
||||
|
||||
+void GetProcessCreationTime(ProcessInfo& process_info) {
|
||||
+ HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_info.pid);
|
||||
>>>>>>> 41a13099e8 (fix(windows): make the compiled addon prove its own CreationTime support)
|
||||
+ if (hProcess == NULL) {
|
||||
+ return;
|
||||
+ }
|
||||
@@ -205,6 +329,7 @@ index 3eea92077c4d1d433119361d5c432881859131e9..738775f6fcdfb676054386fe34c03803
|
||||
+ CloseHandle(hProcess);
|
||||
+}
|
||||
+
|
||||
<<<<<<< HEAD
|
||||
+// Per documentation, it is not recommended to add or subtract values from the FILETIME
|
||||
+// structure, or to cast it to ULARGE_INTEGER as this can cause alignment faults on 64-bit Windows.
|
||||
+// Copy the high and low part to a ULARGE_INTEGER and peform arithmetic on that instead.
|
||||
@@ -243,11 +368,56 @@ index 3eea92077c4d1d433119361d5c432881859131e9..738775f6fcdfb676054386fe34c03803
|
||||
+ ULONGLONG endSysTime = GetTotalTime(&sysKernelTime, &sysUserTime);
|
||||
+
|
||||
+ cpu_info.cpu = 100.0 * (endProcTime - cpu_info.initialProcRunTime) / (endSysTime - cpu_info.initialSystemTime);
|
||||
=======
|
||||
void GetProcessMemoryUsage(ProcessInfo& process_info) {
|
||||
DWORD pid = process_info.pid;
|
||||
HANDLE hProcess;
|
||||
diff --git a/src/process.h b/src/process.h
|
||||
index 82f8e4bcfa742551e5d874a7632736a7611d7aa7..087bf4ca81768918922a1e26836c70d7aec349ec 100644
|
||||
--- a/src/process.h
|
||||
+++ b/src/process.h
|
||||
@@ -22,18 +22,22 @@ struct ProcessInfo {
|
||||
DWORD ppid;
|
||||
DWORD memory; // Reported in bytes
|
||||
std::string commandLine;
|
||||
+ ULONGLONG creationTimeMs;
|
||||
};
|
||||
|
||||
enum ProcessDataFlags {
|
||||
NONE = 0,
|
||||
MEMORY = 1,
|
||||
- COMMANDLINE = 2
|
||||
+ COMMANDLINE = 2,
|
||||
+ CREATIONTIME = 4
|
||||
};
|
||||
|
||||
uint32_t GetRawProcessList(std::vector<ProcessInfo>& process_info, DWORD flags);
|
||||
|
||||
void GetProcessMemoryUsage(ProcessInfo& process_info);
|
||||
|
||||
+void GetProcessCreationTime(ProcessInfo& process_info);
|
||||
+
|
||||
void GetCpuUsage(Cpu& cpu_info, bool first_run);
|
||||
|
||||
#endif // SRC_PROCESS_H_
|
||||
diff --git a/src/process_worker.cc b/src/process_worker.cc
|
||||
index c9e3457a759c1acaa2644231a4917d45aed951f8..89f3339d5dc3b454397941c958b95965afed5788 100644
|
||||
--- a/src/process_worker.cc
|
||||
+++ b/src/process_worker.cc
|
||||
@@ -43,6 +43,11 @@ void GetProcessesWorker::OnOK() {
|
||||
Napi::String::New(env, pinfo.commandLine));
|
||||
}
|
||||
|
||||
+ if ((CREATIONTIME & process_data_flags_) && pinfo.creationTimeMs != 0) {
|
||||
+ object.Set("creationTimeMs",
|
||||
+ Napi::Number::New(env, static_cast<double>(pinfo.creationTimeMs)));
|
||||
>>>>>>> 41a13099e8 (fix(windows): make the compiled addon prove its own CreationTime support)
|
||||
+ }
|
||||
+ } else {
|
||||
+ cpu_info.cpu = std::numeric_limits<double>::quiet_NaN();
|
||||
+ }
|
||||
+
|
||||
<<<<<<< HEAD
|
||||
+ CloseHandle(hProcess);
|
||||
}
|
||||
\ No newline at end of file
|
||||
@@ -448,3 +618,49 @@ index ea822b120e8038a4803e34647042f08f4aaf5ca1..25907c0bf542bed6c72b1b462b19bcf3
|
||||
+ return StoreCommandLineUtf8(process_info, command_line->Buffer,
|
||||
+ command_line->Length / sizeof(wchar_t));
|
||||
+}
|
||||
=======
|
||||
result.Set(i, object);
|
||||
}
|
||||
|
||||
diff --git a/typings/windows-process-tree.d.ts b/typings/windows-process-tree.d.ts
|
||||
index 08bdac2fdc5ead6f0fcfb5ee5a021e2298c7d523..2c515242d78f9d8b8e63271d86f59d1024d71604 100644
|
||||
--- a/typings/windows-process-tree.d.ts
|
||||
+++ b/typings/windows-process-tree.d.ts
|
||||
@@ -7,9 +7,17 @@ declare module '@vscode/windows-process-tree' {
|
||||
export enum ProcessDataFlag {
|
||||
None = 0,
|
||||
Memory = 1,
|
||||
- CommandLine = 2
|
||||
+ CommandLine = 2,
|
||||
+ CreationTime = 4
|
||||
}
|
||||
|
||||
+ /**
|
||||
+ * The flag bits the compiled addon actually understands, or undefined off
|
||||
+ * win32. `ProcessDataFlag` above is source; this is what the binary reports,
|
||||
+ * so it is the only way to tell a patched build from a stale prebuilt.
|
||||
+ */
|
||||
+ export const supportedProcessDataFlags: number | undefined;
|
||||
+
|
||||
export interface IProcessInfo {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
@@ -24,6 +32,9 @@ declare module '@vscode/windows-process-tree' {
|
||||
* The string returned is at most 512 chars, strings exceeding this length are truncated.
|
||||
*/
|
||||
commandLine?: string;
|
||||
+
|
||||
+ /** Process creation time in Unix milliseconds. */
|
||||
+ creationTimeMs?: number;
|
||||
}
|
||||
|
||||
export interface IProcessCpuInfo extends IProcessInfo {
|
||||
@@ -35,6 +46,7 @@ declare module '@vscode/windows-process-tree' {
|
||||
name: string;
|
||||
memory?: number;
|
||||
commandLine?: string;
|
||||
+ creationTimeMs?: number;
|
||||
children: IProcessTreeNode[];
|
||||
}
|
||||
|
||||
>>>>>>> 41a13099e8 (fix(windows): make the compiled addon prove its own CreationTime support)
|
||||
|
||||
@@ -98,18 +98,31 @@ function assertPatchApplied() {
|
||||
'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"'],
|
||||
['lib/index.js', '["CreationTime"] = 4'],
|
||||
['lib/index.js', 'exports.supportedProcessDataFlags'],
|
||||
['lib/index.js', 'creationTimeMs,'],
|
||||
['lib/index.ts', 'CreationTime = 4'],
|
||||
['typings/windows-process-tree.d.ts', 'creationTimeMs?: number']
|
||||
['lib/index.ts', 'export const supportedProcessDataFlags'],
|
||||
['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']
|
||||
]
|
||||
for (const [relativePath, expected] of requiredCreationTimeSources) {
|
||||
if (!readFileSync(join(PACKAGE_DIR, relativePath), 'utf8').includes(expected)) {
|
||||
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.'
|
||||
@@ -213,18 +226,51 @@ function repairCreationTimeSources() {
|
||||
)
|
||||
})
|
||||
|
||||
rewrite('src/addon.cc', (source, eol) => {
|
||||
if (source.includes('exports.Set("supportedProcessDataFlags"')) {
|
||||
return source
|
||||
}
|
||||
return source.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) => {
|
||||
if (source.includes('CreationTime')) {
|
||||
return source
|
||||
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";`
|
||||
)
|
||||
}
|
||||
return relativePath.endsWith('.ts')
|
||||
? source.replace(' CommandLine = 2', ` CommandLine = 2,${eol} CreationTime = 4`)
|
||||
: source.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}`)
|
||||
}
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,6 +279,13 @@ function repairCreationTimeSources() {
|
||||
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('creationTimeMs?: number')) {
|
||||
next = next.replace(
|
||||
/ commandLine\?: string;\r?\n/,
|
||||
@@ -241,6 +294,11 @@ function repairCreationTimeSources() {
|
||||
` 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
||||
import { release } from 'node:os'
|
||||
import { basename, dirname, resolve } from 'node:path'
|
||||
import {
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { assertNodePtyJobOwnership } = require('./node-pty-job-ownership.cjs')
|
||||
const { assertWindowsProcessTreeCreationTime } = require('./windows-process-tree-creation-time.cjs')
|
||||
const scriptPath = import.meta.filename
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
const runtime = readRuntimeArg()
|
||||
@@ -262,9 +263,10 @@ function loadNativeModule(moduleName) {
|
||||
// A bare require loads the .node addon on win32, so it catches an ABI
|
||||
// mismatch on its own. What it cannot catch is *which* addon loaded: the
|
||||
// published tarball ships a prebuilt built from unpatched source that is
|
||||
// node-addon-api, so it requires cleanly and then reads every process's
|
||||
// command line out of its address space. Check the binary, not the load.
|
||||
require(moduleName)
|
||||
// node-addon-api, so it requires cleanly, reads every process's command
|
||||
// line out of its address space, and ignores the CreationTime flag. Check
|
||||
// the binary on both counts, not the load.
|
||||
assertWindowsProcessTreeCreationTime({ module: require(moduleName) })
|
||||
if (inspectWindowsProcessTreeAddon(windowsProcessTreeAddonPath()) === 'unpatched') {
|
||||
throw new Error(
|
||||
'the loaded addon still calls ReadProcessMemory, so it was not built from the patched ' +
|
||||
@@ -380,14 +382,18 @@ function getWindowsBuildNumber() {
|
||||
|
||||
function rebuildNodeRuntimeModules(moduleNames) {
|
||||
for (const moduleName of moduleNames) {
|
||||
const moduleDir = dirname(require.resolve(`${moduleName}/package.json`))
|
||||
let moduleDir = dirname(require.resolve(`${moduleName}/package.json`))
|
||||
if (moduleName === '@vscode/windows-process-tree') {
|
||||
// Why before node-gyp: this module is rebuilt precisely because the
|
||||
// binary was the unpatched one, and pnpm materializes it unpatched often
|
||||
// enough that compiling the source as-is would just rebuild the same
|
||||
// reader and fail the verify pass.
|
||||
// reader and fail the verify pass. The patched binding.gyp then includes
|
||||
// deps/node-addon-api, which the tarball does not ship, and node-gyp must
|
||||
// run from the physical dir -- both reasons live in
|
||||
// windows-process-tree-gyp-rebuild.mjs.
|
||||
ensureWindowsProcessTreeCommandLinePatch(moduleDir)
|
||||
stageWindowsProcessTreeNodeAddonApiHeaders(moduleDir)
|
||||
moduleDir = realpathSync(moduleDir)
|
||||
}
|
||||
console.warn(`[native-runtime] Rebuilding ${moduleName} with node-gyp.`)
|
||||
runPnpm(['exec', 'node-gyp', 'rebuild'], { cwd: moduleDir })
|
||||
|
||||
@@ -140,6 +140,8 @@ const NATIVE_RUNTIME_PREFIXES = [
|
||||
'config/scripts/ensure-native-runtime',
|
||||
'config/scripts/rebuild-native-deps',
|
||||
'config/scripts/node-pty-job-ownership',
|
||||
'config/scripts/windows-process-tree-creation-time',
|
||||
'config/scripts/windows-process-tree-gyp-rebuild',
|
||||
'config/scripts/electron-builder-native-rebuild',
|
||||
'config/patches/node-pty@',
|
||||
'config/patches/@vscode__windows-process-tree'
|
||||
|
||||
@@ -567,6 +567,15 @@ function loadNativeModule(moduleName) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (moduleName === '@vscode/windows-process-tree') {
|
||||
// The tarball prebuilt loads under Electron too -- the addon is N-API, so
|
||||
// a bare require proves nothing about which source it was built from.
|
||||
const { assertWindowsProcessTreeCreationTime } = projectRequire(
|
||||
'./config/scripts/windows-process-tree-creation-time.cjs'
|
||||
)
|
||||
assertWindowsProcessTreeCreationTime({ module: projectRequire(moduleName) })
|
||||
return
|
||||
}
|
||||
projectRequire(moduleName)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Prove the COMPILED addon understands `CREATIONTIME`, not just the patched JS.
|
||||
*
|
||||
* Unlike node-pty, this package ships a prebuilt `.node` at the same
|
||||
* `build/Release/` path node-gyp writes to, so neither a load nor a path check
|
||||
* can tell a stale prebuilt from a source build. pnpm patches the source tree
|
||||
* and leaves that prebuilt in place, which is how `ProcessDataFlag.CreationTime`
|
||||
* came to exist in `lib/index.js` on a binary that ignores flag 4 -- the gate
|
||||
* read true and every row came back without `creationTimeMs`.
|
||||
*
|
||||
* `supportedProcessDataFlags` is exported by the patched `addon.cc`, so its
|
||||
* presence is the binary's own answer. Shared by the Node and Electron probes
|
||||
* the way `node-pty-job-ownership.cjs` is.
|
||||
*/
|
||||
|
||||
/** `ProcessDataFlags::CREATIONTIME` in src/process.h. */
|
||||
const CREATION_TIME_FLAG = 4
|
||||
|
||||
function assertWindowsProcessTreeCreationTime({ module, platform = process.platform }) {
|
||||
if (platform !== 'win32') {
|
||||
return
|
||||
}
|
||||
const supported = module?.supportedProcessDataFlags
|
||||
if (typeof supported === 'number' && (supported & CREATION_TIME_FLAG) !== 0) {
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
[
|
||||
'@vscode/windows-process-tree does not report CreationTime support',
|
||||
`(supportedProcessDataFlags=${String(supported)}).`,
|
||||
'That is the tarball prebuilt, not a build of the patched source, so every',
|
||||
'process row comes back without creationTimeMs: Windows descendant exit',
|
||||
'verification cannot identify a PID and structured Claude/Codex chat runs',
|
||||
'with an unprovable child-tree reaper.',
|
||||
'Rebuild it from source so config/patches/@vscode__windows-process-tree@0.8.0.patch applies.'
|
||||
].join(' ')
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = { assertWindowsProcessTreeCreationTime, CREATION_TIME_FLAG }
|
||||
@@ -368,6 +368,24 @@ on any other OS keeps using the scan.
|
||||
to Unix ms; a process that denies the handle is emitted with the field
|
||||
absent, never zero, because callers must be able to tell "cannot identify"
|
||||
from a timestamp.
|
||||
5. **`supportedProcessDataFlags`.** `addon.cc` exports the flag bits the
|
||||
compiled binary understands, and `lib/index.js` re-exports it.
|
||||
|
||||
Why a fifth hunk and not just the enum: unlike `node-pty`, this package
|
||||
publishes a prebuilt `.node` at the same `build/Release/` path node-gyp
|
||||
writes to. pnpm patches the source tree and leaves that prebuilt alone, so a
|
||||
host can hold a patched `lib/index.js` — `ProcessDataFlag.CreationTime` and
|
||||
all — over a binary that ignores flag 4. CI produced exactly that: the gate
|
||||
read available and every row came back without `creationTimeMs`. Neither a
|
||||
load check nor a path check can see the difference, so the binary has to say
|
||||
so itself.
|
||||
|
||||
Two readers depend on it. `isWindowsProcessStartTimeAvailable()` returns
|
||||
false unless this bit is set, because claiming otherwise leaves
|
||||
`captureWindowsDescendantSnapshot` returning null forever while structured
|
||||
chat believes it has a reaper. And `windows-process-tree-creation-time.cjs`
|
||||
asserts it during install, which is what forces a from-source rebuild —
|
||||
the same role `node-pty-job-ownership.cjs` plays for node-pty's job exports.
|
||||
|
||||
The typings claim `commandLine` is truncated at 512 characters. Measured, it is
|
||||
not: the longest observed on a real host was 26,059.
|
||||
|
||||
@@ -56,8 +56,11 @@ describe('supportsClaudeStructuredLocation', () => {
|
||||
|
||||
it('accepts Windows local locations once creation-time proof is available', () => {
|
||||
previousPlatform = setPlatform('win32')
|
||||
// supportedProcessDataFlags is the addon's own report; the enum alone is
|
||||
// not proof, because pnpm patches the source over the tarball's prebuilt.
|
||||
__setWindowsProcessTreeLoaderForTests(() => ({
|
||||
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 },
|
||||
supportedProcessDataFlags: 7,
|
||||
getAllProcesses: () => undefined
|
||||
}))
|
||||
expect(
|
||||
|
||||
@@ -149,6 +149,7 @@ describe('windows process table', () => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||||
__setWindowsProcessTreeLoaderForTests(() => ({
|
||||
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 },
|
||||
supportedProcessDataFlags: 7,
|
||||
getAllProcesses
|
||||
}))
|
||||
})
|
||||
@@ -327,10 +328,23 @@ describe('windows process table', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('only advertises PID-safe ownership when the native creation-time field exists', () => {
|
||||
it('only advertises PID-safe ownership when the BINARY reports creation-time support', () => {
|
||||
expect(isWindowsProcessStartTimeAvailable()).toBe(true)
|
||||
|
||||
// The shape CI produced: pnpm patched the source tree, so the enum carries
|
||||
// CreationTime, while the tarball's prebuilt .node still ignores flag 4.
|
||||
// Believing the enum here is what let structured chat run with a reaper
|
||||
// that can never identify a PID.
|
||||
__setWindowsProcessTreeLoaderForTests(() => ({
|
||||
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2 },
|
||||
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 },
|
||||
supportedProcessDataFlags: 3,
|
||||
getAllProcesses
|
||||
}))
|
||||
expect(isWindowsProcessStartTimeAvailable()).toBe(false)
|
||||
|
||||
// An addon predating the export at all reports nothing, which is also false.
|
||||
__setWindowsProcessTreeLoaderForTests(() => ({
|
||||
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 },
|
||||
getAllProcesses
|
||||
}))
|
||||
expect(isWindowsProcessStartTimeAvailable()).toBe(false)
|
||||
@@ -654,12 +668,21 @@ describe('resolving the native reader', () => {
|
||||
}
|
||||
})
|
||||
|
||||
function addonReturning(rows: unknown): { getProcessList: ReturnType<typeof vi.fn> } {
|
||||
function addonReturning(rows: unknown): {
|
||||
getProcessList: ReturnType<typeof vi.fn>
|
||||
supportedProcessDataFlags: number
|
||||
} {
|
||||
return {
|
||||
getProcessList: vi.fn((cb: (r: unknown) => void) => cb(rows))
|
||||
getProcessList: vi.fn((cb: (r: unknown) => void) => cb(rows)),
|
||||
supportedProcessDataFlags: 7
|
||||
}
|
||||
}
|
||||
|
||||
/** An addon built before the creation-time patch: no capability export at all. */
|
||||
function staleAddonReturning(rows: unknown): { getProcessList: ReturnType<typeof vi.fn> } {
|
||||
return { getProcessList: vi.fn((cb: (r: unknown) => void) => cb(rows)) }
|
||||
}
|
||||
|
||||
it('prefers the npm package where the desktop app installs it', async () => {
|
||||
const resolve = vi.fn((specifier: string) => {
|
||||
if (specifier === PACKAGE_SPECIFIER) {
|
||||
@@ -726,6 +749,22 @@ describe('resolving the native reader', () => {
|
||||
expect(addon.getProcessList).toHaveBeenCalledWith(expect.any(Function), 4)
|
||||
})
|
||||
|
||||
it('trusts the staged addon on its own report, not on ours', async () => {
|
||||
// A relay carrying an addon built before the creation-time patch still
|
||||
// enumerates, so the table stays usable -- but it cannot prove identity,
|
||||
// and saying otherwise would hand teardown a PID it can never re-check.
|
||||
const addon = staleAddonReturning(NATIVE)
|
||||
__setWindowsProcessTreeRequireForTests((specifier: string) => {
|
||||
if (specifier === ADDON_SPECIFIER) {
|
||||
return addon
|
||||
}
|
||||
throw new Error('MODULE_NOT_FOUND')
|
||||
})
|
||||
await expect(readWindowsProcessTableFresh()).resolves.toHaveLength(2)
|
||||
expect(isWindowsProcessTableAvailable()).toBe(true)
|
||||
expect(isWindowsProcessStartTimeAvailable()).toBe(false)
|
||||
})
|
||||
|
||||
it('reaches the CIM scan when neither the package nor the addon is present', async () => {
|
||||
const cimScan = vi
|
||||
.fn()
|
||||
|
||||
@@ -75,6 +75,13 @@ type WindowsProcessTreeModule = {
|
||||
CommandLine: number
|
||||
CreationTime?: number
|
||||
}
|
||||
/**
|
||||
* Flag bits the COMPILED addon reports, straight from `addon.cc`. Absent on a
|
||||
* build that predates the patch — which is not the same question as the enum
|
||||
* above, because pnpm patches the source tree and leaves the tarball's
|
||||
* prebuilt `.node` in place.
|
||||
*/
|
||||
supportedProcessDataFlags?: number
|
||||
getAllProcesses: (
|
||||
callback: (processes: NativeProcessInfo[] | undefined) => void,
|
||||
flags?: number
|
||||
@@ -109,6 +116,7 @@ type WindowsProcessTreeAddon = {
|
||||
callback: (processes: NativeProcessInfo[] | undefined) => void,
|
||||
flags: number
|
||||
) => void
|
||||
supportedProcessDataFlags?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +124,8 @@ type WindowsProcessTreeAddon = {
|
||||
* is listed for completeness and is deliberately never set — see the projections
|
||||
* below.
|
||||
*
|
||||
* Why `CreationTime` can be named here rather than probed: the bare addon is a
|
||||
* content-hashed relay artifact, so it ships in the same immutable relay
|
||||
* directory as the bundle reading it and can never be an older build than the
|
||||
* code asking for the bit.
|
||||
* Naming `CreationTime` here only decides what we ASK for; whether the binary
|
||||
* answers is `supportedProcessDataFlags`, which the addon reports itself.
|
||||
*/
|
||||
const PROCESS_DATA_FLAG = { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 } as const
|
||||
|
||||
@@ -170,6 +176,7 @@ let cimScan: () => Promise<WindowsProcessRow[]> = readWindowsProcessRowsWithCim
|
||||
function adaptAddon(addon: WindowsProcessTreeAddon): WindowsProcessTreeModule {
|
||||
return {
|
||||
ProcessDataFlag: PROCESS_DATA_FLAG,
|
||||
supportedProcessDataFlags: addon.supportedProcessDataFlags,
|
||||
getAllProcesses: (callback, flags) => addon.getProcessList(callback, flags ?? 0)
|
||||
}
|
||||
}
|
||||
@@ -502,13 +509,23 @@ export function isWindowsProcessTableAvailable(): boolean {
|
||||
|
||||
/**
|
||||
* PID-reuse-safe ownership needs the native creation-time field, not merely a
|
||||
* process list. An install whose pnpm patch never applied exposes the table
|
||||
* without that field; keep structured ownership unavailable on those hosts
|
||||
* instead of fabricating proof from a PID.
|
||||
* process list.
|
||||
*
|
||||
* Why the binary's own answer and not the enum: pnpm patches the package's
|
||||
* source tree but leaves the tarball's prebuilt `.node` at the same
|
||||
* `build/Release/` path, so a host can hold a patched `lib/index.js` — enum and
|
||||
* all — over a binary that ignores flag 4. CI produced exactly that: the enum
|
||||
* said available, and every row came back without `creationTimeMs`. Answering
|
||||
* true there is worse than answering false: the descendant snapshot then
|
||||
* returns null forever and the exit proof latches `unverifiable`, while
|
||||
* structured chat believes it has a reaper.
|
||||
*/
|
||||
export function isWindowsProcessStartTimeAvailable(): boolean {
|
||||
const native = moduleLoader()
|
||||
return native !== null && typeof native.ProcessDataFlag.CreationTime === 'number'
|
||||
return (
|
||||
native !== null &&
|
||||
((native.supportedProcessDataFlags ?? 0) & PROCESS_DATA_FLAG.CreationTime) !== 0
|
||||
)
|
||||
}
|
||||
|
||||
function resetSnapshotReaders(): void {
|
||||
|
||||
Reference in New Issue
Block a user