windows-development-sandbox 1.13.2 → 1.15.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 (153) hide show
  1. package/config.example.json +2 -1
  2. package/dist/app/features/defs.d.ts +1 -1
  3. package/dist/app/features/projects/projects.def.d.ts +1 -1
  4. package/dist/app/features/projects/projects.run.d.ts +1 -1
  5. package/dist/app/features/projects/projects.run.js +10 -2
  6. package/dist/app/features/sandbox/sandbox.def.js +3 -1
  7. package/dist/app/features/settings/categories.def.js +1 -0
  8. package/dist/app/features/storage/storage.run.js +1 -1
  9. package/dist/bin/agent-sandbox.js +2 -2
  10. package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
  11. package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
  12. package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
  13. package/dist/bin/claude-host.js +2 -2
  14. package/dist/bin/claude-sbx.js +2 -2
  15. package/dist/bin/codex-sbx.js +2 -2
  16. package/dist/bin/gemini-sbx.js +2 -2
  17. package/dist/bin/sandbox.js +2 -2
  18. package/dist/bin/sbx-hide.js +2 -2
  19. package/dist/bin/sbx-show.js +2 -2
  20. package/dist/cli/commands/launch/launch.js +1 -1
  21. package/dist/cli/commands/projects/here.js +2 -0
  22. package/dist/cli/commands/repair/checks/contained.js +1 -1
  23. package/dist/cli/services/health-service.js +15 -3
  24. package/dist/cli/services/projects-service.d.ts +8 -3
  25. package/dist/cli/services/projects-service.js +21 -4
  26. package/dist/core/config/defaults.js +1 -0
  27. package/dist/core/schema/config.d.ts +4 -0
  28. package/dist/core/schema/projects.d.ts +4 -0
  29. package/dist/core/schema/projects.js +3 -0
  30. package/dist/guest/boot/boot.js +8 -2
  31. package/dist/guest/daemon/daemon.js +3 -0
  32. package/dist/guest/daemon/live-folders.d.ts +48 -0
  33. package/dist/guest/daemon/live-folders.js +245 -0
  34. package/dist/guest/daemon/live.d.ts +4 -0
  35. package/dist/guest/daemon/live.js +57 -0
  36. package/dist/guest/daemon/sessions.d.ts +7 -0
  37. package/dist/guest/daemon/sessions.js +23 -2
  38. package/dist/guest/entries/daemon.js +5 -2
  39. package/dist/guest/runtime/csc.d.ts +1 -0
  40. package/dist/guest/runtime/csc.js +1 -0
  41. package/dist/guest/runtime/secrets.d.ts +5 -2
  42. package/dist/guest/runtime/secrets.js +12 -5
  43. package/dist/guest/sessions/pty-host.d.ts +10 -0
  44. package/dist/guest/sessions/pty-host.js +48 -3
  45. package/dist/host/channel/channel.js +1 -1
  46. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  47. package/dist/host/engines/hcs/live-folders.js +347 -0
  48. package/dist/host/engines/index.d.ts +1 -0
  49. package/dist/host/engines/index.js +1 -0
  50. package/dist/host/install/install-record.d.ts +1 -0
  51. package/dist/host/install/install-record.js +2 -0
  52. package/dist/host/launch/generate.js +8 -1
  53. package/dist/host/projects/join.d.ts +4 -0
  54. package/dist/host/projects/join.js +11 -3
  55. package/dist/host/projects/mapper.d.ts +34 -5
  56. package/dist/host/projects/mapper.js +88 -20
  57. package/dist/host/projects/registry.d.ts +2 -0
  58. package/dist/host/projects/registry.js +1 -1
  59. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  60. package/dist/host/sandbox/contained-deps.js +6 -4
  61. package/dist/host/secrets/launch-secrets.js +2 -1
  62. package/dist/host/sharing/lifecycle.js +9 -1
  63. package/dist/host/storage/contained/contained-storage.js +13 -0
  64. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  65. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  66. package/dist/host/storage/contained/helper-port.js +162 -0
  67. package/dist/host/storage/contained/index.d.ts +1 -0
  68. package/dist/host/storage/contained/index.js +1 -0
  69. package/dist/host/web/bridge.d.ts +5 -0
  70. package/dist/host/web/bridge.js +11 -1
  71. package/dist/host/web/server.d.ts +2 -0
  72. package/dist/host/web/server.js +11 -1
  73. package/dist/runtime/agent.js +23 -0
  74. package/dist/runtime/daemon.js +523 -135
  75. package/dist/runtime/db.js +23 -0
  76. package/dist/runtime/main.js +221 -15
  77. package/dist/runtime/netproxy.js +23 -0
  78. package/dist/runtime/view.js +209 -12
  79. package/dist/selftest/channel.test.js +72 -1
  80. package/dist/selftest/contained.test.js +1 -37
  81. package/dist/selftest/hcs.test.js +5 -2
  82. package/dist/selftest/index.js +4 -0
  83. package/dist/selftest/install.test.js +11 -0
  84. package/dist/selftest/layout.test.js +1 -0
  85. package/dist/selftest/live-daemon.test.js +51 -1
  86. package/dist/selftest/live-folders.test.d.ts +2 -0
  87. package/dist/selftest/live-folders.test.js +588 -0
  88. package/dist/selftest/live-terminal.test.js +112 -4
  89. package/dist/selftest/projects.test.js +7 -7
  90. package/dist/selftest/storage-helper.test.d.ts +2 -0
  91. package/dist/selftest/storage-helper.test.js +493 -0
  92. package/dist/selftest/support/engine-golden.js +1 -0
  93. package/dist/selftest/support/fixtures.d.ts +1 -0
  94. package/dist/selftest/support/live.js +2 -0
  95. package/dist/selftest/support/vhdx.d.ts +14 -0
  96. package/dist/selftest/support/vhdx.js +72 -0
  97. package/dist/selftest/web.test.js +14 -2
  98. package/dist/shared/channel/index.d.ts +1 -0
  99. package/dist/shared/channel/index.js +1 -0
  100. package/dist/shared/channel/live.d.ts +53 -0
  101. package/dist/shared/channel/live.js +92 -0
  102. package/dist/shared/channel/run-dir.d.ts +11 -0
  103. package/dist/shared/channel/run-dir.js +20 -0
  104. package/dist/shared/channel/secrets.d.ts +1 -0
  105. package/dist/shared/channel/secrets.js +5 -0
  106. package/dist/shared/paths/projects.d.ts +6 -1
  107. package/dist/shared/paths/projects.js +21 -1
  108. package/dist/shared/pty/index.d.ts +3 -1
  109. package/dist/shared/pty/index.js +3 -1
  110. package/dist/shared/pty/key-gate.d.ts +24 -0
  111. package/dist/shared/pty/key-gate.js +45 -0
  112. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  113. package/dist/shared/pty/mouse-modes.js +155 -0
  114. package/dist/shared/pty/terminal.d.ts +0 -5
  115. package/dist/shared/pty/terminal.js +51 -20
  116. package/dist/sync/watcher.js +10 -1
  117. package/dist/web/index.html +1 -1
  118. package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
  119. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  120. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  121. package/native/csharp/HcsHost/Hcs.cs +31 -3
  122. package/native/csharp/HcsHost/Program.cs +54 -6
  123. package/native/csharp/HcsHost/Share.cs +64 -11
  124. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  125. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  126. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  127. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  128. package/native/csharp/StorageHelper/Client.cs +297 -0
  129. package/native/csharp/StorageHelper/Log.cs +177 -0
  130. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  131. package/native/csharp/StorageHelper/Operations.cs +596 -0
  132. package/native/csharp/StorageHelper/Pins.cs +270 -0
  133. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  134. package/native/csharp/StorageHelper/Program.cs +349 -0
  135. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  136. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  137. package/native/csharp/StorageHelper/Rules.cs +156 -0
  138. package/native/csharp/StorageHelper/Server.cs +656 -0
  139. package/native/csharp/StorageHelper/Settings.cs +297 -0
  140. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  141. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  142. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  143. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  144. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  145. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  146. package/native/csharp/StorageHelper/Win32.cs +250 -0
  147. package/native/scripts/contained-common.ps1 +4 -0
  148. package/native/scripts/hcs-live-share.ps1 +191 -0
  149. package/native/scripts/set-storage-helper.ps1 +203 -0
  150. package/package.json +1 -1
  151. package/sandbox/csharp/BindMount.cs +120 -0
  152. package/sandbox/csharp/PtyHost.cs +236 -1
  153. package/schemas/config.schema.json +8 -0
