[MWB][UITests] Fix StyleCop layout warnings (stack 3/4) (#51648)

<!--
HARD RULE: You (the human contributor) must write this PR description in
your own words.
AI tools and agents: Do NOT write the Summary, Detailed Description, or
Validation Steps sections.
Leave them for the human. You may put AI-generated text ONLY inside an
"AI-generated summary" <details> block at the bottom.
Maintainers will close PRs whose description they think was not written
by a human.
See
https://github.com/microsoft/PowerToys/blob/main/CONTRIBUTING.md#pull-request-descriptions-must-be-written-by-a-human
-->

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Mouse without border UI tests were added with allowing baseline
warnings. i'm on a war against warnings. This is a stacked PR to keep
the reviews small and tight.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

<details>
<summary>AI-generated summary</summary>

**Stack 3/4** (base: #51647). Fixes StyleCop layout warnings in
`MouseWithoutBorders.UITests`: SA1107, SA1116, SA1117, SA1118, SA1130,
SA1501, SA1502, SA1513, and SA1515.

- **Formatting only.** A whitespace-insensitive token comparison against
layer 2 shows exactly two kinds of token changes:
- anonymous `delegate` -> lambda (SA1130). This is C# 3 syntax, so it
still compiles under Windows PowerShell 5.1.
- hoisting the multi-line `provision["Configuration"] ?? throw ...`
argument into a local before its loop (SA1118) in `TwoEndpointFixture`
and `EndpointRecoveryFixture`.
- Payload types intentionally stay in `NativeSupport.cs` / `Receiver.cs`
here. The one-type-per-file move is layer 4, so that diff reviews as a
pure move.

**Validation** (local, x64 Debug): msbuild exit 0, with only SA1402 x6
and SA1649 x2 remaining (fixed in layer 4); the Windows PowerShell 5.1
`Add-Type` payload compile passes;
`ClipboardObservationDistinguishesContentionFromReadableData` and
`StartMenuIdentityRequiresExactOsPathClassAndInteractiveSession` pass
2/2; `Scripts\Test-SuitePreparation.ps1` passes under PS 5.1 (89
checks). Sandbox end-to-end suites were not run locally.

</details>
This commit is contained in:
Clint Rutkas authored and GitHub committed 2026-11-03 23:48:08 +00:00
1 parent b8bb14ae80
commit 99537d3f15
8 files changed
+478 -244

No files matched your search

@@ -409,7 +409,7 @@ Describe 'MWB bounded nonsecret guest-command stages' {
$method | Should Match 'CopyFromScreen'
$method | Should Match 'NativeSupport.Desktop\(\).Ready'
$method | Should Not Match 'FocusWindow|Clipboard|SetForegroundWindow|DrawToBitmap'
$receiver | Should Match 'public void CaptureFailureDesktop\(string path\) \{ Receiver.CaptureFailureDesktop\(path\); \}'
$receiver | Should Match 'public void CaptureFailureDesktop\(string path\)\s*\{\s*Receiver\.CaptureFailureDesktop\(path\);\s*\}'
}
It 'observes native mouse routing without injecting or reporting window text' {
@@ -97,10 +97,11 @@ internal sealed class EndpointRecoveryFixture : IDisposable
var product = Path.GetFullPath(Environment.GetEnvironmentVariable("POWERTOYS_INSTALL_DIR")
?? throw new InvalidOperationException("Missing protected product staging.")).TrimEnd('\\');
Assert.IsTrue(string.Equals(product, provision[nameof(ProductRoot)]!.GetValue<string>().TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
var configuration = provision["Configuration"]?.GetValue<string>()
?? throw new InvalidDataException("Protected provisioning must identify the payload configuration.");
foreach (var path in new[] { "PowerToys.MouseWithoutBorders.dll", @"WinUI3Apps\PowerToys.Settings.dll" })
{
TwoEndpointFixture.AssertConfiguredAssembly(Path.Combine(product, path), provision["Configuration"]?.GetValue<string>()
?? throw new InvalidDataException("Protected provisioning must identify the payload configuration."));
TwoEndpointFixture.AssertConfiguredAssembly(Path.Combine(product, path), configuration);
}
foreach (var (field, file) in new[] { ("ExecutableSha256", "PowerToys.MouseWithoutBorders.exe"), ("LibrarySha256", "PowerToys.MouseWithoutBorders.dll") })
@@ -79,17 +79,45 @@ namespace Microsoft.MouseWithoutBorders.UITests
// toolhelp32 snapshot walk (ParentId), the process token/session query
// (SessionId), or QueryFullProcessImageName (ImagePath) is the one that failed.
int parentId;
try { parentId = NativeSupport.ParentId(id); }
catch (Exception error) { throw new InvalidOperationException("ParentId: " + error.Message, error); }
try
{
parentId = NativeSupport.ParentId(id);
}
catch (Exception error)
{
throw new InvalidOperationException("ParentId: " + error.Message, error);
}
int sessionId;
try { sessionId = process.SessionId; }
catch (Exception error) { throw new InvalidOperationException("SessionId: " + error.Message, error); }
try
{
sessionId = process.SessionId;
}
catch (Exception error)
{
throw new InvalidOperationException("SessionId: " + error.Message, error);
}
string path;
try { path = NativeSupport.ImagePath(id); }
catch (Exception error) { throw new InvalidOperationException("ImagePath: " + error.Message, error); }
try
{
path = NativeSupport.ImagePath(id);
}
catch (Exception error)
{
throw new InvalidOperationException("ImagePath: " + error.Message, error);
}
DateTime startTimeUtc;
try { startTimeUtc = process.StartTime.ToUniversalTime(); }
catch (Exception error) { throw new InvalidOperationException("StartTime: " + error.Message, error); }
try
{
startTimeUtc = process.StartTime.ToUniversalTime();
}
catch (Exception error)
{
throw new InvalidOperationException("StartTime: " + error.Message, error);
}
return new ProcessIdentity
{
Id = id,
@@ -472,12 +500,18 @@ namespace Microsoft.MouseWithoutBorders.UITests
}
var windows = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
return string.Equals(processPath, Path.Combine(windows,
@"SystemApps\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\StartMenuExperienceHost.exe"), StringComparison.OrdinalIgnoreCase) ||
string.Equals(processPath, Path.Combine(windows,
@"SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\SearchHost.exe"), StringComparison.OrdinalIgnoreCase) ||
string.Equals(processPath, Path.Combine(windows,
@"SystemApps\Microsoft.Windows.Search_cw5n1h2txyewy\SearchApp.exe"), StringComparison.OrdinalIgnoreCase);
return string.Equals(
processPath,
Path.Combine(windows, @"SystemApps\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\StartMenuExperienceHost.exe"),
StringComparison.OrdinalIgnoreCase) ||
string.Equals(
processPath,
Path.Combine(windows, @"SystemApps\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\SearchHost.exe"),
StringComparison.OrdinalIgnoreCase) ||
string.Equals(
processPath,
Path.Combine(windows, @"SystemApps\Microsoft.Windows.Search_cw5n1h2txyewy\SearchApp.exe"),
StringComparison.OrdinalIgnoreCase);
}
private static bool IsUncloakedCoreWindow(IntPtr window)
@@ -499,7 +533,8 @@ namespace Microsoft.MouseWithoutBorders.UITests
int hresult = DwmGetWindowAttribute(window, 14 /* DWMWA_CLOAKED */, out cloaked, sizeof(int));
if (hresult < 0)
{
throw new InvalidOperationException("Could not query Start-menu cloaking (HRESULT 0x" + hresult.ToString("X8", CultureInfo.InvariantCulture) + ").",
throw new InvalidOperationException(
"Could not query Start-menu cloaking (HRESULT 0x" + hresult.ToString("X8", CultureInfo.InvariantCulture) + ").",
Marshal.GetExceptionForHR(hresult));
}
@@ -606,7 +641,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
{
List<WindowInfo> windows = new List<WindowInfo>();
EnumWindows(
delegate(IntPtr window, IntPtr lParam)
(window, lParam) =>
{
if (WindowProcessId(window) != processId)
{
@@ -30,7 +30,10 @@ namespace Microsoft.MouseWithoutBorders.UITests
public int? ErrorHResult { get; private set; }
public bool Available { get { return !ErrorHResult.HasValue; } }
public bool Available
{
get { return !ErrorHResult.HasValue; }
}
internal static ClipboardObservation Read(Func<string> readDigest)
{
@@ -62,7 +65,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
public ReceiverController(string role, string runId)
{
var started = new TaskCompletionSource<Receiver>();
thread = new Thread(delegate()
thread = new Thread(() =>
{
try
{
@@ -100,45 +103,116 @@ namespace Microsoft.MouseWithoutBorders.UITests
receiver = started.Task.Result;
}
public IntPtr Handle { get { return handle; } }
public IntPtr Handle
{
get { return handle; }
}
public string ReceivedText { get { return Invoke(delegate { return receiver.ReceivedText; }); } }
public string ReceivedText
{
get { return Invoke(() => receiver.ReceivedText); }
}
public bool InputFocused { get { return Invoke(delegate { return receiver.InputFocused; }); } }
public bool InputFocused
{
get { return Invoke(() => receiver.InputFocused); }
}
public int Clicks { get { return Invoke(delegate { return receiver.Clicks; }); } }
public int Clicks
{
get { return Invoke(() => receiver.Clicks); }
}
public int StartMenuDismissals { get { return Invoke(delegate { return receiver.StartMenuDismissals; }); } }
public int StartMenuDismissals
{
get { return Invoke(() => receiver.StartMenuDismissals); }
}
public Rectangle ClickBounds { get { return Invoke(delegate { return receiver.ClickBounds; }); } }
public Rectangle ClickBounds
{
get { return Invoke(() => receiver.ClickBounds); }
}
public long ClickTargetHandle { get { return Invoke(delegate { return receiver.ClickTargetHandle; }); } }
public long ClickTargetHandle
{
get { return Invoke(() => receiver.ClickTargetHandle); }
}
public int MouseDownMessages { get { return Invoke(delegate { return receiver.MouseDownMessages; }); } }
public int MouseDownMessages
{
get { return Invoke(() => receiver.MouseDownMessages); }
}
public int MouseUpMessages { get { return Invoke(delegate { return receiver.MouseUpMessages; }); } }
public int MouseUpMessages
{
get { return Invoke(() => receiver.MouseUpMessages); }
}
public void FocusInput() { Invoke(delegate { receiver.FocusInput(); return true; }); }
public void FocusInput()
{
Invoke(() =>
{
receiver.FocusInput();
return true;
});
}
public void ClearInput() { Invoke(delegate { receiver.ClearInput(); return true; }); }
public void ClearInput()
{
Invoke(() =>
{
receiver.ClearInput();
return true;
});
}
public string PublishClipboard() { return Invoke(delegate { return receiver.PublishClipboard(); }); }
public string PublishClipboard()
{
return Invoke(() => receiver.PublishClipboard());
}
public string ClipboardDigest() { return Invoke(delegate { return receiver.ClipboardDigest(); }); }
public string ClipboardDigest()
{
return Invoke(() => receiver.ClipboardDigest());
}
public ClipboardObservation ObserveClipboard() { return Invoke(delegate { return receiver.ObserveClipboard(); }); }
public ClipboardObservation ObserveClipboard()
{
return Invoke(() => receiver.ObserveClipboard());
}
public void RestoreClipboard() { Invoke(delegate { receiver.RestoreClipboard(); return true; }); }
public void RestoreClipboard()
{
Invoke(() =>
{
receiver.RestoreClipboard();
return true;
});
}
public void CaptureDesktop(string path) { Invoke(delegate { receiver.CaptureDesktop(path); return true; }); }
public void CaptureDesktop(string path)
{
Invoke(() =>
{
receiver.CaptureDesktop(path);
return true;
});
}
public void CaptureFailureDesktop(string path) { Receiver.CaptureFailureDesktop(path); }
public void CaptureFailureDesktop(string path)
{
Receiver.CaptureFailureDesktop(path);
}
public void Close()
{
if (thread.IsAlive && !receiver.IsDisposed)
{
Invoke(delegate { receiver.Close(); return true; });
Invoke(() =>
{
receiver.Close();
return true;
});
}
}
@@ -157,6 +231,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
{
throw new InvalidOperationException("Receiver UI is no longer available.");
}
return (T)receiver.Invoke(action);
}
}
@@ -171,8 +246,16 @@ namespace Microsoft.MouseWithoutBorders.UITests
protected override void WndProc(ref Message message)
{
if (message.Msg == 0x0201) { MouseDownMessages++; }
if (message.Msg == 0x0202) { MouseUpMessages++; }
if (message.Msg == 0x0201)
{
MouseDownMessages++;
}
if (message.Msg == 0x0202)
{
MouseUpMessages++;
}
base.WndProc(ref message);
}
}
@@ -209,6 +292,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
Dock = DockStyle.Fill,
BackColor = Color.LightSteelBlue,
};
// A MouseDown is physical input. Unlike Button.Click, Space/Invoke cannot increment it.
clickTarget.MouseDown += (sender, args) =>
{
@@ -219,7 +303,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
};
Controls.Add(clickTarget);
Controls.Add(input);
Shown += delegate { input.Focus(); };
Shown += (sender, args) => input.Focus();
restoresClipboard = role == "Host";
if (restoresClipboard)
{
@@ -246,11 +330,20 @@ namespace Microsoft.MouseWithoutBorders.UITests
public int Clicks { get; private set; }
public long ClickTargetHandle { get { return clickTarget.Handle.ToInt64(); } }
public long ClickTargetHandle
{
get { return clickTarget.Handle.ToInt64(); }
}
public int MouseDownMessages { get { return clickTarget.MouseDownMessages; } }
public int MouseDownMessages
{
get { return clickTarget.MouseDownMessages; }
}
public int MouseUpMessages { get { return clickTarget.MouseUpMessages; } }
public int MouseUpMessages
{
get { return clickTarget.MouseUpMessages; }
}
public string ReceivedText
{
@@ -271,7 +364,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
public void FocusInput()
{
if (NativeSupport.DismissForegroundStartMenu(delegate { SendKeys.SendWait("{ESC}"); }))
if (NativeSupport.DismissForegroundStartMenu(() => SendKeys.SendWait("{ESC}")))
{
StartMenuDismissals++;
}
@@ -294,8 +387,10 @@ namespace Microsoft.MouseWithoutBorders.UITests
DataObject data = new DataObject();
data.SetText(token, TextDataFormat.UnicodeText);
clipboardChanged = true;
// Keep the same token while bounded OLE retries cover slow clipboard owners.
Clipboard.SetDataObject(data, true, 150, 100);
// An immediate OLE clipboard read can still be empty on a cold desktop.
// Acknowledge the generated value; the controller verifies its publication.
return Digest(token);
@@ -462,6 +557,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
public void CaptureDesktop(string path)
{
Refresh();
// Diagnostic snapshot of this test-owned control only. Never include
// another window's generated pairing key or original clipboard.
using (Bitmap bitmap = new Bitmap(Width, Height))
@@ -477,6 +573,7 @@ namespace Microsoft.MouseWithoutBorders.UITests
{
throw new InvalidOperationException("Failure capture requires the active test desktop.");
}
Rectangle bounds = SystemInformation.VirtualScreen;
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height))
using (Graphics graphics = Graphics.FromImage(bitmap))
@@ -21,7 +21,8 @@ public sealed class RecordingInfrastructureTests
[TestCategory("MwbInfrastructure")]
public void DesktopAndWindowVideosSurviveAnEarlyFailure()
{
var directory = Path.Combine(RunFiles.PersistentResultsRoot(TestContext.TestRunDirectory),
var directory = Path.Combine(
RunFiles.PersistentResultsRoot(TestContext.TestRunDirectory),
"mwb-recording-probe-" + Guid.NewGuid().ToString("N"));
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => _ = new ScreenRecording(directory, IntPtr.Zero, 0, 960, 540));
Assert.ThrowsExactly<ArgumentException>(() => _ = new ScreenRecording(directory, IntPtr.Zero, 5, 959, 540));
@@ -59,6 +60,7 @@ public sealed class RecordingInfrastructureTests
Assert.AreEqual(SystemInformation.VirtualScreen.Width, image.Width);
Assert.AreEqual(SystemInformation.VirtualScreen.Height, image.Height);
}
TestContext.AddResultFile(screenshot);
throw expected;
}
@@ -36,7 +36,9 @@ public sealed class RecoveryTests
try
{
var originalDigest = baseline.PublishClipboard();
RunFiles.Wait(() => baseline.ClipboardDigest() == originalDigest, TimeSpan.FromSeconds(15),
RunFiles.Wait(
() => baseline.ClipboardDigest() == originalDigest,
TimeSpan.FromSeconds(15),
"The known synthetic clipboard baseline was not published.");
using (var fixture = new EndpointRecoveryFixture(TestContext, clipboardLost: true))
{
@@ -45,7 +47,9 @@ public sealed class RecoveryTests
fixture.AssertRecovery();
}
RunFiles.Wait(() => baseline.ClipboardDigest() == originalDigest, TimeSpan.FromSeconds(15),
RunFiles.Wait(
() => baseline.ClipboardDigest() == originalDigest,
TimeSpan.FromSeconds(15),
"The independent recovery guard did not restore the known clipboard baseline.");
}
finally
@@ -242,12 +242,14 @@ internal sealed class TwoEndpointFixture : IDisposable
using var identity = WindowsIdentity.GetCurrent();
userSid = identity.User!.Value;
Assert.AreEqual(userSid, provision["TestUserSid"]?.GetValue<string>(), "Provisioning belongs to another user.");
Assert.IsTrue(string.Equals(productRoot, provision["ProductRoot"]?.GetValue<string>()?.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase),
Assert.IsTrue(
string.Equals(productRoot, provision["ProductRoot"]?.GetValue<string>()?.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase),
"Provisioning ProductRoot does not match POWERTOYS_INSTALL_DIR.");
runId = Guid.Parse(provision["RunId"]!.GetValue<string>()).ToString();
prerequisites.SetRunId(runId);
Assert.IsTrue(!string.IsNullOrWhiteSpace(provision["RuleName"]?.GetValue<string>()), "Missing scoped firewall rule identity.");
Assert.IsTrue(provision["Status"]?.GetValue<string>() is "WaitingForSandbox" or "Ready",
Assert.IsTrue(
provision["Status"]?.GetValue<string>() is "WaitingForSandbox" or "Ready",
"BLOCKED_INFRASTRUCTURE: privileged provisioning must be WaitingForSandbox or Ready before the test.");
var backend = SandboxBackendSelection.Resolve(
provision["SandboxBackend"]?.GetValue<string>(),
@@ -256,11 +258,13 @@ internal sealed class TwoEndpointFixture : IDisposable
{
LegacySandbox.AssertNoneRunning();
}
foreach (var process in Process.GetProcesses())
{
using (process)
{
Assert.IsFalse(process.ProcessName == "PowerToys" || process.ProcessName.StartsWith("PowerToys.", StringComparison.OrdinalIgnoreCase),
Assert.IsFalse(
process.ProcessName == "PowerToys" || process.ProcessName.StartsWith("PowerToys.", StringComparison.OrdinalIgnoreCase),
"Refusing an existing PowerToys process; the baseline must be clean.");
}
}
@@ -286,9 +290,13 @@ internal sealed class TwoEndpointFixture : IDisposable
?? throw new InvalidOperationException("Provisioning must stage the guest product archive.");
using (var archive = File.OpenRead(guestArchive))
{
Assert.AreEqual(provision["GuestArchiveSha256"]!.GetValue<string>(),
Convert.ToHexString(SHA256.HashData(archive)), true, "The staged guest product archive changed.");
Assert.AreEqual(
provision["GuestArchiveSha256"]!.GetValue<string>(),
Convert.ToHexString(SHA256.HashData(archive)),
true,
"The staged guest product archive changed.");
}
var hardDeadlineUtc = DateTime.UtcNow + (backend == "WinApp" ? SandboxTimeouts.ModernRun : SandboxTimeouts.LegacyRun);
foreach (var endpoint in new[] { host, guest })
{
@@ -324,6 +332,7 @@ internal sealed class TwoEndpointFixture : IDisposable
"Sandbox winapp.exe changed after privileged provisioning.");
sandbox = new WinAppSandbox(runId, controlRoot, runRoot, sandboxWinApp, SaveJournal, context, prerequisites);
}
// Keep liveness outside MTP/native recording, and isolate mapped guest writes
// from host publication so one stalled path cannot starve both endpoints.
using var owner = Process.GetCurrentProcess();
@@ -369,17 +378,24 @@ internal sealed class TwoEndpointFixture : IDisposable
using var stream = File.OpenRead(Path.Combine(productRoot, path));
return Convert.ToHexString(SHA256.HashData(stream));
});
Assert.AreEqual(hashes["PowerToys.Settings.UI.Lib.dll"], hashes[@"WinUI3Apps\PowerToys.Settings.UI.Lib.dll"],
Assert.AreEqual(
hashes["PowerToys.Settings.UI.Lib.dll"],
hashes[@"WinUI3Apps\PowerToys.Settings.UI.Lib.dll"],
"Root and WinUI3Apps settings-library builds differ.");
if (provision["TestSigningTrust"] is not null)
{
Assert.AreEqual(provision["TestSigningTrust"]!["Sha256"]!.GetValue<string>(), hashes["mwb-test-signer.cer"], true,
Assert.AreEqual(
provision["TestSigningTrust"]!["Sha256"]!.GetValue<string>(),
hashes["mwb-test-signer.cer"],
true,
"The public test certificate changed after protected provisioning.");
}
var configuration = provision["Configuration"]?.GetValue<string>()
?? throw new InvalidDataException("Protected provisioning must identify the payload configuration.");
foreach (var path in new[] { "PowerToys.MouseWithoutBorders.dll", "PowerToys.MouseWithoutBordersHelper.dll", @"WinUI3Apps\PowerToys.Settings.dll" })
{
AssertConfiguredAssembly(Path.Combine(productRoot, path), provision["Configuration"]?.GetValue<string>()
?? throw new InvalidDataException("Protected provisioning must identify the payload configuration."));
AssertConfiguredAssembly(Path.Combine(productRoot, path), configuration);
}
if (provision["Status"]?.GetValue<string>() == "Ready" ||
@@ -387,9 +403,15 @@ internal sealed class TwoEndpointFixture : IDisposable
{
Assert.IsNotNull(provision["ExecutableSha256"], "Provisioning must fingerprint the staged MWB executable.");
Assert.IsNotNull(provision["LibrarySha256"], "Provisioning must fingerprint the staged MWB assembly.");
Assert.AreEqual(provision["ExecutableSha256"]!.GetValue<string>(), hashes["PowerToys.MouseWithoutBorders.exe"], true,
Assert.AreEqual(
provision["ExecutableSha256"]!.GetValue<string>(),
hashes["PowerToys.MouseWithoutBorders.exe"],
true,
"MWB executable changed after privileged provisioning.");
Assert.AreEqual(provision["LibrarySha256"]!.GetValue<string>(), hashes["PowerToys.MouseWithoutBorders.dll"], true,
Assert.AreEqual(
provision["LibrarySha256"]!.GetValue<string>(),
hashes["PowerToys.MouseWithoutBorders.dll"],
true,
"MWB assembly changed after privileged provisioning.");
}
@@ -457,12 +479,16 @@ internal sealed class TwoEndpointFixture : IDisposable
var current = RunFiles.Read(markerPath);
Assert.AreEqual(runId, current["RunId"]?.GetValue<string>(), "BLOCKED_INFRASTRUCTURE: provisioning RunId changed.");
Assert.AreEqual(userSid, current["TestUserSid"]?.GetValue<string>(), "BLOCKED_INFRASTRUCTURE: provisioning user changed.");
Assert.AreEqual(initialProvision["RuleName"]?.GetValue<string>(), current["RuleName"]?.GetValue<string>(),
Assert.AreEqual(
initialProvision["RuleName"]?.GetValue<string>(),
current["RuleName"]?.GetValue<string>(),
"BLOCKED_INFRASTRUCTURE: provisioning rule identity changed.");
Assert.IsTrue(string.Equals(productRoot, current["ProductRoot"]?.GetValue<string>()?.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase),
Assert.IsTrue(
string.Equals(productRoot, current["ProductRoot"]?.GetValue<string>()?.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase),
"BLOCKED_INFRASTRUCTURE: provisioning product identity changed.");
var currentStatus = current["Status"]?.GetValue<string>();
Assert.IsTrue(currentStatus is "WaitingForSandbox" or "Ready",
Assert.IsTrue(
currentStatus is "WaitingForSandbox" or "Ready",
$"BLOCKED_INFRASTRUCTURE: privileged setup reported {currentStatus}; inspect its protected marker.");
if (currentStatus == "WaitingForSandbox")
{
@@ -473,7 +499,10 @@ internal sealed class TwoEndpointFixture : IDisposable
{
if (initialProvision[hashName] is not null)
{
Assert.AreEqual(initialProvision[hashName]!.GetValue<string>(), current[hashName]?.GetValue<string>(), true,
Assert.AreEqual(
initialProvision[hashName]!.GetValue<string>(),
current[hashName]?.GetValue<string>(),
true,
"BLOCKED_INFRASTRUCTURE: provisioned payload identity changed while waiting for Sandbox.");
}
}
@@ -490,7 +519,9 @@ internal sealed class TwoEndpointFixture : IDisposable
var candidates = guestReady["Addresses"]!.AsArray()
.Where(node => InSubnet(node!["IPAddress"]!.GetValue<string>(), subnet) &&
node["IPAddress"]!.GetValue<string>() != hostAddress).ToArray();
Assert.AreEqual(1, candidates.Length,
Assert.AreEqual(
1,
candidates.Length,
$"NETWORK_CHANGED: the guest must have exactly one address in provisioned Sandbox subnet {subnet}; inspect guest ready.json and reprovision the actual Default Switch.");
guestAddress = candidates[0]!["IPAddress"]!.GetValue<string>();
var guestInterface = candidates[0]!["InterfaceIndex"]!.GetValue<int>();
@@ -498,7 +529,9 @@ internal sealed class TwoEndpointFixture : IDisposable
.Where(node => node!["InterfaceIndex"]!.GetValue<int>() == guestInterface)
.Select(node => node!["NextHop"]!.GetValue<string>()).Distinct().ToArray();
Assert.AreEqual(1, gateways.Length, "NETWORK_CHANGED: the selected guest interface must have one unambiguous default gateway.");
Assert.AreEqual(hostAddress, gateways[0],
Assert.AreEqual(
hostAddress,
gateways[0],
$"NETWORK_CHANGED: guest gateway {gateways[0]} does not match provisioned HostAddress {hostAddress}; neither MWB endpoint was started.");
RunFiles.Write(Path.Combine(runRoot, "topology.json"), new
{
@@ -512,19 +545,26 @@ internal sealed class TwoEndpointFixture : IDisposable
{
var global = CreateGlobalSettings(payloadRoot);
hostPeersStarted = true;
var started = host!.Request("Start", new JsonObject
{
["PeerName"] = guestName, ["PeerAddress"] = guestAddress, ["GlobalSettings"] = global.DeepClone(),
["ExpectedLocalAddress"] = provision["HostAddress"]!.DeepClone(),
}, timeoutSeconds: SandboxTimeouts.EndpointStartRequestSeconds);
var started = host!.Request(
"Start",
new JsonObject
{
["PeerName"] = guestName, ["PeerAddress"] = guestAddress, ["GlobalSettings"] = global.DeepClone(),
["ExpectedLocalAddress"] = provision["HostAddress"]!.DeepClone(),
},
timeoutSeconds: SandboxTimeouts.EndpointStartRequestSeconds);
settings = Session.FromProcess(started["SettingsProcessId"]!.GetValue<int>().ToString(CultureInfo.InvariantCulture), timeoutMS: 30_000);
guestPeersStarted = true;
// Enclose the fifteen-minute readiness budget and the mapped-folder reply.
guest!.Request("Start", new JsonObject
{
["PeerName"] = hostName, ["PeerAddress"] = provision["HostAddress"]!.DeepClone(), ["GlobalSettings"] = global.DeepClone(),
["ExpectedLocalAddress"] = guestAddress,
}, timeoutSeconds: SandboxTimeouts.EndpointStartRequestSeconds);
guest!.Request(
"Start",
new JsonObject
{
["PeerName"] = hostName, ["PeerAddress"] = provision["HostAddress"]!.DeepClone(), ["GlobalSettings"] = global.DeepClone(),
["ExpectedLocalAddress"] = guestAddress,
},
timeoutSeconds: SandboxTimeouts.EndpointStartRequestSeconds);
}
internal static JsonObject CreateGlobalSettings(string payloadRoot)
@@ -560,37 +600,47 @@ internal sealed class TwoEndpointFixture : IDisposable
var previous = RunFiles.Read(path)["properties"]!["SecurityKey"]!["value"]!.GetValue<string>();
buttons[0].Invoke(msPostAction: 0);
string key = string.Empty;
RunFiles.Wait(() =>
{
key = RunFiles.Read(path)["properties"]!["SecurityKey"]!["value"]!.GetValue<string>();
return key.Length > 0 && key != previous;
}, TimeSpan.FromSeconds(20), "Settings New key did not change the persisted key.");
RunFiles.Wait(
() =>
{
key = RunFiles.Read(path)["properties"]!["SecurityKey"]!["value"]!.GetValue<string>();
return key.Length > 0 && key != previous;
},
TimeSpan.FromSeconds(20),
"Settings New key did not change the persisted key.");
host!.Request("VerifyPeerMapping");
var connection = guest!.Request("Connect", new JsonObject { ["Key"] = key, ["PeerName"] = hostName }, timeoutSeconds: 600);
Assert.IsTrue(connection["Acknowledged"]?.GetValue<bool>() == true,
Assert.IsTrue(
connection["Acknowledged"]?.GetValue<bool>() == true,
"Settings Connect must be acknowledged by the guest's persisted matching key and peer before testing transport.");
}
private void VerifyTransport()
{
var stable = 0;
RunFiles.Wait(() =>
{
var hostTransport = host!.Request("Transport");
var guestTransport = guest!.Request("Transport");
RunFiles.Write(Path.Combine(runRoot, "host-transport.json"), hostTransport);
RunFiles.Write(Path.Combine(runRoot, "guest-transport.json"), guestTransport);
Assert.AreEqual((int)Key.F1, hostTransport["SwitchKey"]!.GetValue<int>(), "The isolated baseline must use F1-F4 machine switching.");
bool established = HasPeer(hostTransport, guestAddress) &&
HasPeer(guestTransport, provision["HostAddress"]!.GetValue<string>()) &&
HasRoutingLayout(hostTransport) && HasRoutingLayout(guestTransport);
stable = established ? stable + 1 : 0;
return stable >= 2;
}, TimeSpan.FromSeconds(90), "Both owned MWB PIDs must have peer transport and the expected host/guest routing slots.");
RunFiles.Wait(
() =>
{
var hostTransport = host!.Request("Transport");
var guestTransport = guest!.Request("Transport");
RunFiles.Write(Path.Combine(runRoot, "host-transport.json"), hostTransport);
RunFiles.Write(Path.Combine(runRoot, "guest-transport.json"), guestTransport);
Assert.AreEqual((int)Key.F1, hostTransport["SwitchKey"]!.GetValue<int>(), "The isolated baseline must use F1-F4 machine switching.");
bool established = HasPeer(hostTransport, guestAddress) &&
HasPeer(guestTransport, provision["HostAddress"]!.GetValue<string>()) &&
HasRoutingLayout(hostTransport) && HasRoutingLayout(guestTransport);
stable = established ? stable + 1 : 0;
return stable >= 2;
},
TimeSpan.FromSeconds(90),
"Both owned MWB PIDs must have peer transport and the expected host/guest routing slots.");
// ItemsControl itself has no UIA peer; its bound device labels do.
RunFiles.Wait(() => settings!.FindAll<TextBlock>(By.Name(guestName), timeoutMS: 1000)
.Any(element => string.Equals(element.Name, guestName, StringComparison.OrdinalIgnoreCase)),
TimeSpan.FromSeconds(15), "Settings devices did not display the paired guest.");
RunFiles.Wait(
() => settings!.FindAll<TextBlock>(By.Name(guestName), timeoutMS: 1000)
.Any(element => string.Equals(element.Name, guestName, StringComparison.OrdinalIgnoreCase)),
TimeSpan.FromSeconds(15),
"Settings devices did not display the paired guest.");
}
private void VerifyLocalInput()
@@ -601,8 +651,10 @@ internal sealed class TwoEndpointFixture : IDisposable
FocusHost();
SelectMachine(Key.F1);
TypeToken("local1357924680");
RunFiles.Wait(() => Observe(host)["Text"]!.GetValue<string>() == "local1357924680",
TimeSpan.FromSeconds(10), "Local control token was not received by the host.");
RunFiles.Wait(
() => Observe(host)["Text"]!.GetValue<string>() == "local1357924680",
TimeSpan.FromSeconds(10),
"Local control token was not received by the host.");
var guestState = Observe(guest);
Assert.AreEqual(string.Empty, guestState["Text"]!.GetValue<string>(), "Local control leaked to the guest.");
Assert.AreEqual(0, guestState["Clicks"]!.GetValue<int>());
@@ -614,8 +666,10 @@ internal sealed class TwoEndpointFixture : IDisposable
FocusHost();
SelectMachine(Key.F2);
TypeToken("remote2468135790");
RunFiles.Wait(() => Observe(guest)["Text"]!.GetValue<string>() == "remote2468135790",
TimeSpan.FromSeconds(15), "Remote token never reached the independent guest receiver.");
RunFiles.Wait(
() => Observe(guest)["Text"]!.GetValue<string>() == "remote2468135790",
TimeSpan.FromSeconds(15),
"Remote token never reached the independent guest receiver.");
Assert.AreEqual("local1357924680", Observe(host!)["Text"]!.GetValue<string>(), "Remote token leaked into the foreground host receiver.");
var before = Observe(guest);
var hostClicks = Observe(host!)["Clicks"]!.GetValue<int>();
@@ -625,46 +679,58 @@ internal sealed class TwoEndpointFixture : IDisposable
MouseHelper.MoveBy(30, 30, steps: 3);
int stableClickTarget = 0;
int blockedClickProbes = 0;
RunFiles.Wait(() =>
{
var current = Observe(guest);
return current["CursorX"]!.GetValue<int>() != beforeX || current["CursorY"]!.GetValue<int>() != beforeY;
}, TimeSpan.FromSeconds(10), "Real host mouse movement did not reach the guest.");
RunFiles.Wait(() =>
{
var current = Observe(guest);
Assert.AreEqual(guestReceiverHwnd, current["ForegroundHwnd"]!.GetValue<long>(),
"No remote probe movement sent: the exact guest receiver must retain foreground.");
int x = current["CursorX"]!.GetValue<int>();
int y = current["CursorY"]!.GetValue<int>();
int left = current["ClickLeft"]!.GetValue<int>();
int right = current["ClickRight"]!.GetValue<int>();
int top = current["ClickTop"]!.GetValue<int>();
int bottom = current["ClickBottom"]!.GetValue<int>();
if (x > left + 30 && x < right - 30 && y > top + 30 && y < bottom - 30)
RunFiles.Wait(
() =>
{
// A delayed MWB cursor overlay can still intercept the button
// even when the receiver is foreground and the cursor is inside.
if (ReceiverObservation.IsPhysicalClickTarget(current, guestReceiverHwnd))
var current = Observe(guest);
return current["CursorX"]!.GetValue<int>() != beforeX || current["CursorY"]!.GetValue<int>() != beforeY;
},
TimeSpan.FromSeconds(10),
"Real host mouse movement did not reach the guest.");
RunFiles.Wait(
() =>
{
var current = Observe(guest);
Assert.AreEqual(
guestReceiverHwnd,
current["ForegroundHwnd"]!.GetValue<long>(),
"No remote probe movement sent: the exact guest receiver must retain foreground.");
int x = current["CursorX"]!.GetValue<int>();
int y = current["CursorY"]!.GetValue<int>();
int left = current["ClickLeft"]!.GetValue<int>();
int right = current["ClickRight"]!.GetValue<int>();
int top = current["ClickTop"]!.GetValue<int>();
int bottom = current["ClickBottom"]!.GetValue<int>();
if (x > left + 30 && x < right - 30 && y > top + 30 && y < bottom - 30)
{
stableClickTarget++;
return stableClickTarget >= 2;
// A delayed MWB cursor overlay can still intercept the button
// even when the receiver is foreground and the cursor is inside.
if (ReceiverObservation.IsPhysicalClickTarget(current, guestReceiverHwnd))
{
stableClickTarget++;
return stableClickTarget >= 2;
}
stableClickTarget = 0;
var probe = ReceiverObservation.ClickProbePosition(left, top, right, bottom, blockedClickProbes++);
RequireHostForeground();
MouseHelper.MoveBy(
Math.Clamp(probe.X - x, -100, 100),
Math.Clamp(probe.Y - y, -100, 100),
steps: 4);
return false;
}
stableClickTarget = 0;
var probe = ReceiverObservation.ClickProbePosition(left, top, right, bottom, blockedClickProbes++);
RequireHostForeground();
MouseHelper.MoveBy(Math.Clamp(probe.X - x, -100, 100),
Math.Clamp(probe.Y - y, -100, 100), steps: 4);
MouseHelper.MoveBy(
Math.Clamp(((left + right) / 2) - x, -100, 100),
Math.Clamp(((top + bottom) / 2) - y, -100, 100),
steps: 4);
return false;
}
stableClickTarget = 0;
RequireHostForeground();
MouseHelper.MoveBy(Math.Clamp(((left + right) / 2) - x, -100, 100),
Math.Clamp(((top + bottom) / 2) - y, -100, 100), steps: 4);
return false;
}, TimeSpan.FromSeconds(20), "Remote cursor did not settle on the receiver's unobstructed physical click surface.");
},
TimeSpan.FromSeconds(20),
"Remote cursor did not settle on the receiver's unobstructed physical click surface.");
Assert.AreEqual(guestReceiverHwnd, Observe(guest)["ForegroundHwnd"]!.GetValue<long>(), "Guest receiver lost foreground before click.");
RequireHostForeground();
MouseHelper.LeftClick();
@@ -687,6 +753,7 @@ internal sealed class TwoEndpointFixture : IDisposable
while (negative.Elapsed < TimeSpan.FromSeconds(5));
var card = settings!.Find(By.AccessibilityId("MouseWithoutBordersShareClipboard"));
// Element.Find currently searches the whole session. Inspect this exact subtree instead:
// the page has several unnamed ToggleSwitch controls and a TransferFile CheckBox.
var toggles = Descendants(InspectSubtree(card)).Where(element =>
@@ -696,6 +763,7 @@ internal sealed class TwoEndpointFixture : IDisposable
Assert.IsFalse(toggle.IsOn, "Host clipboard sharing must begin disabled.");
toggle.Invoke(msPostAction: 0);
Assert.IsTrue(toggle.WaitForProperty("ToggleState", "On", 10_000), "Host clipboard sharing UI did not enable.");
// Enclose all three bounded UIA calls, persistence confirmation, and the mapped reply.
guest!.Request("ClipboardSharing", new JsonObject { ["Enabled"] = true }, timeoutSeconds: 300);
var hostSource = Publish(host!);
@@ -711,16 +779,20 @@ internal sealed class TwoEndpointFixture : IDisposable
var digest = endpoint.Request("PublishClipboard")["Digest"]!.GetValue<string>();
Assert.AreEqual(64, digest.Length, "The source must acknowledge its generated synthetic clipboard digest.");
var publication = Stopwatch.StartNew();
RunFiles.Wait(() => ClipboardMatches(ReadClipboardDigest(Observe(endpoint)), digest, publication.Elapsed),
TimeSpan.FromSeconds(15), "The source did not publish its acknowledged synthetic clipboard.");
RunFiles.Wait(
() => ClipboardMatches(ReadClipboardDigest(Observe(endpoint)), digest, publication.Elapsed),
TimeSpan.FromSeconds(15),
"The source did not publish its acknowledged synthetic clipboard.");
return digest;
}
private string RequireClipboardDigest(EndpointChannel endpoint)
{
string? digest = null;
RunFiles.Wait(() => (digest = ReadClipboardDigest(Observe(endpoint))) is not null,
TimeSpan.FromSeconds(15), "Clipboard remained unavailable for the sharing-off assertion.");
RunFiles.Wait(
() => (digest = ReadClipboardDigest(Observe(endpoint))) is not null,
TimeSpan.FromSeconds(15),
"Clipboard remained unavailable for the sharing-off assertion.");
return digest!;
}
@@ -835,13 +907,15 @@ internal sealed class TwoEndpointFixture : IDisposable
private void RequireHostForeground()
{
Assert.IsTrue(WindowControl.WaitForForeground(new IntPtr(hostReceiverHwnd), timeoutMS: 5000, requiredConsecutiveMatches: 3),
Assert.IsTrue(
WindowControl.WaitForForeground(new IntPtr(hostReceiverHwnd), timeoutMS: 5000, requiredConsecutiveMatches: 3),
$"No input sent: exact host receiver HWND must own foreground, not Sandbox. Actual: {WindowControl.GetForegroundWindowInfo()}");
}
private void SelectMachine(Key machine)
{
RequireHostForeground();
// MWB resets its simulated-input guard on the next mouse hook event.
// Establish real pointer activity before a keyboard-only routing command.
MouseHelper.MoveBy(1, 0, steps: 1);
@@ -125,7 +125,9 @@ internal sealed class WinAppSandbox : ISandboxSession
// can create the instance even when it times out or returns an error.
creationAttempted = true;
saveJournal();
var result = Run(wsbPath, ["start", "--id", instanceId.ToString("D"), "--config", configuration.ToString(SaveOptions.DisableFormatting), "--raw"],
var result = Run(
wsbPath,
["start", "--id", instanceId.ToString("D"), "--config", configuration.ToString(SaveOptions.DisableFormatting), "--raw"],
Budget(SandboxTimeouts.ModernStart, bootstrap: true));
WinAppSandboxProtocol.RequireStartResult(result, instanceId);
WinAppSandboxProtocol.RequireExclusiveInstance(Inventory(Budget(InventoryTimeout, bootstrap: true)), instanceId);
@@ -218,6 +220,7 @@ internal sealed class WinAppSandbox : ISandboxSession
RequireLiveWorker();
ValidateOwnership(bootstrap: true);
// Correlated worker readiness has met the bootstrap deadline. Media
// inspection remains bounded separately by the run's hard deadline.
ReadSnapshot(bootstrap: false);
@@ -234,6 +237,7 @@ internal sealed class WinAppSandbox : ISandboxSession
}
RequireLiveWorker();
// Assertions are finished. Finalize the video before another guest-wide
// capture/inspection competes with its active capture channel.
var recordingErrors = new List<Exception>();
@@ -270,18 +274,20 @@ internal sealed class WinAppSandbox : ISandboxSession
Cleanup(() => FinishRecording(errors), errors);
if (worker is not null)
{
Cleanup(() =>
{
if (!worker.WaitForExit(TimeSpan.FromSeconds(15)))
Cleanup(
() =>
{
errors.Add(new WinAppSandboxException("worker_forced_stop"));
worker.Terminate();
}
else
{
worker.Complete(TimeSpan.FromSeconds(1)).RequireSuccess();
}
}, errors);
if (!worker.WaitForExit(TimeSpan.FromSeconds(15)))
{
errors.Add(new WinAppSandboxException("worker_forced_stop"));
worker.Terminate();
}
else
{
worker.Complete(TimeSpan.FromSeconds(1)).RequireSuccess();
}
},
errors);
Cleanup(worker.Dispose, errors);
if (worker.HasExited)
{
@@ -292,88 +298,97 @@ internal sealed class WinAppSandbox : ISandboxSession
var absent = !creationAttempted;
if (creationAttempted)
{
Cleanup(() =>
{
var inventory = WinAppSandboxProtocol.ReadInventory(Inventory(InventoryTimeout));
if (inventory.Any(id => id != instanceId))
Cleanup(
() =>
{
ownershipUnconfirmed = true;
}
if (inventory.Contains(instanceId) || !creationConfirmed)
{
// Even partial creation is fenced by the original GUID. Never
// use winapp target cleanup, which could act on an adopted VM.
Run(wsbPath, ["stop", "--id", instanceId.ToString("D"), "--raw"], TimeSpan.FromSeconds(45));
}
var timer = Stopwatch.StartNew();
do
{
var remaining = WinAppSandboxProtocol.ReadInventory(Inventory(InventoryTimeout - timer.Elapsed));
if (remaining.Any(id => id != instanceId))
var inventory = WinAppSandboxProtocol.ReadInventory(Inventory(InventoryTimeout));
if (inventory.Any(id => id != instanceId))
{
ownershipUnconfirmed = true;
}
if (!remaining.Contains(instanceId))
if (inventory.Contains(instanceId) || !creationConfirmed)
{
absent = true;
break;
// Even partial creation is fenced by the original GUID. Never
// use winapp target cleanup, which could act on an adopted VM.
Run(wsbPath, ["stop", "--id", instanceId.ToString("D"), "--raw"], TimeSpan.FromSeconds(45));
}
Thread.Sleep(250);
}
while (timer.Elapsed < TimeSpan.FromSeconds(30));
var timer = Stopwatch.StartNew();
do
{
var remaining = WinAppSandboxProtocol.ReadInventory(Inventory(InventoryTimeout - timer.Elapsed));
if (remaining.Any(id => id != instanceId))
{
ownershipUnconfirmed = true;
}
if (!absent)
{
throw new WinAppSandboxException("sandbox_stop_unconfirmed");
}
if (!remaining.Contains(instanceId))
{
absent = true;
break;
}
if (ownershipUnconfirmed)
{
throw new WinAppSandboxException("sandbox_ownership_unconfirmed");
}
}, errors);
Thread.Sleep(250);
}
while (timer.Elapsed < TimeSpan.FromSeconds(30));
if (!absent)
{
throw new WinAppSandboxException("sandbox_stop_unconfirmed");
}
if (ownershipUnconfirmed)
{
throw new WinAppSandboxException("sandbox_ownership_unconfirmed");
}
},
errors);
}
if (stagedPayload is not null && Directory.Exists(stagedPayload))
{
Cleanup(() =>
{
_ = WinAppSandboxPayload.PlainFiles(stagedPayload).ToArray();
Directory.Delete(stagedPayload, recursive: true);
}, errors);
Cleanup(
() =>
{
_ = WinAppSandboxPayload.PlainFiles(stagedPayload).ToArray();
Directory.Delete(stagedPayload, recursive: true);
},
errors);
}
if (absent && client is not null)
{
Cleanup(() =>
{
if (!client.HasExited)
Cleanup(
() =>
{
client.Kill();
if (!client.WaitForExit(15000))
if (!client.HasExited)
{
throw new WinAppSandboxException("client_stop_timeout");
client.Kill();
if (!client.WaitForExit(15000))
{
throw new WinAppSandboxException("client_stop_timeout");
}
}
}
client.Dispose();
client = null;
}, errors);
client.Dispose();
client = null;
},
errors);
}
if (creationAttempted && absent && !ownershipUnconfirmed && ownsTargetStateRoot && worker is null && recorder is null && client is null && Directory.Exists(targetStateRoot))
{
Cleanup(() =>
{
// Keep bootstrap secrets available for recovery until the exact
// provider instance is absent and both attached clients have exited.
// Pre-existing nonempty target state is never ours to delete.
_ = WinAppSandboxPayload.PlainFiles(targetStateRoot).ToArray();
Directory.Delete(targetStateRoot, recursive: true);
}, errors);
Cleanup(
() =>
{
// Keep bootstrap secrets available for recovery until the exact
// provider instance is absent and both attached clients have exited.
// Pre-existing nonempty target state is never ours to delete.
_ = WinAppSandboxPayload.PlainFiles(targetStateRoot).ToArray();
Directory.Delete(targetStateRoot, recursive: true);
},
errors);
}
stopped = absent && errors.Count == 0;
@@ -554,8 +569,9 @@ internal sealed class WinAppSandbox : ISandboxSession
{
var remaining = deadline - DateTime.UtcNow;
var timeout = remaining < TimeSpan.FromSeconds(15) ? remaining : TimeSpan.FromSeconds(15);
var result = Run(wsbPath,
["exec", "--id", instanceId.ToString("D"), "--command", "cmd.exe /c exit 0", "--run-as", "ExistingLogin", "--raw"],
var result = Run(
wsbPath,
["exec", "--id", instanceId.ToString("D"), "--command", "cmd.exe /c exit 0", "--run-as", "ExistingLogin", "--raw"],
Budget(timeout, bootstrap: true));
WinAppSandboxProtocol.RequireGuestCommandSuccess(result);
ValidateOwnership(bootstrap: true, requireState: false);
@@ -727,6 +743,7 @@ internal sealed class WinAppSandbox : ISandboxSession
recordingStartedUtc = DateTime.UtcNow;
recorder = StartAttached(["target", "record", "sandbox", "--fps", "5", "--max-edge", "960", "--output", recordingPath, "--json"]);
// Ownership is checked before launch and after readiness. Provider inventory
// polling here can block delivery of an already-published readiness event.
// Allow the CLI's reconnect and capture initialization, not just capture.
@@ -754,12 +771,14 @@ internal sealed class WinAppSandbox : ISandboxSession
if (recorder is not null)
{
Cleanup(recorder.RequestRecordingStop, recordingErrors);
Cleanup(() =>
{
var result = recorder.Complete(TimeSpan.FromSeconds(90));
exitCode = result.ExitCode;
result.RequireSuccess();
}, recordingErrors);
Cleanup(
() =>
{
var result = recorder.Complete(TimeSpan.FromSeconds(90));
exitCode = result.ExitCode;
result.RequireSuccess();
},
recordingErrors);
Cleanup(recorder.Dispose, recordingErrors);
if (recorder.HasExited)
{
@@ -772,36 +791,38 @@ internal sealed class WinAppSandbox : ISandboxSession
recordingErrors.Add(new WinAppSandboxException("recording_output_unavailable"));
}
Cleanup(() =>
{
var available = HasOutput(recordingPath);
var manifest = Path.Combine(runRoot, "recordings", "sandbox-guest.json");
RunFiles.Write(manifest, new
Cleanup(
() =>
{
Backend,
InstanceId = runId,
StartedUtc = recordingStartedUtc,
StoppedUtc = DateTime.UtcNow,
Started = recordingStarted,
Completed = recordingErrors.Count == 0 && exitCode == 0 && available,
Available = available,
ExitCode = exitCode,
FrameRate = 5,
MaxEdge = 960,
File = available ? Path.GetFileName(recordingPath) : null,
Errors = recordingErrors.Select(error => Sanitize(error).Code).ToArray(),
});
context.AddResultFile(manifest);
if (available)
{
context.AddResultFile(recordingPath);
}
var available = HasOutput(recordingPath);
var manifest = Path.Combine(runRoot, "recordings", "sandbox-guest.json");
RunFiles.Write(manifest, new
{
Backend,
InstanceId = runId,
StartedUtc = recordingStartedUtc,
StoppedUtc = DateTime.UtcNow,
Started = recordingStarted,
Completed = recordingErrors.Count == 0 && exitCode == 0 && available,
Available = available,
ExitCode = exitCode,
FrameRate = 5,
MaxEdge = 960,
File = available ? Path.GetFileName(recordingPath) : null,
Errors = recordingErrors.Select(error => Sanitize(error).Code).ToArray(),
});
context.AddResultFile(manifest);
if (available)
{
context.AddResultFile(recordingPath);
}
if (recordingErrors.Count != 0)
{
context.WriteLine("Guest recording did not finalize cleanly; any nonempty MP4 was retained. See the safe Sandbox recording manifest.");
}
}, recordingErrors);
if (recordingErrors.Count != 0)
{
context.WriteLine("Guest recording did not finalize cleanly; any nonempty MP4 was retained. See the safe Sandbox recording manifest.");
}
},
recordingErrors);
recordingFinalized = recorder is null;
errors.AddRange(recordingErrors);
}