vona-core 5.1.59 → 5.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.59",
4
- "gitHead": "c94b659a5ab1819ad7714ddacb6875c4f8cbc022",
3
+ "version": "5.1.60",
4
+ "gitHead": "6c07b381eb4c210c080363cbd4bb3577e5984e8a",
5
5
  "description": "vona",
6
6
  "keywords": [
7
7
  "cabloy",
@@ -1,59 +0,0 @@
1
- import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import type { Writable } from 'node:stream';
3
- export interface WindowsProcessSnapshot {
4
- pid: number;
5
- startTime: string;
6
- host: string;
7
- state: 'running';
8
- args: string;
9
- }
10
- export interface WindowsNativeResult {
11
- type: 'drained';
12
- result: {
13
- code: number;
14
- signal: null;
15
- };
16
- failed: boolean;
17
- }
18
- export interface WindowsNativeJobConfig {
19
- jobId: string;
20
- command: string;
21
- args: string[];
22
- cwd: string;
23
- env: NodeJS.ProcessEnv;
24
- /** Negative capability probe for native tests: invalid Job assignment must prevent user code. */
25
- rejectJobAssignment?: boolean;
26
- }
27
- export interface WindowsNativeJob {
28
- ready: Promise<WindowsProcessSnapshot>;
29
- started: Promise<void>;
30
- wait: () => Promise<WindowsNativeResult>;
31
- authorize: () => void;
32
- /** Exact live PID/FILETIME membership in this helper's private Job; never arbitrary Job membership. */
33
- contains: (identity: {
34
- pid: number;
35
- startTime: string;
36
- }) => Promise<boolean>;
37
- stop: () => void;
38
- /** Closing this pipe is the parent-death protocol; it never signals a historical PID. */
39
- disconnect: () => void;
40
- }
41
- /** src is included in the published package; build tools need not copy the native assets. */
42
- export declare function resolveWindowsProcessAssets(moduleUrl?: string): {
43
- script: string;
44
- source: string;
45
- };
46
- /**
47
- * Creates the external native helper, but does not authorize application launch.
48
- * The caller MUST persist ready's helper identity before authorize().
49
- */
50
- export declare function createWindowsNativeJob(config: WindowsNativeJobConfig, output?: {
51
- stdout: Writable;
52
- stderr: Writable;
53
- /** Native crash tests use the live ChildProcess handle, never a historical PID. */
54
- onHelperSpawn?: (helper: ChildProcessWithoutNullStreams) => void;
55
- }): Promise<WindowsNativeJob>;
56
- /** Query only the relevant owner/helper PIDs. Access/query errors are never absence. */
57
- export declare function inspectWindowsProcesses(pids: readonly number[]): Promise<WindowsProcessSnapshot[]>;
58
- export declare function inspectWindowsProcess(pid: number): Promise<WindowsProcessSnapshot | undefined>;
59
- //# sourceMappingURL=testProcessWindowsNative.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testProcessWindowsNative.d.ts","sourceRoot":"","sources":["../../../src/lib/core/testProcessWindowsNative.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAa5C,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC;IACvC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,iGAAiG;IACjG,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACzC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,uGAAuG;IACvG,QAAQ,EAAE,CAAC,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7E,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,yFAAyF;IACzF,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,6FAA6F;AAC7F,wBAAgB,2BAA2B,CAAC,SAAS,SAAkB;;;EAmBtE;AAqHD;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,sBAAsB,EAC9B,MAAM,GAAE;IACN,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,mFAAmF;IACnF,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,8BAA8B,KAAK,IAAI,CAAC;CACb,GACrD,OAAO,CAAC,gBAAgB,CAAC,CA+O3B;AAED,wFAAwF;AACxF,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAgFnC;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAE7C"}
@@ -1,336 +0,0 @@
1
- using System;
2
- using System.Collections.Generic;
3
- using System.ComponentModel;
4
- using System.Diagnostics;
5
- using System.Globalization;
6
- using System.IO;
7
- using System.IO.Pipes;
8
- using System.Runtime.InteropServices;
9
- using System.Text;
10
- using System.Text.Json;
11
- using System.Threading;
12
- using System.Threading.Tasks;
13
- using Microsoft.Win32.SafeHandles;
14
-
15
- namespace Cabloy
16
- {
17
- // The PowerShell process, not the application subtree, owns the only Job handle.
18
- // No fallback to AssignProcessToJobObject: job membership is a creation attribute.
19
- public static class TestProcessWindowsNative
20
- {
21
- const int MaxFrame = 1024 * 1024;
22
- const uint CREATE_SUSPENDED = 0x4, CREATE_UNICODE_ENVIRONMENT = 0x400;
23
- const uint EXTENDED_STARTUPINFO_PRESENT = 0x80000, CREATE_NO_WINDOW = 0x08000000;
24
- const uint STARTF_USESTDHANDLES = 0x100, HANDLE_FLAG_INHERIT = 1;
25
- const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, SYNCHRONIZE = 0x100000;
26
- const uint WAIT_OBJECT_0 = 0, WAIT_TIMEOUT = 258;
27
- const int ERROR_INVALID_PARAMETER = 87;
28
- const long PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x20002;
29
- const long PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D;
30
- static readonly UTF8Encoding Utf8 = new UTF8Encoding(false, true);
31
-
32
- [StructLayout(LayoutKind.Sequential)] struct FILETIME { public uint Low, High; }
33
- [StructLayout(LayoutKind.Sequential)] struct IO_COUNTERS { public ulong A, B, C, D, E, F; }
34
- [StructLayout(LayoutKind.Sequential)] struct BASIC_LIMIT {
35
- public long PerProcessUserTimeLimit, PerJobUserTimeLimit;
36
- public uint LimitFlags;
37
- public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize;
38
- public uint ActiveProcessLimit;
39
- public UIntPtr Affinity;
40
- public uint PriorityClass, SchedulingClass;
41
- }
42
- [StructLayout(LayoutKind.Sequential)] struct EXTENDED_LIMIT {
43
- public BASIC_LIMIT BasicLimitInformation;
44
- public IO_COUNTERS IoInfo;
45
- public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed;
46
- }
47
- [StructLayout(LayoutKind.Sequential)] struct BASIC_ACCOUNTING {
48
- public long TotalUserTime, TotalKernelTime, ThisPeriodTotalUserTime, ThisPeriodTotalKernelTime;
49
- public uint TotalPageFaultCount, TotalProcesses, ActiveProcesses, TotalTerminatedProcesses;
50
- }
51
- [StructLayout(LayoutKind.Sequential)] struct SECURITY_ATTRIBUTES {
52
- public int Length;
53
- public IntPtr SecurityDescriptor;
54
- [MarshalAs(UnmanagedType.Bool)] public bool InheritHandle;
55
- }
56
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct STARTUPINFO {
57
- public int cb;
58
- public string Reserved, Desktop, Title;
59
- public uint X, Y, XSize, YSize, XCountChars, YCountChars, FillAttribute, Flags;
60
- public short ShowWindow, Reserved2;
61
- public IntPtr Reserved2Pointer, StdInput, StdOutput, StdError;
62
- }
63
- [StructLayout(LayoutKind.Sequential)] struct STARTUPINFOEX {
64
- public STARTUPINFO StartupInfo;
65
- public IntPtr AttributeList;
66
- }
67
- [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION {
68
- public IntPtr Process, Thread;
69
- public uint ProcessId, ThreadId;
70
- }
71
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern IntPtr CreateJobObjectW(IntPtr attributes, string name);
72
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref EXTENDED_LIMIT info, uint length);
73
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool QueryInformationJobObject(IntPtr job, int infoClass, out BASIC_ACCOUNTING info, uint length, IntPtr returned);
74
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint code);
75
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool IsProcessInJob(IntPtr process, IntPtr job, [MarshalAs(UnmanagedType.Bool)] out bool result);
76
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
77
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
78
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, int flags, ref IntPtr size);
79
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previous, IntPtr returned);
80
- [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
81
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string cwd, ref STARTUPINFOEX startup, out PROCESS_INFORMATION info);
82
- [DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
83
- [DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
84
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
85
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetProcessTimes(IntPtr process, out FILETIME creation, out FILETIME exit, out FILETIME kernel, out FILETIME user);
86
- [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
87
- [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
88
- [DllImport("kernel32.dll")] static extern uint GetCurrentProcessId();
89
- [DllImport("kernel32.dll", SetLastError = true)] static extern bool CreatePipe(out IntPtr read, out IntPtr write, ref SECURITY_ATTRIBUTES attributes, uint size);
90
- [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern IntPtr CreateFileW(string path, uint access, uint share, ref SECURITY_ATTRIBUTES attributes, uint creation, uint flags, IntPtr template);
91
-
92
- static Exception Native(string operation) { return new Win32Exception(Marshal.GetLastWin32Error(), operation); }
93
- static void Check(bool ok, string operation) { if (!ok) throw Native(operation); }
94
- static void Close(ref IntPtr handle) { if (handle != IntPtr.Zero && handle != new IntPtr(-1)) CloseHandle(handle); handle = IntPtr.Zero; }
95
- static string CreationTime(IntPtr handle) {
96
- FILETIME creation, exit, kernel, user;
97
- Check(GetProcessTimes(handle, out creation, out exit, out kernel, out user), "GetProcessTimes");
98
- return (((ulong)creation.High << 32) | creation.Low).ToString(CultureInfo.InvariantCulture);
99
- }
100
- static void Emit(object value) { Console.Out.WriteLine(JsonSerializer.Serialize(value)); Console.Out.Flush(); }
101
- static string ReadFrame() {
102
- var text = new StringBuilder();
103
- for (;;) {
104
- int ch = Console.In.Read();
105
- if (ch == -1) { if (text.Length != 0) throw new IOException("Truncated native control frame"); return null; }
106
- if (ch == '\n') return text.ToString();
107
- if (text.Length >= MaxFrame) throw new IOException("Native control frame exceeds limit");
108
- text.Append((char)ch);
109
- }
110
- }
111
- static JsonDocument RequiredFrame() {
112
- var frame = ReadFrame();
113
- if (frame == null) throw new IOException("Parent disconnected before launch authorization");
114
- return JsonDocument.Parse(frame, new JsonDocumentOptions { MaxDepth = 16 });
115
- }
116
- static bool Exited(IntPtr process) {
117
- uint state = WaitForSingleObject(process, 0);
118
- if (state == WAIT_OBJECT_0) return true;
119
- if (state == WAIT_TIMEOUT) return false;
120
- throw Native("WaitForSingleObject");
121
- }
122
- static uint Active(IntPtr job) {
123
- BASIC_ACCOUNTING info;
124
- Check(QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf<BASIC_ACCOUNTING>(), IntPtr.Zero), "QueryInformationJobObject");
125
- return info.ActiveProcesses;
126
- }
127
- static string Quote(string value) {
128
- if (value.IndexOf('\0') >= 0) throw new ArgumentException("NUL in process argument");
129
- var result = new StringBuilder("\"");
130
- int slashes = 0;
131
- foreach (char ch in value) {
132
- if (ch == '\\') { slashes++; continue; }
133
- result.Append('\\', ch == '"' ? slashes * 2 + 1 : slashes);
134
- result.Append(ch); slashes = 0;
135
- }
136
- result.Append('\\', slashes * 2); result.Append('"'); return result.ToString();
137
- }
138
- static string EnvironmentBlock(JsonElement env) {
139
- var sorted = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
140
- foreach (var entry in env.EnumerateObject()) {
141
- string value = entry.Value.GetString();
142
- if (entry.Name.Length == 0 || entry.Name.IndexOfAny(new char[] {'=', '\0'}) >= 0 || value == null || value.IndexOf('\0') >= 0)
143
- throw new ArgumentException("Invalid Windows process environment");
144
- sorted.Add(entry.Name, value);
145
- }
146
- var text = new StringBuilder();
147
- foreach (var entry in sorted) text.Append(entry.Key).Append('=').Append(entry.Value).Append('\0');
148
- // Even an empty environment needs two terminating UTF-16 NULs.
149
- if (sorted.Count == 0) text.Append('\0');
150
- return text.Append('\0').ToString();
151
- }
152
- static FileStream ReadPipe(out IntPtr write) {
153
- var attributes = new SECURITY_ATTRIBUTES { Length = Marshal.SizeOf<SECURITY_ATTRIBUTES>(), InheritHandle = true };
154
- IntPtr read;
155
- Check(CreatePipe(out read, out write, ref attributes, 0), "CreatePipe");
156
- try {
157
- Check(SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0), "SetHandleInformation(pipe)");
158
- return new FileStream(new SafeFileHandle(read, true), FileAccess.Read, 4096, false);
159
- } catch { Close(ref read); Close(ref write); throw; }
160
- }
161
- static NamedPipeClientStream ConnectOutput(string name) {
162
- if (!System.Text.RegularExpressions.Regex.IsMatch(name, "^cabloy-test-[a-f0-9]{64}-(out|err)$")) throw new ArgumentException("Invalid output pipe name");
163
- var pipe = new NamedPipeClientStream(".", name, PipeDirection.Out, PipeOptions.Asynchronous);
164
- try { pipe.Connect(10000); return pipe; } catch { pipe.Dispose(); throw; }
165
- }
166
- static int Inspect() {
167
- using (var request = RequiredFrame()) {
168
- var rows = new List<object>();
169
- foreach (var item in request.RootElement.GetProperty("pids").EnumerateArray()) {
170
- uint pid = item.GetUInt32();
171
- if (pid == 0) throw new ArgumentException("Invalid process PID");
172
- IntPtr handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, false, pid);
173
- if (handle == IntPtr.Zero) {
174
- int error = Marshal.GetLastWin32Error();
175
- if (error == ERROR_INVALID_PARAMETER) continue; // Only confirmed absent PIDs are omitted.
176
- throw new Win32Exception(error, "OpenProcess(" + pid.ToString(CultureInfo.InvariantCulture) + ")");
177
- }
178
- try {
179
- string startTime = CreationTime(handle);
180
- if (!Exited(handle)) rows.Add(new { pid = pid, startTime = startTime });
181
- } finally { Close(ref handle); }
182
- }
183
- Emit(new { type = "inspection", processes = rows });
184
- return 0;
185
- }
186
- }
187
- static void Containment(IntPtr job, string jobId, string frame) {
188
- using (var request = JsonDocument.Parse(frame, new JsonDocumentOptions { MaxDepth = 16 })) {
189
- var query = request.RootElement;
190
- if (query.GetProperty("type").GetString() != "containment" || query.GetProperty("jobId").GetString() != jobId)
191
- throw new IOException("Invalid native containment request");
192
- string requestId = query.GetProperty("requestId").GetString();
193
- string creation = query.GetProperty("startTime").GetString();
194
- if (!System.Text.RegularExpressions.Regex.IsMatch(requestId ?? "", "^[a-f0-9]{32}$") ||
195
- !System.Text.RegularExpressions.Regex.IsMatch(creation ?? "", "^[1-9][0-9]{0,19}$"))
196
- throw new IOException("Invalid native containment identity");
197
- uint pid = query.GetProperty("pid").GetUInt32();
198
- if (pid == 0) throw new IOException("Invalid native containment PID");
199
- bool contained = false;
200
- IntPtr process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, false, pid);
201
- // Missing, denied, terminated, mismatched and query errors all deny, never infer membership.
202
- if (process != IntPtr.Zero) {
203
- try {
204
- bool inJob;
205
- contained = !Exited(process) && CreationTime(process) == creation &&
206
- IsProcessInJob(process, job, out inJob) && inJob && !Exited(process);
207
- } catch { contained = false; }
208
- finally { Close(ref process); }
209
- }
210
- Emit(new { type = "containment", requestId = requestId, jobId = jobId, contained = contained });
211
- }
212
- }
213
- static int Job() {
214
- IntPtr job = IntPtr.Zero, attributes = IntPtr.Zero, jobValue = IntPtr.Zero, handlesValue = IntPtr.Zero, environment = IntPtr.Zero;
215
- IntPtr outWrite = IntPtr.Zero, errWrite = IntPtr.Zero, input = IntPtr.Zero;
216
- PROCESS_INFORMATION target = new PROCESS_INFORMATION();
217
- bool initialized = false;
218
- using (var request = RequiredFrame()) {
219
- var config = request.RootElement;
220
- string jobId = config.GetProperty("jobId").GetString();
221
- Guid parsed;
222
- if (!Guid.TryParse(jobId, out parsed)) throw new ArgumentException("Invalid jobId");
223
- try {
224
- job = CreateJobObjectW(IntPtr.Zero, null); // unnamed; no other process can open it by name
225
- if (job == IntPtr.Zero) throw Native("CreateJobObjectW");
226
- Check(SetHandleInformation(job, HANDLE_FLAG_INHERIT, 0), "SetHandleInformation(job)");
227
- var limits = new EXTENDED_LIMIT();
228
- limits.BasicLimitInformation.LimitFlags = 0x2000; // KILL_ON_JOB_CLOSE, explicitly no breakaway flags
229
- Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf<EXTENDED_LIMIT>()), "SetInformationJobObject");
230
- Emit(new { type = "ready", jobId = jobId, pid = GetCurrentProcessId(), startTime = CreationTime(GetCurrentProcess()) });
231
- // The controller publishes the helper identity durably BEFORE sending authorize.
232
- var authorization = Task.Run(() => RequiredFrame());
233
- if (!authorization.Wait(30000)) throw new TimeoutException("Launch authorization timed out");
234
- using (var authorize = authorization.GetAwaiter().GetResult()) {
235
- if (authorize.RootElement.GetProperty("type").GetString() != "authorize" || authorize.RootElement.GetProperty("jobId").GetString() != jobId)
236
- throw new IOException("Launch not authorized");
237
- }
238
- using (var stdout = ConnectOutput(config.GetProperty("stdoutPipe").GetString()))
239
- using (var stderr = ConnectOutput(config.GetProperty("stderrPipe").GetString()))
240
- using (var outRead = ReadPipe(out outWrite))
241
- using (var errRead = ReadPipe(out errWrite)) {
242
- var security = new SECURITY_ATTRIBUTES { Length = Marshal.SizeOf<SECURITY_ATTRIBUTES>(), InheritHandle = true };
243
- input = CreateFileW("NUL", 0x80000000, 3, ref security, 3, 0, IntPtr.Zero);
244
- if (input == new IntPtr(-1)) throw Native("CreateFileW(NUL)");
245
- IntPtr size = IntPtr.Zero;
246
- InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref size);
247
- if (size == IntPtr.Zero) throw Native("InitializeProcThreadAttributeList(size)");
248
- attributes = Marshal.AllocHGlobal(size);
249
- Check(InitializeProcThreadAttributeList(attributes, 2, 0, ref size), "InitializeProcThreadAttributeList"); initialized = true;
250
- jobValue = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(jobValue, job);
251
- // Test-only negative capability probe. It can only prevent launch, never weaken containment.
252
- if (config.TryGetProperty("rejectJobAssignment", out var reject) && reject.GetBoolean()) Marshal.WriteIntPtr(jobValue, IntPtr.Zero);
253
- Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(PROC_THREAD_ATTRIBUTE_JOB_LIST), jobValue, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero), "UpdateProcThreadAttribute(JOB_LIST)");
254
- handlesValue = Marshal.AllocHGlobal(IntPtr.Size * 3);
255
- Marshal.WriteIntPtr(handlesValue, 0, input); Marshal.WriteIntPtr(handlesValue, IntPtr.Size, outWrite); Marshal.WriteIntPtr(handlesValue, IntPtr.Size * 2, errWrite);
256
- Check(UpdateProcThreadAttribute(attributes, 0, new IntPtr(PROC_THREAD_ATTRIBUTE_HANDLE_LIST), handlesValue, new IntPtr(IntPtr.Size * 3), IntPtr.Zero, IntPtr.Zero), "UpdateProcThreadAttribute(HANDLE_LIST)");
257
- var startup = new STARTUPINFOEX();
258
- startup.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>(); startup.StartupInfo.Flags = STARTF_USESTDHANDLES;
259
- startup.StartupInfo.StdInput = input; startup.StartupInfo.StdOutput = outWrite; startup.StartupInfo.StdError = errWrite; startup.AttributeList = attributes;
260
- string command = config.GetProperty("command").GetString();
261
- if (!Path.IsPathFullyQualified(command) || !File.Exists(command) || !command.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Windows owned command must be an absolute .exe path; launch npm via node's npm-cli.js");
262
- var commandLine = new StringBuilder(Quote(command));
263
- foreach (var argument in config.GetProperty("args").EnumerateArray()) commandLine.Append(' ').Append(Quote(argument.GetString()));
264
- if (commandLine.Length >= 32767) throw new ArgumentException("Windows command line too long");
265
- environment = Marshal.StringToHGlobalUni(EnvironmentBlock(config.GetProperty("env")));
266
- Check(CreateProcessW(command, commandLine, IntPtr.Zero, IntPtr.Zero, true,
267
- CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
268
- environment, config.GetProperty("cwd").GetString(), ref startup, out target), "CreateProcessW(JOB_LIST)");
269
- Close(ref outWrite); Close(ref errWrite); Close(ref input);
270
- // Creation-time membership is already established. A crash even here closes the Job and kills the suspended process.
271
- if (Active(job) != 1) throw new IOException("Created process was not accounted in the owned Job");
272
- var outputTask = outRead.CopyToAsync(stdout);
273
- var errorTask = errRead.CopyToAsync(stderr);
274
- if (ResumeThread(target.Thread) == UInt32.MaxValue) throw Native("ResumeThread");
275
- Close(ref target.Thread);
276
- Emit(new { type = "started" });
277
- var control = Task.Run(() => ReadFrame());
278
- bool failed = false, terminated = false;
279
- long rootExitAt = -1, stopAt = -1;
280
- var clock = Stopwatch.StartNew();
281
- for (;;) {
282
- bool rootExited = Exited(target.Process);
283
- uint active = Active(job);
284
- if (rootExited && rootExitAt < 0) rootExitAt = clock.ElapsedMilliseconds;
285
- if (control.IsCompleted && !terminated) {
286
- bool disconnect = false;
287
- try {
288
- string frame = control.GetAwaiter().GetResult();
289
- if (frame == null) disconnect = true;
290
- else {
291
- Containment(job, jobId, frame);
292
- control = Task.Run(() => ReadFrame());
293
- }
294
- } catch { disconnect = true; }
295
- if (disconnect) {
296
- // EOF, explicit stop, malformed input, and reader faults still terminate.
297
- failed = true; terminated = true; stopAt = clock.ElapsedMilliseconds;
298
- Check(TerminateJobObject(job, 1), "TerminateJobObject(parent stop/disconnect)");
299
- }
300
- }
301
- if (rootExited && active == 0) break;
302
- if (!terminated && rootExitAt >= 0 && clock.ElapsedMilliseconds - rootExitAt >= 1000) {
303
- failed = true; terminated = true; stopAt = clock.ElapsedMilliseconds;
304
- Check(TerminateJobObject(job, 1), "TerminateJobObject(remaining descendants)");
305
- }
306
- if (terminated && clock.ElapsedMilliseconds - stopAt > 10000) throw new TimeoutException("Owned Windows Job did not drain");
307
- Thread.Sleep(25);
308
- }
309
- uint exitCode; Check(GetExitCodeProcess(target.Process, out exitCode), "GetExitCodeProcess");
310
- if (!Task.WaitAll(new Task[] { outputTask, errorTask }, 10000)) throw new TimeoutException("Owned process output did not drain");
311
- stdout.Flush(); stderr.Flush();
312
- Emit(new { type = "drained", code = exitCode, failed = failed });
313
- return 0; // Controller must observe this successful helper exit before durable drainage.
314
- }
315
- } finally {
316
- // Sole Job handle always closes, including assignment/resume/query/output failures.
317
- Close(ref job); Close(ref target.Thread); Close(ref target.Process);
318
- Close(ref input); Close(ref outWrite); Close(ref errWrite);
319
- if (initialized) DeleteProcThreadAttributeList(attributes);
320
- if (attributes != IntPtr.Zero) Marshal.FreeHGlobal(attributes);
321
- if (jobValue != IntPtr.Zero) Marshal.FreeHGlobal(jobValue);
322
- if (handlesValue != IntPtr.Zero) Marshal.FreeHGlobal(handlesValue);
323
- if (environment != IntPtr.Zero) Marshal.FreeHGlobal(environment);
324
- }
325
- }
326
- }
327
- public static int Run(string mode) {
328
- try {
329
- Console.InputEncoding = Utf8; Console.OutputEncoding = Utf8;
330
- if (mode == "inspect") return Inspect();
331
- if (mode == "job") return Job();
332
- throw new ArgumentException("Invalid native process mode");
333
- } catch (Exception error) { Console.Error.WriteLine(error.ToString()); return 1; }
334
- }
335
- }
336
- }
@@ -1,20 +0,0 @@
1
- # PowerShell 7 is an explicit runtime dependency; this file is never shell-interpolated.
2
- # stdout belongs exclusively to the bounded native JSON control protocol.
3
- param(
4
- [Parameter(Mandatory = $true)]
5
- [ValidateSet('job', 'inspect')]
6
- [string] $Mode
7
- )
8
- $ErrorActionPreference = 'Stop'
9
- $ProgressPreference = 'SilentlyContinue'
10
- try {
11
- if ($PSVersionTable.PSVersion.Major -lt 7 -or -not $IsWindows) {
12
- throw 'The isolated Windows process backend requires PowerShell 7 on native Windows.'
13
- }
14
- Add-Type -Path (Join-Path $PSScriptRoot 'testProcessWindowsNative.cs') -ErrorAction Stop
15
- $code = [Cabloy.TestProcessWindowsNative]::Run($Mode)
16
- exit $code
17
- } catch {
18
- [Console]::Error.WriteLine($_.Exception.ToString())
19
- exit 1
20
- }
@@ -1,531 +0,0 @@
1
- import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- import type { Server, Socket } from 'node:net';
3
- import type { Writable } from 'node:stream';
4
-
5
- import { spawn } from 'node:child_process';
6
- import { randomBytes } from 'node:crypto';
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import { createServer } from 'node:net';
9
- import { hostname } from 'node:os';
10
- import { dirname, join } from 'node:path';
11
- import { fileURLToPath } from 'node:url';
12
-
13
- const MAX_FRAME = 1024 * 1024;
14
- const START_TIMEOUT = 30_000;
15
-
16
- export interface WindowsProcessSnapshot {
17
- pid: number;
18
- startTime: string;
19
- host: string;
20
- state: 'running';
21
- args: string;
22
- }
23
-
24
- export interface WindowsNativeResult {
25
- type: 'drained';
26
- result: { code: number; signal: null };
27
- failed: boolean;
28
- }
29
-
30
- export interface WindowsNativeJobConfig {
31
- jobId: string;
32
- command: string;
33
- args: string[];
34
- cwd: string;
35
- env: NodeJS.ProcessEnv;
36
- /** Negative capability probe for native tests: invalid Job assignment must prevent user code. */
37
- rejectJobAssignment?: boolean;
38
- }
39
-
40
- export interface WindowsNativeJob {
41
- ready: Promise<WindowsProcessSnapshot>;
42
- started: Promise<void>;
43
- wait: () => Promise<WindowsNativeResult>;
44
- authorize: () => void;
45
- /** Exact live PID/FILETIME membership in this helper's private Job; never arbitrary Job membership. */
46
- contains: (identity: { pid: number; startTime: string }) => Promise<boolean>;
47
- stop: () => void;
48
- /** Closing this pipe is the parent-death protocol; it never signals a historical PID. */
49
- disconnect: () => void;
50
- }
51
-
52
- /** src is included in the published package; build tools need not copy the native assets. */
53
- export function resolveWindowsProcessAssets(moduleUrl = import.meta.url) {
54
- let directory = dirname(fileURLToPath(moduleUrl));
55
- for (;;) {
56
- const manifest = join(directory, 'package.json');
57
- if (existsSync(manifest)) {
58
- const info = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
59
- if (info.name === 'vona-core') {
60
- const script = join(directory, 'src/lib/core/testProcessWindowsNative.ps1');
61
- const source = join(directory, 'src/lib/core/testProcessWindowsNative.cs');
62
- if (!existsSync(script) || !existsSync(source)) {
63
- throw new Error('Installed vona-core is missing the Windows native process assets');
64
- }
65
- return { script, source };
66
- }
67
- }
68
- const parent = dirname(directory);
69
- if (parent === directory) throw new Error('Cannot resolve packaged Windows process assets');
70
- directory = parent;
71
- }
72
- }
73
-
74
- function requireWindows() {
75
- if (process.platform !== 'win32')
76
- throw new Error('The Windows process backend requires native Windows and PowerShell 7 (pwsh)');
77
- }
78
-
79
- function spawnHelper(mode: 'job' | 'inspect'): ChildProcessWithoutNullStreams {
80
- requireWindows();
81
- const { script } = resolveWindowsProcessAssets();
82
- return spawn(
83
- 'pwsh.exe',
84
- ['-NoLogo', '-NoProfile', '-NonInteractive', '-File', script, '-Mode', mode],
85
- {
86
- shell: false,
87
- windowsHide: true,
88
- stdio: ['pipe', 'pipe', 'pipe'],
89
- },
90
- );
91
- }
92
-
93
- function frame(value: unknown) {
94
- const result = `${JSON.stringify(value)}\n`;
95
- if (Buffer.byteLength(result, 'utf8') > MAX_FRAME)
96
- throw new Error('Windows native control frame exceeds limit');
97
- return result;
98
- }
99
-
100
- function deferred<T>() {
101
- let resolve!: (value: T) => void;
102
- let reject!: (reason: Error) => void;
103
- const promise = new Promise<T>((yes, no) => {
104
- resolve = yes;
105
- reject = no;
106
- });
107
- // Startup can fail before callers begin awaiting the subsequent stage.
108
- void promise.catch(() => {});
109
- return { promise, resolve, reject };
110
- }
111
-
112
- function errorOf(value: unknown) {
113
- return value instanceof Error ? value : new Error(String(value));
114
- }
115
-
116
- /** Strict, bounded line protocol. Target stdout/stderr never enter this parser. */
117
- function consumeFrames(
118
- child: ChildProcessWithoutNullStreams,
119
- receive: (value: Record<string, unknown>) => void,
120
- fail: (error: Error) => void,
121
- ) {
122
- let buffer = Buffer.alloc(0);
123
- child.stdout.on('data', (chunk: Buffer) => {
124
- try {
125
- buffer = Buffer.concat([buffer, chunk]);
126
- for (;;) {
127
- const newline = buffer.indexOf(10);
128
- if (newline < 0) break;
129
- if (newline > MAX_FRAME) throw new Error('Windows native control frame exceeds limit');
130
- const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, newline));
131
- buffer = buffer.subarray(newline + 1);
132
- const value: unknown = JSON.parse(text);
133
- if (!value || typeof value !== 'object' || Array.isArray(value))
134
- throw new Error('Invalid Windows native control frame');
135
- receive(value as Record<string, unknown>);
136
- }
137
- if (buffer.length > MAX_FRAME) throw new Error('Windows native control frame exceeds limit');
138
- } catch (error) {
139
- fail(errorOf(error));
140
- }
141
- });
142
- child.stdout.on('end', () => {
143
- if (buffer.length) fail(new Error('Truncated Windows native control frame'));
144
- });
145
- }
146
-
147
- interface OutputBridge {
148
- name: string;
149
- close: () => void;
150
- ended: Promise<void>;
151
- }
152
-
153
- async function outputBridge(name: string, destination: Writable): Promise<OutputBridge> {
154
- const ended = deferred<void>();
155
- let socket: Socket | undefined;
156
- const server: Server = createServer(connection => {
157
- if (socket) {
158
- connection.destroy();
159
- return;
160
- }
161
- socket = connection;
162
- server.close();
163
- connection.on('error', error => ended.reject(error));
164
- connection.on('end', () => ended.resolve());
165
- connection.on('close', () => {
166
- if (!connection.readableEnded)
167
- ended.reject(new Error('Windows target output pipe closed prematurely'));
168
- });
169
- connection.pipe(destination, { end: false });
170
- });
171
- server.on('error', error => ended.reject(error));
172
- await new Promise<void>((resolve, reject) => {
173
- server.once('error', reject);
174
- server.listen(`\\\\.\\pipe\\${name}`, () => {
175
- server.off('error', reject);
176
- resolve();
177
- });
178
- });
179
- return {
180
- name,
181
- ended: ended.promise,
182
- close: () => {
183
- socket?.destroy();
184
- server.close();
185
- },
186
- };
187
- }
188
-
189
- /**
190
- * Creates the external native helper, but does not authorize application launch.
191
- * The caller MUST persist ready's helper identity before authorize().
192
- */
193
- export async function createWindowsNativeJob(
194
- config: WindowsNativeJobConfig,
195
- output: {
196
- stdout: Writable;
197
- stderr: Writable;
198
- /** Native crash tests use the live ChildProcess handle, never a historical PID. */
199
- onHelperSpawn?: (helper: ChildProcessWithoutNullStreams) => void;
200
- } = { stdout: process.stdout, stderr: process.stderr },
201
- ): Promise<WindowsNativeJob> {
202
- requireWindows();
203
- const suffix = randomBytes(32).toString('hex');
204
- const stdout = await outputBridge(`cabloy-test-${suffix}-out`, output.stdout);
205
- let stderr: OutputBridge;
206
- try {
207
- stderr = await outputBridge(`cabloy-test-${suffix}-err`, output.stderr);
208
- } catch (error) {
209
- stdout.close();
210
- throw error;
211
- }
212
- let child: ChildProcessWithoutNullStreams;
213
- try {
214
- child = spawnHelper('job');
215
- } catch (error) {
216
- stdout.close();
217
- stderr.close();
218
- throw error;
219
- }
220
- const ready = deferred<WindowsProcessSnapshot>();
221
- const started = deferred<void>();
222
- const closed = deferred<WindowsNativeResult>();
223
- let phase: 'initial' | 'ready' | 'authorized' | 'started' | 'drained' = 'initial';
224
- let proof: WindowsNativeResult | undefined;
225
- let failure: Error | undefined;
226
- let diagnostics = '';
227
- let stopping = false;
228
- let didClose = false;
229
- let timeout: NodeJS.Timeout | undefined;
230
- const queries = new Map<
231
- string,
232
- { result: ReturnType<typeof deferred<boolean>>; timer: NodeJS.Timeout }
233
- >();
234
- const rejectQueries = (error: Error, preserveResponses = false) => {
235
- for (const { result, timer } of queries.values()) {
236
- clearTimeout(timer);
237
- result.reject(error);
238
- }
239
- // Stop can race with valid replies already in the pipe. Keep only their bounded
240
- // correlation records until the helper replies or closes, never grant success.
241
- if (!preserveResponses) queries.clear();
242
- };
243
- const disconnect = () => {
244
- if (!child.stdin.destroyed) child.stdin.end();
245
- };
246
- const fail = (error: Error) => {
247
- failure ??= error;
248
- rejectQueries(error);
249
- ready.reject(error);
250
- started.reject(error);
251
- closed.reject(error);
252
- clearTimeout(timeout);
253
- disconnect();
254
- // Do not wait indefinitely for a stuck compiler/native inspector. Closing the
255
- // protocol/output handles makes its next operation fail closed; no PID signal.
256
- stdout.close();
257
- stderr.close();
258
- child.stdout.destroy();
259
- child.stderr.destroy();
260
- child.unref();
261
- };
262
- timeout = setTimeout(
263
- () => fail(new Error('Windows native helper startup timed out')),
264
- START_TIMEOUT,
265
- );
266
- child.stdin.on('error', error => fail(error));
267
- child.on('error', error => fail(error));
268
- child.stderr.on('data', (chunk: Buffer) => {
269
- diagnostics = (diagnostics + chunk.toString('utf8')).slice(-16_384);
270
- });
271
- consumeFrames(
272
- child,
273
- value => {
274
- if (value.type === 'ready' && phase === 'initial') {
275
- if (
276
- value.jobId !== config.jobId ||
277
- value.pid !== child.pid ||
278
- typeof value.startTime !== 'string' ||
279
- !/^\d{1,20}$/.test(value.startTime)
280
- ) {
281
- throw new Error('Invalid Windows native helper identity');
282
- }
283
- phase = 'ready';
284
- ready.resolve({
285
- pid: value.pid as number,
286
- startTime: value.startTime,
287
- host: hostname(),
288
- state: 'running',
289
- args: '',
290
- });
291
- } else if (value.type === 'started' && phase === 'authorized') {
292
- phase = 'started';
293
- clearTimeout(timeout);
294
- started.resolve();
295
- } else if (value.type === 'containment' && phase === 'started') {
296
- if (
297
- typeof value.requestId !== 'string' ||
298
- value.jobId !== config.jobId ||
299
- typeof value.contained !== 'boolean'
300
- ) {
301
- throw new Error('Invalid Windows native containment response');
302
- }
303
- const query = queries.get(value.requestId);
304
- if (!query) throw new Error('Unknown Windows native containment response');
305
- queries.delete(value.requestId);
306
- clearTimeout(query.timer);
307
- query.result.resolve(value.contained && !stopping && !failure && !didClose);
308
- } else if (value.type === 'drained' && phase === 'started') {
309
- rejectQueries(new Error('Windows native Job has drained'));
310
- if (
311
- !Number.isSafeInteger(value.code) ||
312
- (value.code as number) < 0 ||
313
- (value.code as number) > 0xffffffff ||
314
- typeof value.failed !== 'boolean'
315
- ) {
316
- throw new Error('Invalid Windows native drainage proof');
317
- }
318
- phase = 'drained';
319
- proof = {
320
- type: 'drained',
321
- result: { code: value.code as number, signal: null },
322
- failed: value.failed || stopping,
323
- };
324
- } else {
325
- throw new Error('Unexpected Windows native control message');
326
- }
327
- },
328
- fail,
329
- );
330
- child.once('close', (code, signal) => {
331
- didClose = true;
332
- rejectQueries(new Error('Windows native Job helper is no longer live'));
333
- clearTimeout(timeout);
334
- void (async () => {
335
- try {
336
- if (failure) throw new Error('Windows native helper control failed', { cause: failure });
337
- if (code !== 0 || signal || !proof) {
338
- throw new Error(
339
- `Windows native helper exited without proving Job shutdown${diagnostics ? `: ${diagnostics.trim()}` : ''}`,
340
- );
341
- }
342
- timeout = setTimeout(
343
- () => fail(new Error('Windows native output bridge did not close in time')),
344
- 10_000,
345
- );
346
- await Promise.all([stdout.ended, stderr.ended]);
347
- clearTimeout(timeout);
348
- closed.resolve(proof);
349
- } catch (error) {
350
- const cause = errorOf(error);
351
- ready.reject(cause);
352
- started.reject(cause);
353
- closed.reject(cause);
354
- } finally {
355
- clearTimeout(timeout);
356
- stdout.close();
357
- stderr.close();
358
- }
359
- })();
360
- });
361
- try {
362
- const env: Record<string, string> = {};
363
- // Node's Windows spawn convention: case-insensitive keys, first lexical spelling wins.
364
- const keys = new Set<string>();
365
- for (const key of Object.keys(config.env).sort()) {
366
- const value = config.env[key];
367
- if (value !== undefined && !keys.has(key.toUpperCase())) {
368
- keys.add(key.toUpperCase());
369
- env[key] = value;
370
- }
371
- }
372
- child.stdin.write(frame({ ...config, env, stdoutPipe: stdout.name, stderrPipe: stderr.name }));
373
- output.onHelperSpawn?.(child);
374
- } catch (error) {
375
- fail(errorOf(error));
376
- }
377
- const stop = () => {
378
- if (stopping) return;
379
- stopping = true;
380
- rejectQueries(new Error('Windows native Job is stopping'), true);
381
- if (proof) proof.failed = true;
382
- if (!didClose) {
383
- clearTimeout(timeout);
384
- timeout = setTimeout(
385
- () => fail(new Error('Windows native helper did not prove shutdown after stop')),
386
- 25_000,
387
- );
388
- }
389
- disconnect();
390
- };
391
- return {
392
- ready: ready.promise,
393
- started: started.promise,
394
- wait: () => closed.promise,
395
- authorize: () => {
396
- if (phase !== 'ready' || stopping || didClose || failure)
397
- throw new Error('Windows native launch authorization is no longer valid');
398
- phase = 'authorized';
399
- child.stdin.write(frame({ type: 'authorize', jobId: config.jobId }));
400
- },
401
- contains: async identity => {
402
- if (
403
- phase !== 'started' ||
404
- stopping ||
405
- didClose ||
406
- failure ||
407
- queries.size >= 32 ||
408
- !Number.isSafeInteger(identity.pid) ||
409
- identity.pid <= 0 ||
410
- identity.pid > 0xffffffff ||
411
- typeof identity.startTime !== 'string' ||
412
- !/^[1-9]\d{0,19}$/.test(identity.startTime)
413
- ) {
414
- throw new Error('Windows native containment query is unavailable or invalid');
415
- }
416
- const requestId = randomBytes(16).toString('hex');
417
- const result = deferred<boolean>();
418
- const timer = setTimeout(() => {
419
- fail(new Error('Windows native containment query timed out'));
420
- }, 4000);
421
- queries.set(requestId, { result, timer });
422
- try {
423
- child.stdin.write(
424
- frame({
425
- type: 'containment',
426
- requestId,
427
- jobId: config.jobId,
428
- pid: identity.pid,
429
- startTime: identity.startTime,
430
- }),
431
- );
432
- } catch (error) {
433
- fail(errorOf(error));
434
- }
435
- return await result.promise;
436
- },
437
- stop,
438
- disconnect: stop,
439
- };
440
- }
441
-
442
- /** Query only the relevant owner/helper PIDs. Access/query errors are never absence. */
443
- export async function inspectWindowsProcesses(
444
- pids: readonly number[],
445
- ): Promise<WindowsProcessSnapshot[]> {
446
- requireWindows();
447
- if (pids.some(pid => !Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffffffff))
448
- throw new Error('Invalid Windows process PID');
449
- const unique = [...new Set(pids)];
450
- if (unique.length > 4096) throw new Error('Too many Windows process identities to inspect');
451
- if (!unique.length) return [];
452
- const child = spawnHelper('inspect');
453
- let response: WindowsProcessSnapshot[] | undefined;
454
- let failure: Error | undefined;
455
- let diagnostics = '';
456
- const inspection = deferred<WindowsProcessSnapshot[]>();
457
- const fail = (error: Error) => {
458
- failure ??= error;
459
- inspection.reject(error);
460
- child.stdin.destroy();
461
- child.stdout.destroy();
462
- child.stderr.destroy();
463
- child.unref();
464
- };
465
- const timeout = setTimeout(
466
- () => fail(new Error('Windows native process inspection timed out')),
467
- START_TIMEOUT,
468
- );
469
- child.stdin.on('error', fail);
470
- child.stderr.on('data', (chunk: Buffer) => {
471
- diagnostics = (diagnostics + chunk.toString('utf8')).slice(-16_384);
472
- });
473
- consumeFrames(
474
- child,
475
- value => {
476
- if (response || value.type !== 'inspection' || !Array.isArray(value.processes))
477
- throw new Error('Invalid Windows native process inspection');
478
- const seen = new Set<number>();
479
- response = value.processes.map((row: unknown) => {
480
- if (
481
- !row ||
482
- typeof row !== 'object' ||
483
- !('pid' in row) ||
484
- !('startTime' in row) ||
485
- typeof row.pid !== 'number' ||
486
- !unique.includes(row.pid) ||
487
- seen.has(row.pid) ||
488
- typeof row.startTime !== 'string' ||
489
- !/^\d{1,20}$/.test(row.startTime)
490
- ) {
491
- throw new Error('Invalid Windows native process identity');
492
- }
493
- seen.add(row.pid);
494
- return {
495
- pid: row.pid,
496
- startTime: row.startTime,
497
- host: hostname(),
498
- state: 'running',
499
- args: '',
500
- };
501
- });
502
- },
503
- fail,
504
- );
505
- try {
506
- child.once('error', fail);
507
- child.once('close', (code, signal) => {
508
- if (failure) {
509
- inspection.reject(failure);
510
- } else if (code !== 0 || signal || !response) {
511
- inspection.reject(
512
- new Error(
513
- `Cannot inspect Windows process identities${diagnostics ? `: ${diagnostics.trim()}` : ''}`,
514
- ),
515
- );
516
- } else {
517
- inspection.resolve(response);
518
- }
519
- });
520
- child.stdin.end(frame({ pids: unique }));
521
- return await inspection.promise;
522
- } finally {
523
- clearTimeout(timeout);
524
- }
525
- }
526
-
527
- export async function inspectWindowsProcess(
528
- pid: number,
529
- ): Promise<WindowsProcessSnapshot | undefined> {
530
- return (await inspectWindowsProcesses([pid]))[0];
531
- }