windows-development-sandbox 1.27.3 → 1.29.0

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 (125) hide show
  1. package/README.md +5 -4
  2. package/dist/app/features/sandbox/sandbox.run.js +1 -1
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
  4. package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
  5. package/dist/bin/sandbox.js +10772 -3
  6. package/dist/cli/commands/launch/launch.js +1 -1
  7. package/dist/cli/commands/launch/running.js +2 -2
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/menu/pages/agents-page.js +1 -1
  10. package/dist/cli/services/sandbox-service.js +1 -1
  11. package/dist/core/schema/sandbox.js +2 -2
  12. package/dist/guest/apps/app-installer.js +1 -1
  13. package/dist/guest/apps/store-check.d.ts +2 -0
  14. package/dist/guest/apps/store-check.js +3 -1
  15. package/dist/guest/boot/guide-lines.d.ts +2 -0
  16. package/dist/guest/boot/guide-lines.js +7 -0
  17. package/dist/guest/boot/guide.js +2 -1
  18. package/dist/guest/daemon/heartbeat.js +1 -0
  19. package/dist/guest/governor/core.d.ts +53 -1
  20. package/dist/guest/governor/core.js +95 -1
  21. package/dist/guest/governor/decide.d.ts +4 -0
  22. package/dist/guest/governor/decide.js +2 -0
  23. package/dist/guest/governor/governor.js +23 -9
  24. package/dist/guest/toolchains/runtimes.js +7 -6
  25. package/dist/host/accounts/deliver.js +16 -2
  26. package/dist/host/apps/store-package.d.ts +2 -0
  27. package/dist/host/apps/store-package.js +16 -15
  28. package/dist/host/channel/attach.js +1 -1
  29. package/dist/host/channel/channel.js +3 -3
  30. package/dist/host/guard/commands.js +1 -0
  31. package/dist/host/health/path-copies.d.ts +1 -1
  32. package/dist/host/health/path-copies.js +2 -2
  33. package/dist/host/install/index.d.ts +1 -1
  34. package/dist/host/install/index.js +1 -1
  35. package/dist/host/install/shims.d.ts +8 -1
  36. package/dist/host/install/shims.js +39 -1
  37. package/dist/host/launch/generate.js +4 -0
  38. package/dist/host/launch/index.d.ts +1 -0
  39. package/dist/host/launch/index.js +1 -0
  40. package/dist/host/launch/pc-logs.d.ts +6 -0
  41. package/dist/host/launch/pc-logs.js +129 -0
  42. package/dist/host/life/crash-report.d.ts +4 -0
  43. package/dist/host/life/crash-report.js +30 -1
  44. package/dist/host/life/last-end.js +1 -0
  45. package/dist/host/sandbox/platform.js +1 -1
  46. package/dist/host/sandbox/window.js +1 -1
  47. package/dist/host/tools/windows-terminal.d.ts +4 -1
  48. package/dist/host/tools/windows-terminal.js +25 -9
  49. package/dist/runtime/agent.js +6 -2
  50. package/dist/runtime/daemon.js +111 -16
  51. package/dist/runtime/db.js +6 -2
  52. package/dist/runtime/main.js +199 -35
  53. package/dist/runtime/netproxy.js +6 -2
  54. package/dist/runtime/tool.js +6 -2
  55. package/dist/runtime/view.js +126 -28
  56. package/dist/selftest/agent-paths.test.js +3 -1
  57. package/dist/selftest/channel.test.js +33 -0
  58. package/dist/selftest/cli.test.js +5 -5
  59. package/dist/selftest/config.test.js +1 -1
  60. package/dist/selftest/credentials.test.js +1 -1
  61. package/dist/selftest/engine.test.js +1 -1
  62. package/dist/selftest/generate.test.js +66 -1
  63. package/dist/selftest/hcs.test.js +1 -1
  64. package/dist/selftest/health.test.js +21 -10
  65. package/dist/selftest/heavy-load.test.js +112 -4
  66. package/dist/selftest/host-sim.test.js +13 -14
  67. package/dist/selftest/install.test.js +6 -2
  68. package/dist/selftest/live-terminal.test.js +103 -0
  69. package/dist/selftest/personalization.test.js +2 -1
  70. package/dist/selftest/store-app.test.js +62 -4
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/web-preview.js +2 -2
  73. package/dist/shared/accounts/kinds.d.ts +5 -0
  74. package/dist/shared/accounts/kinds.js +8 -0
  75. package/dist/shared/apps/index.d.ts +1 -1
  76. package/dist/shared/apps/index.js +1 -1
  77. package/dist/shared/apps/selection.d.ts +1 -0
  78. package/dist/shared/apps/selection.js +4 -3
  79. package/dist/shared/apps/store-package.d.ts +13 -0
  80. package/dist/shared/apps/store-package.js +24 -0
  81. package/dist/shared/channel/schema.d.ts +2 -0
  82. package/dist/shared/channel/schema.js +4 -0
  83. package/dist/shared/memory/policy.d.ts +12 -0
  84. package/dist/shared/memory/policy.js +4 -0
  85. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  86. package/dist/shared/pty/clipboard-watch.js +40 -0
  87. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  88. package/dist/shared/pty/ctrl-c.js +35 -19
  89. package/dist/shared/pty/index.d.ts +1 -0
  90. package/dist/shared/pty/index.js +1 -0
  91. package/dist/shared/pty/terminal.d.ts +1 -1
  92. package/dist/shared/pty/terminal.js +16 -13
  93. package/dist/shared/web/ctrl-c.d.ts +26 -0
  94. package/dist/shared/web/ctrl-c.js +64 -0
  95. package/dist/shared/web/index.d.ts +1 -0
  96. package/dist/shared/web/index.js +1 -0
  97. package/dist/web/index.html +1 -1
  98. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  99. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  100. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  101. package/native/csharp/HostTools/Window.cs +1 -1
  102. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  103. package/native/scripts/get-store-package.ps1 +24 -13
  104. package/package.json +4 -14
  105. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  106. package/sandbox/csharp/MemGov.cs +752 -7
  107. package/sandbox/scripts/install-store-package.ps1 +11 -4
  108. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  109. package/schemas/config.schema.json +2 -2
  110. package/schemas/sandbox-config.schema.json +1 -1
  111. package/dist/bin/agent-sandbox.d.ts +0 -2
  112. package/dist/bin/agent-sandbox.js +0 -9
  113. package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
  114. package/dist/bin/claude-host.d.ts +0 -2
  115. package/dist/bin/claude-host.js +0 -11
  116. package/dist/bin/claude-sbx.d.ts +0 -2
  117. package/dist/bin/claude-sbx.js +0 -9
  118. package/dist/bin/codex-sbx.d.ts +0 -2
  119. package/dist/bin/codex-sbx.js +0 -9
  120. package/dist/bin/gemini-sbx.d.ts +0 -2
  121. package/dist/bin/gemini-sbx.js +0 -9
  122. package/dist/bin/sbx-hide.d.ts +0 -2
  123. package/dist/bin/sbx-hide.js +0 -9
  124. package/dist/bin/sbx-show.d.ts +0 -2
  125. package/dist/bin/sbx-show.js +0 -9
