mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-11-04 12:01:21 +00:00
[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:
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' {
|
||||
|
||||
+3
-2
@@ -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") })
|
||||
|
||||
+51
-16
@@ -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))
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+165
-91
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user