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.
- package/config.example.json +2 -1
- package/dist/app/features/defs.d.ts +1 -1
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/projects/projects.run.js +10 -2
- package/dist/app/features/sandbox/sandbox.def.js +3 -1
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/projects/here.js +2 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/services/health-service.js +15 -3
- package/dist/cli/services/projects-service.d.ts +8 -3
- package/dist/cli/services/projects-service.js +21 -4
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/schema/config.d.ts +4 -0
- package/dist/core/schema/projects.d.ts +4 -0
- package/dist/core/schema/projects.js +3 -0
- package/dist/guest/boot/boot.js +8 -2
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/daemon/live-folders.d.ts +48 -0
- package/dist/guest/daemon/live-folders.js +245 -0
- package/dist/guest/daemon/live.d.ts +4 -0
- package/dist/guest/daemon/live.js +57 -0
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +5 -2
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +5 -2
- package/dist/guest/runtime/secrets.js +12 -5
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/engines/hcs/live-folders.d.ts +117 -0
- package/dist/host/engines/hcs/live-folders.js +347 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/launch/generate.js +8 -1
- package/dist/host/projects/join.d.ts +4 -0
- package/dist/host/projects/join.js +11 -3
- package/dist/host/projects/mapper.d.ts +34 -5
- package/dist/host/projects/mapper.js +88 -20
- package/dist/host/projects/registry.d.ts +2 -0
- package/dist/host/projects/registry.js +1 -1
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +23 -0
- package/dist/runtime/daemon.js +523 -135
- package/dist/runtime/db.js +23 -0
- package/dist/runtime/main.js +221 -15
- package/dist/runtime/netproxy.js +23 -0
- package/dist/runtime/view.js +209 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +1 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-folders.test.d.ts +2 -0
- package/dist/selftest/live-folders.test.js +588 -0
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/projects.test.js +7 -7
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/live.js +2 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/live.d.ts +53 -0
- package/dist/shared/channel/live.js +92 -0
- package/dist/shared/channel/run-dir.d.ts +11 -0
- package/dist/shared/channel/run-dir.js +20 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +5 -0
- package/dist/shared/paths/projects.d.ts +6 -1
- package/dist/shared/paths/projects.js +21 -1
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +10 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
- package/native/csharp/HcsHost/FolderAccess.cs +361 -0
- package/native/csharp/HcsHost/Hcs.cs +31 -3
- package/native/csharp/HcsHost/Program.cs +54 -6
- package/native/csharp/HcsHost/Share.cs +64 -11
- package/native/csharp/HcsHost/WsbGuard.cs +130 -0
- package/native/csharp/HcsHost/WsbShare.cs +295 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/contained-common.ps1 +4 -0
- package/native/scripts/hcs-live-share.ps1 +191 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/BindMount.cs +120 -0
- package/sandbox/csharp/PtyHost.cs +236 -1
- 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
|
|
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,
|