@@ -4,6 +4,11 @@
4
4
  // stdout, a JSON line every --interval seconds (default 2):
5
5
  // {"ramTotalMb":..,"ramAvailMb":..,"commitLimitMb":..,"commitAvailMb":..,"procs":[{"pid":..,"name":"rustc","mb":..,"prio":"normal"}]}
6
6
  // procs = the processes in the agents' job (the job ptyhost makes for everything the agents start), nothing else.
7
+ // gateHeld = the build programs the memory gate holds right now (0 without a gate).
8
+ // serve [--interval s] [--gate growthMb:windowMs:releaseMs:maxHoldMs --gate-names a,b,c]
9
+ // With --gate, the memory gate (see GateLoop) also writes event lines on stdout:
10
+ // {"event":"gate","kind":"ready|off|trip|hold|release","at":<ms since 1970 UTC>, ...}
11
+ // Without --gate it never touches the job's completion port and writes no event.
7
12
  // stdin, one command per line:
8
13
  // priority <pid> <idle|below|normal> lowers (or restores) the priority of a process in that job
9
14
  // iopriority <pid> <verylow|low|normal> the disk queue priority of a process in that job
@@ -51,6 +56,89 @@ static class MemGov
51
56
  private const int IoPriorityVeryLow = 0;
52
57
  private const int IoPriorityLow = 1;
53
58
  private const int IoPriorityNormal = 2;
59
+ // The memory gate: a notification limit on the agents' job, reported through a completion port.
60
+ private const uint JobObjectSetAttributes = 0x0002;
61
+ private const int JobObjectAssociateCompletionPortInformation = 7;
62
+ private const int JobObjectNotificationLimitInformation = 12;
63
+ private const int JobObjectLimitViolationInformation = 13;
64
+ private const uint JobObjectLimitJobMemory = 0x00000200;
65
+ private const uint JobMsgNewProcess = 6;
66
+ private const uint JobMsgExitProcess = 7;
67
+ private const uint JobMsgAbnormalExitProcess = 8;
68
+ private const uint JobMsgNotificationLimit = 11;
69
+ private const uint GatePollMs = 100;
70
+ private const int GateTickMs = 500;
71
+ private const int GateRearmStepMb = 64;
72
+ private const int ErrorAccessDenied = 5;
73
+ private const int MaxTripList = 64;
74
+ private const int MaxGateNames = 64;
75
+ private static readonly IntPtr InvalidHandleValue = new IntPtr(-1);
76
+ private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
77
+
78
+ // JOBOBJECT_ASSOCIATE_COMPLETION_PORT.
79
+ [StructLayout(LayoutKind.Sequential)]
80
+ private struct CompletionPortLink
81
+ {
82
+ public IntPtr CompletionKey;
83
+ public IntPtr CompletionPort;
84
+ }
85
+
86
+ // JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION (48 bytes on x64).
87
+ [StructLayout(LayoutKind.Sequential)]
88
+ private struct NotificationLimit
89
+ {
90
+ public ulong IoReadBytesLimit;
91
+ public ulong IoWriteBytesLimit;
92
+ public long PerJobUserTimeLimit;
93
+ public ulong JobMemoryLimit;
94
+ public int RateControlTolerance;
95
+ public int RateControlToleranceInterval;
96
+ public uint LimitFlags;
97
+ }
98
+
99
+ // JOBOBJECT_LIMIT_VIOLATION_INFORMATION (80 bytes on x64).
100
+ [StructLayout(LayoutKind.Sequential)]
101
+ private struct LimitViolation
102
+ {
103
+ public uint LimitFlags;
104
+ public uint ViolationLimitFlags;
105
+ public ulong IoReadBytes;
106
+ public ulong IoReadBytesLimit;
107
+ public ulong IoWriteBytes;
108
+ public ulong IoWriteBytesLimit;
109
+ public long PerJobUserTime;
110
+ public long PerJobUserTimeLimit;
111
+ public ulong JobMemory;
112
+ public ulong JobMemoryLimit;
113
+ public int RateControlTolerance;
114
+ public int RateControlToleranceLimit;
115
+ }
116
+
117
+ private sealed class GateSettings
118
+ {
119
+ public int GrowthMb;
120
+ public int WindowMs;
121
+ public int ReleaseMs;
122
+ public int MaxHoldMs;
123
+ public HashSet<string> Names;
124
+ }
125
+
126
+ /// <summary>A process the gate watches (or holds); the handle stays open while it is in Young or GateHeld.</summary>
127
+ private sealed class Tracked
128
+ {
129
+ public IntPtr Handle;
130
+ public string Name;
131
+ public DateTime Started;
132
+ public DateTime HeldAt;
133
+ }
134
+
135
+ private sealed class Released
136
+ {
137
+ public int Pid;
138
+ public string Name;
139
+ public long HeldMs;
140
+ public bool Forced;
141
+ }
54
142
 
