mcp-pacemaker 2.0.2 → 2.1.1

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/CONTRIBUTING.md +46 -13
  3. package/README.md +109 -524
  4. package/THIRD_PARTY_NOTICES.txt +1 -28
  5. package/autostart/windows/account-task.ps1 +167 -0
  6. package/autostart/windows/inspect-task.ps1 +42 -0
  7. package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
  8. package/autostart/windows/legacy-access.ps1 +359 -0
  9. package/autostart/windows/legacy-task.ps1 +301 -0
  10. package/autostart/windows/register-task.ps1 +11 -2
  11. package/bin/account-task-adapter.mjs +47 -0
  12. package/bin/account-upgrade-controller.mjs +198 -0
  13. package/bin/account-upgrade-worker.mjs +141 -0
  14. package/bin/account-worker-session.mjs +215 -0
  15. package/bin/cli-dispatch.mjs +154 -0
  16. package/bin/cli-main.mjs +1063 -0
  17. package/bin/cli-selection.mjs +43 -0
  18. package/bin/cli.mjs +7 -876
  19. package/bin/dependency-lock.json +642 -0
  20. package/bin/legacy-1.3.json +32 -0
  21. package/bin/legacy-access.mjs +73 -0
  22. package/bin/legacy-installation.mjs +273 -0
  23. package/bin/legacy-task.mjs +76 -0
  24. package/bin/legacy-upgrade.mjs +527 -0
  25. package/bin/managed-runtime.mjs +170 -0
  26. package/bin/managed-state.mjs +257 -0
  27. package/bin/managed-upgrade.mjs +581 -0
  28. package/bin/mcp-bridge.mjs +150 -4
  29. package/bin/pooling-batches.mjs +13 -0
  30. package/bin/pooling-writer.mjs +4 -0
  31. package/bin/prewarm.mjs +59 -25
  32. package/bin/runtime-context.mjs +100 -0
  33. package/bin/service-control.mjs +32 -4
  34. package/bin/stable-launcher.mjs +54 -0
  35. package/bin/task-only-controller.mjs +226 -0
  36. package/bin/task-scope.mjs +65 -0
  37. package/bin/task-transaction-protocol.mjs +173 -0
  38. package/bin/top.mjs +22 -10
  39. package/bin/update-check.mjs +13 -2
  40. package/bin/upgrade-autostart.mjs +47 -0
  41. package/bin/upgrade-capability.json +9 -0
  42. package/bin/upgrade-dependencies.mjs +183 -0
  43. package/bin/upgrade-package.mjs +205 -0
  44. package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
  45. package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
  46. package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
  47. package/bin/windows-legacy/src/LegacyNative.cs +832 -0
  48. package/bin/windows-legacy/src/LegacyProcessBroker.cs +961 -0
  49. package/bin/windows-legacy-process.mjs +248 -0
  50. package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
  51. package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
  52. package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
  53. package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
  54. package/bin/windows-process-lifetime.mjs +218 -0
  55. package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
  56. package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
  57. package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
  58. package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
  59. package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
  60. package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
  61. package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
  62. package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
  63. package/bin/windows-task-channel.mjs +335 -0
  64. package/docs/account-upgrades.md +218 -0
  65. package/docs/compatibility.md +39 -0
  66. package/docs/configuration.md +146 -0
  67. package/docs/dashboards.md +125 -0
  68. package/docs/execution-context.md +119 -0
  69. package/docs/http-api.md +47 -0
  70. package/docs/install.md +52 -0
  71. package/docs/operations.md +99 -0
  72. package/docs/upgrades.md +324 -0
  73. package/package.json +19 -5
  74. package/supervisor/supervise.mjs +226 -40
  75. package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
  76. package/tools/windows-legacy/README.md +251 -0
  77. package/tools/windows-legacy/build.ps1 +122 -0
  78. package/tools/windows-legacy/inventory.mjs +92 -0
  79. package/tools/windows-process-lifetime/README.md +119 -0
  80. package/tools/windows-process-lifetime/build.ps1 +122 -0
  81. package/tools/windows-process-lifetime/inventory.mjs +86 -0
  82. package/tools/windows-task-channel/README.md +329 -0
  83. package/tools/windows-task-channel/build.ps1 +107 -0
  84. package/tools/windows-task-channel/inventory.mjs +117 -0
  85. package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
  86. package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
  87. package/ui/dist/assets/index-PVcuDNrN.css +1 -0
  88. package/ui/dist/index.html +2 -2
  89. package/ui/dist/third-party-manifest.json +37 -25
  90. package/ui/dist/assets/index-CaLPg0c-.css +0 -1
