windows-development-sandbox 1.11.3 → 1.11.5

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 (36) hide show
  1. package/dist/app/features/accounts/accounts.def.js +0 -1
  2. package/dist/app/features/data/data.def.d.ts +1 -1
  3. package/dist/app/features/data/data.def.js +41 -48
  4. package/dist/app/features/defs.d.ts +7 -0
  5. package/dist/app/features/defs.js +7 -3
  6. package/dist/app/features/health/health.def.js +28 -9
  7. package/dist/app/features/sandbox/sandbox.def.js +4 -4
  8. package/dist/app/features/storage/storage.def.js +8 -13
  9. package/dist/app/features/updates/updates.def.js +2 -3
  10. package/dist/guest/toolchains/tools/cli.js +19 -0
  11. package/dist/host/life/host-memory.d.ts +18 -0
  12. package/dist/host/life/host-memory.js +66 -0
  13. package/dist/host/web/runner.d.ts +1 -1
  14. package/dist/host/web/server.d.ts +12 -3
  15. package/dist/host/web/server.js +20 -6
  16. package/dist/runtime/daemon.js +16 -1
  17. package/dist/runtime/main.js +16 -1
  18. package/dist/runtime/view.js +2 -0
  19. package/dist/selftest/app.test.js +21 -9
  20. package/dist/selftest/health.test.js +35 -0
  21. package/dist/selftest/live-terminal.test.js +30 -0
  22. package/dist/selftest/support/web-preview.js +3 -0
  23. package/dist/selftest/web.test.js +33 -0
  24. package/dist/shared/pty/client.d.ts +0 -1
  25. package/dist/shared/pty/client.js +5 -0
  26. package/dist/sync/watcher.d.ts +2 -0
  27. package/dist/sync/watcher.js +7 -2
  28. package/dist/web/index.html +2 -2
  29. package/dist/web/{main-Z645W45E.js → main-QL7KHCEE.js} +76 -76
  30. package/dist/web/{main-6JF3ASB5.css → main-RQFN4WBY.css} +1 -1
  31. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  32. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  33. package/native/csharp/HostTools/MemoryStatus.cs +58 -2
  34. package/native/csharp/HostTools/Program.cs +7 -1
  35. package/package.json +1 -1
  36. package/sandbox/csharp/PtyHost.cs +228 -2
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.11.3.0")]
12
- [assembly: AssemblyFileVersion("1.11.3.0")]
13
- [assembly: AssemblyInformationalVersion("1.11.3")]
11
+ [assembly: AssemblyVersion("1.11.5.0")]
12
+ [assembly: AssemblyFileVersion("1.11.5.0")]
13
+ [assembly: AssemblyInformationalVersion("1.11.5")]
14
14
  [assembly: ComVisible(false)]
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.11.3.0")]
12
- [assembly: AssemblyFileVersion("1.11.3.0")]
13
- [assembly: AssemblyInformationalVersion("1.11.3")]
11
+ [assembly: AssemblyVersion("1.11.5.0")]
12
+ [assembly: AssemblyFileVersion("1.11.5.0")]
13
+ [assembly: AssemblyInformationalVersion("1.11.5")]
14
14
  [assembly: ComVisible(false)]
@@ -6,6 +6,7 @@ using System.ComponentModel;
6
6
  using System.Diagnostics;
7
7
  using System.Globalization;
8
8
  using System.Runtime.InteropServices;
9
+ using System.Threading;
9
10
  using Microsoft.Win32;
10
11
 
11
12
  namespace AgentSandbox
@@ -108,18 +109,73 @@ namespace AgentSandbox
108
109
  {
109
110
  try
110
111
  {
111
- using (PerformanceCounter counter = new PerformanceCounter("PhysicalDisk", "Current Disk Queue Length", "_Total", true))
112
+ // Made once and kept: creating a performance counter costs far more than reading one.
113
+ if (diskCounter == null && !noDiskCounter)
112
114
  {
113
- return ",\"diskQueue\":" + Math.Round(counter.NextValue()).ToString("0", CultureInfo.InvariantCulture);
115
+ diskCounter = new PerformanceCounter("PhysicalDisk", "Current Disk Queue Length", "_Total", true);
114
116
  }
117
+ if (diskCounter == null)
118
+ {
119
+ return "";
120
+ }
121
+ return ",\"diskQueue\":" + Math.Round(diskCounter.NextValue()).ToString("0", CultureInfo.InvariantCulture);
115
122
  }
116
123
  catch (Exception)
117
124
  {
118
125
  // No performance counters on this PC: the watchdog then looks at memory and CPU only.
126
+ noDiskCounter = true;
127
+ diskCounter = null;
119
128
  return "";
120
129
  }
121
130
  }