55
143
  // JOBOBJECT_CPU_RATE_CONTROL_INFORMATION (CpuRate in hundredths of a percent).
56
144
  [StructLayout(LayoutKind.Sequential)]
@@ -103,6 +191,21 @@ static class MemGov
103
191
  [DllImport("kernel32.dll", SetLastError = true)]
104
192
  public static extern bool QueryInformationJobObject(IntPtr job, int infoClass, IntPtr info, int length, IntPtr returnLength);
105
193
 
194
+ [DllImport("kernel32.dll", SetLastError = true)]
195
+ public static extern bool QueryInformationJobObject(IntPtr job, int infoClass, ref LimitViolation info, int length, IntPtr returnLength);
196
+
197
+ [DllImport("kernel32.dll", SetLastError = true)]
198
+ public static extern IntPtr CreateIoCompletionPort(IntPtr file, IntPtr existing, UIntPtr key, uint threads);
199
+
200
+ [DllImport("kernel32.dll", SetLastError = true)]
201
+ public static extern bool GetQueuedCompletionStatus(IntPtr port, out uint bytes, out UIntPtr key, out IntPtr overlapped, uint ms);
202
+
203
+ [DllImport("kernel32.dll", EntryPoint = "SetInformationJobObject", SetLastError = true)]
204
+ public static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref CompletionPortLink info, int length);
205
+
206
+ [DllImport("kernel32.dll", EntryPoint = "SetInformationJobObject", SetLastError = true)]
207
+ public static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref NotificationLimit info, int length);
208
+
106
209
  [DllImport("kernel32.dll", SetLastError = true)]
107
210
  public static extern bool IsProcessInJob(IntPtr process, IntPtr job, out bool result);
108
211
 
@@ -162,20 +265,41 @@ static class MemGov
162
265
  private static IntPtr throttleJob = IntPtr.Zero;
163
266
  private static volatile bool stop;
164
267
 
268
+ // The memory gate (null gate = off). Young, GateHeld and the ledger Paused are guarded by Gate; the rest belongs to GateLoop.
269
+ private static readonly object OutLock = new object();
270
+ private static GateSettings gate = null;
271
+ private static readonly Dictionary<int, Tracked> Young = new Dictionary<int, Tracked>();
272
+ private static readonly Dictionary<int, Tracked> GateHeld = new Dictionary<int, Tracked>();
273
+ private static List<KeyValuePair<DateTime, long>> window = new List<KeyValuePair<DateTime, long>>();
274
+ private static DateTime lastTrip = DateTime.MinValue;
275
+ private static DateTime lastRelease = DateTime.MinValue;
276
+ private static long armedMb = -1;
277
+ private static HashSet<int> seenPids = new HashSet<int>();
278
+
165
279
  static int Main(string[] args)