@@ -0,0 +1,468 @@
1
+ using System;
2
+ using System.ComponentModel;
3
+ using System.Diagnostics;
4
+ using System.Globalization;
5
+ using System.Runtime.InteropServices;
6
+ using System.Text;
7
+ using System.Threading;
8
+ using Microsoft.Win32.SafeHandles;
9
+
10
+ internal sealed class ProcessLifetimeHelper
11
+ {
12
+ private string stage = "arguments";
13
+
14
+ private static int Main(string[] args)
15
+ {
16
+ var helper = new ProcessLifetimeHelper();
17
+ if (args.Length > 0 &&
18
+ args[0] == "observe-owner")
19
+ {
20
+ return helper.ObserveOwner(args);
21
+ }
22
+
23
+ return helper.Run(args);
24
+ }
25
+
26
+ private int Run(string[] args)
27
+ {
28
+ SafeNativeHandle parent = null;
29
+ SafeNativeHandle job = null;
30
+ SafeNativeHandle drainMarker = null;
31
+ try
32
+ {
33
+ int expectedParent = 0;
34
+ bool validRequest = args.Length == 2 &&
35
+ args[0] == "watch-parent" &&
36
+ int.TryParse(args[1], out expectedParent);
37
+ if (!validRequest)
38
+ {
39
+ throw new ArgumentException();
40
+ }
41
+
42
+ stage = "parent";
43
+ var current = new IntPtr(-1);
44
+ int parentId = GetParentId();
45
+ if (parentId != expectedParent)
46
+ {
47
+ throw new ArgumentException();
48
+ }
49
+ // A held process handle, not a later PID lookup, owns the lifetime.
50
+ parent = Native.OpenProcess(0x00100000 | 0x1000 | 0x0100 | 0x0001, false, parentId);
51
+ Require(!parent.IsInvalid);
52
+ long parentCreated;
53
+ long selfCreated;
54
+ long unusedExit;
55
+ long unusedKernel;
56
+ long unusedUser;
57
+ Require(Native.GetProcessTimes(parent.DangerousGetHandle(), out parentCreated,
58
+ out unusedExit, out unusedKernel, out unusedUser));
59
+ Require(Native.GetProcessTimes(current, out selfCreated,
60
+ out unusedExit, out unusedKernel, out unusedUser));
61
+ if (parentCreated > selfCreated ||
62
+ Native.WaitForSingleObject(parent, 0) != 0x00000102)
63
+ {
64
+ throw new InvalidOperationException();
65
+ }
66
+
67
+ stage = "create-drain-marker";
68
+ Native.SetLastError(0);
69
+ drainMarker = Native.CreateEventW(IntPtr.Zero, true, false,
70
+ DrainEventName(Native.GetCurrentProcessId(), selfCreated));
71
+ int creationError = Marshal.GetLastWin32Error();
72
+ Require(!drainMarker.IsInvalid);
73
+ if (creationError == 183)
74
+ {
75
+ throw new InvalidOperationException();
76
+ }
77
+
78
+ stage = "create-job";
79
+ // NULL security attributes make the unnamed job handle non-inheritable.
80
+ // This helper was born before assignment and remains outside this job.
81
+ job = Native.CreateJobObjectW(IntPtr.Zero, null);
82
+ Require(!job.IsInvalid);
83
+ var limits = new Native.ExtendedLimitInformation();
84
+ limits.Basic.LimitFlags = 0x00002000; // KILL_ON_JOB_CLOSE, no breakaway flags.
85
+ Require(Native.SetInformationJobObject(job, 9, ref limits, Marshal.SizeOf(limits)));
86
+
87
+ stage = "assign";
88
+ Require(Native.AssignProcessToJobObject(job, parent));
89
+ bool member;
90
+ Require(Native.IsProcessInJob(parent, job, out member));
91
+ if (!member)
92
+ {
93
+ throw new InvalidOperationException();
94
+ }
95
+
96
+ // The bridge awaits this line before reading config or spawning any work.
97
+ // All subsequent children inherit membership atomically in CreateProcess.
98
+ stage = "ready";
99
+ Console.Out.WriteLine("MCP_JOB_READY " + Native.GetCurrentProcessId() + " " +
100
+ selfCreated.ToString(CultureInfo.InvariantCulture));
101
+ Console.Out.Flush();
102
+ stage = "wait";
103
+ if (Native.WaitForSingleObject(parent, 0xffffffff) != 0)
104
+ {
105
+ throw new Win32Exception(Marshal.GetLastWin32Error());
106
+ }
107
+
108
+ stage = "drain";
109
+ Require(Native.TerminateJobObject(job, 1));
110
+ var deadline = Stopwatch.StartNew();
111
+ while (true)
112
+ {
113
+ var accounting = new Native.BasicAccountingInformation();
114
+ int returned;
115
+ Require(Native.QueryInformationJobObject(job, 1, out accounting,
116
+ Marshal.SizeOf(accounting), out returned));
117
+ if (accounting.ActiveProcesses == 0)
118
+ {
119
+ break;
120
+ }
121
+
122
+ if (deadline.ElapsedMilliseconds >= 5000)
123
+ {
124
+ throw new TimeoutException();
125
+ }
126
+
127
+ Thread.Sleep(10);
128
+ }
129
+
130
+ stage = "mark-drained";
131
+ Require(Native.SetEvent(drainMarker));
132
+ return 0;
133
+ }
134
+ catch (Exception error)
135
+ {
136
+ var native = error as Win32Exception;
137
+ Console.Error.WriteLine("MCP_JOB_ERROR stage=" + stage +
138
+ " type=" + error.GetType().Name +
139
+ (native == null ? "" : " nativeError=" + native.NativeErrorCode));
140
+ return 1;
141
+ }
142
+ finally
143
+ {
144
+ // Closing the only job handle terminates remaining members even when
145
+ // Node could not run its cleanup. Helper failure is also fail-closed.
146
+ if (job != null)
147
+ {
148
+ job.Dispose();
149
+ }
150
+
151
+ if (drainMarker != null)
152
+ {
153
+ drainMarker.Dispose();
154
+ }
155
+
156
+ if (parent != null)
157
+ {
158
+ parent.Dispose();
159
+ }
160
+ }
161
+ }
162
+
163
+ private int ObserveOwner(string[] args)
164
+ {
165
+ SafeNativeHandle owner = null;
166
+ SafeNativeHandle parent = null;
167
+ SafeNativeHandle drainMarker = null;
168
+ try
169
+ {
170
+ int ownerId = 0;
171
+ long expectedCreated = 0;
172
+ bool validRequest = args.Length == 3 &&
173
+ int.TryParse(args[1], out ownerId) &&
174
+ long.TryParse(args[2], NumberStyles.None, CultureInfo.InvariantCulture, out expectedCreated) &&
175
+ ownerId > 0 &&
176
+ expectedCreated > 0;
177
+ if (!validRequest)
178
+ {
179
+ throw new ArgumentException();
180
+ }
181
+
182
+ stage = "observe-parent";
183
+ parent = Native.OpenProcess(0x00100000 | 0x1000, false, GetParentId());
184
+ Require(!parent.IsInvalid);
185
+ long selfCreated = GetCreationTime(new IntPtr(-1));
186
+ if (GetCreationTime(parent.DangerousGetHandle()) > selfCreated ||
187
+ Native.WaitForSingleObject(parent, 0) != 0x00000102)
188
+ {
189
+ throw new InvalidOperationException();
190
+ }
191
+
192
+ stage = "observe-owner";
193
+ owner = Native.OpenProcess(0x00100000 | 0x1000, false, ownerId);
194
+ Require(!owner.IsInvalid);
195
+ if (GetCreationTime(owner.DangerousGetHandle()) != expectedCreated ||
196
+ !string.Equals(GetImage(owner.DangerousGetHandle()), GetImage(new IntPtr(-1)),
197
+ StringComparison.OrdinalIgnoreCase))
198
+ {
199
+ throw new InvalidOperationException();
200
+ }
201
+
202
+ stage = "observe-marker";
203
+ drainMarker = Native.OpenEventW(0x00100000, false, DrainEventName((uint)ownerId, expectedCreated));
204
+ Require(!drainMarker.IsInvalid);
205
+
206
+ // Both identities and the read-only drain event are held before stop.
207
+ Console.Out.WriteLine("MCP_JOB_OBSERVER_READY");
208
+ Console.Out.Flush();
209
+ var handles = new[] { owner.DangerousGetHandle(), parent.DangerousGetHandle() };
210
+ uint result = Native.WaitForMultipleObjects(2, handles, false, 0xffffffff);
211
+ stage = "observe-exit";
212
+ if (result != 0)
213
+ {
214
+ throw new InvalidOperationException();
215
+ }
216
+
217
+ uint code;
218
+ Require(Native.GetExitCodeProcess(owner, out code));
219
+ if (code != 0)
220
+ {
221
+ throw new Win32Exception(unchecked((int)code));
222
+ }
223
+
224
+ stage = "verify-drain";
225
+ uint drainState = Native.WaitForSingleObject(drainMarker, 0);
226
+ if (drainState == 0xffffffff)
227
+ {
228
+ throw new Win32Exception(Marshal.GetLastWin32Error());
229
+ }
230
+
231
+ if (drainState != 0)
232
+ {
233
+ throw new Win32Exception(unchecked((int)drainState));
234
+ }
235
+
236
+ // Exit zero alone is not proof: even TerminateProcess(..., 0) must
237
+ // not pass without the event set after actual zero job accounting.
238
+ Console.Out.WriteLine("MCP_JOB_DRAINED");
239
+ Console.Out.Flush();
240
+ return 0;
241
+ }
242
+ catch (Exception error)
243
+ {
244
+ var native = error as Win32Exception;
245
+ Console.Error.WriteLine("MCP_JOB_ERROR stage=" + stage +
246
+ " type=" + error.GetType().Name +
247
+ (native == null ? "" : " nativeError=" + native.NativeErrorCode));
248
+ return 1;
249
+ }
250
+ finally
251
+ {
252
+ if (drainMarker != null)
253
+ {
254
+ drainMarker.Dispose();
255
+ }
256
+
257
+ if (owner != null)
258
+ {
259
+ owner.Dispose();
260
+ }
261
+
262
+ if (parent != null)
263
+ {
264
+ parent.Dispose();
265
+ }
266
+ }
267
+ }
268
+
269
+ private int GetParentId()
270
+ {
271
+ var basic = new Native.ProcessBasicInformation();
272
+ int returned;
273
+ int status = Native.NtQueryInformationProcess(
274
+ new IntPtr(-1), 0, out basic, Marshal.SizeOf(basic), out returned);
275
+ if (status != 0)
276
+ {
277
+ throw new Win32Exception(status);
278
+ }
279
+
280
+ return checked((int)basic.ParentId.ToInt64());
281
+ }
282
+
283
+ private long GetCreationTime(IntPtr process)
284
+ {
285
+ long created;
286
+ long exit;
287
+ long kernel;
288
+ long user;
289
+ Require(Native.GetProcessTimes(process, out created, out exit, out kernel, out user));
290
+ return created;
291
+ }
292
+
293
+ private string GetImage(IntPtr process)
294
+ {
295
+ var name = new StringBuilder(32768);
296
+ int length = name.Capacity;
297
+ Require(Native.QueryFullProcessImageNameW(process, 0, name, ref length));
298
+ return name.ToString();
299
+ }
300
+
301
+ private string DrainEventName(uint processId, long creation)
302
+ {
303
+ return "Local\\mcp-pacemaker-drained-v1-" +
304
+ processId.ToString(CultureInfo.InvariantCulture) + "-" +
305
+ creation.ToString(CultureInfo.InvariantCulture);
306
+ }
307
+
308
+ private void Require(bool succeeded)
309
+ {
310
+ if (!succeeded)
311
+ {
312
+ throw new Win32Exception(Marshal.GetLastWin32Error());
313
+ }
314
+ }
315
+ }
316
+
317
+ internal sealed class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
318
+ {
319
+ public SafeNativeHandle() : base(true)
320
+ {
321
+ }
322
+
323
+ protected override bool ReleaseHandle()
324
+ {
325
+ return Native.CloseHandle(handle);
326
+ }
327
+ }
328
+
329
+ internal static class Native
330
+ {
331
+ [StructLayout(LayoutKind.Sequential)]
332
+ internal struct ProcessBasicInformation
333
+ {
334
+ internal IntPtr Reserved1;
335
+ internal IntPtr Peb;
336
+ internal IntPtr Reserved2;
337
+ internal IntPtr Reserved3;
338
+ internal IntPtr ProcessId;
339
+ internal IntPtr ParentId;
340
+ }
341
+
342
+ [StructLayout(LayoutKind.Sequential)]
343
+ internal struct BasicLimitInformation
344
+ {
345
+ internal long ProcessTime;
346
+ internal long JobTime;
347
+ internal uint LimitFlags;
348
+ internal UIntPtr MinimumWorkingSet;
349
+ internal UIntPtr MaximumWorkingSet;
350
+ internal uint ActiveProcessLimit;
351
+ internal UIntPtr Affinity;
352
+ internal uint PriorityClass;
353
+ internal uint SchedulingClass;
354
+ }
355
+
356
+ [StructLayout(LayoutKind.Sequential)]
357
+ internal struct IoCounters
358
+ {
359
+ internal ulong ReadOperations;
360
+ internal ulong WriteOperations;
361
+ internal ulong OtherOperations;
362
+ internal ulong ReadBytes;
363
+ internal ulong WriteBytes;
364
+ internal ulong OtherBytes;
365
+ }
366
+
367
+ [StructLayout(LayoutKind.Sequential)]
368
+ internal struct ExtendedLimitInformation
369
+ {
370
+ internal BasicLimitInformation Basic;
371
+ internal IoCounters Io;
372
+ internal UIntPtr ProcessMemoryLimit;
373
+ internal UIntPtr JobMemoryLimit;
374
+ internal UIntPtr PeakProcessMemory;
375
+ internal UIntPtr PeakJobMemory;
376
+ }
377
+
378
+ [StructLayout(LayoutKind.Sequential)]
379
+ internal struct BasicAccountingInformation
380
+ {
381
+ internal long UserTime;
382
+ internal long KernelTime;
383
+ internal long PeriodUserTime;
384
+ internal long PeriodKernelTime;
385
+ internal uint PageFaults;
386
+ internal uint TotalProcesses;
387
+ internal uint ActiveProcesses;
388
+ internal uint TerminatedProcesses;
389
+ }
390
+
391
+ [DllImport("ntdll.dll")]
392
+ internal static extern int NtQueryInformationProcess(IntPtr process, int informationClass,
393
+ out ProcessBasicInformation information, int length, out int returned);
394
+
395
+ [DllImport("kernel32.dll", SetLastError = true)]
396
+ internal static extern SafeNativeHandle OpenProcess(uint access,
397
+ [MarshalAs(UnmanagedType.Bool)] bool inherit, int processId);
398
+
399
+ [DllImport("kernel32.dll", SetLastError = true)]
400
+ [return: MarshalAs(UnmanagedType.Bool)]
401
+ internal static extern bool GetProcessTimes(IntPtr process, out long creation,
402
+ out long exit, out long kernel, out long user);
403
+
404
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
405
+ internal static extern SafeNativeHandle CreateJobObjectW(IntPtr attributes, string name);
406
+
407
+ [DllImport("kernel32.dll", SetLastError = true)]
408
+ [return: MarshalAs(UnmanagedType.Bool)]
409
+ internal static extern bool SetInformationJobObject(SafeNativeHandle job, int informationClass,
410
+ ref ExtendedLimitInformation information, int length);
411
+
412
+ [DllImport("kernel32.dll", SetLastError = true)]
413
+ [return: MarshalAs(UnmanagedType.Bool)]
414
+ internal static extern bool AssignProcessToJobObject(SafeNativeHandle job, SafeNativeHandle process);
415
+
416
+ [DllImport("kernel32.dll", SetLastError = true)]
417
+ [return: MarshalAs(UnmanagedType.Bool)]
418
+ internal static extern bool IsProcessInJob(SafeNativeHandle process, SafeNativeHandle job,
419
+ [MarshalAs(UnmanagedType.Bool)] out bool result);
420
+
421
+ [DllImport("kernel32.dll", SetLastError = true)]
422
+ internal static extern uint WaitForSingleObject(SafeNativeHandle handle, uint milliseconds);
423
+
424
+ [DllImport("kernel32.dll", SetLastError = true)]
425
+ internal static extern uint WaitForMultipleObjects(uint count, IntPtr[] handles,
426
+ [MarshalAs(UnmanagedType.Bool)] bool waitAll, uint milliseconds);
427
+
428
+ [DllImport("kernel32.dll", SetLastError = true)]
429
+ [return: MarshalAs(UnmanagedType.Bool)]
430
+ internal static extern bool GetExitCodeProcess(SafeNativeHandle process, out uint code);
431
+
432
+ [DllImport("kernel32.dll")]
433
+ internal static extern uint GetCurrentProcessId();
434
+
435
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
436
+ [return: MarshalAs(UnmanagedType.Bool)]
437
+ internal static extern bool QueryFullProcessImageNameW(IntPtr process, uint flags,
438
+ StringBuilder name, ref int length);
439
+
440
+ [DllImport("kernel32.dll", SetLastError = true)]
441
+ [return: MarshalAs(UnmanagedType.Bool)]
442
+ internal static extern bool TerminateJobObject(SafeNativeHandle job, uint exitCode);
443
+
444
+ [DllImport("kernel32.dll", SetLastError = true)]
445
+ [return: MarshalAs(UnmanagedType.Bool)]
446
+ internal static extern bool QueryInformationJobObject(SafeNativeHandle job, int informationClass,
447
+ out BasicAccountingInformation information, int length, out int returned);
448
+
449
+ [DllImport("kernel32.dll")]
450
+ internal static extern void SetLastError(uint code);
451
+
452
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
453
+ internal static extern SafeNativeHandle CreateEventW(IntPtr attributes,
454
+ [MarshalAs(UnmanagedType.Bool)] bool manualReset,
455
+ [MarshalAs(UnmanagedType.Bool)] bool initialState, string name);
456
+
457
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
458
+ internal static extern SafeNativeHandle OpenEventW(uint access,
459
+ [MarshalAs(UnmanagedType.Bool)] bool inherit, string name);
460
+
461
+ [DllImport("kernel32.dll", SetLastError = true)]
462
+ [return: MarshalAs(UnmanagedType.Bool)]
463
+ internal static extern bool SetEvent(SafeNativeHandle handle);
464
+
465
+ [DllImport("kernel32.dll", SetLastError = true)]
466
+ [return: MarshalAs(UnmanagedType.Bool)]
467
+ internal static extern bool CloseHandle(IntPtr handle);
468
+ }
@@ -0,0 +1,218 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, statSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const HELPER = fileURLToPath(new URL('./windows-lifetime/ProcessLifetimeHelper.exe', import.meta.url));
7
+ const METADATA = fileURLToPath(new URL('./windows-lifetime/ProcessLifetimeHelper.build.json', import.meta.url));
8
+ // A detached helper must not hold the caller's working directory open; that directory can be an
9
+ // instance, config or staging path the caller is entitled to replace or remove. Its own packaged
10
+ // directory is already locked while the running image is mapped, so it adds no new constraint.
11
+ const HELPER_DIRECTORY = fileURLToPath(new URL('./windows-lifetime/', import.meta.url));
12
+
13
+ class WindowsProcessLifetime {
14
+ identityFailure(reason) {
15
+ const error = new Error(`Windows packaged helper identity check failed: ${reason}`);
16
+ error.code = 'MCP_HELPER_IDENTITY';
17
+ return error;
18
+ }
19
+
20
+ verifyHelper() {
21
+ try {
22
+ const bytes = readFileSync(METADATA);
23
+ if (bytes.length > 64 * 1024) throw this.identityFailure('oversized metadata');
24
+ const metadata = JSON.parse(bytes);
25
+ const digest = metadata.binarySha256;
26
+ const valid = metadata.schemaVersion === 1 &&
27
+ metadata.binary === 'ProcessLifetimeHelper.exe' &&
28
+ typeof digest === 'string' &&
29
+ digest.length === 64 &&
30
+ [...digest].every((character) => '0123456789abcdef'.includes(character));
31
+ if (!valid) throw this.identityFailure('invalid recorded SHA256');
32
+ const file = statSync(HELPER);
33
+ if (!file.isFile() ||
34
+ file.size === 0 ||
35
+ file.size > 4 * 1024 * 1024) {
36
+ throw this.identityFailure('invalid binary');
37
+ }
38
+ const actual = createHash('sha256').update(readFileSync(HELPER)).digest('hex');
39
+ if (actual !== digest) throw this.identityFailure('binary SHA256 mismatch');
40
+ } catch (error) {
41
+ if (error.code === 'MCP_HELPER_IDENTITY') throw error;
42
+ throw this.identityFailure(error.code || 'unreadable metadata or binary');
43
+ }
44
+ }
45
+
46
+ decimal(value, maximumLength) {
47
+ return typeof value === 'string' &&
48
+ value.length > 0 &&
49
+ value.length <= maximumLength &&
50
+ value[0] !== '0' &&
51
+ [...value].every((character) => character >= '0' && character <= '9');
52
+ }
53
+
54
+ async start() {
55
+ this.verifyHelper();
56
+ const child = spawn(HELPER, ['watch-parent', String(process.pid)], {
57
+ cwd: HELPER_DIRECTORY, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
58
+ });
59
+ let ready = false;
60
+ let stdout = '';
61
+ let stderr = '';
62
+ let timer;
63
+ try {
64
+ return await new Promise((resolve, reject) => {
65
+ const fail = (detail) => {
66
+ const error = new Error(`Windows process containment failed: ${detail}`);
67
+ if (ready) {
68
+ console.error(error.message);
69
+ process.exit(1);
70
+ }
71
+ reject(error);
72
+ };
73
+ child.on('error', (error) => fail(error.code || 'helper launch error'));
74
+ child.stderr.on('data', (chunk) => {
75
+ stderr = (stderr + chunk).slice(-2048);
76
+ });
77
+ child.stdout.on('error', (error) => fail(error.code || 'helper stdout error'));
78
+ child.stderr.on('error', (error) => fail(error.code || 'helper stderr error'));
79
+ child.stdout.on('data', (chunk) => {
80
+ stdout += chunk;
81
+ if (stdout.length > 128) {
82
+ fail('invalid helper readiness response');
83
+ return;
84
+ }
85
+ if (!stdout.endsWith('\n')) return;
86
+ let line = stdout.slice(0, -1);
87
+ if (line.endsWith('\r')) line = line.slice(0, -1);
88
+ const identity = line.split(' ');
89
+ const valid = identity.length === 3 &&
90
+ identity[0] === 'MCP_JOB_READY' &&
91
+ this.decimal(identity[1], 10) &&
92
+ this.decimal(identity[2], 19) &&
93
+ Number(identity[1]) === child.pid;
94
+ if (valid) {
95
+ ready = true;
96
+ child.unref?.();
97
+ child.stdout.unref?.();
98
+ child.stderr.unref?.();
99
+ resolve({ protocol: 1, ownerPid: child.pid, ownerCreationTime: identity[2] });
100
+ } else {
101
+ fail('invalid helper readiness response');
102
+ }
103
+ });
104
+ child.on('exit', (code, signal) => {
105
+ if (!ready) {
106
+ fail(`${stderr.trim() || 'helper exited before assignment'} (code ${code}, signal ${signal})`);
107
+ } else {
108
+ // Normally the job closes before this callback can run. Never keep
109
+ // serving if its sole lifetime owner has unexpectedly exited.
110
+ console.error('Windows process containment owner exited; stopping bridge.');
111
+ process.exit(1);
112
+ }
113
+ });
114
+ timer = setTimeout(() => fail('helper readiness deadline exceeded'), 5000);
115
+ });
116
+ } catch (error) {
117
+ if (child.exitCode == null &&
118
+ child.signalCode == null) child.kill();
119
+ throw error;
120
+ } finally {
121
+ clearTimeout(timer);
122
+ }
123
+ }
124
+
125
+ async observe(identity) {
126
+ const valid = identity?.protocol === 1 &&
127
+ Number.isSafeInteger(identity.ownerPid) &&
128
+ identity.ownerPid > 0 &&
129
+ this.decimal(identity.ownerCreationTime, 19);
130
+ if (!valid) throw new Error('Invalid Windows lifetime owner identity');
131
+ this.verifyHelper();
132
+ const child = spawn(HELPER, ['observe-owner', String(identity.ownerPid), identity.ownerCreationTime], {
133
+ cwd: HELPER_DIRECTORY, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
134
+ });
135
+ let ready = false;
136
+ let drained = false;
137
+ let stdout = '';
138
+ let stderr = '';
139
+ let timer;
140
+ let resolveReady;
141
+ let rejectReady;
142
+ let resolveDone;
143
+ let rejectDone;
144
+ const armed = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
145
+ const done = new Promise((resolve, reject) => { resolveDone = resolve; rejectDone = reject; });
146
+ // The same error is surfaced by armed when failure precedes API return.
147
+ // Keep done rejectable for callers without a premature unhandled rejection.
148
+ done.catch(() => {});
149
+ const fail = (detail) => {
150
+ const error = new Error(`Windows lifetime verification failed: ${detail}`);
151
+ rejectReady(error);
152
+ rejectDone(error);
153
+ if (child.exitCode == null &&
154
+ child.signalCode == null) child.kill();
155
+ };
156
+ child.on('error', (error) => fail(error.code || 'observer launch error'));
157
+ child.stdout.on('error', (error) => fail(error.code || 'observer stdout error'));
158
+ child.stderr.on('error', (error) => fail(error.code || 'observer stderr error'));
159
+ child.stderr.on('data', (chunk) => { stderr = (stderr + chunk).slice(-2048); });
160
+ child.stdout.on('data', (chunk) => {
161
+ stdout += chunk;
162
+ if (stdout.length > 256) {
163
+ fail('invalid observer response');
164
+ child.kill();
165
+ return;
166
+ }
167
+ let newline;
168
+ while ((newline = stdout.indexOf('\n')) !== -1) {
169
+ let line = stdout.slice(0, newline);
170
+ if (line.endsWith('\r')) line = line.slice(0, -1);
171
+ stdout = stdout.slice(newline + 1);
172
+ if (!ready &&
173
+ line === 'MCP_JOB_OBSERVER_READY') {
174
+ ready = true;
175
+ resolveReady();
176
+ } else if (ready &&
177
+ !drained &&
178
+ line === 'MCP_JOB_DRAINED') {
179
+ drained = true;
180
+ } else {
181
+ fail('invalid observer protocol');
182
+ child.kill();
183
+ }
184
+ }
185
+ });
186
+ child.on('close', (code, signal) => {
187
+ if (code === 0 &&
188
+ signal === null &&
189
+ ready &&
190
+ drained &&
191
+ stdout === '' &&
192
+ stderr === '') {
193
+ resolveDone({ verified: true, ownerExited: true, activeProcesses: 0 });
194
+ } else {
195
+ fail(`${stderr.trim() || 'no verified job drain'} (code ${code}, signal ${signal})`);
196
+ }
197
+ });
198
+ timer = setTimeout(() => fail('observer readiness deadline exceeded'), 5000);
199
+ try {
200
+ await armed;
201
+ return { done, pid: child.pid };
202
+ } catch (error) {
203
+ if (child.exitCode == null &&
204
+ child.signalCode == null) child.kill();
205
+ throw error;
206
+ } finally {
207
+ clearTimeout(timer);
208
+ }
209
+ }
210
+ }
211
+
212
+ export async function ensureWindowsProcessLifetime() {
213
+ return process.platform === 'win32' ? new WindowsProcessLifetime().start() : null;
214
+ }
215
+
216
+ export async function observeWindowsProcessLifetime(identity) {
217
+ return process.platform === 'win32' ? new WindowsProcessLifetime().observe(identity) : null;
218
+ }