windows-development-sandbox 1.13.0 → 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.
Files changed (125) hide show
  1. package/dist/app/features/accounts/accounts.def.d.ts +1 -0
  2. package/dist/app/features/accounts/accounts.def.js +12 -3
  3. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  4. package/dist/app/features/accounts/accounts.run.js +20 -11
  5. package/dist/app/features/defs.d.ts +1 -0
  6. package/dist/app/features/settings/settings-file.d.ts +7 -0
  7. package/dist/app/features/settings/settings-file.js +19 -2
  8. package/dist/app/features/settings/settings-rows.js +9 -1
  9. package/dist/app/features/storage/storage.run.js +1 -1
  10. package/dist/bin/agent-sandbox.js +7 -1
  11. package/dist/bin/chunk-BQ23GBLC.js +78790 -0
  12. package/dist/bin/chunk-F7IMME5G.js +10432 -0
  13. package/dist/bin/chunk-LQHEUN2A.js +7 -0
  14. package/dist/bin/claude-host.js +10 -4
  15. package/dist/bin/claude-sbx.js +8 -2
  16. package/dist/bin/codex-sbx.js +8 -2
  17. package/dist/bin/gemini-sbx.js +8 -2
  18. package/dist/bin/sandbox.js +7 -2
  19. package/dist/bin/sbx-hide.js +8 -2
  20. package/dist/bin/sbx-show.js +8 -2
  21. package/dist/cli/commands/launch/launch.js +1 -1
  22. package/dist/cli/commands/launch/prepare.js +24 -7
  23. package/dist/cli/commands/launch/running.js +4 -0
  24. package/dist/cli/commands/repair/checks/contained.js +1 -1
  25. package/dist/cli/commands/storage/offer.js +1 -1
  26. package/dist/cli/menu/live.d.ts +29 -0
  27. package/dist/cli/menu/live.js +92 -21
  28. package/dist/cli/menu/menu.d.ts +2 -1
  29. package/dist/cli/menu/menu.js +16 -2
  30. package/dist/cli/menu/pages/list-page.d.ts +5 -0
  31. package/dist/cli/menu/pages/list-page.js +19 -0
  32. package/dist/cli/menu/shell.d.ts +2 -0
  33. package/dist/cli/menu/shell.js +1 -1
  34. package/dist/cli/services/accounts-service.d.ts +7 -0
  35. package/dist/cli/services/accounts-service.js +20 -0
  36. package/dist/cli/tui/app.d.ts +4 -0
  37. package/dist/cli/tui/app.js +5 -0
  38. package/dist/cli/tui/terminal.d.ts +6 -0
  39. package/dist/cli/tui/terminal.js +8 -1
  40. package/dist/cli/validate.js +8 -4
  41. package/dist/core/package-info.js +1 -1
  42. package/dist/guest/daemon/sessions.d.ts +7 -0
  43. package/dist/guest/daemon/sessions.js +23 -2
  44. package/dist/guest/entries/daemon.js +1 -0
  45. package/dist/guest/sessions/pty-host.d.ts +10 -0
  46. package/dist/guest/sessions/pty-host.js +48 -3
  47. package/dist/host/channel/channel.js +1 -1
  48. package/dist/host/install/install-record.d.ts +1 -0
  49. package/dist/host/install/install-record.js +2 -0
  50. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  51. package/dist/host/sandbox/contained-deps.js +6 -4
  52. package/dist/host/sandbox/platform.d.ts +2 -0
  53. package/dist/host/sandbox/platform.js +19 -4
  54. package/dist/host/sharing/lifecycle.js +9 -1
  55. package/dist/host/storage/contained/contained-storage.js +13 -0
  56. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  57. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  58. package/dist/host/storage/contained/helper-port.js +162 -0
  59. package/dist/host/storage/contained/index.d.ts +1 -0
  60. package/dist/host/storage/contained/index.js +1 -0
  61. package/dist/host/storage/contained/state.d.ts +3 -2
  62. package/dist/host/storage/contained/state.js +5 -2
  63. package/dist/host/web/bridge.d.ts +5 -0
  64. package/dist/host/web/bridge.js +11 -1
  65. package/dist/host/web/server.d.ts +2 -0
  66. package/dist/host/web/server.js +11 -1
  67. package/dist/runtime/agent.js +9 -0
  68. package/dist/runtime/daemon.js +57 -3
  69. package/dist/runtime/db.js +9 -0
  70. package/dist/runtime/main.js +195 -12
  71. package/dist/runtime/netproxy.js +9 -0
  72. package/dist/runtime/view.js +195 -12
  73. package/dist/selftest/channel.test.js +72 -1
  74. package/dist/selftest/contained.test.js +1 -37
  75. package/dist/selftest/hcs.test.js +5 -2
  76. package/dist/selftest/index.js +2 -0
  77. package/dist/selftest/install.test.js +11 -0
  78. package/dist/selftest/layout.test.js +10 -1
  79. package/dist/selftest/live-daemon.test.js +51 -1
  80. package/dist/selftest/live-terminal.test.js +112 -4
  81. package/dist/selftest/menu-perf.test.js +198 -12
  82. package/dist/selftest/menu.test.js +4 -0
  83. package/dist/selftest/storage-helper.test.d.ts +2 -0
  84. package/dist/selftest/storage-helper.test.js +493 -0
  85. package/dist/selftest/support/vhdx.d.ts +14 -0
  86. package/dist/selftest/support/vhdx.js +72 -0
  87. package/dist/selftest/web.test.js +14 -2
  88. package/dist/shared/channel/run-dir.d.ts +6 -0
  89. package/dist/shared/channel/run-dir.js +9 -0
  90. package/dist/shared/pty/index.d.ts +3 -1
  91. package/dist/shared/pty/index.js +3 -1
  92. package/dist/shared/pty/key-gate.d.ts +24 -0
  93. package/dist/shared/pty/key-gate.js +45 -0
  94. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  95. package/dist/shared/pty/mouse-modes.js +155 -0
  96. package/dist/shared/pty/terminal.d.ts +0 -5
  97. package/dist/shared/pty/terminal.js +51 -20
  98. package/dist/sync/watcher.js +1 -1
  99. package/dist/web/index.html +1 -1
  100. package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
  101. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  102. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  103. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  104. package/native/csharp/StorageHelper/Client.cs +297 -0
  105. package/native/csharp/StorageHelper/Log.cs +177 -0
  106. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  107. package/native/csharp/StorageHelper/Operations.cs +596 -0
  108. package/native/csharp/StorageHelper/Pins.cs +270 -0
  109. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  110. package/native/csharp/StorageHelper/Program.cs +349 -0
  111. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  112. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  113. package/native/csharp/StorageHelper/Rules.cs +156 -0
  114. package/native/csharp/StorageHelper/Server.cs +656 -0
  115. package/native/csharp/StorageHelper/Settings.cs +297 -0
  116. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  117. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  118. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  119. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  120. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  121. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  122. package/native/csharp/StorageHelper/Win32.cs +250 -0
  123. package/native/scripts/set-storage-helper.ps1 +203 -0
  124. package/package.json +1 -1
  125. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -0,0 +1,596 @@