166
280
  {
167
281
  if (args.Length == 0 || args[0] != "serve")
168
282
  {
169
- Console.Error.WriteLine("usage: memgov serve [--interval seconds]");
283
+ Console.Error.WriteLine("usage: memgov serve [--interval seconds] [--gate growthMb:windowMs:releaseMs:maxHoldMs --gate-names a,b,c]");
170
284
  return 2;
171
285
  }
172
286
  int interval = 2;
287
+ string gateSpec = null;
288
+ string gateNames = null;
173
289
  for (int i = 1; i + 1 < args.Length; i++)
174
290
  {
175
291
  if (args[i] == "--interval")
176
292
  {
177
293
  int.TryParse(args[i + 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out interval);
178
294
  }
295
+ else if (args[i] == "--gate")
296
+ {
297
+ gateSpec = args[i + 1];
298
+ }
299
+ else if (args[i] == "--gate-names")
300
+ {
301
+ gateNames = args[i + 1];
302
+ }
179
303
  }
180
304
  if (interval < 1)
181
305
  {
@@ -183,6 +307,21 @@ static class MemGov
183
307
  }
184
308
  // A memgov that ended without resuming (killed, crashed) left its pauses in the file: undo them first.
185
309
  RecoverHeld();
310
+ Thread gateThread = null;
311
+ if (gateSpec != null)
312
+ {
313
+ gate = ParseGate(gateSpec, gateNames);
314
+ if (gate == null)
315
+ {
316
+ EmitOff("bad --gate");
317
+ }
318
+ else
319
+ {
320
+ gateThread = new Thread(GateLoop);
321
+ gateThread.IsBackground = true;
322
+ gateThread.Start();
323
+ }
324
+ }
186
325
  // A memgov that ended while compilers were capped left the cap: lift it before anything else.
187
326
  SetCpuRate(100);
188
327
  Thread commands = new Thread(ReadCommands);
@@ -195,8 +334,7 @@ static class MemGov
195
334
  {
196
335
  try
197
336
  {
198
- Console.Out.WriteLine(Sample());
199
- Console.Out.Flush();
337
+ Emit(Sample());
200
338
  }
201
339
  catch (Exception)
202
340
  {
@@ -207,11 +345,26 @@ static class MemGov
207
345
  Thread.Sleep(100);
208
346
  }
209
347
  }
348
+ if (gateThread != null)
349
+ {
350
+ // The gate lifts its limit and closes its port when it sees stop; give it a moment.
351
+ gateThread.Join(3000);
352
+ }
210
353
  ResumeAll();
211
354
  SetCpuRate(100);
212
355
  return 0;
213
356
  }
214
357
 
358
+ /// <summary>One line on stdout; the sample loop and the gate both write, so one at a time.</summary>
359
+ private static void Emit(string json)
360
+ {
361
+ lock (OutLock)
362
+ {
363
+ Console.Out.WriteLine(json);
364
+ Console.Out.Flush();
365
+ }
366
+ }
367
+
215
368
  /// <summary>The self-test's own job (MEMGOV_JOB_NAME, a name in the agents' job family), or null.</summary>
216
369
  private static string TestJobName()
217
370
  {
@@ -269,7 +422,12 @@ static class MemGov
269
422
  first = false;
270
423
  sb.Append(entry);
271
424
  }
272
- return sb.Append("]}").ToString();
425
+ int heldNow;
426
+ lock (Gate)
427
+ {
428
+ heldNow = GateHeld.Count;
429
+ }
430
+ return sb.Append("],\"gateHeld\":").Append(heldNow.ToString(CultureInfo.InvariantCulture)).Append('}').ToString();
273
431
  }
274
432
 
275
433
  private static readonly int SelfId = Process.GetCurrentProcess().Id;
@@ -567,7 +725,7 @@ static class MemGov
567
725
  {
568
726
  lock (Gate)
569
727
  {
570
- if (Paused.ContainsKey(pid))
728
+ if (Paused.ContainsKey(pid) || GateHeld.ContainsKey(pid))
571
729
  {
572
730
  return;
573
731
  }
@@ -638,16 +796,27 @@ static class MemGov
638
796
  Resume(pid);
639
797
  }
640
798
  Paused.Clear();
799
+ foreach (Tracked t in GateHeld.Values)
800
+ {
801
+ NativeMethods.NtResumeProcess(t.Handle);
802
+ NativeMethods.CloseHandle(t.Handle);
803
+ }
804
+ GateHeld.Clear();
805
+ foreach (Tracked t in Young.Values)
806
+ {
807
+ NativeMethods.CloseHandle(t.Handle);
808
+ }
809
+ Young.Clear();
641
810
  SaveHeld();
642
811
  }
643
812
  }
644
813
 
645
- /// <summary>The processes paused right now, one id per line (empty: the file goes). Called with Gate held.</summary>
814
+ /// <summary>The processes paused or held by the gate right now, one id per line (empty: the file goes). Called with Gate held.</summary>
646
815
  private static void SaveHeld()
647
816
  {
648
817
  try
649
818
  {
650
- if (Paused.Count == 0)
819
+ if (Paused.Count == 0 && GateHeld.Count == 0)
651
820
  {
652
821
  File.Delete(HeldFile);
653
822
  return;
@@ -657,6 +826,13 @@ static class MemGov
657
826
  {
658
827
  lines.Add(pid.ToString(CultureInfo.InvariantCulture));
659
828
  }
829
+ foreach (int pid in GateHeld.Keys)
830
+ {
831
+ if (!Paused.ContainsKey(pid))
832
+ {
833
+ lines.Add(pid.ToString(CultureInfo.InvariantCulture));
834
+ }
835
+ }
660
836
  File.WriteAllLines(HeldFile, lines.ToArray());
661
837
  }
662
838
  catch (IOException)
@@ -698,6 +874,575 @@ static class MemGov
698
874
  }
699
875
  }
700
876
 
