mcp-pacemaker 2.1.0 → 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.
- package/CHANGELOG.md +98 -2
- package/CONTRIBUTING.md +10 -3
- package/README.md +98 -760
- package/autostart/windows/legacy-access.ps1 +359 -0
- package/bin/account-upgrade-controller.mjs +1 -1
- package/bin/account-upgrade-worker.mjs +7 -0
- package/bin/cli-main.mjs +61 -5
- package/bin/dependency-lock.json +642 -0
- package/bin/legacy-access.mjs +73 -0
- package/bin/legacy-installation.mjs +75 -19
- package/bin/legacy-task.mjs +24 -0
- package/bin/legacy-upgrade.mjs +66 -10
- package/bin/managed-upgrade.mjs +102 -12
- package/bin/mcp-bridge.mjs +38 -20
- package/bin/prewarm.mjs +28 -6
- package/bin/service-control.mjs +10 -5
- package/bin/upgrade-capability.json +2 -1
- package/bin/upgrade-dependencies.mjs +67 -6
- package/bin/upgrade-package.mjs +2 -0
- package/bin/windows-legacy/LegacyProcessBroker.build.json +3 -3
- package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
- package/bin/windows-legacy/src/LegacyNative.cs +60 -11
- package/bin/windows-legacy/src/LegacyProcessBroker.cs +317 -197
- package/bin/windows-legacy-process.mjs +12 -5
- package/docs/account-upgrades.md +17 -4
- package/docs/compatibility.md +39 -0
- package/docs/configuration.md +146 -0
- package/docs/dashboards.md +125 -0
- package/docs/execution-context.md +11 -5
- package/docs/http-api.md +47 -0
- package/docs/install.md +52 -0
- package/docs/operations.md +99 -0
- package/docs/upgrades.md +324 -0
- package/package.json +10 -3
- package/tools/windows-legacy/README.md +118 -92
- package/tools/windows-legacy/inventory.mjs +8 -6
- package/ui/dist/third-party-manifest.json +1 -1
|
@@ -14,14 +14,33 @@ using System.Web.Script.Serialization;
|
|
|
14
14
|
|
|
15
15
|
internal sealed class LegacyProcessBroker
|
|
16
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;
|
|
17
32
|
private readonly LegacyNative native = new LegacyNative();
|
|
18
|
-
private readonly JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength =
|
|
33
|
+
private readonly JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = MaximumMessageBytes, RecursionLimit = 20 };
|
|
19
34
|
private readonly List<Held> held = new List<Held>();
|
|
20
|
-
private readonly
|
|
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;
|
|
21
40
|
private readonly List<FileStream> scripts = new List<FileStream>();
|
|
22
41
|
private readonly string user = WindowsIdentity.GetCurrent().User.Value;
|
|
23
42
|
private readonly Stopwatch clock = Stopwatch.StartNew();
|
|
24
|
-
private long deadline =
|
|
43
|
+
private long deadline = RequestMilliseconds;
|
|
25
44
|
private volatile bool committed;
|
|
26
45
|
private string stage = "request";
|
|
27
46
|
private string root;
|
|
@@ -31,7 +50,6 @@ internal sealed class LegacyProcessBroker
|
|
|
31
50
|
private object captureFailure;
|
|
32
51
|
private LegacySnapshot snapshot;
|
|
33
52
|
private LegacyArgumentQueries arguments;
|
|
34
|
-
private bool revalidating;
|
|
35
53
|
private readonly Stopwatch phaseClock = new Stopwatch();
|
|
36
54
|
private object captureDiagnostics;
|
|
37
55
|
private object revalidationDiagnostics;
|
|
@@ -51,6 +69,9 @@ internal sealed class LegacyProcessBroker
|
|
|
51
69
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false, true)) { AutoFlush = true });
|
|
52
70
|
StartWatchdog();
|
|
53
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);
|
|
54
75
|
if (request.action == "verify")
|
|
55
76
|
{
|
|
56
77
|
ValidatePlan(request.expected);
|
|
@@ -68,20 +89,18 @@ internal sealed class LegacyProcessBroker
|
|
|
68
89
|
stage = "capture-roots";
|
|
69
90
|
phaseClock.Start();
|
|
70
91
|
arguments = new LegacyArgumentQueries(CheckPhase);
|
|
71
|
-
arguments.BeginPhase(
|
|
92
|
+
arguments.BeginPhase(MaximumArgumentQueries);
|
|
72
93
|
Held bridge;
|
|
73
94
|
Held supervisor;
|
|
74
95
|
using (var images = new LegacyImageCache(CheckPhase))
|
|
75
96
|
{
|
|
76
|
-
bridge = Capture(native.Listener(port)
|
|
77
|
-
supervisor = Capture(bridge.Info.parentPid
|
|
97
|
+
bridge = Capture(native.Listener(port));
|
|
98
|
+
supervisor = Capture(bridge.Info.parentPid);
|
|
78
99
|
if (bridge.Info.pid == supervisor.Info.pid ||
|
|
79
100
|
bridge.Created < supervisor.Created) throw new InvalidOperationException();
|
|
80
101
|
bridge.Info.parentCreationTime = supervisor.Info.creationTime;
|
|
81
102
|
bridge.KeepArguments = true;
|
|
82
103
|
supervisor.KeepArguments = true;
|
|
83
|
-
bridge.SelectedRoot = true;
|
|
84
|
-
supervisor.SelectedRoot = true;
|
|
85
104
|
var roots = new List<Held> { bridge, supervisor };
|
|
86
105
|
CaptureFacts(roots, images);
|
|
87
106
|
CheckRootArguments(bridge, supervisor);
|
|
@@ -89,9 +108,7 @@ internal sealed class LegacyProcessBroker
|
|
|
89
108
|
if (!snapshot.Contains(bridge.Info.pid, supervisor.Info.pid) ||
|
|
90
109
|
!snapshot.Contains(supervisor.Info.pid, supervisor.Info.parentPid))
|
|
91
110
|
throw new InvalidOperationException("Snapshot root changed");
|
|
92
|
-
|
|
93
|
-
var descendants = held.FindAll(value => value != bridge && value != supervisor);
|
|
94
|
-
CaptureFacts(descendants, images);
|
|
111
|
+
CheckAncestors(bridge, supervisor);
|
|
95
112
|
if (!native.Alive(supervisor.Handle) ||
|
|
96
113
|
!native.Alive(bridge.Handle) ||
|
|
97
114
|
native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
|
|
@@ -104,8 +121,7 @@ internal sealed class LegacyProcessBroker
|
|
|
104
121
|
imageBytesHashed = images.Bytes, planUtf8Bytes = planBytes, memory = native.Memory() };
|
|
105
122
|
}
|
|
106
123
|
if (request.expected != null &&
|
|
107
|
-
|
|
108
|
-
throw new InvalidOperationException("Plan changed");
|
|
124
|
+
!SameRoots(request.expected, plan)) throw new InvalidOperationException("Plan changed");
|
|
109
125
|
token = NewToken();
|
|
110
126
|
Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + 120000);
|
|
111
127
|
Write(new { ok = true, plan, token, diagnostics = captureDiagnostics, brokerIdentity });
|
|
@@ -126,16 +142,15 @@ internal sealed class LegacyProcessBroker
|
|
|
126
142
|
request.token != token ||
|
|
127
143
|
request.planSha256 != plan.planSha256) throw new ArgumentException();
|
|
128
144
|
stage = "revalidate";
|
|
129
|
-
revalidating = true;
|
|
130
145
|
phaseClock.Restart();
|
|
131
|
-
Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds +
|
|
132
|
-
arguments.BeginPhase(
|
|
146
|
+
Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + PhaseMilliseconds + WatchdogMarginMilliseconds);
|
|
147
|
+
arguments.BeginPhase(MaximumArgumentQueries);
|
|
133
148
|
Revalidate(supervisor, bridge);
|
|
134
149
|
arguments.Dispose();
|
|
135
150
|
arguments = null;
|
|
136
151
|
committed = true;
|
|
137
152
|
stage = "stop";
|
|
138
|
-
Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds +
|
|
153
|
+
Interlocked.Exchange(ref deadline, clock.ElapsedMilliseconds + StopMilliseconds + WatchdogMarginMilliseconds);
|
|
139
154
|
var stopWatch = Stopwatch.StartNew();
|
|
140
155
|
var receipt = Stop(supervisor, bridge);
|
|
141
156
|
Write(new { ok = true, receipt, diagnostics = new {
|
|
@@ -205,14 +220,14 @@ internal sealed class LegacyProcessBroker
|
|
|
205
220
|
private Request Read()
|
|
206
221
|
{
|
|
207
222
|
var line = new StringBuilder();
|
|
208
|
-
while (line.Length <
|
|
223
|
+
while (line.Length < MaximumMessageBytes)
|
|
209
224
|
{
|
|
210
225
|
int value = Console.In.Read();
|
|
211
226
|
if (value < 0) throw new EndOfStreamException();
|
|
212
227
|
if (value == 10)
|
|
213
228
|
{
|
|
214
229
|
var text = line.ToString();
|
|
215
|
-
if (Encoding.UTF8.GetByteCount(text) + 1 >
|
|
230
|
+
if (Encoding.UTF8.GetByteCount(text) + 1 > MaximumMessageBytes) throw new ArgumentException("Request bounds");
|
|
216
231
|
return json.Deserialize<Request>(text);
|
|
217
232
|
}
|
|
218
233
|
line.Append((char)value);
|
|
@@ -226,26 +241,13 @@ internal sealed class LegacyProcessBroker
|
|
|
226
241
|
Console.Out.Flush();
|
|
227
242
|
}
|
|
228
243
|
|
|
229
|
-
private Held Capture(int id
|
|
244
|
+
private Held Capture(int id)
|
|
230
245
|
{
|
|
231
246
|
CheckPhase();
|
|
232
|
-
var known = held.Find(value => value.Info.pid == id);
|
|
233
|
-
if (known != null)
|
|
234
|
-
{
|
|
235
|
-
if (!native.Alive(known.Handle) ||
|
|
236
|
-
native.Creation(known.Handle) != known.Created ||
|
|
237
|
-
native.Parent(known.Handle) != known.Info.parentPid ||
|
|
238
|
-
native.Owner(known.Handle) != user) throw new InvalidOperationException("Capture changed");
|
|
239
|
-
if (parent != null &&
|
|
240
|
-
!KnownEdge(known, parent)) return null;
|
|
241
|
-
return known;
|
|
242
|
-
}
|
|
243
|
-
if (held.Count + excluded.Count >= 512) throw new InvalidOperationException("Capture bound");
|
|
244
247
|
var substage = LegacyFailureStage.CaptureOpenQuery;
|
|
245
248
|
long? observedBirth = null;
|
|
246
249
|
int? observedParent = null;
|
|
247
250
|
bool? observedAlive = null;
|
|
248
|
-
bool? observedParentAlive = null;
|
|
249
251
|
bool? observedOwnerMatch = null;
|
|
250
252
|
var handle = Api.OpenProcess(0x00100000 | 0x1000, false, id);
|
|
251
253
|
try
|
|
@@ -260,40 +262,6 @@ internal sealed class LegacyProcessBroker
|
|
|
260
262
|
substage = LegacyFailureStage.CaptureParent;
|
|
261
263
|
int parentId = native.Parent(handle);
|
|
262
264
|
observedParent = parentId;
|
|
263
|
-
if (parent != null)
|
|
264
|
-
{
|
|
265
|
-
bool parentAlive;
|
|
266
|
-
try { parentAlive = native.Alive(parent.Handle); }
|
|
267
|
-
catch (Exception error)
|
|
268
|
-
{
|
|
269
|
-
native.TagFailure(error, LegacyFailureStage.CaptureParentLiveness,
|
|
270
|
-
parent.Info.pid, parent.Created, parent.Info.parentPid, ownerMatched: true);
|
|
271
|
-
throw;
|
|
272
|
-
}
|
|
273
|
-
observedParentAlive = parentAlive;
|
|
274
|
-
bool bornBeforeParent = created < parent.Created;
|
|
275
|
-
bool changedEdge = parentId != parent.Info.pid;
|
|
276
|
-
if (!parentAlive ||
|
|
277
|
-
changedEdge)
|
|
278
|
-
{
|
|
279
|
-
captureFailure = new { pid = id, creationTime = created.ToString(CultureInfo.InvariantCulture),
|
|
280
|
-
actualParentPid = parentId,
|
|
281
|
-
expectedParentPid = parent.Info.pid, parentCreationTime = parent.Info.creationTime,
|
|
282
|
-
parentAlive, bornBeforeParent, changedEdge };
|
|
283
|
-
throw new InvalidOperationException("Unproven ancestry");
|
|
284
|
-
}
|
|
285
|
-
if (bornBeforeParent)
|
|
286
|
-
{
|
|
287
|
-
// A reused creator PID is not ownership of its older survivors.
|
|
288
|
-
// This candidate never receives terminate access or enters held.
|
|
289
|
-
excluded.Add(new ExcludedIdentity { pid = id,
|
|
290
|
-
creationTime = created.ToString(CultureInfo.InvariantCulture),
|
|
291
|
-
parentPid = parentId, parentCreationTime = parent.Info.creationTime,
|
|
292
|
-
reason = "predates-held-parent" });
|
|
293
|
-
handle.Dispose();
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
265
|
substage = LegacyFailureStage.CaptureOwner;
|
|
298
266
|
var owner = native.Owner(handle);
|
|
299
267
|
observedOwnerMatch = owner == user;
|
|
@@ -313,8 +281,7 @@ internal sealed class LegacyProcessBroker
|
|
|
313
281
|
substage = LegacyFailureStage.CaptureImage;
|
|
314
282
|
var image = native.Image(handle);
|
|
315
283
|
var info = new Identity { pid = id, creationTime = created.ToString(CultureInfo.InvariantCulture),
|
|
316
|
-
parentPid = parentId,
|
|
317
|
-
ownerSid = owner, imagePath = image };
|
|
284
|
+
parentPid = parentId, ownerSid = owner, imagePath = image };
|
|
318
285
|
if (!native.Alive(handle)) throw new InvalidOperationException("Capture changed");
|
|
319
286
|
var result = new Held { Handle = handle, Info = info, Created = created };
|
|
320
287
|
held.Add(result);
|
|
@@ -323,52 +290,15 @@ internal sealed class LegacyProcessBroker
|
|
|
323
290
|
catch (Exception error)
|
|
324
291
|
{
|
|
325
292
|
native.TagFailure(error, substage, id, observedBirth, observedParent,
|
|
326
|
-
|
|
327
|
-
observedAlive, observedParentAlive, observedOwnerMatch);
|
|
293
|
+
alive: observedAlive, ownerMatched: observedOwnerMatch);
|
|
328
294
|
handle.Dispose();
|
|
329
295
|
throw;
|
|
330
296
|
}
|
|
331
297
|
}
|
|
332
298
|
|
|
333
|
-
private bool KnownEdge(Held child, Held parent)
|
|
334
|
-
{
|
|
335
|
-
CheckPhase();
|
|
336
|
-
if (!native.Alive(child.Handle) ||
|
|
337
|
-
!native.Alive(parent.Handle) ||
|
|
338
|
-
native.Creation(child.Handle) != child.Created ||
|
|
339
|
-
native.Creation(parent.Handle) != parent.Created ||
|
|
340
|
-
native.Parent(child.Handle) != parent.Info.pid ||
|
|
341
|
-
child.Info.parentPid != parent.Info.pid ||
|
|
342
|
-
native.Owner(child.Handle) != user ||
|
|
343
|
-
native.Owner(parent.Handle) != user)
|
|
344
|
-
throw new InvalidOperationException("Ancestry changed");
|
|
345
|
-
if (child.Created < parent.Created)
|
|
346
|
-
{
|
|
347
|
-
// The selected root stays held; only this false reused-creator edge is ignored.
|
|
348
|
-
if (child.SelectedRoot) return false;
|
|
349
|
-
throw new InvalidOperationException("Unproven ancestry");
|
|
350
|
-
}
|
|
351
|
-
if (child.Info.parentCreationTime != parent.Info.creationTime)
|
|
352
|
-
throw new InvalidOperationException("Ancestry changed");
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private List<int> Children(int parentId)
|
|
357
|
-
{
|
|
358
|
-
CheckPhase();
|
|
359
|
-
var parent = held.Find(value => value.Info.pid == parentId);
|
|
360
|
-
if (snapshot == null ||
|
|
361
|
-
parent == null ||
|
|
362
|
-
!native.Alive(parent.Handle)) throw new InvalidOperationException("Unproven ancestry");
|
|
363
|
-
var ids = snapshot.Children(parentId);
|
|
364
|
-
ids.Sort();
|
|
365
|
-
return ids;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
299
|
private void CheckPhase()
|
|
369
300
|
{
|
|
370
|
-
if (
|
|
371
|
-
(revalidating && phaseClock.ElapsedMilliseconds >= 15000))
|
|
301
|
+
if (phaseClock.ElapsedMilliseconds >= PhaseMilliseconds)
|
|
372
302
|
throw new InvalidOperationException("Capture bound");
|
|
373
303
|
}
|
|
374
304
|
|
|
@@ -409,25 +339,12 @@ internal sealed class LegacyProcessBroker
|
|
|
409
339
|
try
|
|
410
340
|
{
|
|
411
341
|
int bytes = Encoding.UTF8.GetByteCount(json.Serialize(value));
|
|
412
|
-
if (bytes >
|
|
342
|
+
if (bytes > MaximumPlanBytes) throw new InvalidOperationException("Plan byte bound");
|
|
413
343
|
return bytes;
|
|
414
344
|
}
|
|
415
345
|
catch (InvalidOperationException) { throw new InvalidOperationException("Plan byte bound"); }
|
|
416
346
|
}
|
|
417
347
|
|
|
418
|
-
private void CaptureChildren(Held parent, int depth)
|
|
419
|
-
{
|
|
420
|
-
if (depth >= 8) throw new InvalidOperationException("Depth bound");
|
|
421
|
-
foreach (int id in Children(parent.Info.pid))
|
|
422
|
-
{
|
|
423
|
-
if (id == parent.Info.pid) throw new InvalidOperationException("Ancestry cycle");
|
|
424
|
-
var child = Capture(id, parent);
|
|
425
|
-
if (child == null) continue;
|
|
426
|
-
child.Info.parentCreationTime = parent.Info.creationTime;
|
|
427
|
-
CaptureChildren(child, depth + 1);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
348
|
private void CheckRootArguments(Held bridge, Held supervisor)
|
|
432
349
|
{
|
|
433
350
|
var bridgeScript = Path.Combine(root, "bin", "mcp-bridge.mjs");
|
|
@@ -457,6 +374,30 @@ internal sealed class LegacyProcessBroker
|
|
|
457
374
|
supervisor.Info.scriptSha256 = HoldScript(supervisorScript);
|
|
458
375
|
}
|
|
459
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
|
+
|
|
460
401
|
private string HoldScript(string path)
|
|
461
402
|
{
|
|
462
403
|
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
@@ -502,21 +443,27 @@ internal sealed class LegacyProcessBroker
|
|
|
502
443
|
|
|
503
444
|
private Plan MakePlan(Held supervisor, Held bridge)
|
|
504
445
|
{
|
|
505
|
-
var observed = new List<Identity>();
|
|
506
|
-
foreach (var process in held)
|
|
507
|
-
if (process != supervisor &&
|
|
508
|
-
process != bridge) observed.Add(process.Info);
|
|
509
|
-
observed.Sort((left, right) => left.pid.CompareTo(right.pid));
|
|
510
|
-
excluded.Sort((left, right) => left.pid.CompareTo(right.pid));
|
|
511
446
|
var result = new Plan { protocol = 1, kind = "legacy-observed-process-set", port = port, root = root,
|
|
512
447
|
ownerSid = user, roots = new Roots { supervisor = supervisor.Info, bridge = bridge.Info },
|
|
513
|
-
observed =
|
|
514
|
-
scope = "Selected roots
|
|
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." };
|
|
515
450
|
result.observedSetSha256 = native.Hash(Encoding.UTF8.GetBytes(json.Serialize(result.observed)));
|
|
516
451
|
result.planSha256 = DigestPlan(result);
|
|
517
452
|
return result;
|
|
518
453
|
}
|
|
519
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
|
+
|
|
520
467
|
private string DigestPlan(Plan value)
|
|
521
468
|
{
|
|
522
469
|
var saved = value.planSha256;
|
|
@@ -531,11 +478,11 @@ internal sealed class LegacyProcessBroker
|
|
|
531
478
|
if (!native.Alive(supervisor.Handle) ||
|
|
532
479
|
!native.Alive(bridge.Handle) ||
|
|
533
480
|
native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
|
|
534
|
-
var
|
|
535
|
-
arguments.Read(
|
|
481
|
+
var roots = new List<Held> { bridge, supervisor };
|
|
482
|
+
arguments.Read(roots, false);
|
|
536
483
|
using (var images = new LegacyImageCache(CheckPhase))
|
|
537
484
|
{
|
|
538
|
-
foreach (var process in
|
|
485
|
+
foreach (var process in roots)
|
|
539
486
|
{
|
|
540
487
|
CheckPhase();
|
|
541
488
|
if (!native.Alive(process.Handle) ||
|
|
@@ -547,22 +494,9 @@ internal sealed class LegacyProcessBroker
|
|
|
547
494
|
throw new InvalidOperationException("Identity changed");
|
|
548
495
|
}
|
|
549
496
|
RefreshSnapshot();
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (!snapshot.Contains(process.Info.pid, process.Info.parentPid))
|
|
554
|
-
throw new InvalidOperationException("Snapshot held member changed");
|
|
555
|
-
foreach (int id in Children(process.Info.pid))
|
|
556
|
-
{
|
|
557
|
-
var known = held.Find(value => value.Info.pid == id);
|
|
558
|
-
if (known != null)
|
|
559
|
-
{
|
|
560
|
-
if (!KnownEdge(known, process)) continue;
|
|
561
|
-
}
|
|
562
|
-
else if (!ApprovedNonmember(process, id))
|
|
563
|
-
throw new InvalidOperationException("Observed set expanded; replan");
|
|
564
|
-
}
|
|
565
|
-
}
|
|
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");
|
|
566
500
|
if (!native.Alive(supervisor.Handle) ||
|
|
567
501
|
!native.Alive(bridge.Handle) ||
|
|
568
502
|
native.Listener(port) != bridge.Info.pid) throw new InvalidOperationException("Root changed");
|
|
@@ -572,79 +506,254 @@ internal sealed class LegacyProcessBroker
|
|
|
572
506
|
}
|
|
573
507
|
}
|
|
574
508
|
|
|
575
|
-
private bool ApprovedNonmember(Held parent, int id)
|
|
576
|
-
{
|
|
577
|
-
var prior = excluded.Find(value => value.pid == id &&
|
|
578
|
-
value.parentPid == parent.Info.pid &&
|
|
579
|
-
value.parentCreationTime == parent.Info.creationTime);
|
|
580
|
-
if (prior == null) return false;
|
|
581
|
-
using (var process = Api.OpenProcess(0x00100000 | 0x1000, false, id))
|
|
582
|
-
{
|
|
583
|
-
native.Require(!process.IsInvalid);
|
|
584
|
-
long created = native.Creation(process);
|
|
585
|
-
return native.Alive(parent.Handle) &&
|
|
586
|
-
native.Alive(process) &&
|
|
587
|
-
native.Parent(process) == parent.Info.pid &&
|
|
588
|
-
created < parent.Created &&
|
|
589
|
-
created.ToString(CultureInfo.InvariantCulture) == prior.creationTime;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
509
|
private Receipt Stop(Held supervisor, Held bridge)
|
|
594
510
|
{
|
|
595
511
|
var errors = new List<string>();
|
|
596
512
|
var stopClock = Stopwatch.StartNew();
|
|
513
|
+
// The supervisor goes first so it cannot start another bridge.
|
|
597
514
|
Kill(supervisor, stopClock, errors);
|
|
598
515
|
if (!native.Alive(supervisor.Handle))
|
|
599
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);
|
|
600
522
|
Kill(bridge, stopClock, errors);
|
|
601
|
-
|
|
602
|
-
if (held[index] != supervisor &&
|
|
603
|
-
held[index] != bridge) Kill(held[index], stopClock, errors);
|
|
523
|
+
if (!native.Alive(bridge.Handle)) Sweep(stopClock, errors);
|
|
604
524
|
}
|
|
605
525
|
var result = HeldStatus();
|
|
606
526
|
result.errors = errors.ToArray();
|
|
607
527
|
return result;
|
|
608
528
|
}
|
|
609
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
|
+
|
|
610
686
|
private void Kill(Held process, Stopwatch stopClock, List<string> errors)
|
|
611
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;
|
|
612
695
|
try
|
|
613
696
|
{
|
|
614
|
-
if (!native.Alive(process.Handle)) return;
|
|
615
|
-
if (stopClock.ElapsedMilliseconds >=
|
|
697
|
+
if (!native.Alive(process.Handle)) return null;
|
|
698
|
+
if (stopClock.ElapsedMilliseconds >= StopMilliseconds) throw new TimeoutException();
|
|
616
699
|
bool terminated = Api.TerminateProcess(process.Handle, 1);
|
|
617
700
|
int terminationError = terminated ? 0 : Marshal.GetLastWin32Error();
|
|
618
|
-
|
|
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);
|
|
619
715
|
// Parent exit can already be terminating this child. A racing
|
|
620
716
|
// TerminateProcess failure is reconciled only by this held handle
|
|
621
717
|
// becoming signaled within the same total stop budget.
|
|
622
|
-
if (Api.WaitForSingleObject(
|
|
718
|
+
if (Api.WaitForSingleObject(termination.Process.Handle, remaining) != 0)
|
|
623
719
|
{
|
|
624
|
-
if (!
|
|
720
|
+
if (!termination.Terminated) throw new Win32Exception(termination.Error);
|
|
625
721
|
throw new TimeoutException();
|
|
626
722
|
}
|
|
627
723
|
}
|
|
628
|
-
catch (Exception error)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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));
|
|
634
732
|
}
|
|
635
733
|
|
|
636
734
|
private Receipt HeldStatus()
|
|
637
735
|
{
|
|
638
736
|
bool rootsGone = true;
|
|
639
|
-
|
|
737
|
+
int found = 0;
|
|
738
|
+
int stopped = 0;
|
|
640
739
|
foreach (var process in held)
|
|
641
740
|
{
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
process.Info.pid == plan.roots.supervisor.pid
|
|
645
|
-
|
|
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++;
|
|
646
751
|
}
|
|
647
|
-
|
|
752
|
+
bool descendantsGone = swept &&
|
|
753
|
+
stopped == found &&
|
|
754
|
+
rejected.Count == 0;
|
|
755
|
+
return new Receipt { legacyRootStopVerified = rootsGone, observedDescendantsStopped = descendantsGone,
|
|
756
|
+
descendantsFound = found, descendantsStopped = stopped,
|
|
648
757
|
treeCompleteness = "unproven", observedCount = plan.observed.Length,
|
|
649
758
|
excludedCount = plan.excluded.Length, planSha256 = plan.planSha256,
|
|
650
759
|
errors = new string[0], unattributed = "Not discovered or not provable; left untouched." };
|
|
@@ -662,8 +771,8 @@ internal sealed class LegacyProcessBroker
|
|
|
662
771
|
value.roots.supervisor == null ||
|
|
663
772
|
value.observed == null ||
|
|
664
773
|
value.excluded == null ||
|
|
665
|
-
value.observed.Length >
|
|
666
|
-
value.excluded.Length + value.observed.Length >
|
|
774
|
+
value.observed.Length > MaximumPlanIdentities - 2 ||
|
|
775
|
+
value.excluded.Length + value.observed.Length > MaximumPlanIdentities - 2 ||
|
|
667
776
|
value.port < 1 ||
|
|
668
777
|
value.port > 65535 ||
|
|
669
778
|
!Path.IsPathRooted(value.root) ||
|
|
@@ -728,7 +837,10 @@ internal sealed class LegacyProcessBroker
|
|
|
728
837
|
if (index < 2) rootsGone = false; else childrenGone = false;
|
|
729
838
|
}
|
|
730
839
|
}
|
|
731
|
-
|
|
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,
|
|
732
844
|
observedCount = value.observed.Length, excludedCount = value.excluded.Length, treeCompleteness = "unproven",
|
|
733
845
|
planSha256 = value.planSha256, errors = errors.ToArray(),
|
|
734
846
|
unattributed = "Not discovered or not provable; left untouched." };
|
|
@@ -747,17 +859,16 @@ internal sealed class LegacyProcessBroker
|
|
|
747
859
|
// WMI/IO exception text, which can contain paths or command-line content.
|
|
748
860
|
var reasons = new HashSet<string> {
|
|
749
861
|
"Plan changed", "Capture bound", "Capture exited", "Owner mismatch",
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"Root changed", "Identity changed", "Arguments changed",
|
|
753
|
-
"Observed set expanded; replan", "Process query changed",
|
|
862
|
+
"Capture changed", "Child bound", "Root arguments mismatch", "Script bound",
|
|
863
|
+
"Root changed", "Identity changed", "Arguments changed", "Process query changed",
|
|
754
864
|
"Process query unavailable", "Process query generation changed",
|
|
865
|
+
"Run the upgrade from a terminal the 1.3 bridge did not start",
|
|
755
866
|
"Argument bounds", "Listener ambiguity", "Loopback listener missing",
|
|
756
867
|
"Broker parent identity lost", "Script argument not fully qualified",
|
|
757
868
|
"Snapshot schema unavailable", "Snapshot enumeration failed", "Snapshot entry bound",
|
|
758
869
|
"Snapshot duplicate entry", "Snapshot identity unavailable", "Snapshot close failed",
|
|
759
870
|
"Snapshot root changed", "Snapshot held member changed", "Argument query bound",
|
|
760
|
-
"Argument query cancelled", "Image file identity unavailable", "Image file identity changed",
|
|
871
|
+
"Argument query cancelled", "Argument query timed out", "Image file identity unavailable", "Image file identity changed",
|
|
761
872
|
"Image hash bound", "Plan byte bound", "Request bounds"
|
|
762
873
|
};
|
|
763
874
|
return reasons.Contains(error.Message) ? error.Message : null;
|
|
@@ -771,7 +882,14 @@ internal sealed class Held
|
|
|
771
882
|
internal long Created;
|
|
772
883
|
internal string[] Arguments;
|
|
773
884
|
internal bool KeepArguments;
|
|
774
|
-
internal bool
|
|
885
|
+
internal bool Stopping;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
internal sealed class Termination
|
|
889
|
+
{
|
|
890
|
+
internal Held Process;
|
|
891
|
+
internal bool Terminated;
|
|
892
|
+
internal int Error;
|
|
775
893
|
}
|
|
776
894
|
|
|
777
895
|
internal sealed class Request
|
|
@@ -832,6 +950,8 @@ internal sealed class Receipt
|
|
|
832
950
|
{
|
|
833
951
|
public bool legacyRootStopVerified { get; set; }
|
|
834
952
|
public bool observedDescendantsStopped { get; set; }
|
|
953
|
+
public int? descendantsFound { get; set; }
|
|
954
|
+
public int? descendantsStopped { get; set; }
|
|
835
955
|
public string treeCompleteness { get; set; }
|
|
836
956
|
public int observedCount { get; set; }
|
|
837
957
|
public int excludedCount { get; set; }
|