1
+ // The two things the service does: mount one data set's image at its mount folder (making and formatting the image
2
+ // the very first time) and dismount it. The same rules as mount-contained.ps1 and dismount-contained.ps1, on handles:
3
+ // the folders and the image are pinned and checked first (Pins), the image is opened once and that same handle is
4
+ // checked by the disk driver and attached, and the mount point is written on the pinned folder's handle. An existing
5
+ // image is never formatted or deleted; a new one is only made where no file is. A dry run checks everything and
6
+ // changes nothing.
7
+ using System;
8
+ using System.Collections.Generic;
9
+ using System.ComponentModel;
10
+ using System.Diagnostics;
11
+ using System.IO;
12
+ using System.Runtime.InteropServices;
13
+ using System.Security.AccessControl;
14
+ using System.Security.Principal;
15
+ using Microsoft.Win32.SafeHandles;
16
+
17
+ namespace AgentSandbox.StorageHelper
18
+ {
19
+ public sealed class Outcome
20
+ {
21
+ public bool Ok { get; set; }
22
+ public string Message { get; set; }
23
+ public bool Created { get; set; }
24
+ public bool AlreadyMounted { get; set; }
25
+
26
+ /// <summary>The helper leaves this one to the Windows permission prompt (the CLI then uses it).</summary>
27
+ public bool Fallback { get; set; }
28
+
29
+ public string Image { get; set; }
30
+ public string Mount { get; set; }
31
+
32
+ /// <summary>A step on the caller's own files was cancelled after the time limit (the caller may be stalling it).</summary>
33
+ public bool TimedOut { get; set; }
34
+ public List<string> Notes { get; private set; }
35
+
36
+ public Outcome()
37
+ {
38
+ Message = "";
39
+ Notes = new List<string>();
40
+ }
41
+ }
42
+
43
+ public sealed class Request
44
+ {
45
+ public string Verb { get; set; }
46
+ public string Set { get; set; }
47
+ public SecurityIdentifier User { get; set; }
48
+
49
+ /// <summary>The client's token, for the access check before a new image is made; null in a dry run of the tests.</summary>
50
+ public SafeAccessTokenHandle Token { get; set; }
51
+
52
+ public StorageSettings Settings { get; set; }
53
+ public bool DryRun { get; set; }
54
+
55
+ /// <summary>Dismount only clears a stale mount point; an image that is attached stays (the service's start-up tidy).</summary>
56
+ public bool OnlyIfDetached { get; set; }
57
+ }
58
+
59
+ public static class Operations
60
+ {
61
+ private const uint FileAddFile = 0x2;
62
+ private static readonly TimeSpan VolumeWait = TimeSpan.FromSeconds(60);
63
+ private static readonly TimeSpan OpenLimit = TimeSpan.FromSeconds(30);
64
+
65
+ private static class NativeMethods
66
+ {
67
+ [DllImport("advapi32.dll", SetLastError = true)]
68
+ [return: MarshalAs(UnmanagedType.Bool)]
69
+ public static extern bool AccessCheck(byte[] securityDescriptor, SafeAccessTokenHandle token, uint desiredAccess, ref GenericMapping mapping, byte[] privileges, ref uint privilegesLength, out uint granted, [MarshalAs(UnmanagedType.Bool)] out bool status);
70
+
71
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
72
+ [return: MarshalAs(UnmanagedType.Bool)]
73
+ public static extern bool SetFileSecurityW(string fileName, uint securityInformation, byte[] securityDescriptor);
74
+ }
75
+
76
+ [StructLayout(LayoutKind.Sequential)]
77
+ private struct GenericMapping
78
+ {
79
+ public uint Read;
80
+ public uint Write;
81
+ public uint Execute;
82
+ public uint All;
83
+ }
84
+
85
+ public static Outcome Run(Request request)
86
+ {
87
+ Outcome outcome = new Outcome();
88
+ using (Watchdog dog = new Watchdog(OpenLimit))
89
+ {
90
+ RunGuarded(request, outcome);
91
+ if (dog.Fired && !outcome.Ok)
92
+ {
93
+ outcome.TimedOut = true;
94
+ outcome.Message = new TimedOutException().Message;
95
+ }
96
+ }
97
+ return outcome;
98
+ }
99
+
100
+ private static void RunGuarded(Request request, Outcome outcome)
101
+ {
102
+ try
103
+ {
104
+ Rules.CheckMaxGb(request.Settings.MaxGb);
105
+ ContainedPaths paths = Rules.PathsFor(request.Settings.StorageRoot, request.Set);
106
+ outcome.Image = paths.Image;
107
+ outcome.Mount = paths.Mount;
108
+ if (request.Verb == "mount")
109
+ {
110
+ Mount(request, paths, outcome);
111
+ }
112
+ else if (request.Verb == "dismount")
113
+ {
114
+ Dismount(request, paths, outcome);
115
+ }
116
+ else
117
+ {
118
+ throw new RefusedException("Unknown request.");
119
+ }
120
+ outcome.Ok = true;
121
+ }
122
+ catch (LogPendingException e)
123
+ {
124
+ outcome.Ok = false;
125
+ outcome.Fallback = true;
126
+ outcome.Message = e.Message;
127
+ }
128
+ catch (RefusedException e)
129
+ {
130
+ outcome.Ok = false;
131
+ outcome.Message = e.Message;
132
+ }
133
+ catch (Win32Exception e)
134
+ {
135
+ outcome.Ok = false;
136
+ outcome.Message = e.Message;
137
+ }
138
+ catch (InvalidOperationException e)
139
+ {
140
+ outcome.Ok = false;
141
+ outcome.Message = e.Message;
142
+ }
143
+ catch (IOException e)
144
+ {
145
+ outcome.Ok = false;
146
+ outcome.Message = e.Message;
147
+ }
148
+ catch (UnauthorizedAccessException e)
149
+ {
150
+ outcome.Ok = false;
151
+ outcome.Message = e.Message;
152
+ }
153
+ catch (Exception e)
154
+ {
155
+ // Anything else is a fault of this service, not something to show in detail.
156
+ outcome.Ok = false;
157
+ outcome.Message = "The storage helper failed (" + e.GetType().Name + ").";
158
+ }
159
+ }
160
+
161
+ private static void Mount(Request request, ContainedPaths paths, Outcome outcome)
162
+ {
163
+ using (Pins pins = Pins.Take(paths, request.User))
164
+ {
165
+ if (pins.Image == null)
166
+ {
167
+ MountNew(request, paths, pins, outcome);
168
+ }
169
+ else
170
+ {
171
+ MountExisting(request, paths, pins, outcome);
172
+ }
173
+ }
174
+ }
175
+
176
+ private static void MountNew(Request request, ContainedPaths paths, Pins pins, Outcome outcome)
177
+ {
178
+ if (pins.MountIsMountPoint)
179
+ {
180
+ ClearDanglingMountPoint(pins, request.DryRun, outcome);
181
+ }
182
+ RequireEmpty(paths.Mount);
183
+ if (request.Token != null && !MayAddFile(pins.Images, request.Token))
184
+ {
185
+ throw new RefusedException("You may not create files in " + paths.ImagesDir + ".");
186
+ }
187
+ if (request.DryRun)
188
+ {
189
+ outcome.Notes.Add("would create a dynamic VHDX of at most " + request.Settings.MaxGb + " GB, partition, format and mount it");
190
+ outcome.Created = true;
191
+ outcome.Message = "The image would be created and mounted.";
192
+ return;
193
+ }
194
+ DriveInfo drive = new DriveInfo(paths.StorageRoot.Substring(0, 3));
195
+ if (drive.AvailableFreeSpace < 1024L * 1024L * 1024L)
196
+ {
197
+ throw new RefusedException("There is less than 1 GB free where the image would be made.");
198
+ }
199
+ string sddl = "O:" + request.User.Value + "D:P(A;;FA;;;" + request.User.Value + ")(A;;FA;;;SY)(A;;FA;;;BA)";
200
+ RawSecurityDescriptor sd = new RawSecurityDescriptor(sddl);
201
+ byte[] sdBytes = new byte[sd.BinaryLength];
202
+ sd.GetBinaryForm(sdBytes, 0);
203
+ bool attached = false;
204
+ bool mounted = false;
205
+ VirtualDisk disk = VirtualDisk.Create(paths.Image, request.Settings.MaxGb, sdBytes);
206
+ try
207
+ {
208
+ pins.TakeNewImage();
209
+ disk.Attach();
210
+ attached = true;
211
+ int number = disk.DiskNumber();
212
+ if (number < 0)
213
+ {
214
+ throw new InvalidOperationException("The new image did not show up as a disk.");
215
+ }
216
+ List<string> volumes = null;
217
+ // The service's own new disk: no time limit (a format takes a while).
218
+ Watchdog.Suspended(delegate
219
+ {
220
+ Volumes.Partition(number);
221
+ volumes = Volumes.WaitFor(number, 1, VolumeWait);
222
+ if (volumes.Count != 1)
223
+ {
224
+ throw new InvalidOperationException("The new disk has " + volumes.Count + " volumes instead of one.");
225
+ }
226
+ Volumes.Format(volumes[0], "AgentData");
227
+ });
228
+ string volume = volumes[0];
229
+ Volumes.RemoveDriveLetters(volume);
230
+ RequirePlainMountFolder(pins);
231
+ Volumes.SetMountPoint(pins.MountFolder, volume);
232
+ mounted = true;
233
+ // The new volume's root is open to Users by default: only the user and the system may use it.
234
+ RawSecurityDescriptor root = new RawSecurityDescriptor("D:P(A;OICI;FA;;;" + request.User.Value + ")(A;OICI;FA;;;SY)");
235
+ byte[] rootBytes = new byte[root.BinaryLength];
236
+ root.GetBinaryForm(rootBytes, 0);
237
+ if (!NativeMethods.SetFileSecurityW(volume, Win32.DaclSecurityInformation, rootBytes))
238
+ {
239
+ throw Win32.LastError("Could not make the new disk private");
240
+ }
241
+ outcome.Created = true;
242
+ outcome.Message = "The image was created and mounted.";
243
+ }
244
+ catch (Exception e)
245
+ {
246
+ string problem = e.Message;
247
+ if (mounted)
248
+ {
249
+ TryRemoveMountPoint(pins);
250
+ }
251
+ if (attached)
252
+ {
253
+ try
254
+ {
255
+ disk.Detach();
256
+ }
257
+ catch (Win32Exception detach)
258
+ {
259
+ problem += " (it could not be detached again: " + detach.Message + ")";
260
+ }
261
+ }
262
+ disk.Dispose();
263
+ problem += KeepFailedImage(pins, paths);
264
+ throw new InvalidOperationException(problem);
265
+ }
266
+ disk.Dispose();
267
+ }
268
+
269
+ private static void MountExisting(Request request, ContainedPaths paths, Pins pins, Outcome outcome)
270
+ {
271
+ // Read the header while nobody may write it. A sharing violation means the disk driver has it (attached).
272
+ using (SafeFileHandle reader = TryOpenExclusiveRead(paths.Image))
273
+ {
274
+ if (reader == null)
275
+ {
276
+ AlreadyAttached(paths, pins, outcome);
277
+ return;
278
+ }
279
+ RequireSame(pins, reader);
280
+ using (FileStream stream = new FileStream(reader, FileAccess.Read))
281
+ {
282
+ Vhdx.CheckPlain(Vhdx.Read(stream), request.Settings.MaxGb);
283
+ }
284
+ }
285
+ if (pins.MountIsMountPoint)
286
+ {
287
+ ClearDanglingMountPoint(pins, request.DryRun, outcome);
288
+ }
289
+ RequireEmpty(paths.Mount);
290
+ if (request.DryRun)
291
+ {
292
+ outcome.Notes.Add("would attach the image and mount its one NTFS volume at the mount folder");
293
+ outcome.Message = "The image would be mounted.";
294
+ return;
295
+ }
296
+ bool attached = false;
297
+ bool mounted = false;
298
+ using (VirtualDisk disk = VirtualDisk.Open(paths.Image))
299
+ {
300
+ try
301
+ {
302
+ // The path cannot have moved (it is pinned); the file behind it is checked to be the same anyway.
303
+ using (SafeFileHandle again = Win32.OpenNoFollow(paths.Image, Win32.FileReadAttributes | Win32.GenericRead, Win32.FileShareRead | Win32.FileShareWrite))
304
+ {
305
+ if (again == null)
306
+ {
307
+ throw new RefusedException("The image went away.");
308
+ }
309
+ RequireSame(pins, again);
310
+ using (FileStream stream = new FileStream(again, FileAccess.Read))
311
+ {
312
+ Vhdx.CheckPlain(Vhdx.Read(stream), request.Settings.MaxGb);
313
+ }
314
+ }
315
+ disk.CheckPlain(request.Settings.MaxGb);
316
+ if (disk.DiskNumber() >= 0)
317
+ {
318
+ throw new RefusedException("The image was attached by something else just now. Detach it there first.");
319
+ }
320
+ disk.Attach();
321
+ attached = true;
322
+ int number = disk.DiskNumber();
323
+ if (number < 0)
324
+ {
325
+ throw new InvalidOperationException("The image did not show up as a disk.");
326
+ }
327
+ List<string> volumes = null;
328
+ Watchdog.Suspended(delegate { volumes = Volumes.WaitFor(number, 1, VolumeWait); });
329
+ if (Volumes.IsOfflineOrReadOnly(number))
330
+ {
331
+ throw new RefusedException("Windows keeps the image's disk offline (is it a copy of another disk?); refusing it.");
332
+ }
333
+ if (volumes.Count != 1 || Volumes.PartitionCount(number) != 1)
334
+ {
335
+ throw new RefusedException("The image must hold exactly one partition (it has " + Volumes.PartitionCount(number) + ", " + volumes.Count + " volumes); refusing it.");
336
+ }
337
+ string volume = volumes[0];
338
+ if (!string.Equals(Volumes.FileSystemOf(volume), "NTFS", StringComparison.OrdinalIgnoreCase))
339
+ {
340
+ throw new RefusedException("The image's volume is not NTFS; refusing it.");
341
+ }
342
+ Volumes.RemoveDriveLetters(volume);
343
+ RequirePlainMountFolder(pins);
344
+ Volumes.SetMountPoint(pins.MountFolder, volume);
345
+ mounted = true;
346
+ outcome.Message = "The image was mounted.";
347
+ }
348
+ catch (Exception)
349
+ {
350
+ if (mounted)
351
+ {
352
+ TryRemoveMountPoint(pins);
353
+ }
354
+ if (attached)
355
+ {
356
+ try
357
+ {
358
+ disk.Detach();
359
+ }
360
+ catch (Win32Exception)
361
+ {
362
+ // Reported by the next doctor run (the image stays attached, mounted nowhere).
363
+ }
364
+ }
365
+ throw;
366
+ }
367
+ }
368
+ }
369
+
370
+ /// <summary>The image is attached already: fine when it is mounted at this mount folder, refused otherwise.</summary>
371
+ private static void AlreadyAttached(ContainedPaths paths, Pins pins, Outcome outcome)
372
+ {
373
+ using (VirtualDisk disk = VirtualDisk.Open(paths.Image))
374
+ {
375
+ int number = disk.DiskNumber();
376
+ if (number < 0)
377
+ {
378
+ throw new RefusedException("Another program has the image open. Close it and try again.");
379
+ }
380
+ string target = pins.MountIsMountPoint ? Volumes.MountTarget(pins.MountFolder) : null;
381
+ if (target != null && ContainsVolume(Volumes.OnDisk(number), target))
382
+ {
383
+ outcome.AlreadyMounted = true;
384
+ outcome.Message = "The image is mounted already.";
385
+ outcome.Notes.Add("already mounted at the mount folder");
386
+ return;
387
+ }
388
+ throw new RefusedException("The image is attached somewhere else (Disk Management or another tool). Detach it there first.");
389
+ }
390
+ }
391
+
392
+ private static void Dismount(Request request, ContainedPaths paths, Outcome outcome)
393
+ {
394
+ using (Pins pins = Pins.Take(paths, request.User))
395
+ {
396
+ if (pins.Image == null)
397
+ {
398
+ throw new RefusedException("The image is missing: " + paths.Image);
399
+ }
400
+ if (!request.DryRun && !request.OnlyIfDetached && SandboxRunning())
401
+ {
402
+ throw new RefusedException("A Windows Sandbox is running and may be using the image. Close it first.");
403
+ }
404
+ if (request.DryRun)
405
+ {
406
+ outcome.Notes.Add("would take the mount point off the mount folder and detach the image");
407
+ outcome.Message = "The image would be detached.";
408
+ return;
409
+ }
410
+ using (VirtualDisk disk = VirtualDisk.Open(paths.Image))
411
+ {
412
+ using (SafeFileHandle again = Win32.OpenNoFollow(paths.Image, Win32.FileReadAttributes, Win32.FileShareRead | Win32.FileShareWrite | Win32.FileShareDelete))
413
+ {
414
+ if (again == null)
415
+ {
416
+ throw new RefusedException("The image went away.");
417
+ }
418
+ RequireSame(pins, again);
419
+ }
420
+ int number = disk.DiskNumber();
421
+ if (number >= 0 && request.OnlyIfDetached)
422
+ {
423
+ outcome.AlreadyMounted = true;
424
+ outcome.Message = "The image is mounted; left as it is.";
425
+ return;
426
+ }
427
+ if (number < 0)
428
+ {
429
+ if (pins.MountIsMountPoint)
430
+ {
431
+ ClearDanglingMountPoint(pins, false, outcome);
432
+ }
433
+ outcome.Message = "The image is not mounted.";
434
+ outcome.Notes.Add("not attached: nothing to do");
435
+ return;
436
+ }
437
+ if (pins.MountIsMountPoint)
438
+ {
439
+ // Only a mount point of THIS image's volume is taken off; a link to someone else's volume is left.
440
+ string target = Volumes.MountTarget(pins.MountFolder);
441
+ if (target == null || !ContainsVolume(Volumes.OnDisk(number), target))
442
+ {
443
+ throw new RefusedException("The mount folder is a mount point of another disk; not touching it: " + paths.Mount);
444
+ }
445
+ Volumes.RemoveMountPoint(pins.MountFolder);
446
+ }
447
+ disk.Detach();
448
+ if (disk.DiskNumber() >= 0)
449
+ {
450
+ throw new InvalidOperationException("The image is still attached (something is using it). Close programs that use files in it and try again.");
451
+ }
452
+ outcome.Message = "The image was detached.";
453
+ }
454
+ }
455
+ }
456
+
457
+ /// <summary>A mount point whose volume is gone (the PC restarted while mounted) is taken off; any other is refused.</summary>
458
+ private static void ClearDanglingMountPoint(Pins pins, bool dryRun, Outcome outcome)
459
+ {
460
+ string target = Volumes.MountTarget(pins.MountFolder);
461
+ if (target != null && Volumes.Exists(target))
462
+ {
463
+ throw new RefusedException("The mount folder is a mount point already.");
464
+ }
465
+ outcome.Notes.Add("the mount folder had a mount point of a disk that is gone; it is taken off");
466
+ if (!dryRun)
467
+ {
468
+ Volumes.RemoveMountPoint(pins.MountFolder);
469
+ }
470
+ }
471
+
472
+ private static void TryRemoveMountPoint(Pins pins)
473
+ {
474
+ try
475
+ {
476
+ Volumes.RemoveMountPoint(pins.MountFolder);
477
+ }
478
+ catch (Win32Exception)
479
+ {
480
+ // The detach that follows takes the volume away; the next mount clears the stale mount point.
481
+ }
482
+ }
483
+
484
+ /// <summary>A half-made new image is renamed (never deleted), so the next try starts clean and nothing is lost.</summary>
485
+ private static string KeepFailedImage(Pins pins, ContainedPaths paths)
486
+ {
487
+ pins.Dispose();
488
+ string kept = paths.Image + ".failed-" + DateTime.Now.ToString("yyyyMMddHHmmss");
489
+ try
490
+ {
491
+ if (File.Exists(paths.Image))
492
+ {
493
+ File.Move(paths.Image, kept);
494
+ return " (the unfinished image was kept as " + kept + ")";
495
+ }
496
+ return "";
497
+ }
498
+ catch (IOException)
499
+ {
500
+ return " (the unfinished image is still at " + paths.Image + ")";
501
+ }
502
+ }
503
+
504
+ private static void RequireEmpty(string folder)
505
+ {
506
+ using (IEnumerator<string> entries = Directory.EnumerateFileSystemEntries(folder).GetEnumerator())
507
+ {
508
+ if (entries.MoveNext())
509
+ {
510
+ throw new RefusedException("The mount folder is not empty: " + folder);
511
+ }
512
+ }
513
+ }
514
+
515
+ private static void RequirePlainMountFolder(Pins pins)
516
+ {
517
+ if (!pins.MountFolderStillPlain())
518
+ {
519
+ throw new RefusedException("The mount folder became a link while it was being checked; nothing was mounted.");
520
+ }
521
+ }
522
+
523
+ private static void RequireSame(Pins pins, SafeFileHandle other)
524
+ {
525
+ if (!pins.StillTheImage(other))
526
+ {
527
+ throw new RefusedException("The image was swapped while it was being checked; nothing was done.");
528
+ }
529
+ }
530
+
531
+ private static SafeFileHandle TryOpenExclusiveRead(string path)
532
+ {
533
+ SafeFileHandle h = Win32.NativeMethods.CreateFileW(path, Win32.GenericRead | Win32.FileReadAttributes, Win32.FileShareRead, IntPtr.Zero, Win32.OpenExisting, Win32.FlagOpenReparsePoint, IntPtr.Zero);
534
+ if (h.IsInvalid)
535
+ {
536
+ int code = Marshal.GetLastWin32Error();
537
+ h.Dispose();
538
+ if (code == Win32.ErrorSharingViolation)
539
+ {
540
+ return null;
541
+ }
542
+ throw new Win32Exception(code, "Could not read the image: " + new Win32Exception(code).Message);
543
+ }
544
+ return h;
545
+ }
546
+
547
+ private static bool ContainsVolume(List<string> volumes, string target)
548
+ {
549
+ foreach (string v in volumes)
550
+ {
551
+ if (string.Equals(v.TrimEnd('\\'), target.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase))
552
+ {
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+
559
+ /// <summary>Would the user be allowed to create a file in this folder (checked against their own token)?</summary>
560
+ private static bool MayAddFile(SafeFileHandle folder, SafeAccessTokenHandle token)
561
+ {
562
+ RawSecurityDescriptor sd = Win32.SecurityOf(folder);
563
+ byte[] bytes = new byte[sd.BinaryLength];
564
+ sd.GetBinaryForm(bytes, 0);
565
+ GenericMapping mapping = new GenericMapping();
566
+ mapping.Read = 0x120089;
567
+ mapping.Write = 0x120116;
568
+ mapping.Execute = 0x1200A0;
569
+ mapping.All = 0x1F01FF;
570
+ byte[] privileges = new byte[256];
571
+ uint length = (uint)privileges.Length;
572
+ uint granted;
573
+ bool status;
574
+ if (!NativeMethods.AccessCheck(bytes, token, FileAddFile, ref mapping, privileges, ref length, out granted, out status))
575
+ {
576
+ throw Win32.LastError("Could not check your access to the images folder");
577
+ }
578
+ return status;
579
+ }
580
+
581
+ public static bool SandboxRunning()
582
+ {
583
+ foreach (Process p in Process.GetProcesses())
584
+ {
585
+ using (p)
586
+ {
587
+ if (p.ProcessName.StartsWith("WindowsSandbox", StringComparison.OrdinalIgnoreCase))
588
+ {
589
+ return true;
590
+ }
591
+ }
592
+ }
593
+ return false;
594
+ }
595
+ }
596
+ }