122
131
 
132
+ private static PerformanceCounter diskCounter;
133
+ private static bool noDiskCounter;
134
+ private static volatile bool inputClosed;
135
+
136
+ /// <summary>
137
+ /// memory-status --serve S: one JSON line every S seconds until stdin closes (the watcher keeps one of these
138
+ /// instead of starting a program every few seconds). Returns the exit code.
139
+ /// </summary>
140
+ public static int Serve(int seconds)
141
+ {
142
+ Thread reader = new Thread(WaitForInputToClose);
143
+ reader.IsBackground = true;
144
+ reader.Start();
145
+ while (!inputClosed)
146
+ {
147
+ try
148
+ {
149
+ Console.Out.WriteLine(Json());
150
+ Console.Out.Flush();
151
+ }
152
+ catch (Exception)
153
+ {
154
+ // One reading that fails is skipped; the next one tries again.
155
+ }
156
+ for (int waited = 0; waited < seconds * 10 && !inputClosed; waited++)
157
+ {
158
+ Thread.Sleep(100);
159
+ }
160
+ }
161
+ return 0;
162
+ }
163
+
164
+ private static void WaitForInputToClose()
165
+ {
166
+ try
167
+ {
168
+ while (Console.In.ReadLine() != null)
169
+ {
170
+ }
171
+ }
172
+ catch (Exception)
173
+ {
174
+ // A broken input counts as closed.
175
+ }
176
+ inputClosed = true;
177
+ }
178
+
123
179
  private static string Mb(ulong bytes)
124
180
  {
125
181
  return Math.Floor(bytes / BytesPerMb).ToString("0", CultureInfo.InvariantCulture);
@@ -1,7 +1,8 @@
1
1
  // HostTools.exe: what the host side asks of Windows without PowerShell. Built on first use by host/native.ts (csc).
2
2
  // keep-awake --owner-pid N this PC stays awake until process N ends (sandbox.keepAwake)
3
3
  // stop-events --since <iso> the Windows events since then that can end a sandbox, as JSON (host/life)
4
- // memory-status this PC's RAM and commit (what a sandbox's memory is charged against), as JSON
4
+ // memory-status [--serve S] this PC's RAM and commit (what a sandbox's memory is charged against), as JSON; with
5
+ // --serve one line every S seconds until stdin closes
5
6
  // hide-window --wait S hides the sandbox window once it appears; prints the hidden handles as JSON
6
7
  // show-window --handles a,b shows those windows (else any sandbox window); prints how many
7
8
  // user-path --add|--remove D D first on, or off, your PATH; exit 3 when PATH would be too long
@@ -42,6 +43,11 @@ static class Program
42
43
  Console.Out.Write(StopEventsJson(since));
43
44
  return 0;
44
45
  case "memory-status":
46
+ int every;
47
+ if (args.Length >= 3 && args[1] == "--serve" && int.TryParse(args[2], out every) && every >= 1 && every <= 3600)
48
+ {
49
+ return AgentSandbox.HostMemory.Serve(every);
50
+ }
45
51
  try
46
52
  {
47
53
  Console.Out.Write(AgentSandbox.HostMemory.Json());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.11.3",
3
+ "version": "1.11.5",
4
4
  "description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -856,6 +856,221 @@ sealed class ClientHub
856
856
  }
857
857
  }
858
858
 
