windows-development-sandbox 1.13.2 → 1.14.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/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-BQ23GBLC.js} +411 -58
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-F7IMME5G.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-LQHEUN2A.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/repair/checks/contained.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- 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/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- 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 +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -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 +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -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/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- 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 +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-6XNBTJLT.js} +33 -33
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- 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/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// The disk and volume work after an image is attached, all by disk number, volume name and folder handle (no path is
|
|
2
|
+
// looked up again): which volumes are on the disk, its partitions, whether Windows keeps it offline, a new disk's one
|
|
3
|
+
// GPT partition and NTFS format, drive letters to take away, and the mount point itself, written on the pinned mount
|
|
4
|
+
// folder's handle (a folder swapped for a link cannot receive it). Layouts from the Microsoft winioctl.h reference.
|
|
5
|
+
using System;
|
|
6
|
+
using System.Collections.Generic;
|
|
7
|
+
using System.ComponentModel;
|
|
8
|
+
using System.Runtime.InteropServices;
|
|
9
|
+
using System.Text;
|
|
10
|
+
using System.Threading;
|
|
11
|
+
using Microsoft.Win32.SafeHandles;
|
|
12
|
+
|
|
13
|
+
namespace AgentSandbox.StorageHelper
|
|
14
|
+
{
|
|
15
|
+
public static class Volumes
|
|
16
|
+
{
|
|
17
|
+
private const uint IoctlVolumeGetDiskExtents = 0x00560000;
|
|
18
|
+
private const uint IoctlDiskGetDriveLayoutEx = 0x00070050;
|
|
19
|
+
private const uint IoctlDiskSetDriveLayoutEx = 0x0007C054;
|
|
20
|
+
private const uint IoctlDiskCreateDisk = 0x0007C058;
|
|
21
|
+
private const uint IoctlDiskUpdateProperties = 0x00070140;
|
|
22
|
+
private const uint IoctlDiskGetDiskAttributes = 0x000700F0;
|
|
23
|
+
private const uint IoctlDiskSetDiskAttributes = 0x0007C0F4;
|
|
24
|
+
private const uint FsctlSetReparsePoint = 0x000900A4;
|
|
25
|
+
private const uint FsctlGetReparsePoint = 0x000900A8;
|
|
26
|
+
private const uint FsctlDeleteReparsePoint = 0x000900AC;
|
|
27
|
+
private const uint TagMountPoint = 0xA0000003;
|
|
28
|
+
private const ulong DiskAttributeOffline = 0x1;
|
|
29
|
+
private const ulong DiskAttributeReadOnly = 0x2;
|
|
30
|
+
private const int PartitionStyleMbr = 0;
|
|
31
|
+
private const int PartitionStyleGpt = 1;
|
|
32
|
+
private const int LayoutHeader = 48;
|
|
33
|
+
private const int PartitionEntry = 144;
|
|
34
|
+
private const long Alignment = 1024 * 1024;
|
|
35
|
+
private const int FmifsHardDisk = 0xC;
|
|
36
|
+
private const int FmifsDone = 0xB;
|
|
37
|
+
private static readonly Guid BasicData = new Guid("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7");
|
|
38
|
+
|
|
39
|
+
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
|
40
|
+
[return: MarshalAs(UnmanagedType.U1)]
|
|
41
|
+
private delegate bool FormatCallback(int command, int subAction, IntPtr info);
|
|
42
|
+
|
|
43
|
+
private static class NativeMethods
|
|
44
|
+
{
|
|
45
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
46
|
+
public static extern IntPtr FindFirstVolumeW(StringBuilder name, uint size);
|
|
47
|
+
|
|
48
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
49
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
50
|
+
public static extern bool FindNextVolumeW(IntPtr find, StringBuilder name, uint size);
|
|
51
|
+
|
|
52
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
53
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
54
|
+
public static extern bool FindVolumeClose(IntPtr find);
|
|
55
|
+
|
|
56
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
57
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
58
|
+
public static extern bool GetVolumePathNamesForVolumeNameW(string volume, char[] names, uint size, out uint needed);
|
|
59
|
+
|
|
60
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
61
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
62
|
+
public static extern bool DeleteVolumeMountPointW(string mountPoint);
|
|
63
|
+
|
|
64
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
65
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
66
|
+
public static extern bool GetVolumeInformationW(string root, StringBuilder volumeName, uint volumeNameSize, out uint serial, out uint maxComponent, out uint flags, StringBuilder fileSystem, uint fileSystemSize);
|
|
67
|
+
|
|
68
|
+
[DllImport("fmifs.dll", CharSet = CharSet.Unicode)]
|
|
69
|
+
public static extern void FormatEx(string driveRoot, int mediaType, string fileSystem, string label, [MarshalAs(UnmanagedType.U1)] bool quick, int clusterSize, FormatCallback callback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// <summary>The volumes (\\?\Volume{...}\) that lie on this disk.</summary>
|
|
73
|
+
public static List<string> OnDisk(int disk)
|
|
74
|
+
{
|
|
75
|
+
List<string> found = new List<string>();
|
|
76
|
+
StringBuilder name = new StringBuilder(64);
|
|
77
|
+
IntPtr find = NativeMethods.FindFirstVolumeW(name, (uint)name.Capacity);
|
|
78
|
+
if (find == new IntPtr(-1))
|
|
79
|
+
{
|
|
80
|
+
throw Win32.LastError("Could not list the volumes");
|
|
81
|
+
}
|
|
82
|
+
try
|
|
83
|
+
{
|
|
84
|
+
do
|
|
85
|
+
{
|
|
86
|
+
string volume = name.ToString();
|
|
87
|
+
if (DisksOf(volume).Contains(disk))
|
|
88
|
+
{
|
|
89
|
+
found.Add(volume);
|
|
90
|
+
}
|
|
91
|
+
name.Length = 0;
|
|
92
|
+
}
|
|
93
|
+
while (NativeMethods.FindNextVolumeW(find, name, (uint)name.Capacity));
|
|
94
|
+
}
|
|
95
|
+
finally
|
|
96
|
+
{
|
|
97
|
+
NativeMethods.FindVolumeClose(find);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// <summary>The disk numbers a volume lies on (empty when it cannot be asked, e.g. a CD drive without a disc).</summary>
|
|
103
|
+
public static List<int> DisksOf(string volume)
|
|
104
|
+
{
|
|
105
|
+
List<int> disks = new List<int>();
|
|
106
|
+
using (SafeFileHandle h = Win32.NativeMethods.CreateFileW(volume.TrimEnd('\\'), 0, Win32.FileShareRead | Win32.FileShareWrite, IntPtr.Zero, Win32.OpenExisting, 0, IntPtr.Zero))
|
|
107
|
+
{
|
|
108
|
+
if (h.IsInvalid)
|
|
109
|
+
{
|
|
110
|
+
return disks;
|
|
111
|
+
}
|
|
112
|
+
byte[] output = new byte[8 + 24 * 32];
|
|
113
|
+
int returned;
|
|
114
|
+
if (Win32.TryIoctl(h, IoctlVolumeGetDiskExtents, null, output, out returned) != 0)
|
|
115
|
+
{
|
|
116
|
+
return disks;
|
|
117
|
+
}
|
|
118
|
+
int count = Math.Min(BitConverter.ToInt32(output, 0), 32);
|
|
119
|
+
for (int i = 0; i < count; i++)
|
|
120
|
+
{
|
|
121
|
+
disks.Add(BitConverter.ToInt32(output, 8 + 24 * i));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return disks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// <summary>Waits until exactly `count` volumes are on the disk (a new or attached disk takes a moment).</summary>
|
|
128
|
+
public static List<string> WaitFor(int disk, int count, TimeSpan timeout)
|
|
129
|
+
{
|
|
130
|
+
DateTime until = DateTime.UtcNow + timeout;
|
|
131
|
+
List<string> volumes;
|
|
132
|
+
while (true)
|
|
133
|
+
{
|
|
134
|
+
volumes = OnDisk(disk);
|
|
135
|
+
if (volumes.Count >= count || DateTime.UtcNow > until)
|
|
136
|
+
{
|
|
137
|
+
return volumes;
|
|
138
|
+
}
|
|
139
|
+
Thread.Sleep(250);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private static SafeFileHandle OpenDisk(int disk)
|
|
144
|
+
{
|
|
145
|
+
SafeFileHandle h = Win32.NativeMethods.CreateFileW("\\\\.\\PhysicalDrive" + disk, Win32.GenericRead | Win32.GenericWrite, Win32.FileShareRead | Win32.FileShareWrite, IntPtr.Zero, Win32.OpenExisting, 0, IntPtr.Zero);
|
|
146
|
+
if (h.IsInvalid)
|
|
147
|
+
{
|
|
148
|
+
Win32Exception e = Win32.LastError("Could not open the attached disk");
|
|
149
|
+
h.Dispose();
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
return h;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// <summary>How many partitions with a size the disk has.</summary>
|
|
156
|
+
public static int PartitionCount(int disk)
|
|
157
|
+
{
|
|
158
|
+
using (SafeFileHandle h = OpenDisk(disk))
|
|
159
|
+
{
|
|
160
|
+
byte[] layout = new byte[LayoutHeader + PartitionEntry * 128];
|
|
161
|
+
Win32.Ioctl(h, IoctlDiskGetDriveLayoutEx, null, layout, "Could not read the disk's partitions");
|
|
162
|
+
int style = BitConverter.ToInt32(layout, 0);
|
|
163
|
+
int count = Math.Min(BitConverter.ToInt32(layout, 4), 128);
|
|
164
|
+
int used = 0;
|
|
165
|
+
for (int i = 0; i < count; i++)
|
|
166
|
+
{
|
|
167
|
+
int at = LayoutHeader + PartitionEntry * i;
|
|
168
|
+
long length = BitConverter.ToInt64(layout, at + 16);
|
|
169
|
+
bool typed = style == PartitionStyleGpt ? new Guid(Slice(layout, at + 32, 16)) != Guid.Empty : layout[at + 32] != 0;
|
|
170
|
+
if (length > 0 && typed)
|
|
171
|
+
{
|
|
172
|
+
used++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return used;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// <summary>True when Windows keeps the disk offline (e.g. it has the same id as another disk) or read-only.</summary>
|
|
180
|
+
public static bool IsOfflineOrReadOnly(int disk)
|
|
181
|
+
{
|
|
182
|
+
using (SafeFileHandle h = OpenDisk(disk))
|
|
183
|
+
{
|
|
184
|
+
byte[] output = new byte[16];
|
|
185
|
+
Win32.Ioctl(h, IoctlDiskGetDiskAttributes, null, output, "Could not read the disk's state");
|
|
186
|
+
ulong attributes = BitConverter.ToUInt64(output, 8);
|
|
187
|
+
return (attributes & (DiskAttributeOffline | DiskAttributeReadOnly)) != 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// <summary>A brand-new disk (made by this service): online, one GPT basic data partition over all of it.</summary>
|
|
192
|
+
public static void Partition(int disk)
|
|
193
|
+
{
|
|
194
|
+
using (SafeFileHandle h = OpenDisk(disk))
|
|
195
|
+
{
|
|
196
|
+
byte[] attributes = new byte[16];
|
|
197
|
+
Win32.Ioctl(h, IoctlDiskGetDiskAttributes, null, attributes, "Could not read the disk's state");
|
|
198
|
+
if ((BitConverter.ToUInt64(attributes, 8) & (DiskAttributeOffline | DiskAttributeReadOnly)) != 0)
|
|
199
|
+
{
|
|
200
|
+
byte[] set = new byte[40];
|
|
201
|
+
BitConverter.GetBytes(40).CopyTo(set, 0);
|
|
202
|
+
BitConverter.GetBytes(0UL).CopyTo(set, 8);
|
|
203
|
+
BitConverter.GetBytes(DiskAttributeOffline | DiskAttributeReadOnly).CopyTo(set, 16);
|
|
204
|
+
Win32.Ioctl(h, IoctlDiskSetDiskAttributes, set, null, "Could not bring the new disk online");
|
|
205
|
+
}
|
|
206
|
+
byte[] create = new byte[24];
|
|
207
|
+
BitConverter.GetBytes(PartitionStyleGpt).CopyTo(create, 0);
|
|
208
|
+
Guid.NewGuid().ToByteArray().CopyTo(create, 4);
|
|
209
|
+
BitConverter.GetBytes(128).CopyTo(create, 20);
|
|
210
|
+
Win32.Ioctl(h, IoctlDiskCreateDisk, create, null, "Could not initialize the new disk");
|
|
211
|
+
|
|
212
|
+
byte[] layout = new byte[LayoutHeader + PartitionEntry * 128];
|
|
213
|
+
Win32.Ioctl(h, IoctlDiskGetDriveLayoutEx, null, layout, "Could not read the new disk's layout");
|
|
214
|
+
if (BitConverter.ToInt32(layout, 0) != PartitionStyleGpt)
|
|
215
|
+
{
|
|
216
|
+
throw new InvalidOperationException("The new disk did not become a GPT disk.");
|
|
217
|
+
}
|
|
218
|
+
long usableStart = BitConverter.ToInt64(layout, 8 + 16);
|
|
219
|
+
long usableLength = BitConverter.ToInt64(layout, 8 + 24);
|
|
220
|
+
long start = (usableStart + Alignment - 1) / Alignment * Alignment;
|
|
221
|
+
long end = (usableStart + usableLength) / Alignment * Alignment;
|
|
222
|
+
if (end - start < 512 * Alignment)
|
|
223
|
+
{
|
|
224
|
+
throw new InvalidOperationException("The new disk is too small.");
|
|
225
|
+
}
|
|
226
|
+
byte[] set2 = new byte[LayoutHeader + PartitionEntry];
|
|
227
|
+
Array.Copy(layout, 0, set2, 0, LayoutHeader);
|
|
228
|
+
BitConverter.GetBytes(1).CopyTo(set2, 4);
|
|
229
|
+
int at = LayoutHeader;
|
|
230
|
+
BitConverter.GetBytes(PartitionStyleGpt).CopyTo(set2, at);
|
|
231
|
+
BitConverter.GetBytes(start).CopyTo(set2, at + 8);
|
|
232
|
+
BitConverter.GetBytes(end - start).CopyTo(set2, at + 16);
|
|
233
|
+
BitConverter.GetBytes(1).CopyTo(set2, at + 24);
|
|
234
|
+
set2[at + 28] = 1;
|
|
235
|
+
BasicData.ToByteArray().CopyTo(set2, at + 32);
|
|
236
|
+
Guid.NewGuid().ToByteArray().CopyTo(set2, at + 48);
|
|
237
|
+
Encoding.Unicode.GetBytes("AgentData").CopyTo(set2, at + 72);
|
|
238
|
+
Win32.Ioctl(h, IoctlDiskSetDriveLayoutEx, set2, null, "Could not make the partition");
|
|
239
|
+
Win32.Ioctl(h, IoctlDiskUpdateProperties, null, null, "Could not refresh the disk");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// <summary>Quick NTFS format of a volume of the new disk (fmifs, what format.com uses).</summary>
|
|
244
|
+
public static void Format(string volume, string label)
|
|
245
|
+
{
|
|
246
|
+
bool done = false;
|
|
247
|
+
bool ok = false;
|
|
248
|
+
StringBuilder seen = new StringBuilder();
|
|
249
|
+
FormatCallback callback = delegate(int command, int subAction, IntPtr info)
|
|
250
|
+
{
|
|
251
|
+
if (command != 0 && seen.Length < 200)
|
|
252
|
+
{
|
|
253
|
+
seen.Append(command).Append(' ');
|
|
254
|
+
}
|
|
255
|
+
if (command == FmifsDone)
|
|
256
|
+
{
|
|
257
|
+
done = true;
|
|
258
|
+
ok = info != IntPtr.Zero && Marshal.ReadByte(info) != 0;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
};
|
|
262
|
+
// fmifs takes the volume name without its trailing backslash (with it, it fails at once).
|
|
263
|
+
NativeMethods.FormatEx(volume.TrimEnd('\\'), FmifsHardDisk, "NTFS", label, true, 0, callback);
|
|
264
|
+
GC.KeepAlive(callback);
|
|
265
|
+
if (!done || !ok)
|
|
266
|
+
{
|
|
267
|
+
throw new InvalidOperationException("Formatting the new disk failed (steps: " + seen.ToString().Trim() + ").");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public static string FileSystemOf(string volume)
|
|
272
|
+
{
|
|
273
|
+
StringBuilder name = new StringBuilder(261);
|
|
274
|
+
StringBuilder fs = new StringBuilder(261);
|
|
275
|
+
uint serial;
|
|
276
|
+
uint max;
|
|
277
|
+
uint flags;
|
|
278
|
+
if (!NativeMethods.GetVolumeInformationW(volume.EndsWith("\\") ? volume : volume + "\\", name, (uint)name.Capacity, out serial, out max, out flags, fs, (uint)fs.Capacity))
|
|
279
|
+
{
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
return fs.ToString();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// <summary>Takes away any drive letter Windows gave the volume anyway (it can remember one for a disk it saw before).</summary>
|
|
286
|
+
public static void RemoveDriveLetters(string volume)
|
|
287
|
+
{
|
|
288
|
+
char[] buffer = new char[4096];
|
|
289
|
+
uint needed;
|
|
290
|
+
if (!NativeMethods.GetVolumePathNamesForVolumeNameW(volume, buffer, (uint)buffer.Length, out needed))
|
|
291
|
+
{
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
foreach (string path in new string(buffer, 0, (int)Math.Min(needed, (uint)buffer.Length)).Split('\0'))
|
|
295
|
+
{
|
|
296
|
+
if (path.Length == 3 && path[1] == ':' && path[2] == '\\')
|
|
297
|
+
{
|
|
298
|
+
if (!NativeMethods.DeleteVolumeMountPointW(path))
|
|
299
|
+
{
|
|
300
|
+
throw Win32.LastError("Could not take the drive letter " + path + " away from the image");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// <summary>The volume (\\?\Volume{...}\) a mount-point folder points to; null when the folder is no mount point.</summary>
|
|
307
|
+
public static string MountTarget(SafeFileHandle folder)
|
|
308
|
+
{
|
|
309
|
+
byte[] buffer = new byte[16 * 1024];
|
|
310
|
+
int returned;
|
|
311
|
+
int error = Win32.TryIoctl(folder, FsctlGetReparsePoint, null, buffer, out returned);
|
|
312
|
+
if (error != 0)
|
|
313
|
+
{
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (BitConverter.ToUInt32(buffer, 0) != TagMountPoint)
|
|
317
|
+
{
|
|
318
|
+
throw new RefusedException("The mount folder is a link, not a mount point; not touching it.");
|
|
319
|
+
}
|
|
320
|
+
int offset = BitConverter.ToUInt16(buffer, 8);
|
|
321
|
+
int length = BitConverter.ToUInt16(buffer, 10);
|
|
322
|
+
if (16 + offset + length > returned)
|
|
323
|
+
{
|
|
324
|
+
throw new RefusedException("The mount folder has a damaged mount point.");
|
|
325
|
+
}
|
|
326
|
+
string target = Encoding.Unicode.GetString(buffer, 16 + offset, length);
|
|
327
|
+
if (target.StartsWith("\\??\\Volume{", StringComparison.OrdinalIgnoreCase))
|
|
328
|
+
{
|
|
329
|
+
return "\\\\?\\" + target.Substring(4).TrimEnd('\\') + "\\";
|
|
330
|
+
}
|
|
331
|
+
throw new RefusedException("The mount folder points somewhere that is not a volume; not touching it.");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// <summary>Makes the (empty, pinned) folder a mount point of the volume, through its handle.</summary>
|
|
335
|
+
public static void SetMountPoint(SafeFileHandle folder, string volume)
|
|
336
|
+
{
|
|
337
|
+
if (!volume.StartsWith("\\\\?\\Volume{", StringComparison.OrdinalIgnoreCase))
|
|
338
|
+
{
|
|
339
|
+
throw new InvalidOperationException("Not a volume name: " + Safe.Text(volume, 100));
|
|
340
|
+
}
|
|
341
|
+
byte[] substitute = Encoding.Unicode.GetBytes("\\??\\" + volume.Substring(4).TrimEnd('\\') + "\\");
|
|
342
|
+
byte[] print = new byte[0];
|
|
343
|
+
int pathBytes = substitute.Length + 2 + print.Length + 2;
|
|
344
|
+
byte[] data = new byte[16 + pathBytes];
|
|
345
|
+
BitConverter.GetBytes(TagMountPoint).CopyTo(data, 0);
|
|
346
|
+
BitConverter.GetBytes((ushort)(8 + pathBytes)).CopyTo(data, 4);
|
|
347
|
+
BitConverter.GetBytes((ushort)0).CopyTo(data, 8);
|
|
348
|
+
BitConverter.GetBytes((ushort)substitute.Length).CopyTo(data, 10);
|
|
349
|
+
BitConverter.GetBytes((ushort)(substitute.Length + 2)).CopyTo(data, 12);
|
|
350
|
+
BitConverter.GetBytes((ushort)print.Length).CopyTo(data, 14);
|
|
351
|
+
substitute.CopyTo(data, 16);
|
|
352
|
+
Win32.Ioctl(folder, FsctlSetReparsePoint, data, null, "Could not make the mount folder a mount point");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// <summary>Takes the mount point off the (pinned) folder through its handle; the folder stays, empty.</summary>
|
|
356
|
+
public static void RemoveMountPoint(SafeFileHandle folder)
|
|
357
|
+
{
|
|
358
|
+
byte[] header = new byte[8];
|
|
359
|
+
BitConverter.GetBytes(TagMountPoint).CopyTo(header, 0);
|
|
360
|
+
Win32.Ioctl(folder, FsctlDeleteReparsePoint, header, null, "Could not take the mount point off the mount folder");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// <summary>Is any volume with this name present on the PC now?</summary>
|
|
364
|
+
public static bool Exists(string volume)
|
|
365
|
+
{
|
|
366
|
+
using (SafeFileHandle h = Win32.NativeMethods.CreateFileW(volume.TrimEnd('\\'), 0, Win32.FileShareRead | Win32.FileShareWrite, IntPtr.Zero, Win32.OpenExisting, 0, IntPtr.Zero))
|
|
367
|
+
{
|
|
368
|
+
return !h.IsInvalid;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private static byte[] Slice(byte[] bytes, int offset, int count)
|
|
373
|
+
{
|
|
374
|
+
byte[] part = new byte[count];
|
|
375
|
+
Array.Copy(bytes, offset, part, 0, count);
|
|
376
|
+
return part;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A time limit for the steps that open the caller's own files (their settings, their folders, their image). The caller
|
|
2
|
+
// owns those files, so they could hold an opportunistic lock on one and make an open wait for as long as they like,
|
|
3
|
+
// stalling everyone else's calls. While a Watchdog is armed on a thread, a timer cancels whatever synchronous I/O is
|
|
4
|
+
// blocked on that thread (CancelSynchronousIo) once the limit is up; the open then fails and the call is refused. The
|
|
5
|
+
// long steps that touch only the service's own disk (partitioning and formatting a new image) run with it suspended.
|
|
6
|
+
using System;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using System.Threading;
|
|
9
|
+
|
|
10
|
+
namespace AgentSandbox.StorageHelper
|
|
11
|
+
{
|
|
12
|
+
public sealed class Watchdog : IDisposable
|
|
13
|
+
{
|
|
14
|
+
private const uint ThreadTerminate = 0x0001;
|
|
15
|
+
private const uint ThreadQueryInformation = 0x0040;
|
|
16
|
+
|
|
17
|
+
[ThreadStatic]
|
|
18
|
+
private static Watchdog current;
|
|
19
|
+
|
|
20
|
+
private readonly object gate = new object();
|
|
21
|
+
private readonly TimeSpan limit;
|
|
22
|
+
private readonly IntPtr thread;
|
|
23
|
+
private readonly Timer timer;
|
|
24
|
+
private DateTime deadline;
|
|
25
|
+
private bool finished;
|
|
26
|
+
|
|
27
|
+
private static class NativeMethods
|
|
28
|
+
{
|
|
29
|
+
[DllImport("kernel32.dll")]
|
|
30
|
+
public static extern uint GetCurrentThreadId();
|
|
31
|
+
|
|
32
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
33
|
+
public static extern IntPtr OpenThread(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint threadId);
|
|
34
|
+
|
|
35
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
36
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
37
|
+
public static extern bool CancelSynchronousIo(IntPtr thread);
|
|
38
|
+
|
|
39
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
40
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
41
|
+
public static extern bool CloseHandle(IntPtr handle);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// <summary>Arms a limit for the calling thread; dispose it when the guarded steps are done.</summary>
|
|
45
|
+
public Watchdog(TimeSpan limit)
|
|
46
|
+
{
|
|
47
|
+
this.limit = limit;
|
|
48
|
+
deadline = DateTime.UtcNow + limit;
|
|
49
|
+
thread = NativeMethods.OpenThread(ThreadTerminate | ThreadQueryInformation, false, NativeMethods.GetCurrentThreadId());
|
|
50
|
+
timer = new Timer(Tick, null, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(500));
|
|
51
|
+
current = this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// <summary>True once the limit was reached (something was cancelled, or is being).</summary>
|
|
55
|
+
public bool Fired { get; private set; }
|
|
56
|
+
|
|
57
|
+
/// <summary>The armed watchdog of this thread, if any.</summary>
|
|
58
|
+
public static Watchdog Current
|
|
59
|
+
{
|
|
60
|
+
get { return current; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private void Tick(object state)
|
|
64
|
+
{
|
|
65
|
+
lock (gate)
|
|
66
|
+
{
|
|
67
|
+
if (finished || thread == IntPtr.Zero || DateTime.UtcNow < deadline)
|
|
68
|
+
{
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
Fired = true;
|
|
72
|
+
// Repeated on every tick: an open that starts after a cancel is cancelled too.
|
|
73
|
+
NativeMethods.CancelSynchronousIo(thread);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// <summary>Runs a long step that touches no file of the caller with no limit; the limit starts again after it.</summary>
|
|
78
|
+
public static void Suspended(Action step)
|
|
79
|
+
{
|
|
80
|
+
Watchdog dog = current;
|
|
81
|
+
if (dog == null)
|
|
82
|
+
{
|
|
83
|
+
step();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
lock (dog.gate)
|
|
87
|
+
{
|
|
88
|
+
dog.deadline = DateTime.MaxValue;
|
|
89
|
+
}
|
|
90
|
+
try
|
|
91
|
+
{
|
|
92
|
+
step();
|
|
93
|
+
}
|
|
94
|
+
finally
|
|
95
|
+
{
|
|
96
|
+
lock (dog.gate)
|
|
97
|
+
{
|
|
98
|
+
dog.deadline = DateTime.UtcNow + dog.limit;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public void Dispose()
|
|
104
|
+
{
|
|
105
|
+
lock (gate)
|
|
106
|
+
{
|
|
107
|
+
finished = true;
|
|
108
|
+
}
|
|
109
|
+
timer.Dispose();
|
|
110
|
+
if (current == this)
|
|
111
|
+
{
|
|
112
|
+
current = null;
|
|
113
|
+
}
|
|
114
|
+
if (thread != IntPtr.Zero)
|
|
115
|
+
{
|
|
116
|
+
NativeMethods.CloseHandle(thread);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// <summary>A step on the caller's own files took longer than the limit and was cancelled.</summary>
|
|
122
|
+
public sealed class TimedOutException : Exception
|
|
123
|
+
{
|
|
124
|
+
public TimedOutException() : base("Your storage folder or image took too long to open (another program holds it); try again in a few minutes.")
|
|
125
|
+
{
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|