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,961 @@
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.Management;
8
+ using System.Runtime.InteropServices;
9
+ using System.Security.Cryptography;
10
+ using System.Security.Principal;
11
+ using System.Text;
12
+ using System.Threading;
13
+ using System.Web.Script.Serialization;
14
+
15
+ internal sealed class LegacyProcessBroker
16
+ {
17
+ // A plan names only the two roots. Plans written by 2.1.0 also list up to 510 descendants,
18
+ // and recovery still verifies those, so plan bounds stay at their 2.1.0 values.
19
+ private const int MaximumPlanIdentities = 512;
20
+ private const int MaximumPlanBytes = 240 * 1024;
21
+ private const int MaximumMessageBytes = 262144;
22
+ // One query reads both roots' command lines in each phase.
23
+ private const int MaximumArgumentQueries = 1;
24
+ // Descendants are found while stopping. One busy 1.3 host had 533 processes in its tree.
25
+ private const int MaximumSweptProcesses = 2048;
26
+ private const int MaximumSweepRounds = 16;
27
+ private const int MaximumPinPasses = 8;
28
+ private const long RequestMilliseconds = 15000;
29
+ private const long PhaseMilliseconds = 120000;
30
+ private const long StopMilliseconds = 20000;
31
+ private const long WatchdogMarginMilliseconds = 10000;
32
+ private readonly LegacyNative native = new LegacyNative();
33
+ private readonly JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = MaximumMessageBytes, RecursionLimit = 20 };
34
+ private readonly List<Held> held = new List<Held>();
35
+ private readonly HashSet<string> rejected = new HashSet<string>();
36
+ // The processes that started this broker, by PID and creation time; the sweep never adopts them.
37
+ private readonly Dictionary<int, long> ancestors = new Dictionary<int, long>();
38
+ private readonly int brokerId = Process.GetCurrentProcess().Id;
39
+ private bool swept;
40
+ private readonly List<FileStream> scripts = new List<FileStream>();
41
+ private readonly string user = WindowsIdentity.GetCurrent().User.Value;
42
+ private readonly Stopwatch clock = Stopwatch.StartNew();
43
+ private long deadline = RequestMilliseconds;
44
+ private volatile bool committed;
45
+ private string stage = "request";
46
+ private string root;
47
+ private int port;
48
+ private string token;
49
+ private Plan plan;
50
+ private object captureFailure;
51
+ private LegacySnapshot snapshot;
52
+ private LegacyArgumentQueries arguments;
53
+ private readonly Stopwatch phaseClock = new Stopwatch();
54
+ private object captureDiagnostics;
55
+ private object revalidationDiagnostics;
56
+ private object brokerIdentity;
57
+
58
+ private static int Main(string[] args)
59
+ {
60
+ return new LegacyProcessBroker().Run(args);
61
+ }
62
+
63
+ private int Run(string[] args)
64
+ {
65
+ try
66
+ {
67
+ if (args.Length != 0) throw new ArgumentException();
68
+ Console.SetIn(new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false, true), false));
69
+ Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false, true)) { AutoFlush = true });
70
+ StartWatchdog();
71
+ var request = Read();
72
+ // Phase budgets start once the request is read, so a slow broker start on a loaded
73
+ // host does not consume the capture budget.
74
+ Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + PhaseMilliseconds + WatchdogMarginMilliseconds);
75
+ if (request.action == "verify")
76
+ {
77
+ ValidatePlan(request.expected);
78
+ Write(new { ok = true, receipt = VerifyGone(request.expected), brokerIdentity });
79
+ return 0;
80
+ }
81
+ if (request.action != "prepare") throw new ArgumentException();
82
+ if (request.expected != null) ValidatePlan(request.expected);
83
+ root = Path.GetFullPath(request.root).TrimEnd(Path.DirectorySeparatorChar);
84
+ port = request.port;
85
+ if (!Path.IsPathRooted(request.root) ||
86
+ port < 1 ||
87
+ port > 65535 ||
88
+ !Directory.Exists(root)) throw new ArgumentException();
89
+ stage = "capture-roots";
90
+ phaseClock.Start();
91
+ arguments = new LegacyArgumentQueries(CheckPhase);
92
+ arguments.BeginPhase(MaximumArgumentQueries);
93
+ Held bridge;
94
+ Held supervisor;
95
+ using (var images = new LegacyImageCache(CheckPhase))
96
+ {
97
+ bridge = Capture(native.Listener(port));
98
+ supervisor = Capture(bridge.Info.parentPid);
99
+ if (bridge.Info.pid == supervisor.Info.pid ||
100
+ bridge.Created < supervisor.Created) throw new InvalidOperationException();
101
+ bridge.Info.parentCreationTime = supervisor.Info.creationTime;
102
+ bridge.KeepArguments = true;
103
+ supervisor.KeepArguments = true;
104
+ var roots = new List<Held> { bridge, supervisor };
105
+ CaptureFacts(roots, images);
106
+ CheckRootArguments(bridge, supervisor);
107
+ RefreshSnapshot();
108
+ if (!snapshot.Contains(bridge.Info.pid, supervisor.Info.pid) ||
109
+ !snapshot.Contains(supervisor.Info.pid, supervisor.Info.parentPid))
110
+ throw new InvalidOperationException("Snapshot root changed");
111
+ CheckAncestors(bridge, supervisor);
112
+ if (!native.Alive(supervisor.Handle) ||
113
+ !native.Alive(bridge.Handle) ||
114
+ native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
115
+ stage = "plan";
116
+ plan = MakePlan(supervisor, bridge);
117
+ int planBytes = PlanBytes(plan);
118
+ CheckPhase();
119
+ captureDiagnostics = new { elapsedMilliseconds = phaseClock.ElapsedMilliseconds,
120
+ argumentQueries = arguments.Count, imageFiles = images.Count,
121
+ imageBytesHashed = images.Bytes, planUtf8Bytes = planBytes, memory = native.Memory() };
122
+ }
123
+ if (request.expected != null &&
124
+ !SameRoots(request.expected, plan)) throw new InvalidOperationException("Plan changed");
125
+ token = NewToken();
126
+ Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + 120000);
127
+ Write(new { ok = true, plan, token, diagnostics = captureDiagnostics, brokerIdentity });
128
+ while (true)
129
+ {
130
+ request = Read();
131
+ if (request.action == "close")
132
+ {
133
+ Write(new { ok = true, closed = true, brokerIdentity });
134
+ return 0;
135
+ }
136
+ if (request.action == "status")
137
+ {
138
+ Write(new { ok = true, receipt = HeldStatus(), brokerIdentity });
139
+ continue;
140
+ }
141
+ if (request.action != "stop" ||
142
+ request.token != token ||
143
+ request.planSha256 != plan.planSha256) throw new ArgumentException();
144
+ stage = "revalidate";
145
+ phaseClock.Restart();
146
+ Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + PhaseMilliseconds + WatchdogMarginMilliseconds);
147
+ arguments.BeginPhase(MaximumArgumentQueries);
148
+ Revalidate(supervisor, bridge);
149
+ arguments.Dispose();
150
+ arguments = null;
151
+ committed = true;
152
+ stage = "stop";
153
+ Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + StopMilliseconds + WatchdogMarginMilliseconds);
154
+ var stopWatch = Stopwatch.StartNew();
155
+ var receipt = Stop(supervisor, bridge);
156
+ Write(new { ok = true, receipt, diagnostics = new {
157
+ revalidation = revalidationDiagnostics, stopMilliseconds = stopWatch.ElapsedMilliseconds,
158
+ memory = native.Memory() }, brokerIdentity });
159
+ return 0;
160
+ }
161
+ }
162
+ catch (Exception error)
163
+ {
164
+ if (arguments != null) { arguments.Dispose(); arguments = null; }
165
+ var win32 = error as Win32Exception;
166
+ captureFailure = captureFailure ?? native.FailureDetails(error);
167
+ Write(new { ok = false, error = "LEGACY_PROCESS_REFUSED", stage,
168
+ type = error.GetType().Name, nativeError = win32 == null ? 0 : win32.NativeErrorCode,
169
+ reason = RefusalReason(error), captureFailure, mutationStarted = committed, treeCompleteness = "unproven", brokerIdentity });
170
+ return 1;
171
+ }
172
+ finally
173
+ {
174
+ if (arguments != null) arguments.Dispose();
175
+ foreach (var process in held) process.Handle.Dispose();
176
+ foreach (var script in scripts) script.Dispose();
177
+ }
178
+ }
179
+
180
+ private void StartWatchdog()
181
+ {
182
+ var self = Api.OpenProcess(0x1000, false, Process.GetCurrentProcess().Id);
183
+ int parentId;
184
+ long selfCreated;
185
+ using (self)
186
+ {
187
+ native.Require(!self.IsInvalid);
188
+ parentId = native.Parent(self);
189
+ selfCreated = native.Creation(self);
190
+ brokerIdentity = new { pid = Process.GetCurrentProcess().Id,
191
+ creationTime = selfCreated.ToString(CultureInfo.InvariantCulture) };
192
+ }
193
+ var parent = Api.OpenProcess(0x00100000 | 0x1000, false, parentId);
194
+ native.Require(!parent.IsInvalid);
195
+ if (native.Creation(parent) > selfCreated ||
196
+ !native.Alive(parent))
197
+ {
198
+ parent.Dispose();
199
+ throw new InvalidOperationException("Broker parent identity lost");
200
+ }
201
+ var thread = new Thread(() =>
202
+ {
203
+ using (parent)
204
+ {
205
+ while (clock.ElapsedMilliseconds < Interlocked.Read(ref deadline))
206
+ {
207
+ uint wait = Api.WaitForSingleObject(parent, 100);
208
+ if (wait == 0 &&
209
+ !committed) Environment.Exit(2);
210
+ if (wait == 0xffffffff) Environment.Exit(2);
211
+ if (wait == 0) Thread.Sleep(100);
212
+ }
213
+ Environment.Exit(3);
214
+ }
215
+ });
216
+ thread.IsBackground = true;
217
+ thread.Start();
218
+ }
219
+
220
+ private Request Read()
221
+ {
222
+ var line = new StringBuilder();
223
+ while (line.Length < MaximumMessageBytes)
224
+ {
225
+ int value = Console.In.Read();
226
+ if (value < 0) throw new EndOfStreamException();
227
+ if (value == 10)
228
+ {
229
+ var text = line.ToString();
230
+ if (Encoding.UTF8.GetByteCount(text) + 1 > MaximumMessageBytes) throw new ArgumentException("Request bounds");
231
+ return json.Deserialize<Request>(text);
232
+ }
233
+ line.Append((char)value);
234
+ }
235
+ throw new ArgumentException("Request bounds");
236
+ }
237
+
238
+ private void Write(object value)
239
+ {
240
+ Console.Out.WriteLine(json.Serialize(value));
241
+ Console.Out.Flush();
242
+ }
243
+
244
+ private Held Capture(int id)
245
+ {
246
+ CheckPhase();
247
+ var substage = LegacyFailureStage.CaptureOpenQuery;
248
+ long? observedBirth = null;
249
+ int? observedParent = null;
250
+ bool? observedAlive = null;
251
+ bool? observedOwnerMatch = null;
252
+ var handle = Api.OpenProcess(0x00100000 | 0x1000, false, id);
253
+ try
254
+ {
255
+ native.Require(!handle.IsInvalid, LegacyFailureApi.OpenProcessQuery);
256
+ substage = LegacyFailureStage.CaptureIdentity;
257
+ bool alive = native.Alive(handle);
258
+ observedAlive = alive;
259
+ if (!alive) throw new InvalidOperationException("Capture exited");
260
+ long created = native.Creation(handle);
261
+ observedBirth = created;
262
+ substage = LegacyFailureStage.CaptureParent;
263
+ int parentId = native.Parent(handle);
264
+ observedParent = parentId;
265
+ substage = LegacyFailureStage.CaptureOwner;
266
+ var owner = native.Owner(handle);
267
+ observedOwnerMatch = owner == user;
268
+ if (owner != user ||
269
+ id == Process.GetCurrentProcess().Id) throw new InvalidOperationException("Owner mismatch");
270
+ substage = LegacyFailureStage.CaptureOpenTerminate;
271
+ var terminable = Api.OpenProcess(0x00100000 | 0x1000 | 1, false, id);
272
+ try
273
+ {
274
+ native.Require(!terminable.IsInvalid, LegacyFailureApi.OpenProcessTerminate);
275
+ if (native.Creation(terminable) != created ||
276
+ !native.Alive(terminable)) throw new InvalidOperationException("Capture changed");
277
+ }
278
+ catch { terminable.Dispose(); throw; }
279
+ handle.Dispose();
280
+ handle = terminable;
281
+ substage = LegacyFailureStage.CaptureImage;
282
+ var image = native.Image(handle);
283
+ var info = new Identity { pid = id, creationTime = created.ToString(CultureInfo.InvariantCulture),
284
+ parentPid = parentId, ownerSid = owner, imagePath = image };
285
+ if (!native.Alive(handle)) throw new InvalidOperationException("Capture changed");
286
+ var result = new Held { Handle = handle, Info = info, Created = created };
287
+ held.Add(result);
288
+ return result;
289
+ }
290
+ catch (Exception error)
291
+ {
292
+ native.TagFailure(error, substage, id, observedBirth, observedParent,
293
+ alive: observedAlive, ownerMatched: observedOwnerMatch);
294
+ handle.Dispose();
295
+ throw;
296
+ }
297
+ }
298
+
299
+ private void CheckPhase()
300
+ {
301
+ if (phaseClock.ElapsedMilliseconds >= PhaseMilliseconds)
302
+ throw new InvalidOperationException("Capture bound");
303
+ }
304
+
305
+ private void RefreshSnapshot()
306
+ {
307
+ var current = new LegacySnapshot();
308
+ current.Read(CheckPhase);
309
+ snapshot = current;
310
+ }
311
+
312
+ private void CaptureFacts(List<Held> processes, LegacyImageCache images)
313
+ {
314
+ arguments.Read(processes, true);
315
+ foreach (var process in processes)
316
+ {
317
+ try
318
+ {
319
+ CheckPhase();
320
+ if (!native.Alive(process.Handle) ||
321
+ native.Creation(process.Handle) != process.Created ||
322
+ native.Owner(process.Handle) != user ||
323
+ native.Parent(process.Handle) != process.Info.parentPid ||
324
+ !Equal(native.Image(process.Handle), process.Info.imagePath))
325
+ throw new InvalidOperationException("Capture changed");
326
+ process.Info.imageSha256 = images.Hash(process.Info.imagePath);
327
+ }
328
+ catch (Exception error)
329
+ {
330
+ native.TagFailure(error, LegacyFailureStage.CaptureImageFacts, process.Info.pid,
331
+ process.Created, process.Info.parentPid, ownerMatched: true);
332
+ throw;
333
+ }
334
+ }
335
+ }
336
+
337
+ private int PlanBytes(Plan value)
338
+ {
339
+ try
340
+ {
341
+ int bytes = Encoding.UTF8.GetByteCount(json.Serialize(value));
342
+ if (bytes > MaximumPlanBytes) throw new InvalidOperationException("Plan byte bound");
343
+ return bytes;
344
+ }
345
+ catch (InvalidOperationException) { throw new InvalidOperationException("Plan byte bound"); }
346
+ }
347
+
348
+ private void CheckRootArguments(Held bridge, Held supervisor)
349
+ {
350
+ var bridgeScript = Path.Combine(root, "bin", "mcp-bridge.mjs");
351
+ var supervisorScript = Path.Combine(root, "supervisor", "supervise.ps1");
352
+ var bridgeArgs = bridge.Arguments;
353
+ var supervisorArgs = supervisor.Arguments;
354
+ var supervisorImage = Path.GetFileName(supervisor.Info.imagePath).ToLowerInvariant();
355
+ bool bridgeValid = string.Equals(Path.GetFileName(bridge.Info.imagePath), "node.exe", StringComparison.OrdinalIgnoreCase) &&
356
+ bridgeArgs.Length == 4 &&
357
+ EqualPath(bridgeArgs[1], bridgeScript) &&
358
+ bridgeArgs[2] == "--port" &&
359
+ bridgeArgs[3] == port.ToString(CultureInfo.InvariantCulture);
360
+ bool supervisorValid = (supervisorImage == "pwsh.exe" || supervisorImage == "powershell.exe") &&
361
+ supervisorArgs.Length == 10 &&
362
+ Equal(supervisorArgs[1], "-NoProfile") &&
363
+ Equal(supervisorArgs[2], "-ExecutionPolicy") &&
364
+ Equal(supervisorArgs[3], "Bypass") &&
365
+ Equal(supervisorArgs[4], "-WindowStyle") &&
366
+ Equal(supervisorArgs[5], "Hidden") &&
367
+ Equal(supervisorArgs[6], "-File") &&
368
+ EqualPath(supervisorArgs[7], supervisorScript) &&
369
+ Equal(supervisorArgs[8], "-Port") &&
370
+ supervisorArgs[9] == port.ToString(CultureInfo.InvariantCulture);
371
+ if (!bridgeValid ||
372
+ !supervisorValid) throw new InvalidOperationException("Root arguments mismatch");
373
+ bridge.Info.scriptSha256 = HoldScript(bridgeScript);
374
+ supervisor.Info.scriptSha256 = HoldScript(supervisorScript);
375
+ }
376
+
377
+ // The upgrade cannot stop the bridge it runs under. A CLI started from inside the 1.3
378
+ // tree, for example through an MCP server's shell, is refused here before any change.
379
+ private void CheckAncestors(Held bridge, Held supervisor)
380
+ {
381
+ int id = Process.GetCurrentProcess().Id;
382
+ long childCreated = long.MaxValue;
383
+ for (int depth = 0; depth < 64; depth++)
384
+ {
385
+ using (var process = Api.OpenProcess(0x1000, false, id))
386
+ {
387
+ if (process.IsInvalid) return;
388
+ long created = native.Creation(process);
389
+ // A parent PID that now names a newer process is not this process's parent.
390
+ if (created > childCreated) return;
391
+ bool insideRoots = id == bridge.Info.pid ||
392
+ id == supervisor.Info.pid;
393
+ if (insideRoots) throw new InvalidOperationException("Run the upgrade from a terminal the 1.3 bridge did not start");
394
+ ancestors[id] = created;
395
+ childCreated = created;
396
+ id = native.Parent(process);
397
+ }
398
+ }
399
+ }
400
+
401
+ private string HoldScript(string path)
402
+ {
403
+ var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
404
+ scripts.Add(stream);
405
+ if (stream.Length > 4 * 1024 * 1024) throw new InvalidOperationException("Script bound");
406
+ using (var hash = SHA256.Create())
407
+ return BitConverter.ToString(hash.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
408
+ }
409
+
410
+ private bool Equal(string left, string right) { return string.Equals(left, right, StringComparison.OrdinalIgnoreCase); }
411
+ private bool EqualPath(string left, string right)
412
+ {
413
+ // Target argv belongs to the target's cwd/drive context, never ours.
414
+ // Only unambiguous drive-absolute or UNC paths can identify its script.
415
+ if (!FullyQualifiedScriptPath(left))
416
+ throw new InvalidOperationException("Script argument not fully qualified");
417
+ return Equal(Path.GetFullPath(left), Path.GetFullPath(right));
418
+ }
419
+
420
+ private bool FullyQualifiedScriptPath(string value)
421
+ {
422
+ if (string.IsNullOrEmpty(value) ||
423
+ value.Length < 3) return false;
424
+ bool driveLetter = (value[0] >= 'A' && value[0] <= 'Z') ||
425
+ (value[0] >= 'a' && value[0] <= 'z');
426
+ if (driveLetter &&
427
+ value[1] == ':' &&
428
+ (value[2] == '\\' || value[2] == '/')) return true;
429
+ var path = value.Replace('/', '\\');
430
+ if (!path.StartsWith(@"\\", StringComparison.Ordinal) ||
431
+ path[2] == '?' ||
432
+ path[2] == '.') return false;
433
+ int serverEnd = path.IndexOf('\\', 2);
434
+ if (serverEnd <= 2 ||
435
+ serverEnd + 1 >= path.Length) return false;
436
+ int shareEnd = path.IndexOf('\\', serverEnd + 1);
437
+ if (shareEnd < 0) shareEnd = path.Length;
438
+ var share = path.Substring(serverEnd + 1, shareEnd - serverEnd - 1);
439
+ return share.Length > 0 &&
440
+ share != "." &&
441
+ share != "..";
442
+ }
443
+
444
+ private Plan MakePlan(Held supervisor, Held bridge)
445
+ {
446
+ var result = new Plan { protocol = 1, kind = "legacy-observed-process-set", port = port, root = root,
447
+ ownerSid = user, roots = new Roots { supervisor = supervisor.Info, bridge = bridge.Info },
448
+ observed = new Identity[0], excluded = new ExcludedIdentity[0], treeCompleteness = "unproven",
449
+ scope = "Selected roots only. When stopping, processes they started are found by parent and stopped; other processes are untouched." };
450
+ result.observedSetSha256 = native.Hash(Encoding.UTF8.GetBytes(json.Serialize(result.observed)));
451
+ result.planSha256 = DigestPlan(result);
452
+ return result;
453
+ }
454
+
455
+ // Sessions start and end all the time, so only the roots bind a reopened plan. A plan
456
+ // written by 2.1.0 also lists the descendants it captured; those are not compared.
457
+ private bool SameRoots(Plan expected, Plan actual)
458
+ {
459
+ return json.Serialize(RootBinding(expected)) == json.Serialize(RootBinding(actual));
460
+ }
461
+
462
+ private object RootBinding(Plan value)
463
+ {
464
+ return new { value.protocol, value.kind, value.port, value.root, value.ownerSid, value.roots };
465
+ }
466
+
467
+ private string DigestPlan(Plan value)
468
+ {
469
+ var saved = value.planSha256;
470
+ value.planSha256 = null;
471
+ var digest = native.Hash(Encoding.UTF8.GetBytes(json.Serialize(value)));
472
+ value.planSha256 = saved;
473
+ return digest;
474
+ }
475
+
476
+ private void Revalidate(Held supervisor, Held bridge)
477
+ {
478
+ if (!native.Alive(supervisor.Handle) ||
479
+ !native.Alive(bridge.Handle) ||
480
+ native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
481
+ var roots = new List<Held> { bridge, supervisor };
482
+ arguments.Read(roots, false);
483
+ using (var images = new LegacyImageCache(CheckPhase))
484
+ {
485
+ foreach (var process in roots)
486
+ {
487
+ CheckPhase();
488
+ if (!native.Alive(process.Handle) ||
489
+ native.Creation(process.Handle) != process.Created ||
490
+ native.Parent(process.Handle) != process.Info.parentPid ||
491
+ native.Owner(process.Handle) != user ||
492
+ !Equal(native.Image(process.Handle), process.Info.imagePath) ||
493
+ images.Hash(process.Info.imagePath) != process.Info.imageSha256)
494
+ throw new InvalidOperationException("Identity changed");
495
+ }
496
+ RefreshSnapshot();
497
+ if (!snapshot.Contains(bridge.Info.pid, supervisor.Info.pid) ||
498
+ !snapshot.Contains(supervisor.Info.pid, supervisor.Info.parentPid))
499
+ throw new InvalidOperationException("Snapshot held member changed");
500
+ if (!native.Alive(supervisor.Handle) ||
501
+ !native.Alive(bridge.Handle) ||
502
+ native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
503
+ CheckPhase();
504
+ revalidationDiagnostics = new { elapsedMilliseconds = phaseClock.ElapsedMilliseconds,
505
+ argumentQueries = arguments.Count, imageFiles = images.Count, imageBytesHashed = images.Bytes };
506
+ }
507
+ }
508
+
509
+ private Receipt Stop(Held supervisor, Held bridge)
510
+ {
511
+ var errors = new List<string>();
512
+ var stopClock = Stopwatch.StartNew();
513
+ // The supervisor goes first so it cannot start another bridge.
514
+ Kill(supervisor, stopClock, errors);
515
+ if (!native.Alive(supervisor.Handle))
516
+ {
517
+ // When a Node process exits, its job object ends its direct children with it (1.3
518
+ // wraps each bare server command in cmd.exe), and their own children then name a
519
+ // parent nobody holds. So the bridge's whole tree is held before the bridge stops.
520
+ bool settled;
521
+ Pin(stopClock, errors, out settled);
522
+ Kill(bridge, stopClock, errors);
523
+ if (!native.Alive(bridge.Handle)) Sweep(stopClock, errors);
524
+ }
525
+ var result = HeldStatus();
526
+ result.errors = errors.ToArray();
527
+ return result;
528
+ }
529
+
530
+ // A busy bridge starts and ends sessions every few seconds, so its descendants are found
531
+ // while stopping instead of being frozen into the plan. Each round first holds everything
532
+ // new that descends from a held process, then stops every held process not yet stopped
533
+ // and waits for it, even if the tree was still growing. The next round finds anything
534
+ // those processes started before they died.
535
+ private void Sweep(Stopwatch stopClock, List<string> errors)
536
+ {
537
+ try
538
+ {
539
+ for (int round = 0; round < MaximumSweepRounds; round++)
540
+ {
541
+ // Once the stop budget is spent nothing more can be terminated, so the sweep ends.
542
+ CheckStop(stopClock);
543
+ bool settled;
544
+ int pinned = Pin(stopClock, errors, out settled);
545
+ var terminations = new List<Termination>();
546
+ foreach (var process in held)
547
+ {
548
+ if (process.Stopping) continue;
549
+ var termination = Terminate(process, stopClock, errors);
550
+ if (termination != null) terminations.Add(termination);
551
+ }
552
+ bool done = settled &&
553
+ pinned == 0 &&
554
+ terminations.Count == 0;
555
+ if (done)
556
+ {
557
+ swept = true;
558
+ return;
559
+ }
560
+ foreach (var termination in terminations) AwaitExit(termination, stopClock, errors);
561
+ }
562
+ errors.Add("sweep:rounds");
563
+ }
564
+ catch (Exception error)
565
+ {
566
+ errors.Add("sweep:" + (RefusalReason(error) ?? error.GetType().Name));
567
+ }
568
+ }
569
+
570
+ // Holds every process that descends from a held process, repeating until a snapshot shows
571
+ // nothing new (settled). Every held handle keeps its PID from being reused, so a process
572
+ // whose parent PID is held and which started no earlier than that parent was started by it.
573
+ // A tree still growing after every pass is left to the caller's next round; a snapshot that
574
+ // cannot be read is reported. Nothing is stopped here, and nothing is thrown.
575
+ private int Pin(Stopwatch stopClock, List<string> errors, out bool settled)
576
+ {
577
+ settled = false;
578
+ int total = 0;
579
+ try
580
+ {
581
+ for (int pass = 0; pass < MaximumPinPasses; pass++)
582
+ {
583
+ var current = new LegacySnapshot();
584
+ current.Read(() => CheckStop(stopClock));
585
+ int adopted = 0;
586
+ // Processes held during this pass are walked in the same pass, so one snapshot
587
+ // covers the whole tree; later passes only find processes started since.
588
+ for (int index = 0; index < held.Count; index++)
589
+ {
590
+ var parent = held[index];
591
+ foreach (int id in Children(current, parent.Info.pid))
592
+ {
593
+ if (held.Exists(value => value.Info.pid == id)) continue;
594
+ if (Adopt(id, parent, errors) != null) adopted++;
595
+ }
596
+ }
597
+ total += adopted;
598
+ if (adopted == 0)
599
+ {
600
+ settled = true;
601
+ return total;
602
+ }
603
+ }
604
+ }
605
+ catch (Exception error)
606
+ {
607
+ Reject("pin:" + (RefusalReason(error) ?? error.GetType().Name), errors);
608
+ }
609
+ return total;
610
+ }
611
+
612
+ private List<int> Children(LegacySnapshot current, int parentId)
613
+ {
614
+ var ids = current.Children(parentId);
615
+ ids.Sort();
616
+ return ids;
617
+ }
618
+
619
+ private Held Adopt(int id, Held parent, List<string> errors)
620
+ {
621
+ if (id == brokerId) return null;
622
+ if (held.Count >= MaximumSweptProcesses + 2)
623
+ {
624
+ Reject("sweep:bound", errors);
625
+ return null;
626
+ }
627
+ var opened = Api.OpenProcess(0x00100000 | 0x1000 | 1, false, id);
628
+ if (opened.IsInvalid)
629
+ {
630
+ int code = Marshal.GetLastWin32Error();
631
+ opened.Dispose();
632
+ // 87: the process exited after the snapshot and nothing holds it any more.
633
+ if (code != 87) Reject(id + ":OpenProcess:" + code, errors);
634
+ return null;
635
+ }
636
+ try
637
+ {
638
+ long created = native.Creation(opened);
639
+ long ancestorCreated;
640
+ bool ancestor = ancestors.TryGetValue(id, out ancestorCreated) &&
641
+ ancestorCreated == created;
642
+ // A process older than the held parent got that PID from an earlier owner.
643
+ bool child = native.Parent(opened) == parent.Info.pid &&
644
+ created >= parent.Created;
645
+ if (ancestor ||
646
+ !child)
647
+ {
648
+ opened.Dispose();
649
+ return null;
650
+ }
651
+ // An exited child is held as well: its handle keeps its PID, so its own children
652
+ // stay traceable. Only a live process can belong to another user.
653
+ bool foreign = native.Alive(opened) &&
654
+ native.Owner(opened) != user;
655
+ if (foreign)
656
+ {
657
+ opened.Dispose();
658
+ Reject(id + ":owner", errors);
659
+ return null;
660
+ }
661
+ var result = new Held { Handle = opened, Created = created, Info = new Identity { pid = id,
662
+ creationTime = created.ToString(CultureInfo.InvariantCulture), parentPid = parent.Info.pid,
663
+ parentCreationTime = parent.Info.creationTime, ownerSid = user } };
664
+ held.Add(result);
665
+ return result;
666
+ }
667
+ catch (Exception error)
668
+ {
669
+ opened.Dispose();
670
+ var win32 = error as Win32Exception;
671
+ Reject(id + ":" + error.GetType().Name + (win32 == null ? "" : ":" + win32.NativeErrorCode), errors);
672
+ return null;
673
+ }
674
+ }
675
+
676
+ private void Reject(string reason, List<string> errors)
677
+ {
678
+ if (rejected.Add(reason)) errors.Add(reason);
679
+ }
680
+
681
+ private void CheckStop(Stopwatch stopClock)
682
+ {
683
+ if (stopClock.ElapsedMilliseconds >= StopMilliseconds) throw new TimeoutException();
684
+ }
685
+
686
+ private void Kill(Held process, Stopwatch stopClock, List<string> errors)
687
+ {
688
+ var termination = Terminate(process, stopClock, errors);
689
+ if (termination != null) AwaitExit(termination, stopClock, errors);
690
+ }
691
+
692
+ private Termination Terminate(Held process, Stopwatch stopClock, List<string> errors)
693
+ {
694
+ process.Stopping = true;
695
+ try
696
+ {
697
+ if (!native.Alive(process.Handle)) return null;
698
+ if (stopClock.ElapsedMilliseconds >= StopMilliseconds) throw new TimeoutException();
699
+ bool terminated = Api.TerminateProcess(process.Handle, 1);
700
+ int terminationError = terminated ? 0 : Marshal.GetLastWin32Error();
701
+ return new Termination { Process = process, Terminated = terminated, Error = terminationError };
702
+ }
703
+ catch (Exception error)
704
+ {
705
+ RecordStopError(process, error, errors);
706
+ return null;
707
+ }
708
+ }
709
+
710
+ private void AwaitExit(Termination termination, Stopwatch stopClock, List<string> errors)
711
+ {
712
+ try
713
+ {
714
+ uint remaining = (uint)Math.Max(0, StopMilliseconds - stopClock.ElapsedMilliseconds);
715
+ // Parent exit can already be terminating this child. A racing
716
+ // TerminateProcess failure is reconciled only by this held handle
717
+ // becoming signaled within the same total stop budget.
718
+ if (Api.WaitForSingleObject(termination.Process.Handle, remaining) != 0)
719
+ {
720
+ if (!termination.Terminated) throw new Win32Exception(termination.Error);
721
+ throw new TimeoutException();
722
+ }
723
+ }
724
+ catch (Exception error) { RecordStopError(termination.Process, error, errors); }
725
+ }
726
+
727
+ private void RecordStopError(Held process, Exception error, List<string> errors)
728
+ {
729
+ var win32 = error as Win32Exception;
730
+ errors.Add(process.Info.pid + ":" + error.GetType().Name +
731
+ (win32 == null ? "" : ":" + win32.NativeErrorCode));
732
+ }
733
+
734
+ private Receipt HeldStatus()
735
+ {
736
+ bool rootsGone = true;
737
+ int found = 0;
738
+ int stopped = 0;
739
+ foreach (var process in held)
740
+ {
741
+ bool alive = native.Alive(process.Handle);
742
+ bool root = process.Info.pid == plan.roots.bridge.pid ||
743
+ process.Info.pid == plan.roots.supervisor.pid;
744
+ if (root)
745
+ {
746
+ if (alive) rootsGone = false;
747
+ continue;
748
+ }
749
+ found++;
750
+ if (!alive) stopped++;
751
+ }
752
+ bool descendantsGone = swept &&
753
+ stopped == found &&
754
+ rejected.Count == 0;
755
+ return new Receipt { legacyRootStopVerified = rootsGone, observedDescendantsStopped = descendantsGone,
756
+ descendantsFound = found, descendantsStopped = stopped,
757
+ treeCompleteness = "unproven", observedCount = plan.observed.Length,
758
+ excludedCount = plan.excluded.Length, planSha256 = plan.planSha256,
759
+ errors = new string[0], unattributed = "Not discovered or not provable; left untouched." };
760
+ }
761
+
762
+ private void ValidatePlan(Plan value)
763
+ {
764
+ if (value == null ||
765
+ value.protocol != 1 ||
766
+ value.kind != "legacy-observed-process-set" ||
767
+ value.treeCompleteness != "unproven" ||
768
+ value.ownerSid != user ||
769
+ value.roots == null ||
770
+ value.roots.bridge == null ||
771
+ value.roots.supervisor == null ||
772
+ value.observed == null ||
773
+ value.excluded == null ||
774
+ value.observed.Length > MaximumPlanIdentities - 2 ||
775
+ value.excluded.Length + value.observed.Length > MaximumPlanIdentities - 2 ||
776
+ value.port < 1 ||
777
+ value.port > 65535 ||
778
+ !Path.IsPathRooted(value.root) ||
779
+ DigestPlan(value) != value.planSha256 ||
780
+ native.Hash(Encoding.UTF8.GetBytes(json.Serialize(value.observed))) != value.observedSetSha256)
781
+ throw new ArgumentException("Invalid protected plan");
782
+ PlanBytes(value);
783
+ var ids = new HashSet<int>();
784
+ var identities = new List<Identity> { value.roots.supervisor, value.roots.bridge };
785
+ identities.AddRange(value.observed);
786
+ foreach (var identity in identities)
787
+ {
788
+ long created;
789
+ if (identity == null ||
790
+ identity.pid <= 0 ||
791
+ !ids.Add(identity.pid) ||
792
+ identity.ownerSid != user ||
793
+ !Path.IsPathRooted(identity.imagePath) ||
794
+ !long.TryParse(identity.creationTime, NumberStyles.None, CultureInfo.InvariantCulture, out created) ||
795
+ created <= 0) throw new ArgumentException("Invalid captured identity");
796
+ }
797
+ foreach (var identity in value.excluded)
798
+ {
799
+ long created;
800
+ long parentCreated;
801
+ if (identity == null ||
802
+ identity.pid <= 0 ||
803
+ !ids.Add(identity.pid) ||
804
+ identity.reason != "predates-held-parent" ||
805
+ !long.TryParse(identity.creationTime, NumberStyles.None, CultureInfo.InvariantCulture, out created) ||
806
+ !long.TryParse(identity.parentCreationTime, NumberStyles.None, CultureInfo.InvariantCulture, out parentCreated) ||
807
+ created <= 0 ||
808
+ created >= parentCreated ||
809
+ !identities.Exists(parent => parent.pid == identity.parentPid &&
810
+ parent.creationTime == identity.parentCreationTime))
811
+ throw new ArgumentException("Invalid excluded generation evidence");
812
+ }
813
+ }
814
+
815
+ private Receipt VerifyGone(Plan value)
816
+ {
817
+ bool rootsGone = true;
818
+ bool childrenGone = true;
819
+ var errors = new List<string>();
820
+ var identities = new List<Identity> { value.roots.supervisor, value.roots.bridge };
821
+ identities.AddRange(value.observed);
822
+ for (int index = 0; index < identities.Count; index++)
823
+ {
824
+ var identity = identities[index];
825
+ using (var process = Api.OpenProcess(0x00100000 | 0x1000, false, identity.pid))
826
+ {
827
+ if (process.IsInvalid)
828
+ {
829
+ int code = Marshal.GetLastWin32Error();
830
+ if (code == 87) continue;
831
+ errors.Add(identity.pid + ":OpenProcess:" + code);
832
+ if (index < 2) rootsGone = false; else childrenGone = false;
833
+ continue;
834
+ }
835
+ if (native.Creation(process).ToString(CultureInfo.InvariantCulture) != identity.creationTime) continue;
836
+ if (!native.Alive(process)) continue;
837
+ if (index < 2) rootsGone = false; else childrenGone = false;
838
+ }
839
+ }
840
+ // A plan written now lists no descendants, so this read-only check cannot vouch for them.
841
+ bool descendantsVerified = childrenGone &&
842
+ value.observed.Length > 0;
843
+ return new Receipt { legacyRootStopVerified = rootsGone, observedDescendantsStopped = descendantsVerified,
844
+ observedCount = value.observed.Length, excludedCount = value.excluded.Length, treeCompleteness = "unproven",
845
+ planSha256 = value.planSha256, errors = errors.ToArray(),
846
+ unattributed = "Not discovered or not provable; left untouched." };
847
+ }
848
+
849
+ private string NewToken()
850
+ {
851
+ var bytes = new byte[32];
852
+ using (var random = RandomNumberGenerator.Create()) random.GetBytes(bytes);
853
+ return Convert.ToBase64String(bytes);
854
+ }
855
+
856
+ private string RefusalReason(Exception error)
857
+ {
858
+ // Only fixed own-source reasons are safe to expose. Never relay arbitrary
859
+ // WMI/IO exception text, which can contain paths or command-line content.
860
+ var reasons = new HashSet<string> {
861
+ "Plan changed", "Capture bound", "Capture exited", "Owner mismatch",
862
+ "Capture changed", "Child bound", "Root arguments mismatch", "Script bound",
863
+ "Root changed", "Identity changed", "Arguments changed", "Process query changed",
864
+ "Process query unavailable", "Process query generation changed",
865
+ "Run the upgrade from a terminal the 1.3 bridge did not start",
866
+ "Argument bounds", "Listener ambiguity", "Loopback listener missing",
867
+ "Broker parent identity lost", "Script argument not fully qualified",
868
+ "Snapshot schema unavailable", "Snapshot enumeration failed", "Snapshot entry bound",
869
+ "Snapshot duplicate entry", "Snapshot identity unavailable", "Snapshot close failed",
870
+ "Snapshot root changed", "Snapshot held member changed", "Argument query bound",
871
+ "Argument query cancelled", "Argument query timed out", "Image file identity unavailable", "Image file identity changed",
872
+ "Image hash bound", "Plan byte bound", "Request bounds"
873
+ };
874
+ return reasons.Contains(error.Message) ? error.Message : null;
875
+ }
876
+ }
877
+
878
+ internal sealed class Held
879
+ {
880
+ internal SafeProcess Handle;
881
+ internal Identity Info;
882
+ internal long Created;
883
+ internal string[] Arguments;
884
+ internal bool KeepArguments;
885
+ internal bool Stopping;
886
+ }
887
+
888
+ internal sealed class Termination
889
+ {
890
+ internal Held Process;
891
+ internal bool Terminated;
892
+ internal int Error;
893
+ }
894
+
895
+ internal sealed class Request
896
+ {
897
+ public string action { get; set; }
898
+ public int port { get; set; }
899
+ public string root { get; set; }
900
+ public Plan expected { get; set; }
901
+ public string token { get; set; }
902
+ public string planSha256 { get; set; }
903
+ }
904
+
905
+ internal sealed class Identity
906
+ {
907
+ public int pid { get; set; }
908
+ public string creationTime { get; set; }
909
+ public int parentPid { get; set; }
910
+ public string parentCreationTime { get; set; }
911
+ public string ownerSid { get; set; }
912
+ public string imagePath { get; set; }
913
+ public string imageSha256 { get; set; }
914
+ public string argvSha256 { get; set; }
915
+ public string scriptSha256 { get; set; }
916
+ }
917
+
918
+ internal sealed class Roots
919
+ {
920
+ public Identity supervisor { get; set; }
921
+ public Identity bridge { get; set; }
922
+ }
923
+
924
+ internal sealed class ExcludedIdentity
925
+ {
926
+ public int pid { get; set; }
927
+ public string creationTime { get; set; }
928
+ public int parentPid { get; set; }
929
+ public string parentCreationTime { get; set; }
930
+ public string reason { get; set; }
931
+ }
932
+
933
+ internal sealed class Plan
934
+ {
935
+ public int protocol { get; set; }
936
+ public string kind { get; set; }
937
+ public int port { get; set; }
938
+ public string root { get; set; }
939
+ public string ownerSid { get; set; }
940
+ public Roots roots { get; set; }
941
+ public Identity[] observed { get; set; }
942
+ public ExcludedIdentity[] excluded { get; set; }
943
+ public string observedSetSha256 { get; set; }
944
+ public string treeCompleteness { get; set; }
945
+ public string scope { get; set; }
946
+ public string planSha256 { get; set; }
947
+ }
948
+
949
+ internal sealed class Receipt
950
+ {
951
+ public bool legacyRootStopVerified { get; set; }
952
+ public bool observedDescendantsStopped { get; set; }
953
+ public int? descendantsFound { get; set; }
954
+ public int? descendantsStopped { get; set; }
955
+ public string treeCompleteness { get; set; }
956
+ public int observedCount { get; set; }
957
+ public int excludedCount { get; set; }
958
+ public string planSha256 { get; set; }
959
+ public string[] errors { get; set; }
960
+ public string unattributed { get; set; }
961
+ }