@@ -0,0 +1,120 @@
1
+ // Live folders, guest side (docs/design/hcs-live-folders.md): shows a vSMB share the host added to this running sandbox
2
+ // at C:\Workspace\<name> with the bind filter, the way Windows Sandbox's own guest agent shows .wsb mapped folders, so
3
+ // tools see a plain folder (not a link). Run by the session daemon (an administrator) by path; compiled inside the
4
+ // sandbox with the .NET Framework csc.exe (C# 5). Verbs (exit 0 = done, 1 = failed, 2 = refused, 3 = no such share yet):
5
+ // BindMount map <C:\Workspace\name> <agentsbx-share> [rw] the folder shows the share (read-only unless rw)
6
+ // BindMount unmap <C:\Workspace\name> removes that mapping again
7
+ // BindMount mapped <C:\Workspace\name> exit 0 when the bind filter has a mapping there, else 4
8
+ // Only share names with this tool's prefix and only C:\Workspace\<plain name> (no dots or spaces, so no "..", "foo."
9
+ // or "foo " aliases of other folders), so it can never re-point a folder Windows Sandbox mapped. Flag names and the
10
+ // BfGetMappings layout: go-winio pkg/bindfilter (vendored in microsoft/hcsshim) and BuildXL's NativeContainerUtilities.
11
+ using System;
12
+ using System.IO;
13
+ using System.Runtime.InteropServices;
14
+ using System.Text;
15
+ using System.Text.RegularExpressions;
16
+
17
+ static class BindMount
18
+ {
19
+ private const string VsmbRoot = "\\\\?\\GLOBALROOT\\Device\\vmsmb\\VSMB-{dcc079ae-60ba-4d07-847c-3493609c0870}\\";
20
+ private const uint ReadOnlyMapping = 0x1;
21
+ private const uint NoMultipleTargets = 0x40;
22
+ private const uint GetMappingsVolume = 0x1;
23
+ /// <summary>
24
+ /// A share that has not arrived: ERROR_NOT_FOUND (seen in a Windows Sandbox, 2026-10-23, with C:\Workspace there) or
25
+ /// ERROR_FILE_NOT_FOUND (what the design note recorded; also the answer when C:\Workspace itself is missing).
26
+ /// </summary>
27
+ private const int NotFound = unchecked((int)0x80070490);
28
+ private const int FileNotFound = unchecked((int)0x80070002);
29
+ private const int ExitOk = 0;
30
+ private const int ExitFailed = 1;
31
+ private const int ExitRefused = 2;
32
+ private const int ExitNoShare = 3;
33
+ private const int ExitNotMapped = 4;
34
+ private static readonly Regex ShareName = new Regex("^agentsbx-[0-9a-f]{8,32}$");
35
+ private static readonly Regex GuestFolder = new Regex("^C:\\\\Workspace\\\\[a-z0-9][a-z0-9_-]{0,63}$", RegexOptions.IgnoreCase);
36
+
37
+ private static class NativeMethods
38
+ {
39
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
40
+ public static extern int BfSetupFilter(IntPtr job, uint flags, string virtRoot, string target, IntPtr exceptions, uint exceptionCount);
41
+
42
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
43
+ public static extern int BfRemoveMapping(IntPtr job, string virtRoot);
44
+
45
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
46
+ public static extern int BfGetMappings(uint flags, IntPtr job, string virtRoot, IntPtr sid, ref uint bufferSize, byte[] buffer);
47
+ }
48
+
49
+ static int Main(string[] args)
50
+ {
51
+ if ((args.Length == 3 || args.Length == 4) && args[0] == "map")
52
+ {
53
+ if (!GuestFolder.IsMatch(args[1]) || !ShareName.IsMatch(args[2]) || (args.Length == 4 && args[3] != "rw"))
54
+ {
55
+ Console.WriteLine("Refused: map C:\\Workspace\\<plain name> to an agentsbx- share only");
56
+ return ExitRefused;
57
+ }
58
+ if (Directory.Exists(args[1]) || File.Exists(args[1]))
59
+ {
60
+ Console.WriteLine("Refused: " + args[1] + " exists already");
61
+ return ExitRefused;
62
+ }
63
+ uint flags = NoMultipleTargets | (args.Length == 4 ? 0u : ReadOnlyMapping);
64
+ int hr = NativeMethods.BfSetupFilter(IntPtr.Zero, flags, args[1], VsmbRoot + args[2] + "\\", IntPtr.Zero, 0);
65
+ Console.WriteLine("BfSetupFilter 0x" + hr.ToString("X8"));
66
+ return hr == 0 ? ExitOk : (hr == NotFound || hr == FileNotFound ? ExitNoShare : ExitFailed);
67
+ }
68
+ if (args.Length == 2 && (args[0] == "unmap" || args[0] == "mapped"))
69
+ {
70
+ if (!GuestFolder.IsMatch(args[1]))
71
+ {
72
+ Console.WriteLine("Refused: only C:\\Workspace\\<plain name>");
73
+ return ExitRefused;
74
+ }
75
+ if (args[0] == "mapped")
76
+ {
77
+ bool found;
78
+ int listed = HasMapping(args[1], out found);
79
+ Console.WriteLine("BfGetMappings 0x" + listed.ToString("X8") + (found ? " mapped" : " not mapped"));
80
+ return listed != 0 ? ExitFailed : (found ? ExitOk : ExitNotMapped);
81
+ }
82
+ int hr = NativeMethods.BfRemoveMapping(IntPtr.Zero, args[1]);
83
+ Console.WriteLine("BfRemoveMapping 0x" + hr.ToString("X8"));
84
+ return hr == 0 ? ExitOk : ExitFailed;
85
+ }
86
+ Console.WriteLine("Usage: BindMount map <folder> <share> [rw] | unmap <folder> | mapped <folder>");
87
+ return ExitRefused;
88
+ }
89
+
90
+ /// <summary>Whether the bind filter has a mapping whose virtual root is `folder` (BfGetMappings on C:).</summary>
91
+ private static int HasMapping(string folder, out bool found)
92
+ {
93
+ found = false;
94
+ uint size = 1024 * 1024;
95
+ byte[] buffer = new byte[size];
96
+ int hr = NativeMethods.BfGetMappings(GetMappingsVolume, IntPtr.Zero, "C:\\", IntPtr.Zero, ref size, buffer);
97
+ if (hr != 0 || size < 12)
98
+ {
99
+ return hr != 0 ? hr : ExitFailed;
100
+ }
101
+ uint count = BitConverter.ToUInt32(buffer, 8);
102
+ string wanted = "\\" + folder.Substring(3).TrimEnd('\\');
103
+ for (int i = 0; i < count && 12 + (i * 20) + 20 <= buffer.Length; i++)
104
+ {
105
+ int entry = 12 + (i * 20);
106
+ uint length = BitConverter.ToUInt32(buffer, entry);
107
+ uint offset = BitConverter.ToUInt32(buffer, entry + 4);
108
+ if (offset + length > buffer.Length)
109
+ {
110
+ continue;
111
+ }
112
+ string root = Encoding.Unicode.GetString(buffer, (int)offset, (int)length).TrimEnd('\\');
113
+ if (root.EndsWith(wanted, StringComparison.OrdinalIgnoreCase))
114
+ {
115
+ found = true;
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+ }
@@ -3,7 +3,12 @@
3
3
  // .NET Framework csc.exe, so this is C# 5 (no string interpolation, no ?., no expression-bodied members).
4
4
  //
5
5
  // Usage: ptyhost.exe <port> <token> <cols> <rows> <logFile> <commandLine> [<restartCommandLine>]
6
+ // ptyhost.exe --end-tree <recordFile> ends what a ptyhost that was itself ended (Task Manager, a crash) left
7
+ // running, from the process tree it recorded (PTYHOST_TREE_FILE)
6
8
  //
9
+ // PTYHOST_TREE_FILE: where the program's process tree (ProcessTree: what it started, by id and start time) is kept
10
+ // while it runs; deleted when ptyhost ends it itself. A file left behind means ptyhost did not get to: the session
11
+ // daemon then runs --end-tree on it.
7
12
  // Wire format, both directions: [type:1][length:4, little endian][payload]
8
13
  // client -> host 1 HELLO [cols:2][rows:2][flags:1][token utf8] flags bit 0 = control-only (no output, no sizing),
9
14
  // bit 1 = status query (answered with STATUS, then closed)
@@ -37,6 +42,10 @@ static class PtyHost
37
42
  {
38
43
  static int Main(string[] args)
39
44
  {
45
+ if (args.Length == 2 && args[0] == "--end-tree")
46
+ {
47
+ return ProcessTree.EndRecorded(args[1]);
48
+ }
40
49
  if (args.Length < 6)
41
50
  {
42
51
  Console.Error.WriteLine("usage: ptyhost <port> <token> <cols> <rows> <logFile> <commandLine> [<restartCommandLine>]");
@@ -886,9 +895,24 @@ sealed class ProcessTree
886
895
  private const uint SnapProcess = 0x00000002;
887
896
  private const uint ProcessTerminate = 0x0001;
888
897
  private const uint ProcessQueryLimited = 0x1000;
898
+ private const uint Synchronize = 0x00100000;
899
+ private const uint WaitTimeout = 0x102;
889
900
  private const int FollowMs = 3000;
890
901
  private const int MaxPath = 260;
902
+ private const uint MoveReplaceExisting = 0x1;
903
+ private const uint MoveWriteThrough = 0x8;
904
+ /// <summary>A record larger than this is not one ptyhost wrote (thousands of processes).</summary>
905
+ private const int MaxRecordBytes = 1 << 20;
906
+ /// <summary>--end-tree exit codes: the ptyhost that wrote the record still runs; no readable record.</summary>
907
+ private const int StillRunning = 3;
908
+ private const int Unreadable = 4;
891
909
  private static readonly IntPtr InvalidHandle = new IntPtr(-1);
910
+ private static readonly Regex RecordEntry = new Regex("\\{\"pid\":(\\d{1,10}),\"start\":\"(\\d{1,20})\"\\}");
911
+ /// <summary>The pseudo console's own process (Windows' or conpty.dll's), started by ptyhost itself.</summary>
912
+ private static readonly HashSet<string> ConsoleHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
913
+ {
914
+ "conhost.exe", "OpenConsole.exe",
915
+ };
892
916
  private static readonly HashSet<string> Shared = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
893
917
  {
894
918
  "postgres.exe", "pg_ctl.exe", "mysqld.exe", "mariadbd.exe", "redis-server.exe", "mongod.exe", "sccache.exe",
@@ -937,13 +961,28 @@ sealed class ProcessTree
937
961
 
938
962
  [DllImport("kernel32.dll", SetLastError = true)]
939
963
  public static extern bool CloseHandle(IntPtr handle);
964
+
965
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
966
+ public static extern bool MoveFileExW(string existing, string replacement, uint flags);
967
+
968
+ [DllImport("kernel32.dll")]
969
+ public static extern int GetCurrentProcessId();
970
+
971
+ [DllImport("kernel32.dll", SetLastError = true)]
972
+ public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
940
973
  }
941
974
 
942
975
  private readonly object gate = new object();
943
976
  /// <summary>The program and what it started: process id -> start time.</summary>
944
977
  private readonly Dictionary<int, long> known = new Dictionary<int, long>();
978
+ /// <summary>The program (closing the console ends it), spared by EndAll (0 in --end-tree: none).</summary>
945
979
  private readonly int root;
946
980
  private readonly Timer timer;
981
+ /// <summary>Where the tree is recorded (PTYHOST_TREE_FILE), or null.</summary>
982
+ private readonly string recordFile;
983
+ private int recorded = -1;
984
+ /// <summary>The last process snapshot Refresh took (Record reuses it), or null.</summary>
985
+ private List<ProcessEntry> lastSnapshot;
947
986
  private bool ended;
948
987
 
949
988
  public ProcessTree(IntPtr rootProcess)
@@ -954,9 +993,24 @@ sealed class ProcessTree
954
993
  {
955
994
  known[root] = start;
956
995
  }
996
+ string file = Environment.GetEnvironmentVariable("PTYHOST_TREE_FILE");
997
+ recordFile = string.IsNullOrEmpty(file) ? null : file;
998
+ lock (gate)
999
+ {
1000
+ Record();
1001
+ }
957
1002
  timer = new Timer(Follow, null, FollowMs, FollowMs);
958
1003
  }
959
1004
 
1005
+ /// <summary>--end-tree: the processes of a record that still run as the same process (id and start time).</summary>
1006
+ private ProcessTree(Dictionary<int, long> alive)
1007
+ {
1008
+ foreach (KeyValuePair<int, long> p in alive)
1009
+ {
1010
+ known[p.Key] = p.Value;
1011
+ }
1012
+ }
1013
+
960
1014
  private void Follow(object unused)
961
1015
  {
962
1016
  lock (gate)
@@ -964,8 +1018,158 @@ sealed class ProcessTree
964
1018
  if (!ended)
965
1019
  {
966
1020
  Refresh();
1021
+ Record();
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ /// <summary>
1027
+ /// Writes the tree to the record file when it grew: {"ptyhost":{"pid":1,"start":"2"},"processes":[{...}]}, start
1028
+ /// times as FILETIME strings (too large for a JSON number). Written aside and moved over the old one, so a ptyhost
1029
+ /// ended at any moment leaves a whole record.
1030
+ /// </summary>
1031
+ private void Record()
1032
+ {
1033
+ if (recordFile == null)
1034
+ {
1035
+ return;
1036
+ }
1037
+ // The pseudo console's process too: one whose ptyhost was ended does not always go away by itself.
1038
+ int self = NativeMethods.GetCurrentProcessId();
1039
+ long selfStart = StartTime(self);
1040
+ Dictionary<int, long> all = new Dictionary<int, long>(known);
1041
+ foreach (ProcessEntry p in lastSnapshot ?? Snapshot())
1042
+ {
1043
+ int pid = (int)p.ProcessId;
1044
+ if ((int)p.ParentProcessId != self || !ConsoleHosts.Contains(p.ExeFile) || all.ContainsKey(pid))
1045
+ {
1046
+ continue;
1047
+ }
1048
+ long start = StartTime(pid);
1049
+ if (start != 0 && start >= selfStart)
1050
+ {
1051
+ all[pid] = start;
967
1052
  }
968
1053
  }
1054
+ if (all.Count == recorded)
1055
+ {
1056
+ return;
1057
+ }
1058
+ StringBuilder json = new StringBuilder();
1059
+ json.Append("{\"ptyhost\":").Append(Entry(self, selfStart)).Append(",\"processes\":[");
1060
+ bool first = true;
1061
+ foreach (KeyValuePair<int, long> p in all)
1062
+ {
1063
+ json.Append(first ? "" : ",").Append(Entry(p.Key, p.Value));
1064
+ first = false;
1065
+ }
1066
+ json.Append("]}");
1067
+ string temp = recordFile + ".tmp";
1068
+ try
1069
+ {
1070
+ File.WriteAllText(temp, json.ToString());
1071
+ if (NativeMethods.MoveFileExW(temp, recordFile, MoveReplaceExisting | MoveWriteThrough))
1072
+ {
1073
+ recorded = all.Count;
1074
+ }
1075
+ }
1076
+ catch (IOException)
1077
+ {
1078
+ // Tried again on the next round (every FollowMs).
1079
+ }
1080
+ catch (UnauthorizedAccessException)
1081
+ {
1082
+ // Same.
1083
+ }
1084
+ }
1085
+
1086
+ private static string Entry(int pid, long start)
1087
+ {
1088
+ return "{\"pid\":" + pid + ",\"start\":\"" + start + "\"}";
1089
+ }
1090
+
1091
+ /// <summary>The tree ended with ptyhost's own help: nothing is left for the daemon to end.</summary>
1092
+ private void Unrecord()
1093
+ {
1094
+ if (recordFile == null)
1095
+ {
1096
+ return;
1097
+ }
1098
+ try
1099
+ {
1100
+ File.Delete(recordFile);
1101
+ }
1102
+ catch (IOException)
1103
+ {
1104
+ // The daemon finds the processes gone (their ids and start times no longer match).
1105
+ }
1106
+ catch (UnauthorizedAccessException)
1107
+ {
1108
+ // Same.
1109
+ }
1110
+ }
1111
+
1112
+ /// <summary>
1113
+ /// --end-tree: ends what a ptyhost recorded, unless that ptyhost still runs. Only processes that still run as the
1114
+ /// recorded process (same id and start time) count, and the processes they started since; the Shared ones
1115
+ /// (databases, sccache ...) are spared as always. Prints how many were ended. 0 = done, 3 = the ptyhost still
1116
+ /// runs, 4 = no readable record.
1117
+ /// </summary>
1118
+ public static int EndRecorded(string file)
1119
+ {
1120
+ string text;
1121
+ try
1122
+ {
1123
+ FileInfo info = new FileInfo(file);
1124
+ if (!info.Exists || info.Length > MaxRecordBytes)
1125
+ {
1126
+ return Unreadable;
1127
+ }
1128
+ text = File.ReadAllText(file);
1129
+ }
1130
+ catch (IOException)
1131
+ {
1132
+ return Unreadable;
1133
+ }
1134
+ catch (UnauthorizedAccessException)
1135
+ {
1136
+ return Unreadable;
1137
+ }
1138
+ if (!text.StartsWith("{\"ptyhost\":"))
1139
+ {
1140
+ return Unreadable;
1141
+ }
1142
+ MatchCollection entries = RecordEntry.Matches(text);
1143
+ if (entries.Count == 0)
1144
+ {
1145
+ return Unreadable;
1146
+ }
1147
+ Dictionary<int, long> alive = new Dictionary<int, long>();
1148
+ for (int i = 0; i < entries.Count; i++)
1149
+ {
1150
+ int pid;
1151
+ long start;
1152
+ if (!int.TryParse(entries[i].Groups[1].Value, out pid) ||
1153
+ !long.TryParse(entries[i].Groups[2].Value, out start))
1154
+ {
1155
+ return Unreadable;
1156
+ }
1157
+ bool same = pid != 0 && start != 0 && Running(pid, start);
1158
+ if (i == 0)
1159
+ {
1160
+ if (same)
1161
+ {
1162
+ return StillRunning;
1163
+ }
1164
+ }
1165
+ else if (same)
1166
+ {
1167
+ alive[pid] = start;
1168
+ }
1169
+ }
1170
+ int ended = new ProcessTree(alive).EndAll();
1171
+ Console.Out.WriteLine("ended " + ended + " process(es)");
1172
+ return 0;
969
1173
  }
970
1174
 
971
1175
  /// <summary>Adds every process whose parent is known (and that started after it), until nothing new is found.</summary>
@@ -976,6 +1180,7 @@ sealed class ProcessTree
976
1180
  return;
977
1181
  }
978
1182
  List<ProcessEntry> all = Snapshot();
1183
+ lastSnapshot = all;
979
1184
  bool added = true;
980
1185
  while (added)
981
1186
  {
@@ -1012,7 +1217,10 @@ sealed class ProcessTree
1012
1217
  }
1013
1218
  Refresh();
1014
1219
  ended = true;
1015
- timer.Dispose();
1220
+ if (timer != null)
1221
+ {
1222
+ timer.Dispose();
1223
+ }
1016
1224
  int count = 0;
1017
1225
  foreach (KeyValuePair<int, long> p in known)
1018
1226
  {
@@ -1021,6 +1229,7 @@ sealed class ProcessTree
1021
1229
  count++;
1022
1230
  }
1023
1231
  }
1232
+ Unrecord();
1024
1233
  return count;
1025
1234
  }
1026
1235
  }
@@ -1073,6 +1282,32 @@ sealed class ProcessTree
1073
1282
  }
1074
1283
  }
1075
1284
 
1285
+ /// <summary>
1286
+ /// True while process pid is the one that started at `start` and has not ended (an ended process someone still
1287
+ /// holds a handle to keeps its id and start time).
1288
+ /// </summary>
1289
+ private static bool Running(int pid, long start)
1290
+ {
1291
+ IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited | Synchronize, false, pid);
1292
+ if (process == IntPtr.Zero)
1293
+ {
1294
+ return false;
1295
+ }
1296
+ try
1297
+ {
1298
+ long creation;
1299
+ long exit;
1300
+ long kernel;
1301
+ long user;
1302
+ return NativeMethods.GetProcessTimes(process, out creation, out exit, out kernel, out user) &&
1303
+ creation == start && NativeMethods.WaitForSingleObject(process, 0) == WaitTimeout;
1304
+ }
1305
+ finally
1306
+ {
1307
+ NativeMethods.CloseHandle(process);
1308
+ }
1309
+ }
1310
+
1076
1311
  private static bool Terminate(int pid)
1077
1312
  {
1078
1313
  IntPtr process = NativeMethods.OpenProcess(ProcessTerminate, false, pid);
@@ -1801,6 +1801,14 @@
1801
1801
  "wsbShare": {
1802
1802
  "type": "boolean",
1803
1803
  "description": "On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart."
1804
+ },
1805
+ "liveFolders": {
1806
+ "type": "string",
1807
+ "enum": [
1808
+ "off",
1809
+ "hcs"
1810
+ ],
1811
+ "description": "ALPHA, UNTESTED ON A REAL PC: \"hcs\" adds a project folder to the running sandbox through the Host Compute Service where `wsb share` is missing (one administrator prompt per add or remove; always read-only in this alpha). If it fails, the sandbox may close: save your work first. Turn it on before the sandbox starts. \"off\" (default) offers a restart instead. The real-PC tests still to pass are in docs/design/hcs-live-folders.md (\"Test plan\")."
1804
1812
  }
1805
1813
  },
1806
1814
  "additionalProperties": false,