windows-development-sandbox 1.11.4 → 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.
- package/dist/app/features/accounts/accounts.def.js +0 -1
- package/dist/app/features/health/health.def.js +6 -3
- package/dist/app/features/sandbox/sandbox.def.js +4 -4
- package/dist/host/web/runner.d.ts +1 -1
- package/dist/host/web/server.d.ts +12 -3
- package/dist/host/web/server.js +20 -6
- package/dist/runtime/daemon.js +2 -0
- package/dist/runtime/main.js +2 -0
- package/dist/runtime/view.js +2 -0
- package/dist/selftest/health.test.js +35 -0
- package/dist/selftest/live-terminal.test.js +30 -0
- package/dist/selftest/support/web-preview.js +3 -0
- package/dist/selftest/web.test.js +33 -0
- package/dist/shared/pty/client.d.ts +0 -1
- package/dist/shared/pty/client.js +5 -0
- package/dist/web/index.html +2 -2
- package/dist/web/{main-SLQNJRXD.js → main-QL7KHCEE.js} +62 -62
- package/dist/web/{main-MU5XWZMS.css → main-RQFN4WBY.css} +1 -1
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- 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.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.11.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.11.
|
|
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.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.11.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.11.
|
|
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)]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windows-development-sandbox",
|
|
3
|
-
"version": "1.11.
|
|
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
|
|