877
+ // ---- The memory gate -------------------------------------------------------------------------------------------
878
+ // When the agents' programs gain committed memory faster than GrowthMb per WindowMs, build programs that start
879
+ // (or just started) are frozen and let go one at a time, oldest first, never longer than MaxHoldMs. Windows tells
880
+ // us through a notification limit on the job (a completion port); nothing is ever killed.
881
+
882
+ /// <summary>growthMb:windowMs:releaseMs:maxHoldMs and the names the gate may freeze; null when either is invalid.</summary>
883
+ private static GateSettings ParseGate(string spec, string names)
884
+ {
885
+ string[] parts = spec.Split(':');
886
+ if (parts.Length != 4 || names == null)
887
+ {
888
+ return null;
889
+ }
890
+ int[] v = new int[4];
891
+ for (int i = 0; i < 4; i++)
892
+ {
893
+ if (!int.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out v[i]))
894
+ {
895
+ return null;
896
+ }
897
+ }
898
+ if (v[0] < 64 || v[1] < 500 || v[1] > 60000 || v[2] < 100 || v[2] > 60000 || v[3] < 1000 || v[3] > MaxSuspendMs)
899
+ {
900
+ return null;
901
+ }
902
+ HashSet<string> set = new HashSet<string>();
903
+ foreach (string raw in names.Split(','))
904
+ {
905
+ string name = raw.Trim().ToLowerInvariant();
906
+ if (name.Length > 0)
907
+ {
908
+ set.Add(name);
909
+ }
910
+ }
911
+ if (set.Count == 0 || set.Count > MaxGateNames)
912
+ {
913
+ return null;
914
+ }
915
+ GateSettings g = new GateSettings();
916
+ g.GrowthMb = v[0];
917
+ g.WindowMs = v[1];
918
+ g.ReleaseMs = v[2];
919
+ g.MaxHoldMs = v[3];
920
+ g.Names = set;
921
+ return g;
922
+ }
923
+
924
+ private static string EventStart(string kind)
925
+ {
926
+ long at = (long)(DateTime.UtcNow - Epoch).TotalMilliseconds;
927
+ return "{\"event\":\"gate\",\"kind\":\"" + kind + "\",\"at\":" + Num(at);
928
+ }
929
+
930
+ private static string Num(long value)
931
+ {
932
+ return value.ToString(CultureInfo.InvariantCulture);
933
+ }
934
+
935
+ private static void EmitOff(string reason)
936
+ {
937
+ if (reason.Length > 150)
938
+ {
939
+ reason = reason.Substring(0, 150);
940
+ }
941
+ Emit(EventStart("off") + ",\"reason\":" + Json(reason) + "}");
942
+ }
943
+
944
+ /// <summary>Opens the job, joins its completion port, then reacts to births, exits and notifications until stop.</summary>
945
+ private static void GateLoop()
946
+ {
947
+ IntPtr h = IntPtr.Zero;
948
+ IntPtr port = IntPtr.Zero;
949
+ try
950
+ {
951
+ // The job does not exist until the first ptyhost makes it.
952
+ while (!stop)
953
+ {
954
+ h = NativeMethods.OpenJobObjectW(JobObjectQuery | JobObjectSetAttributes, false, JobName());
955
+ if (h != IntPtr.Zero)
956
+ {
957
+ break;
958
+ }
959
+ if (Marshal.GetLastWin32Error() == ErrorAccessDenied)
960
+ {
961
+ EmitOff("cannot open the agents' job (access denied)");
962
+ return;
963
+ }
964
+ Thread.Sleep(500);
965
+ }
966
+ if (h == IntPtr.Zero)
967
+ {
968
+ return;
969
+ }
970
+ bool notify = true;
971
+ port = NativeMethods.CreateIoCompletionPort(InvalidHandleValue, IntPtr.Zero, UIntPtr.Zero, 1);
972
+ if (port == IntPtr.Zero)
973
+ {
974
+ notify = false;
975
+ }
976
+ else
977
+ {
978
+ // First drop a dead memgov's port (one port per job), then take the job over.
979
+ SetPortLink(h, IntPtr.Zero, IntPtr.Zero);
980
+ if (!SetPortLink(h, new IntPtr(1), port))
981
+ {
982
+ NativeMethods.CloseHandle(port);
983
+ port = IntPtr.Zero;
984
+ notify = false;
985
+ }
986
+ }
987
+ DateTime now = DateTime.UtcNow;
988
+ long jobMb = JobCommitMb();
989
+ window = new List<KeyValuePair<DateTime, long>>();
990
+ window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
991
+ if (notify && !SetLimit(h, jobMb + gate.GrowthMb))
992
+ {
993
+ notify = false;
994
+ }
995
+ if (!notify)
996
+ {
997
+ seenPids = new HashSet<int>(JobProcessIds());
998
+ }
999
+ Emit(EventStart("ready") + ",\"mode\":\"" + (notify ? "notify" : "poll") + "\",\"growthMb\":" + Num(gate.GrowthMb)
1000
+ + ",\"windowMs\":" + Num(gate.WindowMs) + "}");
1001
+ DateTime lastTick = DateTime.UtcNow;
1002
+ while (!stop)
1003
+ {
1004
+ if (notify)
1005
+ {
1006
+ uint msg;
1007
+ UIntPtr key;
1008
+ IntPtr overlapped;
1009
+ if (NativeMethods.GetQueuedCompletionStatus(port, out msg, out key, out overlapped, GatePollMs))
1010
+ {
1011
+ int pid = (int)overlapped.ToInt64();
1012
+ if (msg == JobMsgNewProcess)
1013
+ {
1014
+ OnBirth(pid);
1015
+ }
1016
+ else if (msg == JobMsgExitProcess || msg == JobMsgAbnormalExitProcess)
1017
+ {
1018
+ OnExit(pid);
1019
+ }
1020
+ else if (msg == JobMsgNotificationLimit)
1021
+ {
1022
+ OnNotified(h, pid);
1023
+ }
1024
+ }
1025
+ }
1026
+ else
1027
+ {
1028
+ Thread.Sleep((int)GatePollMs);
1029
+ }
1030
+ if ((DateTime.UtcNow - lastTick).TotalMilliseconds >= GateTickMs)
1031
+ {
1032
+ lastTick = DateTime.UtcNow;
1033
+ Tick(h, notify);
1034
+ }
1035
+ ReleaseDue();
1036
+ }
1037
+ }
1038
+ catch (Exception e)
1039
+ {
1040
+ EmitOff(e.Message);
1041
+ }
1042
+ finally
1043
+ {
1044
+ if (h != IntPtr.Zero)
1045
+ {
1046
+ SetLimit(h, 0);
1047
+ SetPortLink(h, IntPtr.Zero, IntPtr.Zero);
1048
+ NativeMethods.CloseHandle(h);
1049
+ }
1050
+ if (port != IntPtr.Zero)
1051
+ {
1052
+ NativeMethods.CloseHandle(port);
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ private static bool SetPortLink(IntPtr h, IntPtr completionKey, IntPtr completionPort)
1058
+ {
1059
+ CompletionPortLink link = new CompletionPortLink();
1060
+ link.CompletionKey = completionKey;
1061
+ link.CompletionPort = completionPort;
1062
+ return NativeMethods.SetInformationJobObject(h, JobObjectAssociateCompletionPortInformation, ref link, Marshal.SizeOf(typeof(CompletionPortLink)));
1063
+ }
1064
+
1065
+ /// <summary>The notification mark in MB of job commit (0 removes it).</summary>
1066
+ private static bool SetLimit(IntPtr h, long mb)
1067
+ {
1068
+ NotificationLimit limit = new NotificationLimit();
1069
+ limit.LimitFlags = mb > 0 ? JobObjectLimitJobMemory : 0;
1070
+ limit.JobMemoryLimit = mb > 0 ? (ulong)(mb * BytesPerMb) : 0;
1071
+ bool ok = NativeMethods.SetInformationJobObject(h, JobObjectNotificationLimitInformation, ref limit, Marshal.SizeOf(typeof(NotificationLimit)));
1072
+ if (ok)
1073
+ {
1074
+ armedMb = mb;
1075
+ }
1076
+ return ok;
1077
+ }
1078
+
1079
+ /// <summary>What the job's processes (not this one) hold in private memory, in MB.</summary>
1080
+ private static long JobCommitMb()
1081
+ {
1082
+ ulong total = 0;
1083
+ int self = SelfId;
1084
+ foreach (int pid in JobProcessIds())
1085
+ {
1086
+ if (pid == self)
1087
+ {
1088
+ continue;
1089
+ }
1090
+ IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited, false, pid);
1091
+ if (process == IntPtr.Zero)
1092
+ {
1093
+ continue;
1094
+ }
1095
+ try
1096
+ {
1097
+ MemoryCounters counters = new MemoryCounters();
1098
+ counters.Size = (uint)Marshal.SizeOf(typeof(MemoryCounters));
1099
+ if (NativeMethods.GetProcessMemoryInfo(process, ref counters, counters.Size))
1100
+ {
1101
+ total += counters.PrivateUsage.ToUInt64();
1102
+ }
1103
+ }
1104
+ finally
1105
+ {
1106
+ NativeMethods.CloseHandle(process);
1107
+ }
1108
+ }
1109
+ return (long)(total / (ulong)BytesPerMb);
1110
+ }
1111
+
1112
+ /// <summary>The lower-case program name (no folder, no .exe) of an open process, or null.</summary>
1113
+ private static string ImageName(IntPtr process)
1114
+ {
1115
+ StringBuilder path = new StringBuilder(1024);
1116
+ uint size = (uint)path.Capacity;
1117
+ if (!NativeMethods.QueryFullProcessImageNameW(process, 0, path, ref size))
1118
+ {
1119
+ return null;
1120
+ }
1121
+ return Path.GetFileNameWithoutExtension(path.ToString(0, (int)size)).ToLowerInvariant();
1122
+ }
1123
+
1124
+ /// <summary>
1125
+ /// Starts watching a process if the gate may freeze it by name; returns its name, or null when it is not one of
1126
+ /// those (or is already watched or held).
1127
+ /// </summary>
1128
+ private static string Track(int pid)
1129
+ {
1130
+ if (pid <= 0 || pid == SelfId)
1131
+ {
1132
+ return null;
1133
+ }
1134
+ IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited | ProcessSuspendResume, false, pid);
1135
+ if (process == IntPtr.Zero)
1136
+ {
1137
+ return null;
1138
+ }
1139
+ bool kept = false;
1140
+ try
1141
+ {
1142
+ string name = ImageName(process);
1143
+ if (name == null || !gate.Names.Contains(name))
1144
+ {
1145
+ return null;
1146
+ }
1147
+ lock (Gate)
1148
+ {
1149
+ if (Young.ContainsKey(pid) || GateHeld.ContainsKey(pid))
1150
+ {
1151
+ return null;
1152
+ }
1153
+ Tracked t = new Tracked();
1154
+ t.Handle = process;
1155
+ t.Name = name;
1156
+ t.Started = DateTime.UtcNow;
1157
+ Young[pid] = t;
1158
+ }
1159
+ kept = true;
1160
+ return name;
1161
+ }
1162
+ finally
1163
+ {
1164
+ if (!kept)
1165
+ {
1166
+ NativeMethods.CloseHandle(process);
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ private static void OnBirth(int pid)
1172
+ {
1173
+ string name = Track(pid);
1174
+ if (name != null && BurstActive(DateTime.UtcNow) && Hold(pid))
1175
+ {
1176
+ Emit(EventStart("hold") + ",\"pid\":" + Num(pid) + ",\"name\":" + Json(name) + "}");
1177
+ }
1178
+ }
1179
+
1180
+ private static void OnExit(int pid)
1181
+ {
1182
+ lock (Gate)
1183
+ {
1184
+ Tracked t;
1185
+ if (Young.TryGetValue(pid, out t))
1186
+ {
1187
+ NativeMethods.CloseHandle(t.Handle);
1188
+ Young.Remove(pid);
1189
+ }
1190
+ if (GateHeld.TryGetValue(pid, out t))
1191
+ {
1192
+ NativeMethods.CloseHandle(t.Handle);
1193
+ GateHeld.Remove(pid);
1194
+ SaveHeld();
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ /// <summary>The job crossed its mark. The query re-arms the notification, so it comes first and always.</summary>
1200
+ private static void OnNotified(IntPtr h, int pid)
1201
+ {
1202
+ LimitViolation v = new LimitViolation();
1203
+ bool queried = NativeMethods.QueryInformationJobObject(h, JobObjectLimitViolationInformation, ref v, Marshal.SizeOf(typeof(LimitViolation)), IntPtr.Zero);
1204
+ long jobMb;
1205
+ if (queried && (v.ViolationLimitFlags & JobObjectLimitJobMemory) != 0)
1206
+ {
1207
+ jobMb = (long)(v.JobMemory / (ulong)BytesPerMb);
1208
+ }
1209
+ else
1210
+ {
1211
+ jobMb = JobCommitMb();
1212
+ }
1213
+ Trip(h, true, pid, jobMb);
1214
+ }
1215
+
1216
+ /// <summary>The lowest job commit in the window and when it was.</summary>
1217
+ private static long WindowMin(out DateTime at)
1218
+ {
1219
+ long min = long.MaxValue;
1220
+ at = DateTime.UtcNow;
1221
+ foreach (KeyValuePair<DateTime, long> entry in window)
1222
+ {
1223
+ if (entry.Value < min)
1224
+ {
1225
+ min = entry.Value;
1226
+ at = entry.Key;
1227
+ }
1228
+ }
1229
+ return min;
1230
+ }
1231
+
1232
+ /// <summary>Every GateTickMs: the window, young entries, births in poll mode, the software trip and the moving mark.</summary>
1233
+ private static void Tick(IntPtr h, bool notify)
1234
+ {
1235
+ DateTime now = DateTime.UtcNow;
1236
+ long jobMb = JobCommitMb();
1237
+ window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
1238
+ window.RemoveAll(delegate(KeyValuePair<DateTime, long> e) { return (now - e.Key).TotalMilliseconds > gate.WindowMs; });
1239
+ DateTime minAt;
1240
+ long min = WindowMin(out minAt);
1241
+ lock (Gate)
1242
+ {
1243
+ List<int> old = new List<int>();
1244
+ foreach (KeyValuePair<int, Tracked> p in Young)
1245
+ {
1246
+ if ((now - p.Value.Started).TotalMilliseconds > gate.WindowMs)
1247
+ {
1248
+ old.Add(p.Key);
1249
+ }
1250
+ }
1251
+ foreach (int pid in old)
1252
+ {
1253
+ NativeMethods.CloseHandle(Young[pid].Handle);
1254
+ Young.Remove(pid);
1255
+ }
1256
+ }
1257
+ if (!notify)
1258
+ {
1259
+ List<int> current = JobProcessIds();
1260
+ foreach (int pid in current)
1261
+ {
1262
+ if (!seenPids.Contains(pid))
1263
+ {
1264
+ OnBirth(pid);
1265
+ }
1266
+ }
1267
+ seenPids = new HashSet<int>(current);
1268
+ }
1269
+ if (jobMb - min >= gate.GrowthMb)
1270
+ {
1271
+ Trip(h, notify, 0, jobMb);
1272
+ return;
1273
+ }
1274
+ if (notify)
1275
+ {
1276
+ long target = min + gate.GrowthMb;
1277
+ if (Math.Abs(target - armedMb) >= GateRearmStepMb)
1278
+ {
1279
+ SetLimit(h, target);
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ /// <summary>A burst: freeze the young build programs (and the one that caused it), then move the mark up.</summary>
1285
+ private static void Trip(IntPtr h, bool notify, int causePid, long jobMb)
1286
+ {
1287
+ DateTime now = DateTime.UtcNow;
1288
+ long grew = 0;
1289
+ long ms = 0;
1290
+ if (window.Count > 0)
1291
+ {
1292
+ DateTime minAt;
1293
+ long min = WindowMin(out minAt);
1294
+ grew = Math.Max(0, jobMb - min);
1295
+ ms = Math.Max(0, (long)(now - minAt).TotalMilliseconds);
1296
+ }
1297
+ lastTrip = now;
1298
+ if (causePid > 0)
1299
+ {
1300
+ // The program that caused the notification, even if it is old (a no-op unless the gate may freeze its name).
1301
+ Track(causePid);
1302
+ }
1303
+ List<KeyValuePair<int, Tracked>> victims = new List<KeyValuePair<int, Tracked>>();
1304
+ lock (Gate)
1305
+ {
1306
+ foreach (KeyValuePair<int, Tracked> p in Young)
1307
+ {
1308
+ if (p.Key == causePid || (now - p.Value.Started).TotalMilliseconds < gate.WindowMs)
1309
+ {
1310
+ victims.Add(p);
1311
+ }
1312
+ }
1313
+ }
1314
+ victims.Sort(delegate(KeyValuePair<int, Tracked> a, KeyValuePair<int, Tracked> b) { return a.Value.Started.CompareTo(b.Value.Started); });
1315
+ StringBuilder held = new StringBuilder();
1316
+ int count = 0;
1317
+ foreach (KeyValuePair<int, Tracked> victim in victims)
1318
+ {
1319
+ string name = victim.Value.Name;
1320
+ if (!Hold(victim.Key) || count >= MaxTripList)
1321
+ {
1322
+ continue;
1323
+ }
1324
+ if (count > 0)
1325
+ {
1326
+ held.Append(',');
1327
+ }
1328
+ held.Append("{\"pid\":").Append(Num(victim.Key)).Append(",\"name\":").Append(Json(name)).Append('}');
1329
+ count++;
1330
+ }
1331
+ Emit(EventStart("trip") + ",\"grewMb\":" + Num(grew) + ",\"ms\":" + Num(ms) + ",\"jobMb\":" + Num(jobMb)
1332
+ + ",\"held\":[" + held.ToString() + "]}");
1333
+ window = new List<KeyValuePair<DateTime, long>>();
1334
+ window.Add(new KeyValuePair<DateTime, long>(now, jobMb));
1335
+ if (notify)
1336
+ {
1337
+ SetLimit(h, jobMb + gate.GrowthMb);
1338
+ }
1339
+ }
1340
+
1341
+ /// <summary>
1342
+ /// A burst went through less than WindowMs ago. Not "something is held": with fast compiles the queue would never
1343
+ /// empty, and every later program would start one per ReleaseMs while memory is calm.
1344
+ /// </summary>
1345
+ private static bool BurstActive(DateTime now)
1346
+ {
1347
+ lock (Gate)
1348
+ {
1349
+ return (now - lastTrip).TotalMilliseconds < gate.WindowMs;
1350
+ }
1351
+ }
1352
+
1353
+ /// <summary>Freezes a watched process (unless the governor's timed pause has it); false when it was not frozen.</summary>
1354
+ private static bool Hold(int pid)
1355
+ {
1356
+ lock (Gate)
1357
+ {
1358
+ Tracked t;
1359
+ if (Paused.ContainsKey(pid) || GateHeld.ContainsKey(pid) || !Young.TryGetValue(pid, out t))
1360
+ {
1361
+ return false;
1362
+ }
1363
+ Young.Remove(pid);
1364
+ t.HeldAt = DateTime.UtcNow;
1365
+ GateHeld[pid] = t;
1366
+ // Recorded before the freeze, like the governor's pauses: a memgov that dies right after is undone by the next.
1367
+ SaveHeld();
1368
+ int status = NativeMethods.NtSuspendProcess(t.Handle);
1369
+ if (status != 0)
1370
+ {
1371
+ GateHeld.Remove(pid);
1372
+ NativeMethods.CloseHandle(t.Handle);
1373
+ SaveHeld();
1374
+ return false;
1375
+ }
1376
+ return true;
1377
+ }
1378
+ }
1379
+
1380
+ /// <summary>Lets go what is due: anything held MaxHoldMs, and one process (the oldest) every ReleaseMs after a calm moment.</summary>
1381
+ private static void ReleaseDue()
1382
+ {
1383
+ List<Released> done = new List<Released>();
1384
+ lock (Gate)
1385
+ {
1386
+ if (GateHeld.Count == 0)
1387
+ {
1388
+ return;
1389
+ }
1390
+ DateTime now = DateTime.UtcNow;
1391
+ List<int> forced = new List<int>();
1392
+ foreach (KeyValuePair<int, Tracked> p in GateHeld)
1393
+ {
1394
+ if ((now - p.Value.HeldAt).TotalMilliseconds >= gate.MaxHoldMs)
1395
+ {
1396
+ forced.Add(p.Key);
1397
+ }
1398
+ }
1399
+ foreach (int pid in forced)
1400
+ {
1401
+ done.Add(ReleaseOne(pid, now, true));
1402
+ }
1403
+ if (GateHeld.Count > 0 && (now - lastTrip).TotalMilliseconds >= gate.ReleaseMs
1404
+ && (now - lastRelease).TotalMilliseconds >= gate.ReleaseMs)
1405
+ {
1406
+ int oldest = 0;
1407
+ DateTime oldestAt = DateTime.MaxValue;
1408
+ foreach (KeyValuePair<int, Tracked> p in GateHeld)
1409
+ {
1410
+ if (p.Value.HeldAt < oldestAt || (p.Value.HeldAt == oldestAt && p.Key < oldest))
1411
+ {
1412
+ oldest = p.Key;
1413
+ oldestAt = p.Value.HeldAt;
1414
+ }
1415
+ }
1416
+ done.Add(ReleaseOne(oldest, now, false));
1417
+ lastRelease = now;
1418
+ }
1419
+ if (done.Count > 0)
1420
+ {
1421
+ SaveHeld();
1422
+ }
1423
+ }
1424
+ foreach (Released r in done)
1425
+ {
1426
+ Emit(EventStart("release") + ",\"pid\":" + Num(r.Pid) + ",\"name\":" + Json(r.Name) + ",\"heldMs\":" + Num(r.HeldMs)
1427
+ + ",\"forced\":" + (r.Forced ? "true" : "false") + "}");
1428
+ }
1429
+ }
1430
+
1431
+ /// <summary>Resumes and forgets one held process. Called with Gate held.</summary>
1432
+ private static Released ReleaseOne(int pid, DateTime now, bool forced)
1433
+ {
1434
+ Tracked t = GateHeld[pid];
1435
+ NativeMethods.NtResumeProcess(t.Handle);
1436
+ NativeMethods.CloseHandle(t.Handle);
1437
+ GateHeld.Remove(pid);
1438
+ Released r = new Released();
1439
+ r.Pid = pid;
1440
+ r.Name = t.Name;
1441
+ r.HeldMs = (long)(now - t.HeldAt).TotalMilliseconds;
1442
+ r.Forced = forced;
1443
+ return r;
1444
+ }
1445
+
701
1446
  private static string Mb(ulong bytes)
702
1447
  {
703
1448
  return Math.Floor(bytes / BytesPerMb).ToString("0", CultureInfo.InvariantCulture);