859
+ /// <summary>
860
+ /// Everything a session's program started, followed while it runs (every few seconds, so a process whose parent has
861
+ /// already exited is still known) and ended when the program exits: nothing an agent starts outlives its session.
862
+ /// A process is recognised by its id and start time, so an id Windows has given to another process is never touched.
863
+ /// Shared services an agent may start for every session (databases, the compile cache) are left running.
864
+ /// </summary>
865
+ sealed class ProcessTree
866
+ {
867
+ private const uint SnapProcess = 0x00000002;
868
+ private const uint ProcessTerminate = 0x0001;
869
+ private const uint ProcessQueryLimited = 0x1000;
870
+ private const int FollowMs = 3000;
871
+ private const int MaxPath = 260;
872
+ private static readonly IntPtr InvalidHandle = new IntPtr(-1);
873
+ private static readonly HashSet<string> Shared = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
874
+ {
875
+ "postgres.exe", "pg_ctl.exe", "mysqld.exe", "mariadbd.exe", "redis-server.exe", "mongod.exe", "sccache.exe",
876
+ "sbx-netproxy.exe",
877
+ };
878
+
879
+ // PROCESSENTRY32W.
880
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
881
+ private struct ProcessEntry
882
+ {
883
+ public uint Size;
884
+ public uint Usage;
885
+ public uint ProcessId;
886
+ public IntPtr DefaultHeapId;
887
+ public uint ModuleId;
888
+ public uint Threads;
889
+ public uint ParentProcessId;
890
+ public int PriorityBase;
891
+ public uint Flags;
892
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MaxPath)]
893
+ public string ExeFile;
894
+ }
895
+
896
+ private static class NativeMethods
897
+ {
898
+ [DllImport("kernel32.dll", SetLastError = true)]
899
+ public static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId);
900
+
901
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
902
+ public static extern bool Process32FirstW(IntPtr snapshot, ref ProcessEntry entry);
903
+
904
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
905
+ public static extern bool Process32NextW(IntPtr snapshot, ref ProcessEntry entry);
906
+
907
+ [DllImport("kernel32.dll", SetLastError = true)]
908
+ public static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
909
+
910
+ [DllImport("kernel32.dll", SetLastError = true)]
911
+ public static extern bool GetProcessTimes(IntPtr process, out long creation, out long exit, out long kernel, out long user);
912
+
913
+ [DllImport("kernel32.dll", SetLastError = true)]
914
+ public static extern bool TerminateProcess(IntPtr process, uint exitCode);
915
+
916
+ [DllImport("kernel32.dll", SetLastError = true)]
917
+ public static extern int GetProcessId(IntPtr process);
918
+
919
+ [DllImport("kernel32.dll", SetLastError = true)]
920
+ public static extern bool CloseHandle(IntPtr handle);
921
+ }
922
+
923
+ private readonly object gate = new object();
924
+ /// <summary>The program and what it started: process id -> start time.</summary>
925
+ private readonly Dictionary<int, long> known = new Dictionary<int, long>();
926
+ private readonly int root;
927
+ private readonly Timer timer;
928
+ private bool ended;
929
+
930
+ public ProcessTree(IntPtr rootProcess)
931
+ {
932
+ root = NativeMethods.GetProcessId(rootProcess);
933
+ long start = StartTime(root);
934
+ if (root != 0 && start != 0)
935
+ {
936
+ known[root] = start;
937
+ }
938
+ timer = new Timer(Follow, null, FollowMs, FollowMs);
939
+ }
940
+
941
+ private void Follow(object unused)
942
+ {
943
+ lock (gate)
944
+ {
945
+ if (!ended)
946
+ {
947
+ Refresh();
948
+ }
949
+ }
950
+ }
951
+
952
+ /// <summary>Adds every process whose parent is known (and that started after it), until nothing new is found.</summary>
953
+ private void Refresh()
954
+ {
955
+ if (known.Count == 0)
956
+ {
957
+ return;
958
+ }
959
+ List<ProcessEntry> all = Snapshot();
960
+ bool added = true;
961
+ while (added)
962
+ {
963
+ added = false;
964
+ foreach (ProcessEntry p in all)
965
+ {
966
+ int pid = (int)p.ProcessId;
967
+ int parent = (int)p.ParentProcessId;
968
+ long parentStart;
969
+ if (known.ContainsKey(pid) || !known.TryGetValue(parent, out parentStart) || Shared.Contains(p.ExeFile))
970
+ {
971
+ continue;
972
+ }
973
+ long start = StartTime(pid);
974
+ // A child starts after its parent: an older process that names a reused id as its parent is not ours.
975
+ if (start == 0 || start < parentStart)
976
+ {
977
+ continue;
978
+ }
979
+ known[pid] = start;
980
+ added = true;
981
+ }
982
+ }
983
+ }
984
+
985
+ /// <summary>Stops following and ends what the program started that still runs; returns how many were ended.</summary>
986
+ public int EndAll()
987
+ {
988
+ lock (gate)
989
+ {
990
+ if (ended)
991
+ {
992
+ return 0;
993
+ }
994
+ Refresh();
995
+ ended = true;
996
+ timer.Dispose();
997
+ int count = 0;
998
+ foreach (KeyValuePair<int, long> p in known)
999
+ {
1000
+ if (p.Key != root && StartTime(p.Key) == p.Value && Terminate(p.Key))
1001
+ {
1002
+ count++;
1003
+ }
1004
+ }
1005
+ return count;
1006
+ }
1007
+ }
1008
+
1009
+ private static List<ProcessEntry> Snapshot()
1010
+ {
1011
+ List<ProcessEntry> all = new List<ProcessEntry>();
1012
+ IntPtr snapshot = NativeMethods.CreateToolhelp32Snapshot(SnapProcess, 0);
1013
+ if (snapshot == InvalidHandle || snapshot == IntPtr.Zero)
1014
+ {
1015
+ return all;
1016
+ }
1017
+ try
1018
+ {
1019
+ ProcessEntry entry = new ProcessEntry();
1020
+ entry.Size = (uint)Marshal.SizeOf(typeof(ProcessEntry));
1021
+ bool more = NativeMethods.Process32FirstW(snapshot, ref entry);
1022
+ while (more)
1023
+ {
1024
+ all.Add(entry);
1025
+ more = NativeMethods.Process32NextW(snapshot, ref entry);
1026
+ }
1027
+ }
1028
+ finally
1029
+ {
1030
+ NativeMethods.CloseHandle(snapshot);
1031
+ }
1032
+ return all;
1033
+ }
1034
+
1035
+ /// <summary>When the process started (FILETIME), or 0 when it is gone or cannot be read.</summary>
1036
+ private static long StartTime(int pid)
1037
+ {
1038
+ IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited, false, pid);
1039
+ if (process == IntPtr.Zero)
1040
+ {
1041
+ return 0;
1042
+ }
1043
+ try
1044
+ {
1045
+ long creation;
1046
+ long exit;
1047
+ long kernel;
1048
+ long user;
1049
+ return NativeMethods.GetProcessTimes(process, out creation, out exit, out kernel, out user) ? creation : 0;
1050
+ }
1051
+ finally
1052
+ {
1053
+ NativeMethods.CloseHandle(process);
1054
+ }
1055
+ }
1056
+
1057
+ private static bool Terminate(int pid)
1058
+ {
1059
+ IntPtr process = NativeMethods.OpenProcess(ProcessTerminate, false, pid);
1060
+ if (process == IntPtr.Zero)
1061
+ {
1062
+ return false;
1063
+ }
1064
+ try
1065
+ {
1066
+ return NativeMethods.TerminateProcess(process, 1);
1067
+ }
1068
+ finally
1069
+ {
1070
+ NativeMethods.CloseHandle(process);
1071
+ }
1072
+ }
1073
+ }
859
1074
  /// <summary>One program running inside a pseudo console, with a thread pumping its output to `onOutput`.</summary>
