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,270 @@
1
+ // The folders and the image of one data set, opened as handles and held for the whole call. Each handle is opened
2
+ // without following links and without sharing delete, so while it is open nothing on the way can be renamed, deleted
3
+ // or swapped for a link. Every handle is then checked: not a link, the path Windows reports is the exact path, on a
4
+ // local fixed NTFS drive, and (for the user's own folders and the image) owned by the client with nobody else allowed
5
+ // to change it. These are the checks of contained-common.ps1, done on handles instead of on names.
6
+ using System;
7
+ using System.Collections.Generic;
8
+ using System.Security.AccessControl;
9
+ using System.Security.Principal;
10
+ using Microsoft.Win32.SafeHandles;
11
+
12
+ namespace AgentSandbox.StorageHelper
13
+ {
14
+ public sealed class Pins : IDisposable
15
+ {
16
+ private const uint LookAccess = Win32.ReadControl | Win32.FileReadAttributes | Win32.Synchronize;
17
+ private const uint Share = Win32.FileShareRead | Win32.FileShareWrite;
18
+ private const int WriteRights = 0x2 | 0x4 | 0x10 | 0x40 | 0x100 | 0x10000 | 0x40000 | 0x80000 | 0x10000000 | 0x40000000;
19
+ private static readonly string[] Trusted = { "S-1-5-18", "S-1-5-32-544", "S-1-3-0" };
20
+ private readonly List<SafeFileHandle> held = new List<SafeFileHandle>();
21
+ private readonly ContainedPaths paths;
22
+ private readonly SecurityIdentifier user;
23
+
24
+ private Pins(ContainedPaths paths, SecurityIdentifier user)
25
+ {
26
+ this.paths = paths;
27
+ this.user = user;
28
+ }
29
+
30
+ public SafeFileHandle Images { get; private set; }
31
+ public SafeFileHandle MountFolder { get; private set; }
32
+ public SafeFileHandle Image { get; private set; }
33
+ public bool MountIsMountPoint { get; private set; }
34
+ public Win32.ByHandleFileInformation ImageIdentity { get; private set; }
35
+
36
+ /// <summary>
37
+ /// Opens and checks everything from the drive root down to the mount folder. The mount folder is opened so it can
38
+ /// take a mount point. The image is opened when it exists.
39
+ /// </summary>
40
+ public static Pins Take(ContainedPaths paths, SecurityIdentifier user)
41
+ {
42
+ Pins pins = new Pins(paths, user);
43
+ try
44
+ {
45
+ pins.TakeAll();
46
+ return pins;
47
+ }
48
+ catch
49
+ {
50
+ pins.Dispose();
51
+ throw;
52
+ }
53
+ }
54
+
55
+ private void TakeAll()
56
+ {
57
+ string[] above = Rules.FoldersDownTo(paths.StorageRoot);
58
+ SafeFileHandle root = null;
59
+ for (int i = 0; i < above.Length; i++)
60
+ {
61
+ SafeFileHandle h = Folder(above[i], LookAccess, i == above.Length - 1 ? "The storage folder is missing: " : "A folder is missing: ");
62
+ if (i == 0)
63
+ {
64
+ CheckDrive(h, above[0]);
65
+ }
66
+ root = h;
67
+ }
68
+ string volumeRoot = Win32.VolumePathOf(paths.StorageRoot + "\\");
69
+ if (volumeRoot.TrimEnd('\\').Length != 2)
70
+ {
71
+ throw new RefusedException("The storage folder is inside a mounted disk (" + Safe.Text(volumeRoot, 200) + "); contained storage cannot nest.");
72
+ }
73
+ CheckMine(root, paths.StorageRoot);
74
+ Images = Folder(paths.ImagesDir, LookAccess, "A folder is missing: ");
75
+ CheckMine(Images, paths.ImagesDir);
76
+ SafeFileHandle mounts = Folder(paths.MountsDir, LookAccess, "A folder is missing: ");
77
+ CheckMine(mounts, paths.MountsDir);
78
+ MountFolder = Open(paths.Mount, LookAccess | Win32.FileWriteAttributes | Win32.FileWriteData, true);
79
+ if (MountFolder == null)
80
+ {
81
+ throw new RefusedException("The mount folder is missing: " + paths.Mount);
82
+ }
83
+ Win32.ByHandleFileInformation mountInfo = Win32.InfoOf(MountFolder, paths.Mount);
84
+ if ((mountInfo.FileAttributes & Win32.AttributeDirectory) == 0)
85
+ {
86
+ throw new RefusedException("The mount folder is not a folder: " + paths.Mount);
87
+ }
88
+ MountIsMountPoint = (mountInfo.FileAttributes & Win32.AttributeReparsePoint) != 0;
89
+ CheckPath(MountFolder, paths.Mount);
90
+ CheckMine(MountFolder, paths.Mount);
91
+ Image = Open(paths.Image, LookAccess, false);
92
+ if (Image != null)
93
+ {
94
+ Win32.ByHandleFileInformation info = Win32.InfoOf(Image, paths.Image);
95
+ if ((info.FileAttributes & Win32.AttributeReparsePoint) != 0)
96
+ {
97
+ throw new RefusedException("Refusing a link: " + paths.Image);
98
+ }
99
+ if ((info.FileAttributes & Win32.AttributeDirectory) != 0)
100
+ {
101
+ throw new RefusedException("The image is not a file: " + paths.Image);
102
+ }
103
+ if (info.NumberOfLinks != 1)
104
+ {
105
+ throw new RefusedException("The image has other names (hard links); refusing it: " + paths.Image);
106
+ }
107
+ CheckPath(Image, paths.Image);
108
+ CheckMine(Image, paths.Image);
109
+ ImageIdentity = info;
110
+ }
111
+ RecheckNoLinks();
112
+ }
113
+
114
+ /// <summary>
115
+ /// Every pinned folder is looked at again: a folder that was empty when it was checked could have become a link
116
+ /// before the folder below it was opened. Once all are pinned, none can (they are not empty, and cannot be renamed).
117
+ /// </summary>
118
+ private void RecheckNoLinks()
119
+ {
120
+ foreach (SafeFileHandle h in held)
121
+ {
122
+ if (h == MountFolder || h == Image)
123
+ {
124
+ continue;
125
+ }
126
+ if ((Win32.InfoOf(h, "a folder").FileAttributes & Win32.AttributeReparsePoint) != 0)
127
+ {
128
+ throw new RefusedException("A folder on the way became a link while it was being checked; nothing was done.");
129
+ }
130
+ }
131
+ }
132
+
133
+ /// <summary>True when the mount folder (pinned) is still a plain folder, not a link made after it was checked.</summary>
134
+ public bool MountFolderStillPlain()
135
+ {
136
+ return (Win32.InfoOf(MountFolder, paths.Mount).FileAttributes & Win32.AttributeReparsePoint) == 0;
137
+ }
138
+
139
+ /// <summary>Pins the image after this service made it (it did not exist when the folders were taken).</summary>
140
+ public void TakeNewImage()
141
+ {
142
+ Image = Open(paths.Image, LookAccess, false);
143
+ if (Image == null)
144
+ {
145
+ throw new InvalidOperationException("The new image is not there.");
146
+ }
147
+ CheckPath(Image, paths.Image);
148
+ ImageIdentity = Win32.InfoOf(Image, paths.Image);
149
+ }
150
+
151
+ /// <summary>True when the file at the image path is still the pinned one (same volume and file id).</summary>
152
+ public bool StillTheImage(SafeFileHandle other)
153
+ {
154
+ Win32.ByHandleFileInformation now = Win32.InfoOf(other, paths.Image);
155
+ return now.VolumeSerialNumber == ImageIdentity.VolumeSerialNumber && now.FileIndexHigh == ImageIdentity.FileIndexHigh && now.FileIndexLow == ImageIdentity.FileIndexLow;
156
+ }
157
+
158
+ private SafeFileHandle Folder(string path, uint access, string missing)
159
+ {
160
+ SafeFileHandle h = Open(path, access, true);
161
+ if (h == null)
162
+ {
163
+ throw new RefusedException(missing + path);
164
+ }
165
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, path);
166
+ if ((info.FileAttributes & Win32.AttributeReparsePoint) != 0)
167
+ {
168
+ throw new RefusedException(path.Length <= paths.StorageRoot.Length && !Rules.SamePath(path, paths.StorageRoot) ? "Refusing a link above the storage folder: " + path : "Refusing a link: " + path);
169
+ }
170
+ if ((info.FileAttributes & Win32.AttributeDirectory) == 0)
171
+ {
172
+ throw new RefusedException("Not a folder: " + path);
173
+ }
174
+ CheckPath(h, path);
175
+ return h;
176
+ }
177
+
178
+ private SafeFileHandle Open(string path, uint access, bool folder)
179
+ {
180
+ SafeFileHandle h = Win32.OpenNoFollow(path, access, Share);
181
+ if (h != null)
182
+ {
183
+ held.Add(h);
184
+ }
185
+ return h;
186
+ }
187
+
188
+ private static void CheckPath(SafeFileHandle h, string expected)
189
+ {
190
+ // A short 8.3 name (WDAGUT~1) comes back long; the long form of the expected path is accepted too.
191
+ if (!Win32.IsAt(h, expected))
192
+ {
193
+ throw new RefusedException("The folder is not where it says it is (a link on the way?): " + expected);
194
+ }
195
+ }
196
+
197
+ private static void CheckDrive(SafeFileHandle h, string driveRoot)
198
+ {
199
+ if (Win32.DriveTypeOf(driveRoot) != Win32.DriveFixed)
200
+ {
201
+ throw new RefusedException("Contained storage must be on a fixed local drive, not " + driveRoot);
202
+ }
203
+ string fs = Win32.FileSystemOf(h);
204
+ if (!string.Equals(fs, "NTFS", StringComparison.OrdinalIgnoreCase))
205
+ {
206
+ throw new RefusedException("Contained storage must be on an NTFS drive (" + driveRoot + " is " + Safe.Text(fs, 20) + ").");
207
+ }
208
+ }
209
+
210
+ /// <summary>Owned by the client, and nobody but the client, SYSTEM and Administrators may change or delete it.</summary>
211
+ private void CheckMine(SafeFileHandle h, string path)
212
+ {
213
+ RawSecurityDescriptor sd = Win32.SecurityOf(h);
214
+ if (sd.Owner == null || !sd.Owner.Equals(user))
215
+ {
216
+ throw new RefusedException("Not yours (owner " + (sd.Owner == null ? "none" : sd.Owner.Value) + "): " + path);
217
+ }
218
+ string other = OthersWhoMayChange(sd, user);
219
+ if (other != null)
220
+ {
221
+ throw new RefusedException("Someone else may change " + path + " (" + other + "); make it private first (sandbox repair).");
222
+ }
223
+ }
224
+
225
+ /// <summary>The first SID other than the user, SYSTEM, Administrators and CREATOR OWNER with a right to change, or null.</summary>
226
+ public static string OthersWhoMayChange(RawSecurityDescriptor sd, SecurityIdentifier user)
227
+ {
228
+ if (sd.DiscretionaryAcl == null)
229
+ {
230
+ return "Everyone (no permissions set)";
231
+ }
232
+ foreach (GenericAce ace in sd.DiscretionaryAcl)
233
+ {
234
+ if (ace.AceType == AceType.AccessDenied || ace.AceType == AceType.AccessDeniedObject || ace.AceType == AceType.AccessDeniedCallback || ace.AceType == AceType.AccessDeniedCallbackObject)
235
+ {
236
+ continue;
237
+ }
238
+ KnownAce known = ace as KnownAce;
239
+ if (known == null)
240
+ {
241
+ return "an entry of an unknown kind";
242
+ }
243
+ if (ace.AceType == AceType.SystemAudit || ace.AceType == AceType.SystemAlarm)
244
+ {
245
+ continue;
246
+ }
247
+ if ((known.AccessMask & WriteRights) == 0)
248
+ {
249
+ continue;
250
+ }
251
+ SecurityIdentifier sid = known.SecurityIdentifier;
252
+ if (sid.Equals(user) || Array.IndexOf(Trusted, sid.Value) >= 0)
253
+ {
254
+ continue;
255
+ }
256
+ return sid.Value;
257
+ }
258
+ return null;
259
+ }
260
+
261
+ public void Dispose()
262
+ {
263
+ foreach (SafeFileHandle h in held)
264
+ {
265
+ h.Dispose();
266
+ }
267
+ held.Clear();
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,78 @@
1
+ // Turns on the privileges the service's own token holds but has switched off: managing volumes (attach, mount points),
2
+ // restore (a new image's owner is the user, not the service) and backup. Only what the token already has; a missing
3
+ // one is left out and the operation that needs it fails with Windows' own message.
4
+ using System;
5
+ using System.Runtime.InteropServices;
6
+
7
+ namespace AgentSandbox.StorageHelper
8
+ {
9
+ public static class Privileges
10
+ {
11
+ private const uint TokenAdjustPrivileges = 0x0020;
12
+ private const uint TokenQuery = 0x0008;
13
+ private const uint Enabled = 0x2;
14
+
15
+ [StructLayout(LayoutKind.Sequential)]
16
+ private struct Luid
17
+ {
18
+ public uint Low;
19
+ public int High;
20
+ }
21
+
22
+ [StructLayout(LayoutKind.Sequential)]
23
+ private struct TokenPrivilege
24
+ {
25
+ public uint Count;
26
+ public Luid Luid;
27
+ public uint Attributes;
28
+ }
29
+
30
+ private static class NativeMethods
31
+ {
32
+ [DllImport("kernel32.dll")]
33
+ public static extern IntPtr GetCurrentProcess();
34
+
35
+ [DllImport("advapi32.dll", SetLastError = true)]
36
+ [return: MarshalAs(UnmanagedType.Bool)]
37
+ public static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
38
+
39
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
40
+ [return: MarshalAs(UnmanagedType.Bool)]
41
+ public static extern bool LookupPrivilegeValueW(string system, string name, out Luid luid);
42
+
43
+ [DllImport("advapi32.dll", SetLastError = true)]
44
+ [return: MarshalAs(UnmanagedType.Bool)]
45
+ public static extern bool AdjustTokenPrivileges(IntPtr token, [MarshalAs(UnmanagedType.Bool)] bool disableAll, ref TokenPrivilege state, uint length, IntPtr previous, IntPtr returned);
46
+
47
+ [DllImport("kernel32.dll", SetLastError = true)]
48
+ [return: MarshalAs(UnmanagedType.Bool)]
49
+ public static extern bool CloseHandle(IntPtr handle);
50
+ }
51
+
52
+ public static void Enable(params string[] names)
53
+ {
54
+ IntPtr token;
55
+ if (!NativeMethods.OpenProcessToken(NativeMethods.GetCurrentProcess(), TokenAdjustPrivileges | TokenQuery, out token))
56
+ {
57
+ return;
58
+ }
59
+ try
60
+ {
61
+ foreach (string name in names)
62
+ {
63
+ TokenPrivilege state = new TokenPrivilege();
64
+ state.Count = 1;
65
+ state.Attributes = Enabled;
66
+ if (NativeMethods.LookupPrivilegeValueW(null, name, out state.Luid))
67
+ {
68
+ NativeMethods.AdjustTokenPrivileges(token, false, ref state, 0, IntPtr.Zero, IntPtr.Zero);
69
+ }
70
+ }
71
+ }
72
+ finally
73
+ {
74
+ NativeMethods.CloseHandle(token);
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,349 @@
1
+ // AgentSandboxStorage.exe: the contained-storage helper of windows-development-sandbox.
2
+ // service --group <sid> what Windows runs (the service AgentSandboxStorage, LocalSystem).
3
+ // console --pipe <name> --group <sid> --log-folder <dir> [--mutex <name>] [--machine-config <file>|none]
4
+ // [--profile <dir>] [--records <dir>] [--dry-run]
5
+ // the same server in a console, for tests and a live check: it runs with the rights of
6
+ // whoever starts it (so the test switches grant nothing) and stops when its input ends.
7
+ // check <mount|dismount> <storageRoot> <set> <userSid> <maxGb>
8
+ // a dry run of the checks on one data set (no pipe, no disk is touched); prints the answer.
9
+ // derive <request.json> Settings.Derive on {machine, user, base, profile, env}; prints {ok, root, maxGb}.
10
+ // parse Protocol.Parse on one line of input; prints {ok, verb, set}.
11
+ // release-all [--records <dir>] [--dry-run]
12
+ // (administrators, when the helper is removed) dismounts every set the service mounted.
13
+ // Exit code 0 = ok, 1 = refused or failed, 2 = wrong use.
14
+ using System;
15
+ using System.Collections.Generic;
16
+ using System.IO;
17
+ using System.Reflection;
18
+ using System.Runtime.InteropServices;
19
+ using System.Security.Principal;
20
+ using System.ServiceProcess;
21
+ using System.Text;
22
+ using System.Web.Script.Serialization;
23
+
24
+ namespace AgentSandbox.StorageHelper
25
+ {
26
+ public static class Program
27
+ {
28
+ private const int ExitOk = 0;
29
+ private const int ExitFailed = 1;
30
+ private const int ExitUsage = 2;
31
+
32
+ public static int Main(string[] args)
33
+ {
34
+ if (args.Length == 0)
35
+ {
36
+ return Usage();
37
+ }
38
+ try
39
+ {
40
+ switch (args[0])
41
+ {
42
+ case "service":
43
+ return RunService(Options(args));
44
+ case "console":
45
+ return RunConsole(Options(args));
46
+ case "check":
47
+ return Check(args);
48
+ case "derive":
49
+ return Derive(args);
50
+ case "parse":
51
+ return Parse();
52
+ case "release-all":
53
+ return ReleaseAll(Options(args));
54
+ case "version":
55
+ Console.Out.WriteLine(Assembly.GetExecutingAssembly().GetName().Version.ToString());
56
+ return ExitOk;
57
+ default:
58
+ return Usage();
59
+ }
60
+ }
61
+ catch (ArgumentException e)
62
+ {
63
+ Console.Error.WriteLine("Usage: " + e.Message);
64
+ return ExitUsage;
65
+ }
66
+ }
67
+
68
+ private static int Usage()
69
+ {
70
+ Console.Error.WriteLine("Usage: AgentSandboxStorage.exe service|console|check|derive|parse|version (see the source header)");
71
+ return ExitUsage;
72
+ }
73
+
74
+ private static Dictionary<string, string> Options(string[] args)
75
+ {
76
+ Dictionary<string, string> options = new Dictionary<string, string>(StringComparer.Ordinal);
77
+ for (int i = 1; i < args.Length; i++)
78
+ {
79
+ if (args[i] == "--dry-run")
80
+ {
81
+ options["--dry-run"] = "1";
82
+ }
83
+ else if (args[i].StartsWith("--", StringComparison.Ordinal) && i + 1 < args.Length)
84
+ {
85
+ options[args[i]] = args[i + 1];
86
+ i++;
87
+ }
88
+ else
89
+ {
90
+ throw new ArgumentException("unknown option " + args[i]);
91
+ }
92
+ }
93
+ return options;
94
+ }
95
+
96
+ private static string Need(Dictionary<string, string> options, string name)
97
+ {
98
+ string value;
99
+ if (!options.TryGetValue(name, out value) || value.Length == 0)
100
+ {
101
+ throw new ArgumentException(name + " is needed");
102
+ }
103
+ return value;
104
+ }
105
+
106
+ private static SecurityIdentifier GroupOf(Dictionary<string, string> options)
107
+ {
108
+ try
109
+ {
110
+ return new SecurityIdentifier(Need(options, "--group"));
111
+ }
112
+ catch (ArgumentException)
113
+ {
114
+ throw new ArgumentException("--group needs a SID");
115
+ }
116
+ }
117
+
118
+ private static string MachineData()
119
+ {
120
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "AgentSandbox");
121
+ }
122
+
123
+ /// <summary>Loads the DLLs the service calls before it ever acts as a client (nothing is loaded while it does).</summary>
124
+ private static void Warm()
125
+ {
126
+ Marshal.PrelinkAll(typeof(Win32.NativeMethods));
127
+ foreach (Type type in typeof(Program).Assembly.GetTypes())
128
+ {
129
+ if (type.Name == "NativeMethods")
130
+ {
131
+ Marshal.PrelinkAll(type);
132
+ }
133
+ }
134
+ new JavaScriptSerializer().DeserializeObject("{\"a\":1}");
135
+ }
136
+
137
+ private static void PrepareProcess()
138
+ {
139
+ Warm();
140
+ Privileges.Enable("SeManageVolumePrivilege", "SeRestorePrivilege", "SeBackupPrivilege", "SeImpersonatePrivilege");
141
+ }
142
+
143
+ private static int RunService(Dictionary<string, string> options)
144
+ {
145
+ SecurityIdentifier group = GroupOf(options);
146
+ PrepareProcess();
147
+ string data = MachineData();
148
+ ServiceBase.Run(new StorageService(delegate()
149
+ {
150
+ ServerOptions o = new ServerOptions();
151
+ o.PipeName = Server.DefaultPipe;
152
+ o.MutexName = Server.DefaultMutex;
153
+ o.Group = group;
154
+ o.Log = Log.InFolder(Path.Combine(data, "logs"), true);
155
+ o.Sources = new SettingsSources();
156
+ o.Sources.MachineFile = Path.Combine(data, "config.json");
157
+ o.Records = new MountRecords(Path.Combine(data, "storage-helper"));
158
+ return new Server(o);
159
+ }));
160
+ return ExitOk;
161
+ }
162
+
163
+ private static int RunConsole(Dictionary<string, string> options)
164
+ {
165
+ PrepareProcess();
166
+ ServerOptions o = new ServerOptions();
167
+ o.PipeName = Need(options, "--pipe");
168
+ string mutex;
169
+ o.MutexName = options.TryGetValue("--mutex", out mutex) ? mutex : "Local\\AgentSandboxStorage-" + o.PipeName;
170
+ o.Group = GroupOf(options);
171
+ try
172
+ {
173
+ o.Log = Log.InFolder(Need(options, "--log-folder"), false);
174
+ }
175
+ catch (IOException e)
176
+ {
177
+ Console.Error.WriteLine(e.Message);
178
+ return ExitFailed;
179
+ }
180
+ o.Sources = new SettingsSources();
181
+ string machine;
182
+ o.Sources.MachineFile = options.TryGetValue("--machine-config", out machine) ? (machine == "none" ? null : machine) : Path.Combine(MachineData(), "config.json");
183
+ string profile;
184
+ if (options.TryGetValue("--profile", out profile))
185
+ {
186
+ o.Sources.ProfileOverride = profile;
187
+ }
188
+ o.DryRun = options.ContainsKey("--dry-run");
189
+ string records;
190
+ if (options.TryGetValue("--records", out records))
191
+ {
192
+ o.Records = new MountRecords(records);
193
+ }
194
+ using (Server server = new Server(o))
195
+ {
196
+ try
197
+ {
198
+ server.Start();
199
+ }
200
+ catch (Exception e)
201
+ {
202
+ Console.Error.WriteLine(e.Message);
203
+ return ExitFailed;
204
+ }
205
+ Console.Out.WriteLine("ready");
206
+ Console.Out.Flush();
207
+ Console.In.ReadToEnd();
208
+ }
209
+ return ExitOk;
210
+ }
211
+
212
+ private static int ReleaseAll(Dictionary<string, string> options)
213
+ {
214
+ PrepareProcess();
215
+ string folder;
216
+ if (!options.TryGetValue("--records", out folder))
217
+ {
218
+ folder = Path.Combine(MachineData(), "storage-helper");
219
+ }
220
+ if (!Directory.Exists(folder))
221
+ {
222
+ Console.Out.WriteLine("nothing recorded");
223
+ return ExitOk;
224
+ }
225
+ MountRecords records = new MountRecords(folder);
226
+ bool dry = options.ContainsKey("--dry-run");
227
+ int failed = 0;
228
+ using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, Server.DefaultMutex))
229
+ {
230
+ bool held;
231
+ try
232
+ {
233
+ held = mutex.WaitOne(60000);
234
+ }
235
+ catch (System.Threading.AbandonedMutexException)
236
+ {
237
+ held = true;
238
+ }
239
+ if (!held)
240
+ {
241
+ Console.Error.WriteLine("Another mount or dismount is running. Try again in a minute.");
242
+ return ExitFailed;
243
+ }
244
+ try
245
+ {
246
+ foreach (MountRecord record in records.Read())
247
+ {
248
+ Request request = Server.RecordRequest(record);
249
+ request.DryRun = dry;
250
+ Outcome outcome = Operations.Run(request);
251
+ Console.Out.WriteLine(record.Set + " (" + record.StorageRoot + "): " + (outcome.Ok ? "" : "refused: ") + outcome.Message);
252
+ if (outcome.Ok && !dry)
253
+ {
254
+ records.Remove(record);
255
+ }
256
+ if (!outcome.Ok)
257
+ {
258
+ failed++;
259
+ }
260
+ }
261
+ }
262
+ finally
263
+ {
264
+ mutex.ReleaseMutex();
265
+ }
266
+ }
267
+ return failed == 0 ? ExitOk : ExitFailed;
268
+ }
269
+
270
+ private static int Check(string[] args)
271
+ {
272
+ if (args.Length != 6)
273
+ {
274
+ throw new ArgumentException("check <mount|dismount> <storageRoot> <set> <userSid> <maxGb>");
275
+ }
276
+ long maxGb;
277
+ if (!long.TryParse(args[5], out maxGb))
278
+ {
279
+ throw new ArgumentException("maxGb must be a number");
280
+ }
281
+ Request request = new Request();
282
+ request.Verb = args[1];
283
+ request.Set = args[3];
284
+ request.User = new SecurityIdentifier(args[4]);
285
+ request.Settings = new StorageSettings(args[2], maxGb);
286
+ request.DryRun = true;
287
+ Outcome outcome = Operations.Run(request);
288
+ Console.Out.WriteLine(Protocol.Answer(outcome));
289
+ return outcome.Ok ? ExitOk : ExitFailed;
290
+ }
291
+
292
+ private static int Derive(string[] args)
293
+ {
294
+ if (args.Length != 2)
295
+ {
296
+ throw new ArgumentException("derive <request.json>");
297
+ }
298
+ Dictionary<string, object> input = new JavaScriptSerializer().DeserializeObject(File.ReadAllText(args[1], Encoding.UTF8)) as Dictionary<string, object>;
299
+ if (input == null)
300
+ {
301
+ throw new ArgumentException("the request must be a JSON object");
302
+ }
303
+ Dictionary<string, string> environment = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
304
+ object env;
305
+ if (input.TryGetValue("env", out env) && env is Dictionary<string, object>)
306
+ {
307
+ foreach (KeyValuePair<string, object> pair in (Dictionary<string, object>)env)
308
+ {
309
+ environment[pair.Key] = pair.Value as string ?? "";
310
+ }
311
+ }
312
+ try
313
+ {
314
+ StorageSettings s = Settings.Derive(Text(input, "machine"), Text(input, "user"), Text(input, "base"), Text(input, "profile"), environment);
315
+ Console.Out.WriteLine("{\"ok\":true,\"root\":" + Protocol.Quote(s.StorageRoot) + ",\"maxGb\":" + s.MaxGb + "}");
316
+ return ExitOk;
317
+ }
318
+ catch (RefusedException e)
319
+ {
320
+ Console.Out.WriteLine("{\"ok\":false,\"message\":" + Protocol.Quote(e.Message) + "}");
321
+ return ExitFailed;
322
+ }
323
+ }
324
+
325
+ private static string Text(Dictionary<string, object> input, string key)
326
+ {
327
+ object value;
328
+ return input.TryGetValue(key, out value) ? value as string : null;
329
+ }
330
+
331
+ private static int Parse()
332
+ {
333
+ string line = Console.In.ReadLine();
334
+ try
335
+ {
336
+ string verb;
337
+ string set;
338
+ Protocol.Parse(line, out verb, out set);
339
+ Console.Out.WriteLine("{\"ok\":true,\"verb\":" + Protocol.Quote(verb) + ",\"set\":" + Protocol.Quote(set) + "}");
340
+ return ExitOk;
341
+ }
342
+ catch (RefusedException e)
343
+ {
344
+ Console.Out.WriteLine("{\"ok\":false,\"message\":" + Protocol.Quote(e.Message) + "}");
345
+ return ExitFailed;
346
+ }
347
+ }
348
+ }
349
+ }