860
1075
  sealed class PseudoConsole : IDisposable
861
1076
  {
@@ -867,16 +1082,20 @@ sealed class PseudoConsole : IDisposable
867
1082
  private readonly IntPtr process;
868
1083
  private readonly FileStream input;
869
1084
  private readonly Thread pump;
1085
+ private readonly ProcessTree tree;
1086
+ private readonly Log log;
870
1087
  private readonly object gate = new object();
871
1088
  private bool closed;
872
1089
  private bool disposed;
873
1090
 
874
- private PseudoConsole(IntPtr handle, IntPtr process, FileStream input, Thread pump)
1091
+ private PseudoConsole(IntPtr handle, IntPtr process, FileStream input, Thread pump, Log log)
875
1092
  {
876
1093
  this.handle = handle;
877
1094
  this.process = process;
878
1095
  this.input = input;
879
1096
  this.pump = pump;
1097
+ this.log = log;
1098
+ tree = new ProcessTree(process);
880
1099
  }
881
1100
 
882
1101
  public static PseudoConsole Start(string commandLine, short cols, short rows, Action<byte[]> onOutput, Log log)
@@ -900,7 +1119,7 @@ sealed class PseudoConsole : IDisposable
900
1119
  Thread pump = new Thread(() => Pump(output, onOutput));
901
1120
  pump.IsBackground = true;
902
1121
  pump.Start();
903
- return new PseudoConsole(handle, process, new FileStream(inWrite, FileAccess.Write), pump);
1122
+ return new PseudoConsole(handle, process, new FileStream(inWrite, FileAccess.Write), pump, log);
904
1123
  }
905
1124
 
906
1125
  public int WaitForExit()
@@ -910,6 +1129,13 @@ sealed class PseudoConsole : IDisposable
910
1129
  Native.GetExitCodeProcess(process, out code);
911
1130
  Close();
912
1131
  pump.Join(PumpJoinMs);
1132
+ // Closing the console asked what was attached to it to end; whatever the program started that is still
1133
+ // running now (a server in the background, a build that lost its parent) ends with it.
1134
+ int ended = tree.EndAll();
1135
+ if (ended > 0)
1136
+ {
1137
+ log.Write("ended " + ended + " process(es) the program left running");
1138
+ }
913
1139
  return (int)code;
914
1140
  }
915
1141