Files
MBN_STOCK_WEBVIEW/scripts/Invoke-LegacyPackageInputSmoke.ps1
Wickedness b050f2f06c feat: refine branded operator experience
Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
2026-07-24 01:26:13 +09:00

4337 lines
177 KiB
PowerShell

[CmdletBinding()]
param(
[ValidateSet('Release', 'DebugAppX')]
[string]$BuildFlavor = 'Release',
[ValidateSet('ReadOnly', 'DryRunPlayout', 'FullUiDb', 'ProgramPlaylistDb')]
[string]$Profile = 'ReadOnly',
[ValidateSet('All', 'PList', 'GraphE', 'Catalog', 'Screens')]
[string]$FullUiDbScope = 'All',
[string]$RecoverNamedPlaylistTitle,
[ValidateRange(1024, 65535)]
[int]$Port = 9339,
[string]$Output,
[string]$Screenshot,
[string]$NodePath,
[ValidateRange(10, 120)]
[int]$LaunchTimeoutSeconds = 30,
[ValidateRange(30, 900)]
[int]$HarnessTimeoutSeconds = 180,
# The local-settings/import smoke reuses the exact package identity, launch,
# DryRun and normal-close guards from this script. Returning after the
# definitions keeps those guards single-sourced without launching anything.
[switch]$DefinitionsOnly
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
public static class LegacyPackageInputNative
{
private delegate bool EnumWindowsCallback(IntPtr window, IntPtr parameter);
[StructLayout(LayoutKind.Sequential)]
private struct Point
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
private struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private struct ClientGeometry
{
public int ScreenLeft;
public int ScreenTop;
public int Width;
public int Height;
public double CssScaleX;
public double CssScaleY;
}
[StructLayout(LayoutKind.Sequential)]
private struct MibTcpRowOwnerPid
{
public uint State;
public uint LocalAddress;
public uint LocalPort;
public uint RemoteAddress;
public uint RemotePort;
public uint OwningProcessId;
}
[StructLayout(LayoutKind.Sequential)]
private struct MibTcp6RowOwnerPid
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] LocalAddress;
public uint LocalScopeId;
public uint LocalPort;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] RemoteAddress;
public uint RemoteScopeId;
public uint RemotePort;
public uint State;
public uint OwningProcessId;
}
[StructLayout(LayoutKind.Sequential)]
private struct MouseInput
{
public int Dx;
public int Dy;
public uint MouseData;
public uint Flags;
public uint Time;
public UIntPtr ExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct KeyboardInput
{
public ushort VirtualKey;
public ushort ScanCode;
public uint Flags;
public uint Time;
public UIntPtr ExtraInfo;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)] public MouseInput Mouse;
[FieldOffset(0)] public KeyboardInput Keyboard;
}
[StructLayout(LayoutKind.Sequential)]
private struct Input
{
public uint Type;
public InputUnion Union;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetPackageFullName(
IntPtr process,
ref int packageFullNameLength,
StringBuilder packageFullName);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("iphlpapi.dll", SetLastError = true)]
private static extern uint GetExtendedTcpTable(
IntPtr table,
ref int size,
bool order,
int ipVersion,
int tableClass,
uint reserved);
[DllImport("user32.dll")]
private static extern bool ClientToScreen(IntPtr window, ref Point point);
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsCallback callback, IntPtr parameter);
[DllImport("user32.dll")]
private static extern IntPtr GetWindow(IntPtr window, uint command);
[DllImport("user32.dll")]
private static extern bool IsWindow(IntPtr window);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr window);
[DllImport("user32.dll")]
private static extern bool IsWindowEnabled(IntPtr window);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(
IntPtr window,
StringBuilder value,
int maximum);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(
IntPtr window,
StringBuilder value,
int maximum);
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out Point point);
[DllImport("user32.dll")]
private static extern IntPtr WindowFromPoint(Point point);
[DllImport("user32.dll")]
private static extern IntPtr GetAncestor(IntPtr window, uint flags);
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr window, out Rect rectangle);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr window);
[DllImport("user32.dll")]
private static extern bool BringWindowToTop(IntPtr window);
[DllImport("user32.dll")]
private static extern IntPtr SetActiveWindow(IntPtr window);
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr window);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool AttachThreadInput(uint from, uint to, bool attach);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int index);
[DllImport("user32.dll")]
private static extern uint SendInput(uint count, Input[] inputs, int size);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int virtualKey);
private const int ErrorInsufficientBuffer = 122;
private const int AddressFamilyInterNetwork = 2;
private const int AddressFamilyInterNetworkV6 = 23;
private const int TcpTableOwnerPidAll = 5;
private const uint GetAncestorRoot = 2;
private const uint GetWindowOwner = 4;
private const uint InputMouse = 0;
private const uint InputKeyboard = 1;
private const uint MouseMove = 0x0001;
private const uint MouseLeftDown = 0x0002;
private const uint MouseLeftUp = 0x0004;
private const uint MouseMoveNoCoalesce = 0x2000;
private const uint MouseVirtualDesk = 0x4000;
private const uint MouseAbsolute = 0x8000;
private const uint KeyUp = 0x0002;
private const int VirtualKeyLeftButton = 0x01;
private const ushort VirtualKeyShift = 0x10;
private const ushort VirtualKeyHome = 0x24;
private const ushort VirtualKeyDelete = 0x2E;
private const ushort VirtualKeyEscape = 0x1B;
private const ushort VirtualKeyF2 = 0x71;
private const ushort VirtualKeyF3 = 0x72;
private const int ClientPixelTolerance = 1;
public static bool WindowExists(IntPtr window)
{
return window != IntPtr.Zero && IsWindow(window);
}
public static bool WindowVisible(IntPtr window)
{
return WindowExists(window) && IsWindowVisible(window);
}
public static bool WindowEnabled(IntPtr window)
{
return WindowExists(window) && IsWindowEnabled(window);
}
public static int WindowProcessId(IntPtr window)
{
uint processId;
return GetWindowThreadProcessId(window, out processId) == 0
? 0 : checked((int)processId);
}
public static IntPtr RootWindow(IntPtr window)
{
return window == IntPtr.Zero
? IntPtr.Zero : GetAncestor(window, GetAncestorRoot);
}
public static bool OwnerChainContains(IntPtr window, IntPtr expectedOwner)
{
if (window == IntPtr.Zero || expectedOwner == IntPtr.Zero) return false;
IntPtr expectedRoot = RootWindow(expectedOwner);
IntPtr current = window;
for (int depth = 0; depth < 12 && current != IntPtr.Zero; depth++)
{
if (current == expectedOwner || RootWindow(current) == expectedRoot) return true;
current = GetWindow(current, GetWindowOwner);
}
return false;
}
public static string WindowClassName(IntPtr window)
{
StringBuilder value = new StringBuilder(512);
return window != IntPtr.Zero &&
GetClassName(window, value, value.Capacity) > 0
? value.ToString() : String.Empty;
}
public static string WindowTitle(IntPtr window)
{
StringBuilder value = new StringBuilder(1024);
return window != IntPtr.Zero &&
GetWindowText(window, value, value.Capacity) > 0
? value.ToString() : String.Empty;
}
public static IntPtr[] VisibleTopLevelWindows()
{
List<IntPtr> windows = new List<IntPtr>();
EnumWindowsCallback callback = delegate(IntPtr window, IntPtr parameter)
{
if (window != IntPtr.Zero && IsWindowVisible(window))
{
windows.Add(window);
}
return true;
};
if (!EnumWindows(callback, IntPtr.Zero))
{
throw new InvalidOperationException(
"The native top-level window list is unavailable.");
}
return windows.ToArray();
}
public static string ReadPackageFullName(IntPtr process)
{
int length = 0;
int result = GetPackageFullName(process, ref length, null);
if (result != ErrorInsufficientBuffer || length <= 1 || length > 4096)
{
throw new InvalidOperationException("The process package identity is unavailable.");
}
StringBuilder value = new StringBuilder(length);
result = GetPackageFullName(process, ref length, value);
if (result != 0 || value.Length == 0)
{
throw new InvalidOperationException("The process package identity could not be read.");
}
return value.ToString();
}
public static bool HasTcpConnectionForProcessAndPort(int processId, int port)
{
if (processId <= 0 || port < 1 || port > 65535)
{
throw new InvalidOperationException("The TCP ownership query is invalid.");
}
return HasTcp4Connection(processId, port) || HasTcp6Connection(processId, port);
}
public static void SendRowHeaderDragAfterThenHome(
IntPtr window,
int processId,
int forbiddenPort,
double sourceCssX,
double sourceCssY,
double targetCssX,
double targetCssY,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio)
{
if (window == IntPtr.Zero || processId <= 0 || forbiddenPort < 1 ||
forbiddenPort > 65535 || !IsFinitePositive(viewportCssWidth) ||
!IsFinitePositive(viewportCssHeight) || !IsFinitePositive(devicePixelRatio) ||
devicePixelRatio < 0.25 || devicePixelRatio > 5.0)
{
throw new InvalidOperationException("The Windows input coordinate contract is invalid.");
}
AssertKeyReleased(VirtualKeyLeftButton, "The left mouse button was held before input.");
AssertKeyReleased(VirtualKeyHome, "The Home key was held before input.");
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
AcquireForeground(window, processId, forbiddenPort);
ClientGeometry baseline = ReadAndAssertClientGeometry(
window,
viewportCssWidth,
viewportCssHeight,
devicePixelRatio);
SleepWithWindowGuard(
400,
window,
processId,
forbiddenPort,
viewportCssWidth,
viewportCssHeight,
devicePixelRatio,
baseline);
Point source = CssPointToScreen(
baseline,
sourceCssX,
sourceCssY);
Point target = CssPointToScreen(
baseline,
targetCssX,
targetCssY);
if (source.X == target.X && source.Y == target.Y)
{
throw new InvalidOperationException("The Windows drag source and target are identical.");
}
AssertPointOwnedByWindow(window, source);
AssertPointOwnedByWindow(window, target);
bool leftButtonDown = false;
bool homeKeyDown = false;
try
{
SendMouseMove(source.X, source.Y);
SleepWithInputGuard(
200, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
source, false, false);
AssertKeyReleased(
VirtualKeyLeftButton,
"The left mouse button became held before the mouse press.");
AssertKeyReleased(
VirtualKeyHome,
"The Home key became held before the mouse press.");
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithInputGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
source, true, false);
Point threshold = new Point {
X = source.X + Math.Max(8, (int)Math.Round(12.0 * baseline.CssScaleX)),
Y = source.Y
};
AssertPointOwnedByWindow(window, threshold);
SendMousePath(
source, threshold, 6, 35, window, processId, forbiddenPort,
viewportCssWidth, viewportCssHeight, devicePixelRatio, baseline,
source, target);
SendMousePath(
threshold, target, 24, 35, window, processId, forbiddenPort,
viewportCssWidth, viewportCssHeight, devicePixelRatio, baseline,
source, target);
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
SleepWithInputGuard(
750, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
target, false, false);
AssertKeyReleased(
VirtualKeyHome,
"The Home key became held before the keyboard input.");
SendKeyboard(VirtualKeyHome, false);
homeKeyDown = true;
SleepWithInputGuard(
35, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
target, false, true);
SendKeyboard(VirtualKeyHome, true);
homeKeyDown = false;
SleepWithInputGuard(
300, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
target, false, false);
}
finally
{
Exception cleanupFailure = null;
if (homeKeyDown)
{
try
{
SendKeyboard(VirtualKeyHome, true);
homeKeyDown = false;
}
catch (Exception error)
{
cleanupFailure = error;
}
}
if (leftButtonDown)
{
try
{
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
}
catch (Exception error)
{
if (cleanupFailure == null)
{
cleanupFailure = error;
}
}
}
try
{
AssertKeyReleased(
VirtualKeyLeftButton,
"The left mouse button remained held after input cleanup.");
}
catch (Exception error)
{
if (cleanupFailure == null)
{
cleanupFailure = error;
}
}
try
{
AssertKeyReleased(
VirtualKeyHome,
"The Home key remained held after input cleanup.");
}
catch (Exception error)
{
if (cleanupFailure == null)
{
cleanupFailure = error;
}
}
if (cleanupFailure != null)
{
throw new InvalidOperationException(
"Injected input cleanup could not prove that all keys were released.",
cleanupFailure);
}
}
}
public static void SendShiftClickRangeThenDeleteAndRestore(
IntPtr window,
int processId,
int forbiddenPort,
double rangeStartCssX,
double rangeStartCssY,
double rangeEndCssX,
double rangeEndCssY,
double restoreCssX,
double restoreCssY,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio)
{
if (window == IntPtr.Zero || processId <= 0 || forbiddenPort < 1 ||
forbiddenPort > 65535 || !IsFinitePositive(viewportCssWidth) ||
!IsFinitePositive(viewportCssHeight) || !IsFinitePositive(devicePixelRatio) ||
devicePixelRatio < 0.25 || devicePixelRatio > 5.0)
{
throw new InvalidOperationException("The Windows Shift-range input contract is invalid.");
}
AssertKeyReleased(VirtualKeyLeftButton, "The left mouse button was held before Shift selection.");
AssertKeyReleased(VirtualKeyShift, "Shift was held before Shift selection.");
AssertKeyReleased(VirtualKeyDelete, "Delete was held before Shift selection.");
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
AcquireForeground(window, processId, forbiddenPort);
ClientGeometry baseline = ReadAndAssertClientGeometry(
window, viewportCssWidth, viewportCssHeight, devicePixelRatio);
SleepWithWindowGuard(
500, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
Point rangeStart = CssPointToScreen(
baseline, rangeStartCssX, rangeStartCssY);
Point rangeEnd = CssPointToScreen(
baseline, rangeEndCssX, rangeEndCssY);
Point restore = CssPointToScreen(
baseline, restoreCssX, restoreCssY);
if ((rangeStart.X == rangeEnd.X && rangeStart.Y == rangeEnd.Y) ||
(rangeEnd.X == restore.X && rangeEnd.Y == restore.Y))
{
throw new InvalidOperationException("The Windows Shift-range points are not distinct.");
}
AssertPointOwnedByWindow(window, rangeStart);
AssertPointOwnedByWindow(window, rangeEnd);
AssertPointOwnedByWindow(window, restore);
bool leftButtonDown = false;
bool shiftDown = false;
bool deleteDown = false;
try
{
SendMouseMove(rangeStart.X, rangeStart.Y);
SleepWithWindowGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(rangeStart);
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithWindowGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(rangeStart);
AssertKeyState(
VirtualKeyLeftButton,
true,
"The left mouse button changed during the range-start click.");
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
SleepWithWindowGuard(
300, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
SendKeyboard(VirtualKeyShift, false);
shiftDown = true;
AssertKeyState(VirtualKeyShift, true, "Shift did not remain held for range selection.");
SendMouseMove(rangeEnd.X, rangeEnd.Y);
SleepWithWindowGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(rangeEnd);
AssertKeyState(VirtualKeyShift, true, "Shift changed before the range click.");
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithWindowGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(rangeEnd);
AssertKeyState(VirtualKeyShift, true, "Shift changed during the range click.");
AssertKeyState(
VirtualKeyLeftButton,
true,
"The left mouse button changed during the range-end click.");
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
SendKeyboard(VirtualKeyShift, true);
shiftDown = false;
SleepWithWindowGuard(
300, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
SendKeyboard(VirtualKeyDelete, false);
deleteDown = true;
SleepWithWindowGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertKeyState(VirtualKeyDelete, true, "Delete changed during the delete key press.");
SendKeyboard(VirtualKeyDelete, true);
deleteDown = false;
SleepWithWindowGuard(
650, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertPointOwnedByWindow(window, restore);
SendMouseMove(restore.X, restore.Y);
SleepWithWindowGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(restore);
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithWindowGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(restore);
AssertKeyState(
VirtualKeyLeftButton,
true,
"The left mouse button changed during restored selection.");
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
SleepWithWindowGuard(
300, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
}
finally
{
if (deleteDown) SendKeyboard(VirtualKeyDelete, true);
if (shiftDown) SendKeyboard(VirtualKeyShift, true);
if (leftButtonDown) SendMouseButton(MouseLeftUp);
AssertKeyReleased(VirtualKeyDelete, "Delete remained held after Shift-range cleanup.");
AssertKeyReleased(VirtualKeyShift, "Shift remained held after Shift-range cleanup.");
AssertKeyReleased(
VirtualKeyLeftButton,
"The left mouse button remained held after Shift-range cleanup.");
}
}
public static void SendApplicationClick(
IntPtr window,
int processId,
int forbiddenPort,
double cssX,
double cssY,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
bool allowForegroundTransitionAfterClick)
{
if (window == IntPtr.Zero || processId <= 0 || forbiddenPort < 1 ||
forbiddenPort > 65535 || !IsFinitePositive(viewportCssWidth) ||
!IsFinitePositive(viewportCssHeight) || !IsFinitePositive(devicePixelRatio) ||
devicePixelRatio < 0.25 || devicePixelRatio > 5.0)
{
throw new InvalidOperationException("The Windows click coordinate contract is invalid.");
}
AssertKeyReleased(VirtualKeyLeftButton, "The left mouse button was held before input.");
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
AcquireForeground(window, processId, forbiddenPort);
ClientGeometry baseline = ReadAndAssertClientGeometry(
window, viewportCssWidth, viewportCssHeight, devicePixelRatio);
SleepWithWindowGuard(
150, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
Point point = CssPointToScreen(baseline, cssX, cssY);
AssertPointOwnedByWindow(window, point);
bool leftButtonDown = false;
try
{
SendMouseMove(point.X, point.Y);
SleepWithInputGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, point, point,
point, false, false);
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithInputGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, point, point,
point, true, false);
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
if (allowForegroundTransitionAfterClick)
{
Thread.Sleep(45);
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
}
else
{
SleepWithInputGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, point, point,
point, false, false);
}
}
finally
{
if (leftButtonDown)
{
SendMouseButton(MouseLeftUp);
}
AssertKeyReleased(
VirtualKeyLeftButton,
"The left mouse button remained held after click cleanup.");
}
}
public static void SendApplicationDoubleClick(
IntPtr window,
int processId,
int forbiddenPort,
double cssX,
double cssY,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
int interClickDelayMilliseconds)
{
if (window == IntPtr.Zero || processId <= 0 || forbiddenPort < 1 ||
forbiddenPort > 65535 || !IsFinitePositive(viewportCssWidth) ||
!IsFinitePositive(viewportCssHeight) || !IsFinitePositive(devicePixelRatio) ||
devicePixelRatio < 0.25 || devicePixelRatio > 5.0 ||
interClickDelayMilliseconds < 50 || interClickDelayMilliseconds > 250)
{
throw new InvalidOperationException("The Windows double-click contract is invalid.");
}
AssertKeyReleased(VirtualKeyLeftButton, "The left mouse button was held before double-click input.");
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
AcquireForeground(window, processId, forbiddenPort);
ClientGeometry baseline = ReadAndAssertClientGeometry(
window, viewportCssWidth, viewportCssHeight, devicePixelRatio);
SleepWithWindowGuard(
150, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
Point point = CssPointToScreen(baseline, cssX, cssY);
AssertPointOwnedByWindow(window, point);
bool leftButtonDown = false;
try
{
SendMouseMove(point.X, point.Y);
SleepWithWindowGuard(
100, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(point);
for (int click = 0; click < 2; click++)
{
SendMouseButton(MouseLeftDown);
leftButtonDown = true;
SleepWithWindowGuard(
45, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
AssertCursorAt(point);
AssertKeyState(
VirtualKeyLeftButton,
true,
"The left mouse button changed during double-click input.");
SendMouseButton(MouseLeftUp);
leftButtonDown = false;
if (click == 0)
{
SleepWithWindowGuard(
interClickDelayMilliseconds,
window,
processId,
forbiddenPort,
viewportCssWidth,
viewportCssHeight,
devicePixelRatio,
baseline);
AssertCursorAt(point);
}
}
SleepWithWindowGuard(
300, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline);
}
finally
{
if (leftButtonDown) SendMouseButton(MouseLeftUp);
AssertKeyReleased(
VirtualKeyLeftButton,
"The left mouse button remained held after double-click cleanup.");
}
}
public static IntPtr ReadForegroundWindow()
{
return GetForegroundWindow();
}
public static void AcquireOwnedForeground(
IntPtr window,
IntPtr expectedOwner,
int processId,
int forbiddenPort)
{
if (!WindowVisible(window) || !WindowEnabled(window) ||
WindowProcessId(window) != processId ||
!OwnerChainContains(window, expectedOwner))
{
throw new InvalidOperationException(
"The owned background picker identity changed before foreground acquisition.");
}
AcquireForeground(window, processId, forbiddenPort);
if (RootWindow(GetForegroundWindow()) != window)
{
throw new InvalidOperationException(
"The exact owned background picker does not own foreground input.");
}
}
public static void SendBackgroundFunctionKey(
IntPtr window,
int processId,
int forbiddenPort,
int virtualKey,
bool allowForegroundTransitionAfterKey)
{
ushort key = checked((ushort)virtualKey);
if (key != VirtualKeyF2 && key != VirtualKeyF3)
{
throw new InvalidOperationException(
"Only the closed F2/F3 background shortcut set is allowed.");
}
AssertKeyReleased(key, "The background function key was held before input.");
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
AcquireForeground(window, processId, forbiddenPort);
bool keyDown = false;
try
{
SendKeyboard(key, false);
keyDown = true;
Thread.Sleep(45);
SendKeyboard(key, true);
keyDown = false;
if (allowForegroundTransitionAfterKey)
{
Thread.Sleep(45);
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
}
else
{
Thread.Sleep(150);
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
if (GetForegroundWindow() != window)
{
throw new InvalidOperationException(
"The application lost foreground during the F3 input.");
}
}
}
finally
{
if (keyDown) SendKeyboard(key, true);
AssertKeyReleased(key, "The background function key remained held after input.");
}
}
public static void SendEscapeToForegroundWindow(IntPtr expectedWindow)
{
IntPtr foreground = GetForegroundWindow();
if (expectedWindow == IntPtr.Zero || foreground == IntPtr.Zero ||
GetAncestor(foreground, GetAncestorRoot) != expectedWindow)
{
throw new InvalidOperationException(
"The exact folder picker does not own foreground keyboard input.");
}
AssertKeyReleased(VirtualKeyEscape, "Escape was held before picker cancellation.");
bool escapeDown = false;
try
{
SendKeyboard(VirtualKeyEscape, false);
escapeDown = true;
Thread.Sleep(45);
// Escape cancellation may return foreground to the owner while
// the picker is still being destroyed. Key-up only releases the
// injected key; the caller separately proves that this exact
// picker closes and never sends another Escape.
SendKeyboard(VirtualKeyEscape, true);
escapeDown = false;
}
finally
{
if (escapeDown)
{
SendKeyboard(VirtualKeyEscape, true);
}
AssertKeyReleased(VirtualKeyEscape, "Escape remained held after picker cancellation.");
}
}
private static void AcquireForeground(IntPtr window, int processId, int forbiddenPort)
{
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
if (GetForegroundWindow() == window)
{
return;
}
IntPtr foregroundWindow = GetForegroundWindow();
uint ignored;
uint foregroundThread = foregroundWindow == IntPtr.Zero
? 0
: GetWindowThreadProcessId(foregroundWindow, out ignored);
uint targetThread = GetWindowThreadProcessId(window, out ignored);
uint currentThread = GetCurrentThreadId();
if (targetThread == 0 || currentThread == 0)
{
throw new InvalidOperationException("The foreground input threads are unavailable.");
}
bool attachedForeground = false;
bool attachedTarget = false;
try
{
if (foregroundThread != 0 && foregroundThread != currentThread &&
foregroundThread != targetThread)
{
if (!AttachThreadInput(currentThread, foregroundThread, true))
{
throw new InvalidOperationException("The foreground input queue could not be attached.");
}
attachedForeground = true;
}
if (targetThread != currentThread)
{
if (!AttachThreadInput(currentThread, targetThread, true))
{
throw new InvalidOperationException("The package input queue could not be attached.");
}
attachedTarget = true;
}
if (IsIconic(window))
{
throw new InvalidOperationException("The package window is minimized; geometry is no longer stable.");
}
BringWindowToTop(window);
SetActiveWindow(window);
SetForegroundWindow(window);
}
finally
{
if (attachedTarget)
{
AttachThreadInput(currentThread, targetThread, false);
}
if (attachedForeground)
{
AttachThreadInput(currentThread, foregroundThread, false);
}
}
DateTime deadline = DateTime.UtcNow.AddSeconds(3);
while (DateTime.UtcNow < deadline && GetForegroundWindow() != window)
{
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
Thread.Sleep(10);
AssertNoForbiddenTcp(processId, forbiddenPort);
}
if (GetForegroundWindow() != window)
{
throw new InvalidOperationException("The exact package window could not own foreground input.");
}
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
}
private static void AssertWindowOwnedByProcess(IntPtr window, int processId)
{
uint actualProcessId;
uint threadId = GetWindowThreadProcessId(window, out actualProcessId);
if (threadId == 0 || actualProcessId != (uint)processId)
{
throw new InvalidOperationException("The input window is not owned by the exact application process.");
}
}
private static void AssertNoForbiddenTcp(int processId, int forbiddenPort)
{
if (HasTcpConnectionForProcessAndPort(processId, forbiddenPort))
{
throw new InvalidOperationException("The DryRun application owns a forbidden TCP connection.");
}
}
private static ClientGeometry ReadAndAssertClientGeometry(
IntPtr window,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio)
{
Rect client;
if (!GetClientRect(window, out client) || client.Left != 0 || client.Top != 0 ||
client.Right <= 0 || client.Bottom <= 0)
{
throw new InvalidOperationException("The package client rectangle is invalid.");
}
int deviceWidth = (int)Math.Round(viewportCssWidth * devicePixelRatio);
int deviceHeight = (int)Math.Round(viewportCssHeight * devicePixelRatio);
int logicalWidth = (int)Math.Round(viewportCssWidth);
int logicalHeight = (int)Math.Round(viewportCssHeight);
bool deviceMatches = deviceWidth > 0 && deviceHeight > 0 &&
Math.Abs(client.Right - deviceWidth) <= ClientPixelTolerance &&
Math.Abs(client.Bottom - deviceHeight) <= ClientPixelTolerance;
bool logicalMatches = logicalWidth > 0 && logicalHeight > 0 &&
Math.Abs(client.Right - logicalWidth) <= ClientPixelTolerance &&
Math.Abs(client.Bottom - logicalHeight) <= ClientPixelTolerance;
if (!deviceMatches && !logicalMatches)
{
throw new InvalidOperationException(
"The browser viewport does not match the package client rectangle.");
}
double cssScaleX = client.Right / viewportCssWidth;
double cssScaleY = client.Bottom / viewportCssHeight;
if (!IsFinitePositive(cssScaleX) || !IsFinitePositive(cssScaleY) ||
Math.Abs(cssScaleX - cssScaleY) > 0.02)
{
throw new InvalidOperationException(
"The package client CSS coordinate scale is invalid.");
}
Point origin = new Point { X = 0, Y = 0 };
if (!ClientToScreen(window, ref origin))
{
throw new InvalidOperationException("The package client origin is unavailable.");
}
return new ClientGeometry {
ScreenLeft = origin.X,
ScreenTop = origin.Y,
Width = client.Right,
Height = client.Bottom,
CssScaleX = cssScaleX,
CssScaleY = cssScaleY
};
}
private static void AssertStableClientGeometry(
IntPtr window,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline)
{
ClientGeometry current = ReadAndAssertClientGeometry(
window,
viewportCssWidth,
viewportCssHeight,
devicePixelRatio);
if (current.ScreenLeft != baseline.ScreenLeft ||
current.ScreenTop != baseline.ScreenTop ||
current.Width != baseline.Width || current.Height != baseline.Height)
{
throw new InvalidOperationException("The package client geometry changed during input.");
}
}
private static void AssertWindowGuard(
IntPtr window,
int processId,
int forbiddenPort,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline)
{
AssertWindowOwnedByProcess(window, processId);
AssertNoForbiddenTcp(processId, forbiddenPort);
if (GetForegroundWindow() != window || IsIconic(window))
{
throw new InvalidOperationException("The exact package window lost visible foreground input.");
}
AssertStableClientGeometry(
window,
viewportCssWidth,
viewportCssHeight,
devicePixelRatio,
baseline);
}
private static void AssertInputGuard(
IntPtr window,
int processId,
int forbiddenPort,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline,
Point source,
Point target,
Point expectedCursor,
bool expectLeftButtonDown,
bool expectHomeDown)
{
AssertWindowGuard(
window, processId, forbiddenPort, viewportCssWidth, viewportCssHeight,
devicePixelRatio, baseline);
AssertPointOwnedByWindow(window, source);
AssertPointOwnedByWindow(window, target);
AssertPointOwnedByWindow(window, expectedCursor);
AssertCursorAt(expectedCursor);
AssertKeyState(
VirtualKeyLeftButton,
expectLeftButtonDown,
"The left mouse button state changed during input.");
AssertKeyState(
VirtualKeyHome,
expectHomeDown,
"The Home key state changed during input.");
}
private static void SleepWithWindowGuard(
int milliseconds,
IntPtr window,
int processId,
int forbiddenPort,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline)
{
int remaining = milliseconds;
while (remaining > 0)
{
AssertWindowGuard(
window, processId, forbiddenPort, viewportCssWidth, viewportCssHeight,
devicePixelRatio, baseline);
int slice = Math.Min(10, remaining);
Thread.Sleep(slice);
remaining -= slice;
}
AssertWindowGuard(
window, processId, forbiddenPort, viewportCssWidth, viewportCssHeight,
devicePixelRatio, baseline);
}
private static void SleepWithInputGuard(
int milliseconds,
IntPtr window,
int processId,
int forbiddenPort,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline,
Point source,
Point target,
Point expectedCursor,
bool expectLeftButtonDown,
bool expectHomeDown)
{
int remaining = milliseconds;
while (remaining > 0)
{
AssertInputGuard(
window, processId, forbiddenPort, viewportCssWidth, viewportCssHeight,
devicePixelRatio, baseline, source, target, expectedCursor,
expectLeftButtonDown, expectHomeDown);
int slice = Math.Min(10, remaining);
Thread.Sleep(slice);
remaining -= slice;
}
AssertInputGuard(
window, processId, forbiddenPort, viewportCssWidth, viewportCssHeight,
devicePixelRatio, baseline, source, target, expectedCursor,
expectLeftButtonDown, expectHomeDown);
}
private static void AssertPointOwnedByWindow(IntPtr window, Point point)
{
IntPtr hit = WindowFromPoint(point);
IntPtr root = hit == IntPtr.Zero ? IntPtr.Zero : GetAncestor(hit, GetAncestorRoot);
if (root != window)
{
throw new InvalidOperationException("A Windows input point is obscured by another top-level window.");
}
}
private static void AssertCursorAt(Point expected)
{
Point actual;
if (!GetCursorPos(out actual) || Math.Abs(actual.X - expected.X) > 2 ||
Math.Abs(actual.Y - expected.Y) > 2)
{
throw new InvalidOperationException("The physical cursor diverged during the one-shot input sequence.");
}
}
private static bool HasTcp4Connection(int processId, int port)
{
return ScanTcpTable<MibTcpRowOwnerPid>(
AddressFamilyInterNetwork,
delegate(MibTcpRowOwnerPid row)
{
return row.OwningProcessId == (uint)processId &&
(DecodePort(row.LocalPort) == port || DecodePort(row.RemotePort) == port);
});
}
private static bool HasTcp6Connection(int processId, int port)
{
return ScanTcpTable<MibTcp6RowOwnerPid>(
AddressFamilyInterNetworkV6,
delegate(MibTcp6RowOwnerPid row)
{
return row.OwningProcessId == (uint)processId &&
(DecodePort(row.LocalPort) == port || DecodePort(row.RemotePort) == port);
});
}
private static bool ScanTcpTable<TRow>(int addressFamily, Func<TRow, bool> predicate)
where TRow : struct
{
int size = 0;
uint result = GetExtendedTcpTable(
IntPtr.Zero,
ref size,
false,
addressFamily,
TcpTableOwnerPidAll,
0);
if (result != ErrorInsufficientBuffer || size < sizeof(int) || size > 64 * 1024 * 1024)
{
throw new InvalidOperationException("The native TCP table size is unavailable.");
}
IntPtr table = Marshal.AllocHGlobal(size);
try
{
result = GetExtendedTcpTable(
table,
ref size,
false,
addressFamily,
TcpTableOwnerPidAll,
0);
if (result != 0)
{
throw new InvalidOperationException("The native TCP table could not be read.");
}
int count = Marshal.ReadInt32(table);
int rowSize = Marshal.SizeOf(typeof(TRow));
if (count < 0 || count > 100000 ||
(long)sizeof(int) + (long)count * rowSize > size)
{
throw new InvalidOperationException("The native TCP table layout is invalid.");
}
IntPtr rowPointer = IntPtr.Add(table, sizeof(int));
for (int index = 0; index < count; index++)
{
TRow row = (TRow)Marshal.PtrToStructure(rowPointer, typeof(TRow));
if (predicate(row))
{
return true;
}
rowPointer = IntPtr.Add(rowPointer, rowSize);
}
return false;
}
finally
{
Marshal.FreeHGlobal(table);
}
}
private static int DecodePort(uint value)
{
return (int)(((value & 0x000000ffU) << 8) | ((value & 0x0000ff00U) >> 8));
}
private static bool IsFinitePositive(double value)
{
return !Double.IsNaN(value) && !Double.IsInfinity(value) && value > 0;
}
private static Point CssPointToScreen(
ClientGeometry geometry,
double cssX,
double cssY)
{
if (Double.IsNaN(cssX) || Double.IsInfinity(cssX) ||
Double.IsNaN(cssY) || Double.IsInfinity(cssY) || cssX < 0 || cssY < 0)
{
throw new InvalidOperationException("A CSS input point is invalid.");
}
int x = (int)Math.Round(cssX * geometry.CssScaleX);
int y = (int)Math.Round(cssY * geometry.CssScaleY);
if (x < 0 || x >= geometry.Width || y < 0 || y >= geometry.Height)
{
throw new InvalidOperationException("A CSS input point is outside the package client area.");
}
return new Point { X = geometry.ScreenLeft + x, Y = geometry.ScreenTop + y };
}
private static void SendMousePath(
Point from,
Point to,
int steps,
int delayMilliseconds,
IntPtr window,
int processId,
int forbiddenPort,
double viewportCssWidth,
double viewportCssHeight,
double devicePixelRatio,
ClientGeometry baseline,
Point source,
Point target)
{
for (int index = 1; index <= steps; index++)
{
double amount = (double)index / steps;
int x = (int)Math.Round(from.X + (to.X - from.X) * amount);
int y = (int)Math.Round(from.Y + (to.Y - from.Y) * amount);
SendMouseMove(x, y);
Point current = new Point { X = x, Y = y };
SleepWithInputGuard(
delayMilliseconds, window, processId, forbiddenPort, viewportCssWidth,
viewportCssHeight, devicePixelRatio, baseline, source, target,
current, true, false);
}
}
private static void SendMouseMove(int screenX, int screenY)
{
int left = GetSystemMetrics(76);
int top = GetSystemMetrics(77);
int width = GetSystemMetrics(78);
int height = GetSystemMetrics(79);
if (width <= 1 || height <= 1 || screenX < left || screenX >= left + width ||
screenY < top || screenY >= top + height)
{
throw new InvalidOperationException("The Windows input point is outside the virtual desktop.");
}
int absoluteX = (int)Math.Round((screenX - left) * 65535.0 / (width - 1));
int absoluteY = (int)Math.Round((screenY - top) * 65535.0 / (height - 1));
Input input = new Input {
Type = InputMouse,
Union = new InputUnion {
Mouse = new MouseInput {
Dx = absoluteX,
Dy = absoluteY,
Flags = MouseMove | MouseMoveNoCoalesce | MouseVirtualDesk | MouseAbsolute
}
}
};
SendOne(input);
}
private static void SendMouseButton(uint flags)
{
Input input = new Input {
Type = InputMouse,
Union = new InputUnion { Mouse = new MouseInput { Flags = flags } }
};
SendOne(input);
}
private static void SendKeyboard(ushort virtualKey, bool keyUp)
{
Input input = new Input {
Type = InputKeyboard,
Union = new InputUnion {
Keyboard = new KeyboardInput {
VirtualKey = virtualKey,
Flags = keyUp ? KeyUp : 0
}
}
};
SendOne(input);
}
private static void SendOne(Input input)
{
Input[] inputs = new Input[] { input };
if (SendInput(1, inputs, Marshal.SizeOf(typeof(Input))) != 1)
{
throw new InvalidOperationException("SendInput did not accept the one-shot input event.");
}
}
private static void AssertKeyReleased(int virtualKey, string message)
{
AssertKeyState(virtualKey, false, message);
}
private static void AssertKeyState(int virtualKey, bool expectedDown, string message)
{
bool actualDown = (GetAsyncKeyState(virtualKey) & 0x8000) != 0;
if (actualDown != expectedDown)
{
throw new InvalidOperationException(message);
}
}
}
'@
$PackageName = 'Wickedness.MBNStockWebView.LegacyParity'
$PackageFamilyName = 'Wickedness.MBNStockWebView.LegacyParity_qbv3jkvsn3aj0'
$PackageFullName = 'Wickedness.MBNStockWebView.LegacyParity_0.1.0.0_x64__qbv3jkvsn3aj0'
$PackagePublisher = 'CN=Comtrophy'
$PackageVersion = '0.1.0.0'
$ApplicationId = 'LegacyParityApp'
$ExecutableName = 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe'
$Utf8 = [Text.UTF8Encoding]::new($false)
$StrictUtf8 = [Text.UTF8Encoding]::new($false, $true)
$ExpectedWindowTitle = $Utf8.GetString([Convert]::FromBase64String(
'Vi1TdG9jayDspp3qtozsoJXrs7TshqHstpzsi5zsiqTthZwgZm9yIOunpOydvOqyveygnFRWICgyNi4wMy4yNik='))
$ExpectedDialogTitle = $Utf8.GetString([Convert]::FromBase64String(
'66ek7J286rK97KCcVFY='))
$ExpectedDialogMessage = $Utf8.GetString([Convert]::FromBase64String(
'7ZSE66Gc6re4656o7J2EIOyiheujjO2VmOqyoOyKteuLiOq5jD8='))
$ExpectedPrimaryButton = $Utf8.GetString([Convert]::FromBase64String('7JiI'))
$ExpectedCloseButton = $Utf8.GetString([Convert]::FromBase64String('7JWE64uI7JqU'))
$LiveAuthorizationVariable = 'MBN_STOCK_PLAYOUT_AUTHORIZE_LIVE_OUTPUT'
$K3dNativeHashVariable = 'MBN_STOCK_K3D_NATIVE_SHA256'
$K3dInteropHashVariable = 'MBN_STOCK_K3D_INTEROP_SHA256'
$WebViewArgumentsVariable = 'WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS'
$RepositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$HarnessPath = Join-Path $PSScriptRoot 'Test-LegacyPackageInput.mjs'
$ExpectedInstallRelativePath = switch ($BuildFlavor) {
'Release' {
'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Release\net8.0-windows10.0.19041.0\win-x64'
break
}
'DebugAppX' {
'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug\net8.0-windows10.0.19041.0\win-x64\AppX'
break
}
default { Throw-SafeFailure 'The package build flavor is not closed.' }
}
$PackageModeLabel = "Development $BuildFlavor x64"
$ExpectedInstallLocation = Join-Path $RepositoryRoot $ExpectedInstallRelativePath
$ExpectedInstallLocation = [IO.Path]::GetFullPath($ExpectedInstallLocation)
$ConfigurationPath = Join-Path `
([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) `
'MBN_STOCK_WEBVIEW\Config\playout.local.json'
$OperatorSettingsPath = Join-Path `
([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) `
'MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json'
$BackgroundPickerTitle = $Utf8.GetString([Convert]::FromBase64String(
'64uk7J2MIOyepeuptCDrsLDqsr0g7ISg7YOd'))
$BackgroundDirectoryName = $Utf8.GetString([Convert]::FromBase64String('67Cw6rK9'))
$BackgroundNoneLabel = $Utf8.GetString([Convert]::FromBase64String(
'67Cw6rK97JeG7J2M'))
$LegacyDefaultBackgroundFileName = $Utf8.GetString(
[Convert]::FromBase64String('6riw67O4LnZydg=='))
$ExpectedSavedFixtureGraphicType = $Utf8.GetString(
[Convert]::FromBase64String('MeyXtO2MkOq4sOuzuA=='))
$ExpectedSavedFixtureCurrentSubtype = $Utf8.GetString(
[Convert]::FromBase64String('7ZiE7J6s6rCA'))
$ExpectedSavedFixtureAfterHoursSubtype = $Utf8.GetString(
[Convert]::FromBase64String('7Iuc6rCE7Jm464uo7J286rCA'))
$script:ApplicationProcess = $null
$script:ApplicationWasLaunched = $false
$script:CloseMainWindowCalls = 0
$script:PrimaryInvokeCalls = 0
$script:SingleInstanceActivationCalls = 0
$script:WindowsInputRequestCalls = 0
$script:WindowsInputAcknowledgementCalls = 0
$script:BackgroundShortcutRoot = $null
$script:BackgroundShortcutRootCreated = $false
$script:Phase = 'preflight'
function Throw-SafeFailure([string]$Message) {
throw [InvalidOperationException]::new($Message)
}
function Test-PathEqual([string]$Left, [string]$Right) {
return [string]::Equals(
[IO.Path]::GetFullPath($Left).TrimEnd('\'),
[IO.Path]::GetFullPath($Right).TrimEnd('\'),
[StringComparison]::OrdinalIgnoreCase)
}
function Assert-ExactProcessPackageIdentity([Diagnostics.Process]$Process) {
try {
$actualPackageFullName = [LegacyPackageInputNative]::ReadPackageFullName($Process.Handle)
}
catch {
Throw-SafeFailure 'The running application package identity could not be inspected.'
}
if ([string]$actualPackageFullName -cne $PackageFullName) {
Throw-SafeFailure 'The running application does not have the exact registered package identity.'
}
}
function Resolve-EvidencePath([string]$Value, [string]$DefaultFileName) {
if ([string]::IsNullOrWhiteSpace($Value)) {
return [IO.Path]::GetFullPath((Join-Path `
(Join-Path $RepositoryRoot 'artifacts\package-input-smoke') `
$DefaultFileName))
}
if ([IO.Path]::IsPathRooted($Value)) {
return [IO.Path]::GetFullPath($Value)
}
return [IO.Path]::GetFullPath((Join-Path $RepositoryRoot $Value))
}
function Assert-OutputPathAvailable([string]$Path, [string]$Label) {
if ([IO.File]::Exists($Path) -or [IO.Directory]::Exists($Path)) {
Throw-SafeFailure "$Label already exists; evidence files are never overwritten."
}
$parent = [IO.Path]::GetDirectoryName($Path)
if ([string]::IsNullOrWhiteSpace($parent)) {
Throw-SafeFailure "$Label has no parent directory."
}
if (-not [IO.Directory]::Exists($parent)) {
[void][IO.Directory]::CreateDirectory($parent)
}
$parentItem = Get-Item -LiteralPath $parent -Force
if (($parentItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
Throw-SafeFailure "$Label parent must not be a reparse point."
}
}
function Resolve-NodeExecutable([string]$RequestedPath) {
if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) {
$resolved = [IO.Path]::GetFullPath($RequestedPath)
if (-not [IO.File]::Exists($resolved) -or
[IO.Path]::GetFileName($resolved) -cne 'node.exe') {
Throw-SafeFailure 'The requested Node executable does not exist.'
}
return $resolved
}
$bundled = Join-Path $env:USERPROFILE `
'.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
if ([IO.File]::Exists($bundled)) {
return [IO.Path]::GetFullPath($bundled)
}
$command = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -eq $command) {
Throw-SafeFailure 'Node was not found. Supply -NodePath with an absolute node.exe path.'
}
return [IO.Path]::GetFullPath($command.Source)
}
function Assert-LocalConfigurationIsDryRun {
if (-not [IO.File]::Exists($ConfigurationPath)) {
return
}
$item = Get-Item -LiteralPath $ConfigurationPath -Force
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
$item.Length -gt 1MB) {
Throw-SafeFailure 'The local playout configuration cannot be safely inspected.'
}
try {
$raw = [IO.File]::ReadAllText($ConfigurationPath, [Text.Encoding]::UTF8)
$configuration = $raw | ConvertFrom-Json -ErrorAction Stop
}
catch {
# Do not include parser diagnostics: they can echo configuration content.
Throw-SafeFailure 'The local playout configuration could not be parsed safely.'
}
if ($null -eq $configuration) {
Throw-SafeFailure 'The local playout configuration is empty.'
}
$modeProperties = @($configuration.PSObject.Properties | Where-Object {
[string]::Equals($_.Name, 'Mode', [StringComparison]::OrdinalIgnoreCase)
})
$rawModePropertyCount = [regex]::Matches(
$raw,
'(?i)"mode"\s*:').Count
if ($modeProperties.Count -eq 0 -and $rawModePropertyCount -eq 0) {
# PlayoutOptions.Mode defaults to DryRun when the property is absent.
return
}
if ($modeProperties.Count -ne 1 -or $rawModePropertyCount -ne 1) {
Throw-SafeFailure 'The local playout mode is not uniquely defined.'
}
$mode = $modeProperties[0].Value
$isDryRun = ($mode -is [string] -and
[string]::Equals($mode, 'DryRun', [StringComparison]::OrdinalIgnoreCase)) -or
($mode -isnot [string] -and [string]$mode -ceq '1')
if (-not $isDryRun) {
Throw-SafeFailure 'The local playout configuration does not resolve to DryRun.'
}
}
function Initialize-BackgroundShortcutFixture([string]$InstallLocation) {
if ($Profile -cne 'DryRunPlayout') {
return
}
if ([IO.File]::Exists($OperatorSettingsPath) -or
[IO.Directory]::Exists($OperatorSettingsPath)) {
Throw-SafeFailure (
'The F2/F3 physical test requires the default package folder layout; ' +
'runtime folder overrides are present.')
}
if ([IO.File]::Exists($ConfigurationPath)) {
try {
$raw = [IO.File]::ReadAllText($ConfigurationPath, $StrictUtf8)
$configuration = $raw | ConvertFrom-Json -ErrorAction Stop
}
catch {
Throw-SafeFailure 'The F2/F3 physical test could not inspect its DryRun configuration.'
}
foreach ($propertyName in @('sceneDirectory', 'legacyBackgroundDirectory')) {
$matches = @($configuration.PSObject.Properties | Where-Object {
[string]::Equals(
$_.Name,
$propertyName,
[StringComparison]::OrdinalIgnoreCase)
})
if ($matches.Count -gt 1 -or
($matches.Count -eq 1 -and $null -ne $matches[0].Value)) {
Throw-SafeFailure (
'The F2/F3 physical test requires null scene/background folder overrides.')
}
}
}
$root = [IO.Path]::GetFullPath((Join-Path $InstallLocation $BackgroundDirectoryName))
$expectedRoot = [IO.Path]::GetFullPath((Join-Path `
$ExpectedInstallLocation `
$BackgroundDirectoryName))
if (-not (Test-PathEqual $root $expectedRoot) -or [IO.File]::Exists($root)) {
Throw-SafeFailure 'The exact package-local background test root is unavailable.'
}
if (-not [IO.Directory]::Exists($root)) {
[void][IO.Directory]::CreateDirectory($root)
$script:BackgroundShortcutRootCreated = $true
}
$item = Get-Item -LiteralPath $root -Force
if (-not $item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
Throw-SafeFailure 'The exact package-local background test root is unsafe.'
}
$defaultPath = Join-Path $root $LegacyDefaultBackgroundFileName
if ([IO.File]::Exists($defaultPath) -or [IO.Directory]::Exists($defaultPath)) {
Throw-SafeFailure (
'The F3 fail-closed test requires the external default background asset to be absent.')
}
$script:BackgroundShortcutRoot = $root
}
function Remove-BackgroundShortcutFixture {
if (-not $script:BackgroundShortcutRootCreated -or
[string]::IsNullOrEmpty([string]$script:BackgroundShortcutRoot)) {
return
}
$root = [IO.Path]::GetFullPath([string]$script:BackgroundShortcutRoot)
$expectedRoot = [IO.Path]::GetFullPath((Join-Path `
$ExpectedInstallLocation `
$BackgroundDirectoryName))
if (-not (Test-PathEqual $root $expectedRoot) -or
-not [IO.Directory]::Exists($root)) {
Throw-SafeFailure 'The temporary background shortcut root identity changed.'
}
$item = Get-Item -LiteralPath $root -Force
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
@(Get-ChildItem -LiteralPath $root -Force).Count -ne 0) {
Throw-SafeFailure 'The temporary background shortcut root is no longer empty and was preserved.'
}
[IO.Directory]::Delete($root, $false)
$script:BackgroundShortcutRootCreated = $false
}
function Assert-NoUnsafeEnvironment {
$forbiddenExactNames = @(
$LiveAuthorizationVariable,
$K3dNativeHashVariable,
$K3dInteropHashVariable
)
$targets = @(
[EnvironmentVariableTarget]::Process,
[EnvironmentVariableTarget]::User,
[EnvironmentVariableTarget]::Machine
)
foreach ($target in $targets) {
try {
$environment = [Environment]::GetEnvironmentVariables($target)
}
catch {
Throw-SafeFailure "The $target environment could not be inspected."
}
foreach ($nameObject in $environment.Keys) {
$name = [string]$nameObject
if ($forbiddenExactNames -contains $name -or
$name -like 'MBN_STOCK_PLAYOUT_*' -or
$name -like 'WEBVIEW2_*' -or
$name -like 'COREWEBVIEW2_*') {
# Report names/scopes only. Never report environment values.
Throw-SafeFailure "Unsafe launch environment variable is present: $target/$name."
}
}
}
}
function Get-StreamSha256Hex([IO.Stream]$Stream) {
$algorithm = [Security.Cryptography.SHA256]::Create()
try {
return [BitConverter]::ToString($algorithm.ComputeHash($Stream)).Replace('-', '')
}
finally {
$algorithm.Dispose()
}
}
function Assert-DebugAppXMatchesLatestPackage([string]$InstallLocation) {
if ($BuildFlavor -cne 'DebugAppX') {
return
}
$projectRoot = Join-Path $RepositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp'
$msix = Join-Path $projectRoot `
'AppPackages\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64_Debug_Test\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64_Debug.msix'
if (-not [IO.File]::Exists($msix)) {
Throw-SafeFailure 'The latest Debug MSIX is missing; the registered AppX cannot be proven current.'
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($msix)
try {
$expectedFiles = @(
'AppxManifest.xml',
'Assets/StartupHero.png',
'MBN_STOCK_WEBVIEW.Core.dll',
'MBN_STOCK_WEBVIEW.Infrastructure.dll',
'MBN_STOCK_WEBVIEW.LegacyApplication.dll',
'MBN_STOCK_WEBVIEW.LegacyBridge.dll',
'MBN_STOCK_WEBVIEW.LegacyParityApp.dll',
'Web/Brand/logo.png',
'Web/app.js',
'Web/index.html',
'Web/playout-ui.js',
'Web/styles.css',
'Web/workspace-navigation.js'
)
foreach ($relativePath in $expectedFiles) {
$entry = $archive.GetEntry($relativePath)
$installed = Join-Path $InstallLocation $relativePath.Replace('/', '\')
if ($null -eq $entry -or -not [IO.File]::Exists($installed)) {
Throw-SafeFailure 'The registered Debug AppX is missing a required current package file.'
}
$entryStream = $entry.Open()
try {
$packageHash = Get-StreamSha256Hex $entryStream
}
finally {
$entryStream.Dispose()
}
$installedHash = (Get-FileHash -LiteralPath $installed -Algorithm SHA256).Hash
if ($installedHash -cne $packageHash) {
Throw-SafeFailure 'The registered Debug AppX is stale relative to the latest Debug MSIX.'
}
}
}
finally {
$archive.Dispose()
}
foreach ($relativeSource in @(
'Web\app.js',
'Web\index.html',
'Web\styles.css',
'Web\workspace-navigation.js')) {
$sourcePath = Join-Path $projectRoot $relativeSource
$installedPath = Join-Path $InstallLocation $relativeSource
if (-not [IO.File]::Exists($sourcePath) -or
-not [IO.File]::Exists($installedPath) -or
(Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash -cne
(Get-FileHash -LiteralPath $installedPath -Algorithm SHA256).Hash) {
Throw-SafeFailure 'The registered Debug AppX does not contain the current Web application source.'
}
}
}
function Get-ExactDevelopmentPackage {
$packages = @(Get-AppxPackage -Name $PackageName -ErrorAction Stop)
if ($packages.Count -ne 1) {
Throw-SafeFailure 'Exactly one current-user LegacyParity development package is required.'
}
$package = $packages[0]
if (-not $package.IsDevelopmentMode -or
[string]$package.SignatureKind -cne 'None' -or
[string]$package.Status -cne 'Ok' -or
[string]$package.Name -cne $PackageName -or
[string]$package.PackageFullName -cne $PackageFullName -or
[string]$package.PackageFamilyName -cne $PackageFamilyName -or
[string]$package.Publisher -cne $PackagePublisher -or
[string]$package.Version -cne $PackageVersion -or
[string]$package.Architecture -cne 'X64' -or
-not (Test-PathEqual ([string]$package.InstallLocation) $ExpectedInstallLocation)) {
Throw-SafeFailure "The registered package is not the exact $BuildFlavor x64 development package."
}
try {
$manifest = Get-AppxPackageManifest -Package $package -ErrorAction Stop
}
catch {
Throw-SafeFailure 'The registered package manifest could not be inspected.'
}
$applications = @($manifest.Package.Applications.Application)
if ($applications.Count -ne 1 -or
[string]$applications[0].Id -cne $ApplicationId -or
[string]$applications[0].Executable -cne $ExecutableName -or
[string]$applications[0].EntryPoint -cne 'Windows.FullTrustApplication') {
Throw-SafeFailure 'The registered package application identity is not exact.'
}
$executable = Join-Path ([string]$package.InstallLocation) $ExecutableName
if (-not [IO.File]::Exists($executable)) {
Throw-SafeFailure "The registered $BuildFlavor application executable is missing."
}
Assert-DebugAppXMatchesLatestPackage ([string]$package.InstallLocation)
return [pscustomobject]@{
Package = $package
Executable = [IO.Path]::GetFullPath($executable)
InstallLocation = [IO.Path]::GetFullPath([string]$package.InstallLocation)
}
}
function Get-LegacyApplicationProcesses {
$live = New-Object 'Collections.Generic.List[Diagnostics.Process]'
foreach ($candidate in @(Get-Process `
-Name ([IO.Path]::GetFileNameWithoutExtension($ExecutableName)) `
-ErrorAction SilentlyContinue)) {
try {
$candidate.Refresh()
if (-not $candidate.HasExited) {
[void]$live.Add($candidate)
}
}
catch {
# A process that disappears during enumeration is not a live instance.
}
}
return @($live)
}
function Assert-NoExistingApplication {
if (@(Get-LegacyApplicationProcesses).Count -ne 0) {
Throw-SafeFailure 'A LegacyParity application process is already running.'
}
}
function Get-TcpSnapshot {
try {
return @(Get-NetTCPConnection -ErrorAction Stop)
}
catch {
Throw-SafeFailure 'The TCP connection table could not be inspected.'
}
}
function Assert-PortIsFree([int]$CandidatePort) {
if ($CandidatePort -eq 30001) {
Throw-SafeFailure 'The WebView debugging port must not be the Tornado port.'
}
$listeners = @(Get-TcpSnapshot | Where-Object {
[string]$_.State -ceq 'Listen' -and
$_.LocalPort -eq $CandidatePort
})
if ($listeners.Count -ne 0) {
Throw-SafeFailure 'The chosen loopback debugging port is already in use.'
}
}
function Assert-NoTornadoConnection([int]$ProcessId) {
try {
$hasConnection = [LegacyPackageInputNative]::HasTcpConnectionForProcessAndPort(
$ProcessId,
30001)
}
catch {
Throw-SafeFailure 'The native TCP ownership table could not be inspected.'
}
if ($hasConnection) {
Throw-SafeFailure 'The DryRun application owns a Tornado port connection.'
}
}
function ConvertTo-QuotedProcessArgument([string]$Value) {
if ($Value.IndexOfAny([char[]]@('"', "`r", "`n")) -ge 0) {
Throw-SafeFailure 'A process argument contains a forbidden character.'
}
return '"' + $Value + '"'
}
function Get-ByteArraySha256([byte[]]$Bytes) {
$algorithm = [Security.Cryptography.SHA256]::Create()
try {
return ([BitConverter]::ToString($algorithm.ComputeHash($Bytes))).Replace('-', '')
}
finally {
$algorithm.Dispose()
}
}
function Assert-ExactJsonPropertyNames(
[object]$Value,
[string[]]$ExpectedNames,
[string]$Label) {
if ($null -eq $Value) {
Throw-SafeFailure "$Label is missing."
}
$actualNames = @($Value.PSObject.Properties | ForEach-Object { [string]$_.Name })
if ($actualNames.Count -ne $ExpectedNames.Count) {
Throw-SafeFailure "$Label has an unexpected schema."
}
foreach ($name in $ExpectedNames) {
if ($actualNames -cnotcontains $name) {
Throw-SafeFailure "$Label has an unexpected schema."
}
}
}
function Test-ExactStringSequence([object[]]$Left, [object[]]$Right) {
$leftValues = @($Left | ForEach-Object { [string]$_ })
$rightValues = @($Right | ForEach-Object { [string]$_ })
if ($leftValues.Count -ne $rightValues.Count) {
return $false
}
for ($index = 0; $index -lt $leftValues.Count; $index++) {
if ($leftValues[$index] -cne $rightValues[$index]) {
return $false
}
}
return $true
}
function Read-SmallJsonEvidenceFile([string]$Path, [string]$Label) {
try {
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
$item.Length -le 0 -or $item.Length -gt 16KB) {
Throw-SafeFailure "$Label is not a small regular file."
}
$bytes = [IO.File]::ReadAllBytes($Path)
if ($bytes.Length -ne $item.Length) {
Throw-SafeFailure "$Label changed while it was read."
}
$value = $StrictUtf8.GetString($bytes) | ConvertFrom-Json -ErrorAction Stop
return [pscustomobject]@{
Value = $value
Bytes = $bytes
Sha256 = Get-ByteArraySha256 $bytes
}
}
catch [InvalidOperationException] {
throw
}
catch {
Throw-SafeFailure "$Label could not be validated as UTF-8 JSON."
}
}
function Wait-ReadSmallJsonEvidenceFile(
[string]$Path,
[string]$Label,
[datetime]$Deadline) {
$previousLength = -1L
$stableObservations = 0
while ([datetime]::UtcNow -lt $Deadline) {
if ([IO.File]::Exists($Path)) {
try {
$item = Get-Item -LiteralPath $Path -Force
$isCandidate = -not $item.PSIsContainer -and
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0 -and
$item.Length -gt 0 -and $item.Length -le 16KB
if ($isCandidate -and $item.Length -eq $previousLength) {
$stableObservations++
}
else {
$stableObservations = 0
}
$previousLength = $item.Length
if ($isCandidate -and $stableObservations -ge 1) {
return Read-SmallJsonEvidenceFile $Path $Label
}
}
catch [InvalidOperationException] {
throw
}
catch {
$stableObservations = 0
$previousLength = -1L
}
}
Start-Sleep -Milliseconds 25
}
Throw-SafeFailure "$Label was not published as one stable small regular file."
}
function ConvertTo-FiniteNumber([object]$Value, [string]$Label) {
$isJsonNumber = $Value -is [byte] -or $Value -is [sbyte] -or
$Value -is [int16] -or $Value -is [uint16] -or
$Value -is [int32] -or $Value -is [uint32] -or
$Value -is [int64] -or $Value -is [uint64] -or
$Value -is [single] -or $Value -is [double] -or $Value -is [decimal]
if (-not $isJsonNumber) {
Throw-SafeFailure "$Label is not a JSON number."
}
try {
$number = [Convert]::ToDouble($Value, [Globalization.CultureInfo]::InvariantCulture)
}
catch {
Throw-SafeFailure "$Label is not numeric."
}
if ([double]::IsNaN($number) -or [double]::IsInfinity($number)) {
Throw-SafeFailure "$Label is not finite."
}
return $number
}
function Get-WindowsInputExchangePath(
[string]$BasePath,
[ValidateRange(1, 3)]
[int]$ExchangeIndex) {
if ($ExchangeIndex -eq 1) {
return $BasePath
}
$directory = [IO.Path]::GetDirectoryName($BasePath)
$extension = [IO.Path]::GetExtension($BasePath)
$name = [IO.Path]::GetFileNameWithoutExtension($BasePath)
return Join-Path $directory (
$name + '.exchange-' + $ExchangeIndex.ToString('00') + $extension)
}
function Get-ExpectedWindowsInputExchangeCount(
[string]$HarnessProfile,
[string]$HarnessScope) {
if ($HarnessProfile -ceq 'DryRunPlayout' -or
$HarnessProfile -ceq 'dry-run-playout') {
return 3
}
if (($HarnessProfile -ceq 'FullUiDb' -or $HarnessProfile -ceq 'full-ui-db') -and
($HarnessScope -ceq 'PList' -or $HarnessScope -ceq 'plist' -or
$HarnessScope -ceq 'All' -or $HarnessScope -ceq 'all')) {
return 2
}
return 1
}
function Write-WindowsInputAcknowledgement(
[string]$Path,
[int]$ExchangeIndex,
[string]$Token,
[string]$Operation,
[Collections.IDictionary]$InputCounts) {
$acknowledgement = [ordered]@{
schemaVersion = 1
exchangeIndex = $ExchangeIndex
token = $Token
result = 'PASS'
operation = $Operation
foregroundValidated = $true
packageIdentityValidated = $true
inputRetryCount = 0
}
foreach ($key in $InputCounts.Keys) {
$acknowledgement[$key] = $InputCounts[$key]
}
$bytes = $Utf8.GetBytes(($acknowledgement | ConvertTo-Json -Compress) + "`n")
$parent = [IO.Path]::GetDirectoryName($Path)
$temporaryPath = Join-Path $parent (
([IO.Path]::GetFileName($Path)) + '.' + [Guid]::NewGuid().ToString('N') + '.tmp')
try {
$stream = [IO.FileStream]::new(
$temporaryPath,
[IO.FileMode]::CreateNew,
[IO.FileAccess]::Write,
[IO.FileShare]::None)
try {
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
[IO.File]::Move($temporaryPath, $Path)
}
catch {
try {
if ([IO.File]::Exists($temporaryPath)) {
[IO.File]::Delete($temporaryPath)
}
}
catch {
# Cleanup failure cannot authorize another acknowledgement attempt.
}
Throw-SafeFailure 'The one-shot Windows input acknowledgement could not be created.'
}
return [pscustomobject]@{
Bytes = $bytes
Sha256 = Get-ByteArraySha256 $bytes
}
}
function ConvertTo-ExactInputPoint(
[object]$Point,
[double]$ViewportWidth,
[double]$ViewportHeight,
[string]$Label) {
Assert-ExactJsonPropertyNames $Point @('x', 'y') $Label
$x = ConvertTo-FiniteNumber $Point.x "$Label X"
$y = ConvertTo-FiniteNumber $Point.y "$Label Y"
if ($x -lt 0 -or $x -ge $ViewportWidth -or
$y -lt 0 -or $y -ge $ViewportHeight) {
Throw-SafeFailure "$Label is outside the exact package viewport."
}
return [pscustomobject]@{ X = $x; Y = $y }
}
function Invoke-ExactPlaylistWindowsInputRequest(
[object]$Request,
[Diagnostics.Process]$Application) {
Assert-ExactJsonPropertyNames $Request @(
'schemaVersion', 'exchangeIndex', 'token', 'targetUrl', 'operation',
'sourceRowId', 'targetRowId', 'rangeStartRowId', 'rangeEndRowId',
'position', 'source', 'target', 'rangeStart', 'rangeEnd',
'restoreSelection', 'viewport', 'devicePixelRatio', 'beforeOrder',
'reorderedOrder', 'afterOrder', 'expectedRangeSelectedRowIds',
'expectedSelectedRowId', 'expectedActiveRowIdAfterHome',
'expectedFinalActiveRowId', 'expectedFocusedRowId', 'mouseDownCalls',
'mouseUpCalls', 'homeKeyCalls', 'shiftKeyDownCalls', 'shiftKeyUpCalls',
'deleteKeyCalls', 'inputRetryCount') 'Playlist Windows input request'
Assert-ExactJsonPropertyNames $Request.viewport @('width', 'height') `
'Playlist Windows input viewport'
$sourceRowId = [string]$Request.sourceRowId
$targetRowId = [string]$Request.targetRowId
$rangeStartRowId = [string]$Request.rangeStartRowId
$rangeEndRowId = [string]$Request.rangeEndRowId
$rowIds = @($sourceRowId, $targetRowId, $rangeStartRowId, $rangeEndRowId)
if ($Request.sourceRowId -isnot [string] -or
$Request.targetRowId -isnot [string] -or
$Request.rangeStartRowId -isnot [string] -or
$Request.rangeEndRowId -isnot [string] -or
$Request.position -isnot [string] -or
[string]$Request.position -cne 'after' -or
$sourceRowId -ceq $targetRowId -or
$rangeStartRowId -ceq $rangeEndRowId -or
(@($rowIds | Select-Object -Unique)).Count -ne 4 -or
$rowIds.Where({ $_ -cnotmatch '\Alegacy-stock-[0-9]{8}\z' }).Count -ne 0 -or
[string]$Request.expectedSelectedRowId -cne $sourceRowId -or
[string]$Request.expectedFinalActiveRowId -cne $sourceRowId -or
[string]$Request.expectedFocusedRowId -cne $sourceRowId -or
(ConvertTo-FiniteNumber $Request.mouseDownCalls 'Playlist mouse-down count') -ne 4 -or
(ConvertTo-FiniteNumber $Request.mouseUpCalls 'Playlist mouse-up count') -ne 4 -or
(ConvertTo-FiniteNumber $Request.homeKeyCalls 'Playlist Home count') -ne 1 -or
(ConvertTo-FiniteNumber $Request.shiftKeyDownCalls 'Playlist Shift-down count') -ne 1 -or
(ConvertTo-FiniteNumber $Request.shiftKeyUpCalls 'Playlist Shift-up count') -ne 1 -or
(ConvertTo-FiniteNumber $Request.deleteKeyCalls 'Playlist Delete count') -ne 1) {
Throw-SafeFailure 'The playlist Windows input request is outside the exact gesture contract.'
}
$beforeOrder = @($Request.beforeOrder | ForEach-Object { [string]$_ })
$reorderedOrder = @($Request.reorderedOrder | ForEach-Object { [string]$_ })
$afterOrder = @($Request.afterOrder | ForEach-Object { [string]$_ })
$expectedRange = @($Request.expectedRangeSelectedRowIds | ForEach-Object { [string]$_ })
if ($beforeOrder.Count -ne 5 -or $reorderedOrder.Count -ne 5 -or
$afterOrder.Count -ne 3 -or
(@($beforeOrder | Select-Object -Unique)).Count -ne 5 -or
(@($reorderedOrder | Select-Object -Unique)).Count -ne 5 -or
(@($afterOrder | Select-Object -Unique)).Count -ne 3 -or
-not (Test-ExactStringSequence $expectedRange @($rangeStartRowId, $rangeEndRowId))) {
Throw-SafeFailure 'The playlist Windows input row orders are not the exact five-to-three fixture.'
}
foreach ($rowId in $beforeOrder + $reorderedOrder + $afterOrder) {
if ($rowId -cnotmatch '\Alegacy-stock-[0-9]{8}\z') {
Throw-SafeFailure 'The playlist Windows input request contains an invalid row identity.'
}
}
$computedReorder = New-Object 'Collections.Generic.List[string]'
foreach ($rowId in $beforeOrder) {
if ($rowId -cne $sourceRowId) { [void]$computedReorder.Add($rowId) }
}
$targetIndex = $computedReorder.IndexOf($targetRowId)
if ($targetIndex -lt 0) {
Throw-SafeFailure 'The playlist drag target is absent after removing its source.'
}
$computedReorder.Insert($targetIndex + 1, $sourceRowId)
if (-not (Test-ExactStringSequence @($computedReorder) $reorderedOrder) -or
$reorderedOrder[-2] -cne $rangeStartRowId -or
$reorderedOrder[-1] -cne $rangeEndRowId -or
[string]$Request.expectedActiveRowIdAfterHome -cne $reorderedOrder[0]) {
Throw-SafeFailure 'The playlist reorder/Home/range contract is not exact.'
}
$computedAfter = @($reorderedOrder | Where-Object {
$_ -cne $rangeStartRowId -and $_ -cne $rangeEndRowId
})
if (-not (Test-ExactStringSequence $computedAfter $afterOrder)) {
Throw-SafeFailure 'The playlist Shift-range delete result is not exact.'
}
$viewportWidth = ConvertTo-FiniteNumber $Request.viewport.width `
'Playlist Windows input viewport width'
$viewportHeight = ConvertTo-FiniteNumber $Request.viewport.height `
'Playlist Windows input viewport height'
$devicePixelRatio = ConvertTo-FiniteNumber $Request.devicePixelRatio `
'Playlist Windows input device pixel ratio'
if ($viewportWidth -lt 1 -or $viewportWidth -gt 16384 -or
$viewportHeight -lt 1 -or $viewportHeight -gt 16384 -or
$devicePixelRatio -lt 0.25 -or $devicePixelRatio -gt 5) {
Throw-SafeFailure 'The playlist Windows input viewport contract is invalid.'
}
$source = ConvertTo-ExactInputPoint $Request.source $viewportWidth $viewportHeight 'Playlist drag source'
$target = ConvertTo-ExactInputPoint $Request.target $viewportWidth $viewportHeight 'Playlist drag target'
$rangeStart = ConvertTo-ExactInputPoint $Request.rangeStart $viewportWidth $viewportHeight 'Playlist range start'
$rangeEnd = ConvertTo-ExactInputPoint $Request.rangeEnd $viewportWidth $viewportHeight 'Playlist range end'
$restore = ConvertTo-ExactInputPoint $Request.restoreSelection $viewportWidth $viewportHeight 'Playlist restore selection'
[LegacyPackageInputNative]::SendRowHeaderDragAfterThenHome(
$Application.MainWindowHandle, $Application.Id, 30001,
$source.X, $source.Y, $target.X, $target.Y,
$viewportWidth, $viewportHeight, $devicePixelRatio)
[LegacyPackageInputNative]::SendShiftClickRangeThenDeleteAndRestore(
$Application.MainWindowHandle, $Application.Id, 30001,
$rangeStart.X, $rangeStart.Y, $rangeEnd.X, $rangeEnd.Y,
$restore.X, $restore.Y,
$viewportWidth, $viewportHeight, $devicePixelRatio)
}
function Invoke-ExactNamedPlaylistDoubleClickRequest(
[object]$Request,
[Diagnostics.Process]$Application) {
Assert-ExactJsonPropertyNames $Request @(
'schemaVersion', 'exchangeIndex', 'token', 'targetUrl', 'operation',
'definitionId', 'definitionTitle', 'point', 'viewport',
'devicePixelRatio', 'expectedPlaylistContent', 'mouseDownCalls',
'mouseUpCalls', 'doubleClickIntervalMilliseconds',
'inputRetryCount') 'Named-playlist double-click request'
Assert-ExactJsonPropertyNames $Request.viewport @('width', 'height') `
'Named-playlist double-click viewport'
if ($Request.definitionId -isnot [string] -or
[string]::IsNullOrEmpty([string]$Request.definitionId) -or
([string]$Request.definitionId).Length -gt 256 -or
[string]$Request.definitionId -cmatch '[\x00-\x1F\x7F]' -or
$Request.definitionTitle -isnot [string] -or
([string]$Request.definitionTitle).Length -ne 26 -or
[string]$Request.definitionTitle -cnotmatch
'\ACDX_P_[0-9]{13}_[0-9a-f]{6}\z' -or
(ConvertTo-FiniteNumber $Request.mouseDownCalls 'PList double-click mouse-down count') -ne 2 -or
(ConvertTo-FiniteNumber $Request.mouseUpCalls 'PList double-click mouse-up count') -ne 2 -or
(ConvertTo-FiniteNumber $Request.doubleClickIntervalMilliseconds `
'PList double-click interval') -ne 100) {
Throw-SafeFailure 'The named-playlist double-click identity/count contract is invalid.'
}
$content = @($Request.expectedPlaylistContent)
if ($content.Count -ne 3) {
Throw-SafeFailure 'The named-playlist double-click does not preserve exactly three rows.'
}
foreach ($row in $content) {
Assert-ExactJsonPropertyNames $row @(
'isEnabled', 'marketText', 'stockName', 'graphicType',
'subtype', 'pageText') 'Named-playlist expected row'
if ($row.isEnabled -ne $true -or
$row.marketText -isnot [string] -or
$row.stockName -isnot [string] -or
$row.graphicType -isnot [string] -or
$row.subtype -isnot [string] -or
$row.pageText -isnot [string] -or
[string]$row.graphicType -cne $ExpectedSavedFixtureGraphicType) {
Throw-SafeFailure 'The named-playlist expected row is outside the exact saved fixture.'
}
}
$subtypes = @($content | ForEach-Object { [string]$_.subtype })
if (-not (Test-ExactStringSequence $subtypes @(
$ExpectedSavedFixtureCurrentSubtype,
$ExpectedSavedFixtureCurrentSubtype,
$ExpectedSavedFixtureAfterHoursSubtype))) {
Throw-SafeFailure 'The named-playlist expected row order is not the saved three-row fixture.'
}
$viewportWidth = ConvertTo-FiniteNumber $Request.viewport.width `
'PList double-click viewport width'
$viewportHeight = ConvertTo-FiniteNumber $Request.viewport.height `
'PList double-click viewport height'
$devicePixelRatio = ConvertTo-FiniteNumber $Request.devicePixelRatio `
'PList double-click device pixel ratio'
if ($viewportWidth -lt 1 -or $viewportWidth -gt 16384 -or
$viewportHeight -lt 1 -or $viewportHeight -gt 16384 -or
$devicePixelRatio -lt 0.25 -or $devicePixelRatio -gt 5) {
Throw-SafeFailure 'The named-playlist double-click viewport contract is invalid.'
}
$point = ConvertTo-ExactInputPoint $Request.point $viewportWidth $viewportHeight `
'Named-playlist double-click point'
[LegacyPackageInputNative]::SendApplicationDoubleClick(
$Application.MainWindowHandle, $Application.Id, 30001,
$point.X, $point.Y, $viewportWidth, $viewportHeight,
$devicePixelRatio, 100)
}
function Get-BackgroundPickerWindowHandles {
$handles = New-Object 'Collections.Generic.HashSet[int64]'
foreach ($handle in [LegacyPackageInputNative]::VisibleTopLevelWindows()) {
if ($handle -ne [IntPtr]::Zero) { [void]$handles.Add($handle.ToInt64()) }
}
return $handles
}
function Get-OwnedBackgroundPickerCandidates(
[Collections.Generic.HashSet[int64]]$BaselineHandles,
[Diagnostics.Process]$Application) {
$candidates = @()
foreach ($handle in [LegacyPackageInputNative]::VisibleTopLevelWindows()) {
try {
if ($handle -eq [IntPtr]::Zero -or
$BaselineHandles.Contains($handle.ToInt64()) -or
-not [LegacyPackageInputNative]::WindowVisible($handle) -or
-not [LegacyPackageInputNative]::WindowEnabled($handle) -or
[LegacyPackageInputNative]::WindowProcessId($handle) -ne $Application.Id -or
-not [LegacyPackageInputNative]::OwnerChainContains(
$handle,
$Application.MainWindowHandle) -or
[LegacyPackageInputNative]::WindowClassName($handle) -cne '#32770' -or
[LegacyPackageInputNative]::WindowTitle($handle) -cne
$BackgroundPickerTitle) {
continue
}
$candidates += [pscustomobject]@{
Handle = $handle
ProcessId = [LegacyPackageInputNative]::WindowProcessId($handle)
OwnerLinked = $true
SameProcess = $true
ClassName = [LegacyPackageInputNative]::WindowClassName($handle)
Title = [LegacyPackageInputNative]::WindowTitle($handle)
IsModal = -not [LegacyPackageInputNative]::WindowEnabled(
$Application.MainWindowHandle)
}
}
catch {
# A transient shell window is ignored unless every stable condition
# is true in one observation.
}
}
return @($candidates)
}
function Wait-UniqueOwnedBackgroundPicker(
[Collections.Generic.HashSet[int64]]$BaselineHandles,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath,
[datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
$candidates = @(Get-OwnedBackgroundPickerCandidates `
$BaselineHandles `
$Application)
if ($candidates.Count -gt 1) {
Throw-SafeFailure 'More than one newly opened app-owned background picker exists.'
}
if ($candidates.Count -eq 1) {
$picker = $candidates[0]
if (-not [bool]$picker.IsModal) {
Throw-SafeFailure 'The exact app-owned background picker is not modal.'
}
[LegacyPackageInputNative]::AcquireOwnedForeground(
$picker.Handle,
$Application.MainWindowHandle,
$Application.Id,
30001)
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
$revalidated = @(Get-OwnedBackgroundPickerCandidates `
$BaselineHandles `
$Application)
if ($revalidated.Count -ne 1 -or
$revalidated[0].Handle -ne $picker.Handle -or
[LegacyPackageInputNative]::RootWindow(
[LegacyPackageInputNative]::ReadForegroundWindow()) -ne
$picker.Handle) {
Throw-SafeFailure (
'The exact app-owned background picker identity changed during foreground acquisition.')
}
return $picker
}
Start-Sleep -Milliseconds 50
}
Throw-SafeFailure 'The exact app-owned background picker did not appear in time.'
}
function Wait-BackgroundPickerClosed(
[IntPtr]$PickerHandle,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath,
[datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
if (-not [LegacyPackageInputNative]::WindowExists($PickerHandle) -and
[LegacyPackageInputNative]::WindowEnabled($Application.MainWindowHandle)) {
return
}
Start-Sleep -Milliseconds 50
}
Throw-SafeFailure (
'The exact background picker was not destroyed after the one Escape input.')
}
function Invoke-ExactBackgroundF2CancelRequest(
[object]$Request,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath) {
Assert-ExactJsonPropertyNames $Request @(
'schemaVersion', 'exchangeIndex', 'token', 'targetUrl', 'operation',
'f2KeyCalls', 'escapeKeyCalls', 'inputRetryCount') `
'F2 background picker request'
if ((ConvertTo-FiniteNumber $Request.exchangeIndex 'F2 exchange index') -ne 2 -or
(ConvertTo-FiniteNumber $Request.f2KeyCalls 'F2 key count') -ne 1 -or
(ConvertTo-FiniteNumber $Request.escapeKeyCalls 'F2 Escape count') -ne 1 -or
[string]::IsNullOrEmpty([string]$script:BackgroundShortcutRoot)) {
Throw-SafeFailure 'The F2 background picker request is outside the exact contract.'
}
$defaultPath = Join-Path `
([string]$script:BackgroundShortcutRoot) `
$LegacyDefaultBackgroundFileName
if ([IO.File]::Exists($defaultPath) -or [IO.Directory]::Exists($defaultPath)) {
Throw-SafeFailure 'The external default background appeared before F2 input.'
}
$baseline = Get-BackgroundPickerWindowHandles
[LegacyPackageInputNative]::SendBackgroundFunctionKey(
$Application.MainWindowHandle,
$Application.Id,
30001,
0x71,
$true)
$picker = Wait-UniqueOwnedBackgroundPicker `
$baseline `
$Application `
$ExpectedApplicationPath `
([datetime]::UtcNow.AddSeconds(10))
[LegacyPackageInputNative]::SendEscapeToForegroundWindow($picker.Handle)
Wait-BackgroundPickerClosed `
$picker.Handle `
$Application `
$ExpectedApplicationPath `
([datetime]::UtcNow.AddSeconds(10))
Start-Sleep -Milliseconds 300
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
return [ordered]@{
f2KeyCalls = 1
escapeKeyCalls = 1
pickerUniqueNew = $true
pickerOwnerLinked = [bool]$picker.OwnerLinked
pickerSameProcess = [bool]$picker.SameProcess
pickerClass = [string]$picker.ClassName
pickerTitle = [string]$picker.Title
pickerModal = [bool]$picker.IsModal
pickerClosedAfterEscape = $true
}
}
function Invoke-ExactBackgroundF3NoneRequest(
[object]$Request,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath) {
Assert-ExactJsonPropertyNames $Request @(
'schemaVersion', 'exchangeIndex', 'token', 'targetUrl', 'operation',
'f3KeyCalls', 'inputRetryCount') 'F3 background-none request'
if ((ConvertTo-FiniteNumber $Request.exchangeIndex 'F3 exchange index') -ne 3 -or
(ConvertTo-FiniteNumber $Request.f3KeyCalls 'F3 key count') -ne 1 -or
[string]::IsNullOrEmpty([string]$script:BackgroundShortcutRoot)) {
Throw-SafeFailure 'The F3 background-none request is outside the exact contract.'
}
$defaultPath = Join-Path `
([string]$script:BackgroundShortcutRoot) `
$LegacyDefaultBackgroundFileName
if ([IO.File]::Exists($defaultPath) -or [IO.Directory]::Exists($defaultPath)) {
Throw-SafeFailure (
'The external default background appeared; the one F3 fail-closed input was not sent.')
}
[LegacyPackageInputNative]::SendBackgroundFunctionKey(
$Application.MainWindowHandle,
$Application.Id,
30001,
0x72,
$false)
Start-Sleep -Milliseconds 300
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
return [ordered]@{ f3KeyCalls = 1 }
}
function Invoke-ExactWindowsInputRequest(
[string]$RequestPath,
[string]$AcknowledgementPath,
[int]$ExpectedExchangeIndex,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath) {
$script:WindowsInputRequestCalls++
if ($script:WindowsInputRequestCalls -ne $ExpectedExchangeIndex -or
[IO.File]::Exists($AcknowledgementPath)) {
Throw-SafeFailure 'The Windows input one-shot budget or acknowledgement precondition changed.'
}
$requestEvidence = Wait-ReadSmallJsonEvidenceFile `
$RequestPath `
'Windows input request' `
([datetime]::UtcNow.AddSeconds(3))
$request = $requestEvidence.Value
$token = [string]$request.token
$schemaVersion = ConvertTo-FiniteNumber $request.schemaVersion 'Windows input schema version'
if ($request.token -isnot [string] -or
$request.targetUrl -isnot [string] -or
$request.operation -isnot [string] -or
$schemaVersion -ne 1 -or
(ConvertTo-FiniteNumber $request.exchangeIndex 'Windows input exchange index') -ne
$ExpectedExchangeIndex -or
$token -cnotmatch '\A[0-9A-F]{32}\z' -or
[string]$request.targetUrl -cne 'https://legacy-parity.mbn.local/index.html' -or
(ConvertTo-FiniteNumber $request.inputRetryCount 'Windows input retry count') -ne 0) {
Throw-SafeFailure 'The Windows input request does not match the common closed contract.'
}
$inputCounts = [ordered]@{}
$operation = [string]$request.operation
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
switch -CaseSensitive ($operation) {
'row-header-drag-home-shift-range-delete-restore' {
Invoke-ExactPlaylistWindowsInputRequest `
$request `
$Application
$inputCounts = [ordered]@{
mouseDownCalls = 4
mouseUpCalls = 4
homeKeyCalls = 1
shiftKeyDownCalls = 1
shiftKeyUpCalls = 1
deleteKeyCalls = 1
}
break
}
'named-playlist-load-double-click' {
if ($ExpectedExchangeIndex -ne 2) {
Throw-SafeFailure 'The named-playlist double-click is not the second exchange.'
}
Invoke-ExactNamedPlaylistDoubleClickRequest `
$request `
$Application
$inputCounts = [ordered]@{
mouseDownCalls = 2
mouseUpCalls = 2
doubleClickIntervalMilliseconds = 100
}
break
}
'background-f2-picker-cancel' {
if ($ExpectedExchangeIndex -ne 2) {
Throw-SafeFailure 'The F2 background picker is not the second exchange.'
}
$inputCounts = Invoke-ExactBackgroundF2CancelRequest `
$request `
$Application `
$ExpectedApplicationPath
break
}
'background-f3-none' {
if ($ExpectedExchangeIndex -ne 3) {
Throw-SafeFailure 'The F3 background-none input is not the third exchange.'
}
$inputCounts = Invoke-ExactBackgroundF3NoneRequest `
$request `
$Application `
$ExpectedApplicationPath
break
}
default {
Throw-SafeFailure 'The Windows input operation is outside the closed allowlist.'
}
}
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
$script:WindowsInputAcknowledgementCalls++
if ($script:WindowsInputAcknowledgementCalls -ne $ExpectedExchangeIndex) {
Throw-SafeFailure 'The Windows input acknowledgement budget changed.'
}
$acknowledgementEvidence = Write-WindowsInputAcknowledgement `
$AcknowledgementPath `
$ExpectedExchangeIndex `
$token `
$operation `
$inputCounts
return [pscustomobject]@{
ExchangeIndex = $ExpectedExchangeIndex
Operation = $operation
RequestPath = $RequestPath
AcknowledgementPath = $AcknowledgementPath
RequestSha256 = $requestEvidence.Sha256
AcknowledgementSha256 = $acknowledgementEvidence.Sha256
}
}
function Invoke-NodeHarnessUnderTcpWatch(
[string]$Executable,
[string]$ScriptPath,
[int]$DebugPort,
[string]$OutputPath,
[string]$ScreenshotPath,
[string]$WindowsInputRequestPath,
[string]$WindowsInputAcknowledgementPath,
[string]$HarnessProfile,
[string]$HarnessScope,
[string]$RecoverNamedPlaylistTitle,
[int]$TimeoutSeconds,
[Diagnostics.Process]$Application,
[string]$ExpectedApplicationPath) {
$rawArguments = @(
$ScriptPath,
'--port',
[string]$DebugPort,
'--output',
$OutputPath,
'--screenshot',
$ScreenshotPath,
'--windows-input-request',
$WindowsInputRequestPath,
'--windows-input-ack',
$WindowsInputAcknowledgementPath,
'--profile',
$HarnessProfile,
'--scope',
$HarnessScope,
'--timeout-ms',
[string]($TimeoutSeconds * 1000)
)
if (-not [string]::IsNullOrEmpty($RecoverNamedPlaylistTitle)) {
$rawArguments += @(
'--recover-named-playlist-title',
$RecoverNamedPlaylistTitle)
}
$arguments = $rawArguments |
ForEach-Object { ConvertTo-QuotedProcessArgument ([string]$_) }
$startInfo = New-Object Diagnostics.ProcessStartInfo
$startInfo.FileName = $Executable
$startInfo.WorkingDirectory = $RepositoryRoot
$startInfo.Arguments = $arguments -join ' '
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardInput = $true
try {
$nodeProcess = [Diagnostics.Process]::Start($startInfo)
}
catch {
Throw-SafeFailure 'The real input harness process could not be started.'
}
if ($null -eq $nodeProcess) {
Throw-SafeFailure 'The real input harness process did not start.'
}
$deadline = [datetime]::UtcNow.AddSeconds($TimeoutSeconds + 30)
$monitoringFailure = $null
$expectedWindowsInputExchangeCount = Get-ExpectedWindowsInputExchangeCount `
$HarnessProfile `
$HarnessScope
$nextWindowsInputExchangeIndex = 1
$windowsInputEvidence = New-Object 'Collections.Generic.List[object]'
try {
while (-not $nodeProcess.HasExited) {
if ([datetime]::UtcNow -ge $deadline) {
Throw-SafeFailure 'The input harness exceeded its global deadline.'
}
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
if ($nextWindowsInputExchangeIndex -le $expectedWindowsInputExchangeCount) {
$nextRequestPath = Get-WindowsInputExchangePath `
$WindowsInputRequestPath `
$nextWindowsInputExchangeIndex
$nextAcknowledgementPath = Get-WindowsInputExchangePath `
$WindowsInputAcknowledgementPath `
$nextWindowsInputExchangeIndex
}
if ($nextWindowsInputExchangeIndex -le $expectedWindowsInputExchangeCount -and
[IO.File]::Exists($nextRequestPath)) {
$exchangeEvidence = Invoke-ExactWindowsInputRequest `
$nextRequestPath `
$nextAcknowledgementPath `
$nextWindowsInputExchangeIndex `
$Application `
$ExpectedApplicationPath
[void]$windowsInputEvidence.Add($exchangeEvidence)
$nextWindowsInputExchangeIndex++
}
Start-Sleep -Milliseconds 100
$nodeProcess.Refresh()
}
if ($nextWindowsInputExchangeIndex -ne ($expectedWindowsInputExchangeCount + 1) -or
$windowsInputEvidence.Count -ne $expectedWindowsInputExchangeCount -or
$script:WindowsInputRequestCalls -ne $expectedWindowsInputExchangeCount -or
$script:WindowsInputAcknowledgementCalls -ne $expectedWindowsInputExchangeCount) {
Throw-SafeFailure 'The required ordered one-shot Windows input exchanges were not completed.'
}
for ($exchangeIndex = 1;
$exchangeIndex -le $expectedWindowsInputExchangeCount;
$exchangeIndex++) {
if (-not [IO.File]::Exists((Get-WindowsInputExchangePath `
$WindowsInputRequestPath `
$exchangeIndex)) -or
-not [IO.File]::Exists((Get-WindowsInputExchangePath `
$WindowsInputAcknowledgementPath `
$exchangeIndex))) {
Throw-SafeFailure 'A required Windows input exchange file is missing.'
}
}
Assert-ExactApplicationStillRunning $Application $ExpectedApplicationPath
Assert-NoTornadoConnection $Application.Id
}
catch {
$monitoringFailure = $_.Exception.Message
}
if ($null -ne $monitoringFailure) {
$helperStopped = $nodeProcess.HasExited
if (-not $helperStopped) {
try {
$nodeProcess.StandardInput.WriteLine('CANCEL')
$nodeProcess.StandardInput.Flush()
$helperStopped = $nodeProcess.WaitForExit(5000)
}
catch {
$helperStopped = $false
}
}
if (-not $helperStopped) {
try {
# Only the child Node input driver is terminated after its one-shot
# cancellation cleanup failed. The packaged app remains open.
$nodeProcess.Kill()
$helperStopped = $nodeProcess.WaitForExit(10000)
}
catch {
$helperStopped = $false
}
}
if (-not $helperStopped) {
Throw-SafeFailure (
"$monitoringFailure The Node input helper could not be stopped; " +
'the application was left open and no close was attempted.')
}
Throw-SafeFailure (
"$monitoringFailure The Node input helper was stopped; " +
'the application was left open and no close was attempted.')
}
return [pscustomobject]@{
ExitCode = $nodeProcess.ExitCode
# Windows PowerShell 5.1 throws "Argument types do not match" when an
# array subexpression directly enumerates List[object]. Materialize the
# completed one-shot evidence before returning it to the sealing phase.
WindowsInput = @($windowsInputEvidence.ToArray())
}
}
function Test-ProcessDescendsFrom([int]$ProcessId, [int]$ExpectedAncestorId) {
$visited = New-Object 'Collections.Generic.HashSet[int]'
$current = $ProcessId
for ($depth = 0; $depth -lt 12; $depth++) {
if ($current -eq $ExpectedAncestorId) {
return $true
}
if ($current -le 0 -or -not $visited.Add($current)) {
return $false
}
try {
$record = Get-CimInstance Win32_Process -Filter "ProcessId = $current" `
-ErrorAction Stop
}
catch {
return $false
}
if ($null -eq $record) {
return $false
}
$current = [int]$record.ParentProcessId
}
return $false
}
function Wait-ExactLoopbackListener(
[int]$CandidatePort,
[Diagnostics.Process]$Application,
[datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
if ($Application.HasExited) {
Throw-SafeFailure 'The application exited before the WebView listener appeared.'
}
$listeners = @(Get-TcpSnapshot | Where-Object {
[string]$_.State -ceq 'Listen' -and
$_.LocalPort -eq $CandidatePort
})
if ($listeners.Count -gt 1) {
Throw-SafeFailure 'More than one process is listening on the WebView debugging port.'
}
if ($listeners.Count -eq 1) {
$listener = $listeners[0]
if ([string]$listener.LocalAddress -cne '127.0.0.1') {
Throw-SafeFailure 'The WebView debugging listener is not bound to exact IPv4 loopback.'
}
try {
$owner = Get-Process -Id ([int]$listener.OwningProcess) -ErrorAction Stop
$ownerPath = $owner.Path
}
catch {
Throw-SafeFailure 'The WebView debugging listener owner could not be identified.'
}
if ([string]$owner.ProcessName -cne 'msedgewebview2' -or
[IO.Path]::GetFileName($ownerPath) -cne 'msedgewebview2.exe' -or
-not (Test-ProcessDescendsFrom $owner.Id $Application.Id)) {
Throw-SafeFailure 'The WebView debugging listener is not owned by this application.'
}
return $listener
}
Start-Sleep -Milliseconds 100
}
Throw-SafeFailure 'The exact WebView debugging listener did not appear in time.'
}
function Get-StartedApplication(
[string]$ExpectedPath,
[datetime]$NotBefore,
[datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
$processes = @(Get-LegacyApplicationProcesses)
if ($processes.Count -gt 1) {
Throw-SafeFailure 'More than one LegacyParity process appeared after launch.'
}
if ($processes.Count -eq 1) {
$process = $processes[0]
try {
$process.Refresh()
$path = $process.Path
$startUtc = $process.StartTime.ToUniversalTime()
}
catch {
# A packaged full-trust process can appear in the process table a
# few milliseconds before Path/StartTime become queryable. It has
# not received any test input yet, so keep the launch gate closed
# and retry only while it owns no Tornado connection.
Assert-NoTornadoConnection $process.Id
Start-Sleep -Milliseconds 50
continue
}
if ([string]::IsNullOrWhiteSpace([string]$path)) {
Assert-NoTornadoConnection $process.Id
Start-Sleep -Milliseconds 50
continue
}
if (-not (Test-PathEqual $path $ExpectedPath) -or
$startUtc -lt $NotBefore.AddSeconds(-1)) {
Throw-SafeFailure 'The launched application identity is not exact.'
}
try {
$cim = Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)" `
-ErrorAction Stop
}
catch {
Throw-SafeFailure 'The launched application command line could not be inspected.'
}
if ($null -eq $cim -or
[string]::IsNullOrWhiteSpace([string]$cim.CommandLine) -or
[string]$cim.CommandLine -match '(?i)(?:^|\s)--development-live(?:\s|$)') {
Throw-SafeFailure 'The Release application launch arguments are not safe.'
}
Assert-ExactProcessPackageIdentity $process
return $process
}
Start-Sleep -Milliseconds 100
}
Throw-SafeFailure 'The packaged Release application did not start in time.'
}
function Start-PackagedDryRunApplication(
[pscustomobject]$Registration,
[int]$DebugPort) {
$invokeCommand = Get-Command Invoke-CommandInDesktopPackage -ErrorAction SilentlyContinue
if ($null -eq $invokeCommand -or [string]$invokeCommand.Source -cne 'Appx') {
Throw-SafeFailure 'Invoke-CommandInDesktopPackage from the Appx module is required.'
}
$childPowerShell = Join-Path $env:SystemRoot `
'System32\WindowsPowerShell\v1.0\powershell.exe'
if (-not [IO.File]::Exists($childPowerShell)) {
Throw-SafeFailure 'The child Windows PowerShell executable is missing.'
}
$executableBase64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($Registration.Executable))
$workingDirectoryBase64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($Registration.InstallLocation))
$browserArguments = "--remote-debugging-address=127.0.0.1 --remote-debugging-port=$DebugPort"
$browserArgumentsBase64 = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($browserArguments))
$childScript = @"
`$ErrorActionPreference = 'Stop'
`$executable = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$executableBase64'))
`$workingDirectory = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$workingDirectoryBase64'))
`$browserArguments = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$browserArgumentsBase64'))
foreach (`$nameObject in @([Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::Process).Keys)) {
`$name = [string]`$nameObject
if (`$name -like 'MBN_STOCK_PLAYOUT_*' -or `$name -like 'WEBVIEW2_*' -or
`$name -like 'COREWEBVIEW2_*' -or `$name -ceq '$K3dNativeHashVariable' -or
`$name -ceq '$K3dInteropHashVariable') {
[Environment]::SetEnvironmentVariable(`$name, `$null, [EnvironmentVariableTarget]::Process)
}
}
[Environment]::SetEnvironmentVariable('MBN_STOCK_PLAYOUT_MODE', 'DryRun', [EnvironmentVariableTarget]::Process)
[Environment]::SetEnvironmentVariable('$WebViewArgumentsVariable', `$browserArguments, [EnvironmentVariableTarget]::Process)
`$startInfo = New-Object Diagnostics.ProcessStartInfo
`$startInfo.FileName = `$executable
`$startInfo.WorkingDirectory = `$workingDirectory
`$startInfo.UseShellExecute = `$false
`$startInfo.CreateNoWindow = `$false
`$started = [Diagnostics.Process]::Start(`$startInfo)
if (`$null -eq `$started) { exit 91 }
"@
$encodedCommand = [Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes($childScript))
$childArguments = "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand $encodedCommand"
$notBefore = [datetime]::UtcNow
try {
[void](Invoke-CommandInDesktopPackage `
-PackageFamilyName $PackageFamilyName `
-AppId $ApplicationId `
-Command $childPowerShell `
-Args $childArguments `
-PreventBreakaway `
-ErrorAction Stop)
}
catch {
Throw-SafeFailure 'The package-context launch helper failed.'
}
$deadline = [datetime]::UtcNow.AddSeconds($LaunchTimeoutSeconds)
return Get-StartedApplication $Registration.Executable $notBefore $deadline
}
function Assert-ExactApplicationStillRunning(
[Diagnostics.Process]$Application,
[string]$ExpectedPath) {
try {
$Application.Refresh()
if ($Application.HasExited -or
-not (Test-PathEqual $Application.Path $ExpectedPath) -or
$Application.MainWindowHandle -eq [IntPtr]::Zero -or
[string]$Application.MainWindowTitle -cne $ExpectedWindowTitle) {
Throw-SafeFailure 'The application identity changed during the input smoke test.'
}
Assert-ExactProcessPackageIdentity $Application
}
catch [InvalidOperationException] {
throw
}
catch {
Throw-SafeFailure 'The application identity could not be revalidated.'
}
}
function Wait-StartupIntroDismissed(
[Diagnostics.Process]$Application,
[string]$ExpectedPath) {
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$deadline = [datetime]::UtcNow.AddSeconds(4)
$nameCondition = [Windows.Automation.PropertyCondition]::new(
[Windows.Automation.AutomationElement]::NameProperty,
'매일경제TV V-STOCK 시작 화면')
while ($true) {
Assert-ExactApplicationStillRunning $Application $ExpectedPath
Assert-NoTornadoConnection $Application.Id
$Application.Refresh()
$root = [Windows.Automation.AutomationElement]::FromHandle(
$Application.MainWindowHandle)
if ($null -eq $root) {
Throw-SafeFailure 'The main window is unavailable while waiting for the startup intro.'
}
$intro = $root.FindFirst(
[Windows.Automation.TreeScope]::Descendants,
$nameCondition)
$introIsVisible = $false
if ($null -ne $intro) {
try {
$introIsVisible = -not $intro.Current.IsOffscreen
}
catch [System.Windows.Automation.ElementNotAvailableException] {
$introIsVisible = $false
}
}
if (-not $introIsVisible) {
Assert-ExactApplicationStillRunning $Application $ExpectedPath
Assert-NoTornadoConnection $Application.Id
return
}
if ([datetime]::UtcNow -ge $deadline) {
break
}
$remainingMilliseconds = [int][Math]::Max(
1,
[Math]::Min(50, ($deadline - [datetime]::UtcNow).TotalMilliseconds))
Start-Sleep -Milliseconds $remainingMilliseconds
}
Assert-ExactApplicationStillRunning $Application $ExpectedPath
Assert-NoTornadoConnection $Application.Id
Throw-SafeFailure 'The startup intro remained visible beyond its four-second bound.'
}
function Assert-NoExistingContentDialog([IntPtr]$MainWindowHandle) {
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$root = [Windows.Automation.AutomationElement]::FromHandle($MainWindowHandle)
if ($null -eq $root) {
Throw-SafeFailure 'The main window is unavailable to UI Automation.'
}
$all = $root.FindAll(
[Windows.Automation.TreeScope]::Descendants,
[Windows.Automation.Condition]::TrueCondition)
for ($index = 0; $index -lt $all.Count; $index++) {
$element = $all[$index]
if ($element.Current.ControlType -eq [Windows.Automation.ControlType]::Window -and
[string]$element.Current.ClassName -ceq 'Popup') {
Throw-SafeFailure 'A dialog was already open before the normal close request.'
}
}
}
function Assert-SingleInstanceActivation(
[Diagnostics.Process]$Application,
[string]$ExpectedPath) {
Assert-ExactApplicationStillRunning $Application $ExpectedPath
$Application.Refresh()
$expectedId = $Application.Id
$expectedStartUtc = $Application.StartTime.ToUniversalTime()
$expectedHandle = $Application.MainWindowHandle
$expectedTitle = $Application.MainWindowTitle
$preActivationProcesses = @(Get-LegacyApplicationProcesses)
if ($preActivationProcesses.Count -ne 1 -or
$preActivationProcesses[0].Id -ne $expectedId) {
Throw-SafeFailure 'Single-instance activation requires exactly the pinned original process.'
}
Assert-NoTornadoConnection $expectedId
$explorer = Join-Path $env:SystemRoot 'explorer.exe'
if (-not [IO.File]::Exists($explorer)) {
Throw-SafeFailure 'Explorer is unavailable for the single-instance activation check.'
}
$script:SingleInstanceActivationCalls++
if ($script:SingleInstanceActivationCalls -ne 1) {
Throw-SafeFailure 'The single-instance activation budget changed.'
}
$activationRequestedUtc = [datetime]::UtcNow
try {
[void](Start-Process `
-FilePath $explorer `
-ArgumentList "shell:AppsFolder\$PackageFamilyName!$ApplicationId" `
-WindowStyle Hidden `
-PassThru `
-ErrorAction Stop)
}
catch {
Throw-SafeFailure 'The single-instance package activation could not be requested.'
}
$redirector = $null
$redirectorId = $null
$redirectorStartUtc = $null
$redirectorExited = $false
$stableOriginalSinceUtc = $null
$deadline = [datetime]::UtcNow.AddSeconds(8)
while ([datetime]::UtcNow -lt $deadline) {
$processes = @(Get-LegacyApplicationProcesses)
$originals = @($processes | Where-Object { $_.Id -eq $expectedId })
$additional = @($processes | Where-Object { $_.Id -ne $expectedId })
if ($originals.Count -ne 1 -or $additional.Count -gt 1) {
Throw-SafeFailure 'Package activation did not preserve one original and at most one redirector.'
}
$process = $originals[0]
try {
$process.Refresh()
$sameIdentity = $process.Id -eq $expectedId -and
(Test-PathEqual $process.Path $ExpectedPath) -and
$process.StartTime.ToUniversalTime() -eq $expectedStartUtc -and
$process.MainWindowHandle -eq $expectedHandle -and
[string]$process.MainWindowTitle -ceq $expectedTitle
}
catch {
$sameIdentity = $false
}
if (-not $sameIdentity) {
Throw-SafeFailure 'Package activation changed the running application identity.'
}
Assert-ExactProcessPackageIdentity $process
Assert-NoTornadoConnection $expectedId
if ($additional.Count -eq 1) {
if ($redirectorExited) {
Throw-SafeFailure 'A second redirector appeared after the verified redirector exited.'
}
$candidate = $additional[0]
try {
# The AppLifecycle redirector can exit within a few milliseconds.
# Pin and verify its package handle before reading slower Process
# properties so a legitimate fast redirect is not misclassified as
# an unknown process after it has already terminated.
$candidatePackageFullName =
[LegacyPackageInputNative]::ReadPackageFullName($candidate.Handle)
$candidate.Refresh()
$candidateId = $candidate.Id
$candidateStartUtc = $candidate.StartTime.ToUniversalTime()
$candidatePath = $candidate.Path
$candidateHandle = $candidate.MainWindowHandle
$candidateTitle = [string]$candidate.MainWindowTitle
}
catch {
Throw-SafeFailure 'The transient activation redirector could not be inspected exactly.'
}
if ($candidateStartUtc -lt $activationRequestedUtc -or
[string]$candidatePackageFullName -cne $PackageFullName -or
-not (Test-PathEqual $candidatePath $ExpectedPath) -or
$candidateHandle -ne [IntPtr]::Zero -or
-not [string]::IsNullOrEmpty($candidateTitle)) {
Throw-SafeFailure 'The transient activation redirector identity is not exact.'
}
Assert-NoTornadoConnection $candidateId
if ($null -eq $redirectorId) {
$redirector = $candidate
$redirectorId = $candidateId
$redirectorStartUtc = $candidateStartUtc
}
elseif ($candidateId -ne $redirectorId -or
$candidateStartUtc -ne $redirectorStartUtc) {
Throw-SafeFailure 'More than one transient activation redirector was observed.'
}
# Command-line inspection is opportunistic because a redirector may exit
# between the native identity checks and the management query.
try {
$cimRows = @(Get-CimInstance Win32_Process `
-Filter "ProcessId = $candidateId" `
-ErrorAction Stop)
if ($cimRows.Count -gt 1) {
Throw-SafeFailure 'The transient redirector command line is not unique.'
}
if ($cimRows.Count -eq 1 -and
-not [string]::IsNullOrWhiteSpace([string]$cimRows[0].CommandLine) -and
[string]$cimRows[0].CommandLine -match
'(?i)(?:^|\s)--development-live(?:[=\s]|$)') {
Throw-SafeFailure 'The transient redirector used a development-live argument.'
}
}
catch [InvalidOperationException] {
throw
}
catch {
# The already-verified transient may exit before CIM can inspect it.
}
$stableOriginalSinceUtc = $null
}
elseif ($null -ne $redirectorId) {
try {
$redirector.Refresh()
$isRedirectorExited = $redirector.HasExited
}
catch {
$isRedirectorExited = $true
}
if (-not $isRedirectorExited) {
Throw-SafeFailure 'The pinned redirector disappeared from exact process enumeration before exit.'
}
if (-not $redirectorExited) {
$redirectorExited = $true
$stableOriginalSinceUtc = [datetime]::UtcNow
}
elseif ($null -eq $stableOriginalSinceUtc) {
$stableOriginalSinceUtc = [datetime]::UtcNow
}
if (([datetime]::UtcNow - $stableOriginalSinceUtc).TotalMilliseconds -ge 750) {
$stableOriginalMilliseconds = [int][Math]::Floor(
([datetime]::UtcNow - $stableOriginalSinceUtc).TotalMilliseconds)
Assert-ExactApplicationStillRunning $Application $ExpectedPath
Assert-NoTornadoConnection $expectedId
return [pscustomobject]@{
ActivationCalls = $script:SingleInstanceActivationCalls
ProcessCount = 1
ProcessId = $expectedId
StartTimeUtc = $expectedStartUtc.ToString('o')
MainWindowHandle = [int64]$expectedHandle
RedirectorProcessId = $redirectorId
RedirectorStartTimeUtc = $redirectorStartUtc.ToString('o')
RedirectorExited = $true
StableOriginalMilliseconds = $stableOriginalMilliseconds
}
}
}
Start-Sleep -Milliseconds 25
}
if ($null -eq $redirectorId) {
Throw-SafeFailure 'The single activation did not expose one verifiable transient redirector.'
}
if (-not $redirectorExited) {
Throw-SafeFailure 'The verified transient activation redirector did not exit in time.'
}
Throw-SafeFailure 'The original instance was not stable alone for 750 milliseconds.'
}
function Wait-ExactNeutralCloseDialog(
[IntPtr]$MainWindowHandle,
[Diagnostics.Process]$Application,
[datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
if ($Application.HasExited) {
Throw-SafeFailure 'The application exited before close confirmation.'
}
Assert-NoTornadoConnection $Application.Id
$root = [Windows.Automation.AutomationElement]::FromHandle($MainWindowHandle)
if ($null -eq $root) {
Throw-SafeFailure 'The main window disappeared before close confirmation.'
}
$all = $root.FindAll(
[Windows.Automation.TreeScope]::Descendants,
[Windows.Automation.Condition]::TrueCondition)
$matches = New-Object Collections.Generic.List[object]
for ($index = 0; $index -lt $all.Count; $index++) {
$element = $all[$index]
if ($element.Current.ControlType -eq [Windows.Automation.ControlType]::Window -and
[string]$element.Current.ClassName -ceq 'Popup' -and
[string]$element.Current.Name -ceq $ExpectedDialogTitle) {
[void]$matches.Add($element)
}
}
if ($matches.Count -gt 1) {
Throw-SafeFailure 'The neutral close dialog is not unique.'
}
if ($matches.Count -eq 1) {
Assert-NoTornadoConnection $Application.Id
return $matches[0]
}
Start-Sleep -Milliseconds 100
}
Throw-SafeFailure 'The neutral close dialog did not appear in time.'
}
function Invoke-ExactNormalClose(
[Diagnostics.Process]$Application,
[string]$ExpectedPath) {
Assert-ExactApplicationStillRunning $Application $ExpectedPath
$mainWindowHandle = $Application.MainWindowHandle
Assert-NoExistingContentDialog $mainWindowHandle
$script:CloseMainWindowCalls++
if ($script:CloseMainWindowCalls -ne 1 -or -not $Application.CloseMainWindow()) {
Throw-SafeFailure 'The single normal close request was not accepted.'
}
$dialog = Wait-ExactNeutralCloseDialog `
$mainWindowHandle `
$Application `
([datetime]::UtcNow.AddSeconds(15))
$descendants = $dialog.FindAll(
[Windows.Automation.TreeScope]::Descendants,
[Windows.Automation.Condition]::TrueCondition)
$buttons = New-Object Collections.Generic.List[object]
$texts = New-Object Collections.Generic.List[object]
for ($index = 0; $index -lt $descendants.Count; $index++) {
$element = $descendants[$index]
if ($element.Current.ControlType -eq [Windows.Automation.ControlType]::Button) {
[void]$buttons.Add($element)
}
elseif ($element.Current.ControlType -eq [Windows.Automation.ControlType]::Text) {
[void]$texts.Add($element)
}
}
$primary = @($buttons | Where-Object {
[string]$_.Current.AutomationId -ceq 'PrimaryButton' -and
[string]$_.Current.Name -ceq $ExpectedPrimaryButton -and
$_.Current.IsEnabled -and -not $_.Current.IsOffscreen
})
$close = @($buttons | Where-Object {
[string]$_.Current.AutomationId -ceq 'CloseButton' -and
[string]$_.Current.Name -ceq $ExpectedCloseButton -and
$_.Current.IsEnabled -and -not $_.Current.IsOffscreen
})
$message = @($texts | Where-Object {
[string]$_.Current.Name -ceq $ExpectedDialogMessage
})
if ($buttons.Count -ne 2 -or $primary.Count -ne 1 -or
$close.Count -ne 1 -or $message.Count -ne 1) {
Throw-SafeFailure 'The close dialog is not the exact neutral Yes/No confirmation.'
}
$patternObject = $null
if (-not $primary[0].TryGetCurrentPattern(
[Windows.Automation.InvokePattern]::Pattern,
[ref]$patternObject) -or
$null -eq $patternObject) {
Throw-SafeFailure 'The exact Yes button has no InvokePattern.'
}
$script:PrimaryInvokeCalls++
if ($script:PrimaryInvokeCalls -ne 1) {
Throw-SafeFailure 'The close confirmation invocation budget changed.'
}
($patternObject -as [Windows.Automation.InvokePattern]).Invoke()
$exitDeadline = [datetime]::UtcNow.AddSeconds(30)
while ([datetime]::UtcNow -lt $exitDeadline) {
$Application.Refresh()
if ($Application.HasExited) {
return
}
Assert-NoTornadoConnection $Application.Id
Start-Sleep -Milliseconds 100
}
Throw-SafeFailure 'Normal application exit was not observed after the single confirmation.'
}
function Wait-PortReleased([int]$CandidatePort, [datetime]$Deadline) {
while ([datetime]::UtcNow -lt $Deadline) {
$listeners = @(Get-TcpSnapshot | Where-Object {
[string]$_.State -ceq 'Listen' -and
$_.LocalPort -eq $CandidatePort
})
if ($listeners.Count -eq 0) {
return
}
Start-Sleep -Milliseconds 100
}
Throw-SafeFailure 'The WebView debugging listener remained after normal application exit.'
}
function Read-AndAssertHarnessEvidence(
[string]$OutputPath,
[string]$ScreenshotPath,
[string]$WindowsInputRequestPath,
[string]$WindowsInputAcknowledgementPath,
[string]$HarnessProfile,
[string]$HarnessScope,
[string]$ExpectedRecoveryTitle,
[int]$ExpectedPort,
[int]$ExpectedTimeoutSeconds) {
try {
$evidence = [IO.File]::ReadAllText($OutputPath, [Text.Encoding]::UTF8) |
ConvertFrom-Json -ErrorAction Stop
$result = [string]$evidence.result
$safeResult = $result -ceq 'PASS' -or $result -ceq 'PASS_WITH_WARNING'
$warnings = @($evidence.warnings)
$expectedProfile = switch -CaseSensitive ($HarnessProfile) {
'FullUiDb' { 'full-ui-db'; break }
'DryRunPlayout' { 'dry-run-playout'; break }
'ProgramPlaylistDb' { 'program-playlist-db'; break }
default { 'read-only' }
}
$expectedScope = if ($HarnessProfile -ceq 'FullUiDb') {
$HarnessScope.ToLowerInvariant()
}
else {
'all'
}
$expectedRecoveryRequested = -not [string]::IsNullOrEmpty(
$ExpectedRecoveryTitle)
$recoveryEvidence = if ($null -ne $evidence.fullUiDb) {
$evidence.fullUiDb.namedPlaylistRecovery
}
else {
$null
}
$namedPlaylistDeleteWriteCount = @(
$evidence.safety.approvedDatabaseWriteMessages |
Where-Object {
[string]$_.type -ceq 'delete-selected-named-playlist'
}).Count
$sealedRecoveryMatchCount = if ($expectedRecoveryRequested) {
@($evidence.finalSnapshot.state.namedPlaylist.definitions |
Where-Object {
[string]$_.title -ceq $ExpectedRecoveryTitle
}).Count
}
else {
0
}
$namedPlaylistRecoveryContractOk = if (-not $expectedRecoveryRequested) {
$null -eq $recoveryEvidence
}
elseif ($null -eq $recoveryEvidence -or
[string]$recoveryEvidence.targetTitle -cne $ExpectedRecoveryTitle -or
([string]$recoveryEvidence.targetTitle).Length -ne 26 -or
[string]$recoveryEvidence.targetTitle -cnotmatch
'^CDX_P_[0-9]{13}_[0-9a-f]{6}$' -or
$recoveryEvidence.freshListVerified -ne $true -or
$recoveryEvidence.exactAbsenceVerified -ne $true -or
$sealedRecoveryMatchCount -ne 0) {
$false
}
elseif ([string]$recoveryEvidence.result -ceq 'alreadyAbsent') {
[int]$recoveryEvidence.exactMatchCount -eq 0 -and
[int]$recoveryEvidence.writeCount -eq 0 -and
[string]::IsNullOrEmpty(
[string]$recoveryEvidence.mutationOutcome) -and
$namedPlaylistDeleteWriteCount -eq 1
}
elseif ([string]$recoveryEvidence.result -ceq 'deleted') {
[int]$recoveryEvidence.exactMatchCount -eq 1 -and
[int]$recoveryEvidence.writeCount -eq 1 -and
@('committedFresh', 'committedOptimistic') -ccontains
[string]$recoveryEvidence.mutationOutcome -and
$namedPlaylistDeleteWriteCount -eq 2
}
else {
$false
}
$databaseWriteContractOk = if ($HarnessProfile -ceq 'FullUiDb') {
$evidence.safety.databaseWriteIntentIssued -eq $true -and
@($evidence.safety.approvedDatabaseWriteMessages).Count -ge 3 -and
[string]$evidence.fullUiDb.result -ceq 'PASS' -and
$evidence.fullUiDb.cleanupVerified -eq $true
}
elseif ($HarnessProfile -ceq 'ProgramPlaylistDb') {
$evidence.safety.databaseWriteIntentIssued -eq $true -and
@($evidence.safety.approvedDatabaseWriteMessages).Count -eq 3 -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'create-named-playlist'
}).Count -eq 1 -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'save-current-named-playlist-to'
}).Count -eq 1 -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'delete-selected-named-playlist'
}).Count -eq 1 -and
$null -eq $evidence.fullUiDb -and
[string]$evidence.programPlaylistDb.result -ceq 'PASS' -and
[string]$evidence.programPlaylistDb.title -ceq
[string]$evidence.fixture.namedPlaylistTitle -and
([string]$evidence.programPlaylistDb.title).Length -eq 26 -and
[string]$evidence.programPlaylistDb.title -cmatch
'^CDX_P_[0-9]{13}_[0-9a-f]{6}$' -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.programPlaylistDb.definitionId) -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.programPlaylistDb.saveDefinitionId) -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'create-named-playlist' -and
[string]$_.title -ceq
[string]$evidence.programPlaylistDb.title -and
-not [string]::IsNullOrWhiteSpace([string]$_.requestId)
}).Count -eq 1 -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'save-current-named-playlist-to' -and
[string]$_.definitionId -ceq
[string]$evidence.programPlaylistDb.saveDefinitionId
}).Count -eq 1 -and
@($evidence.safety.approvedDatabaseWriteMessages | Where-Object {
[string]$_.type -ceq 'delete-selected-named-playlist' -and
-not [string]::IsNullOrWhiteSpace([string]$_.requestId)
}).Count -eq 1 -and
[int]$evidence.programPlaylistDb.rowCount -eq 3 -and
[int]$evidence.programPlaylistDb.createWriteCount -eq 1 -and
[int]$evidence.programPlaylistDb.saveWriteCount -eq 1 -and
[int]$evidence.programPlaylistDb.deleteWriteCount -eq 1 -and
$evidence.programPlaylistDb.freshDefinitionReadbackVerified -eq $true -and
$evidence.programPlaylistDb.exactAbsenceVerified -eq $true -and
[string]$evidence.programPlaylistDb.finalPhase -ceq 'idle' -and
@($evidence.finalSnapshot.state.namedPlaylist.definitions |
Where-Object {
[string]$_.title -ceq
[string]$evidence.programPlaylistDb.title
}).Count -eq 0
}
else {
$evidence.safety.databaseWriteIntentIssued -eq $false -and
@($evidence.safety.approvedDatabaseWriteMessages).Count -eq 0 -and
$null -eq $evidence.fullUiDb -and
$null -eq $evidence.programPlaylistDb
}
$playoutIntentContractOk = if ($HarnessProfile -ceq 'DryRunPlayout') {
$dryRunProgramContractOk =
$evidence.safety.playoutIntentIssued -eq $true -and
[string]$evidence.dryRunPlayout.result -ceq 'PASS' -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.selectedEntryId) -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.nextEntryId) -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.stagedEntryId) -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.skippedEntryId) -and
[string]$evidence.dryRunPlayout.stagedEntryId -ceq
[string]$evidence.dryRunPlayout.skippedEntryId -and
[string]$evidence.dryRunPlayout.selectedEntryId -cne
[string]$evidence.dryRunPlayout.nextEntryId -and
[string]$evidence.dryRunPlayout.stagedEntryId -cne
[string]$evidence.dryRunPlayout.selectedEntryId -and
[string]$evidence.dryRunPlayout.skippedEntryId -cne
[string]$evidence.dryRunPlayout.nextEntryId -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.tailAppendedEntryId) -and
[string]$evidence.dryRunPlayout.tailAppendedEntryId -cne
[string]$evidence.dryRunPlayout.selectedEntryId -and
[string]$evidence.dryRunPlayout.tailAppendedEntryId -cne
[string]$evidence.dryRunPlayout.nextEntryId -and
[int]$evidence.dryRunPlayout.playlistCountAfterDoubleClick -eq
([int]$evidence.dryRunPlayout.playlistCountBeforeSelection + 1) -and
[int]$evidence.dryRunPlayout.programCutSelection -ge 0 -and
[int]$evidence.dryRunPlayout.programDragParity.cutPhysicalIndex -ge 0 -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.dryRunPlayout.programDragParity.appendedEntryId) -and
[string]$evidence.dryRunPlayout.programDragParity.appendedEntryId -cne
[string]$evidence.dryRunPlayout.tailAppendedEntryId -and
[string]$evidence.dryRunPlayout.programDragParity.appendedEntryId -cne
[string]$evidence.dryRunPlayout.selectedEntryId -and
[string]$evidence.dryRunPlayout.programDragParity.appendedEntryId -cne
[string]$evidence.dryRunPlayout.nextEntryId -and
[string]$evidence.dryRunPlayout.programDragParity.appendedEntryId -cne
[string]$evidence.dryRunPlayout.skippedEntryId -and
[int]$evidence.dryRunPlayout.programDragParity.appendIntentCount -eq 1 -and
[int]$evidence.dryRunPlayout.programDragParity.cleanupDeleteIntentCount -eq 1 -and
$evidence.dryRunPlayout.programDragParity.onAirEntryPreserved -eq $true -and
[string]$evidence.dryRunPlayout.programEnableParity.currentEntryId -ceq
[string]$evidence.dryRunPlayout.selectedEntryId -and
[string]$evidence.dryRunPlayout.programEnableParity.skippedEntryId -ceq
[string]$evidence.dryRunPlayout.skippedEntryId -and
[string]$evidence.dryRunPlayout.programEnableParity.nextEntryId -ceq
[string]$evidence.dryRunPlayout.nextEntryId -and
[int]$evidence.dryRunPlayout.programEnableParity.futureRowCount -ge 3 -and
$evidence.dryRunPlayout.programEnableParity.mouseCheckboxDisabled -eq $true -and
[int]$evidence.dryRunPlayout.programEnableParity.setAllIntentCount -eq 2 -and
$evidence.dryRunPlayout.programEnableParity.setAllAppliesEveryRow -eq $true -and
[int]$evidence.dryRunPlayout.programEnableParity.toggleActiveIntentCount -eq 1 -and
$evidence.dryRunPlayout.programEnableParity.endOfPlaylistObserved -eq $true -and
$evidence.dryRunPlayout.programEnableParity.playlistNextRestored -eq $true
$dryRunBackgroundContractOk =
[int]$evidence.dryRunPlayout.backgroundShortcutParity.f2IntentCount -eq 1 -and
[int]$evidence.dryRunPlayout.backgroundShortcutParity.f2KeyCalls -eq 1 -and
[int]$evidence.dryRunPlayout.backgroundShortcutParity.escapeKeyCalls -eq 1 -and
$evidence.dryRunPlayout.backgroundShortcutParity.pickerOwnedAndCancelled -eq $true -and
[int]$evidence.dryRunPlayout.backgroundShortcutParity.f3IntentCount -eq 1 -and
[int]$evidence.dryRunPlayout.backgroundShortcutParity.f3KeyCalls -eq 1 -and
$evidence.dryRunPlayout.backgroundShortcutParity.defaultAssetAbsentFailClosed -eq $true -and
$evidence.dryRunPlayout.backgroundShortcutParity.finalBackgroundEnabled -eq $false -and
$evidence.dryRunPlayout.backgroundShortcutParity.finalBackgroundNoneChecked -eq $true -and
[string]$evidence.dryRunPlayout.backgroundShortcutParity.finalPhase -ceq 'idle' -and
$evidence.finalSnapshot.state.playout.backgroundEnabled -eq $false -and
[string]$evidence.finalSnapshot.state.playout.backgroundFileName -ceq '' -and
$evidence.finalSnapshot.dom.backgroundNoneChecked -eq $true -and
[string]$evidence.finalSnapshot.dom.backgroundNameValue -ceq
$BackgroundNoneLabel
$dryRunKeyContractOk =
[int]$evidence.dryRunPlayout.shortcutParity.f8IntentCount -eq 1 -and
[int]$evidence.dryRunPlayout.shortcutParity.escapeIntentCount -eq 1 -and
[string]$evidence.dryRunPlayout.shortcutParity.finalPhase -ceq 'idle'
$dryRunMessageContractOk =
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'take-in'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'next-playout'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'take-out'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'set-all-playlist-enabled'
}).Count -eq 2 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'toggle-active-playlist-enabled'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'choose-background'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'toggle-background'
}).Count -eq 1
$dryRunShortcutContractOk =
$dryRunBackgroundContractOk -and
$dryRunKeyContractOk -and
$dryRunMessageContractOk
$dryRunProgramContractOk -and $dryRunShortcutContractOk
}
elseif ($HarnessProfile -ceq 'ProgramPlaylistDb') {
$evidence.safety.playoutIntentIssued -eq $true -and
$null -eq $evidence.dryRunPlayout -and
[string]$evidence.programPlaylistDb.result -ceq 'PASS' -and
-not [string]::IsNullOrWhiteSpace(
[string]$evidence.programPlaylistDb.currentEntryId) -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'take-in'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'take-out'
}).Count -eq 1 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'next-playout'
}).Count -eq 0 -and
@($evidence.safety.observedOutboundMessageTypes | Where-Object {
[string]$_ -ceq 'prepare-playout'
}).Count -eq 0
}
else {
$evidence.safety.playoutIntentIssued -eq $false -and
$null -eq $evidence.dryRunPlayout -and
$null -eq $evidence.programPlaylistDb
}
$screenshotPathOk =
-not [string]::IsNullOrWhiteSpace([string]$evidence.screenshot.path) -and
(Test-PathEqual ([string]$evidence.screenshot.path) $ScreenshotPath)
$safetyChecks = [ordered]@{
schema = [int]$evidence.schemaVersion -eq 1
profile = [string]$evidence.profile -ceq $expectedProfile
scope = [string]$evidence.scope -ceq $expectedScope
result = $safeResult
warnings = -not ($result -ceq 'PASS_WITH_WARNING' -and $warnings.Count -eq 0)
error = $null -eq $evidence.error
expectedUrl = [string]$evidence.target.expectedUrl -ceq
'https://legacy-parity.mbn.local/index.html'
port = [int]$evidence.target.port -eq $ExpectedPort
timeout = [int]$evidence.target.timeoutMilliseconds -eq
($ExpectedTimeoutSeconds * 1000)
actualUrl = [string]$evidence.target.url -ceq
'https://legacy-parity.mbn.local/index.html'
mode = [string]$evidence.safety.requiredMode -ceq 'dryRun'
phase = [string]$evidence.safety.requiredPhase -ceq 'idle'
appClose = $evidence.safety.appCloseRequested -eq $false
playout = [bool]$playoutIntentContractOk
databaseWrite = [bool]$databaseWriteContractOk
namedPlaylistRecovery = [bool]$namedPlaylistRecoveryContractOk
externalCancellation = $evidence.safety.externalCancellationRequested -eq $false
blockedOutbound = @($evidence.safety.blockedOutboundMessages).Count -eq 0
stateViolations = @($evidence.safety.stateViolations).Count -eq 0
screenshotPath = $screenshotPathOk
}
$failedSafetyChecks = @($safetyChecks.GetEnumerator() | Where-Object {
$_.Value -ne $true
} | ForEach-Object { $_.Key })
if ($failedSafetyChecks.Count -ne 0) {
$playoutDiagnostic = if ($failedSafetyChecks -ccontains 'playout') {
$playoutValueType = if ($null -eq $playoutIntentContractOk) {
'null'
}
else {
$playoutIntentContractOk.GetType().FullName
}
" Playout value type: $playoutValueType; value: " +
"'$([string]$playoutIntentContractOk)'." +
$(if ($HarnessProfile -ceq 'DryRunPlayout') {
" Program contract: '$([string]$dryRunProgramContractOk)';" +
" background contract: '$([string]$dryRunBackgroundContractOk)';" +
" key contract: '$([string]$dryRunKeyContractOk)';" +
" message contract: '$([string]$dryRunMessageContractOk)'."
}
else {
''
})
}
else {
''
}
Throw-SafeFailure (
'The real input evidence does not satisfy the DryRun safety contract. ' +
'Failed checks: ' + ($failedSafetyChecks -join ', ') + '.' +
$playoutDiagnostic)
}
if ($HarnessProfile -ceq 'ReadOnly' -or
$HarnessProfile -ceq 'DryRunPlayout' -or
$HarnessProfile -ceq 'ProgramPlaylistDb') {
$allowedOutbound = @(
'ready', 'search-stocks', 'select-stock', 'cut-pointer-down',
'cut-key-down', 'drop-selected-cuts', 'select-tab',
'hover-swap-tabs',
'activate-playlist-row',
'select-playlist-row', 'move-selected-playlist-rows',
'reorder-playlist-rows',
'select-playlist-boundary', 'delete-selected-playlist-rows',
'set-playlist-enabled',
'refresh-named-playlists')
if ($HarnessProfile -ceq 'DryRunPlayout') {
$allowedOutbound += @(
'choose-background', 'toggle-background',
'take-in', 'next-playout', 'take-out',
'set-all-playlist-enabled',
'toggle-active-playlist-enabled')
}
elseif ($HarnessProfile -ceq 'ProgramPlaylistDb') {
$allowedOutbound += @(
'select-named-playlist',
'create-named-playlist',
'save-current-named-playlist-to',
'delete-selected-named-playlist',
'dismiss-dialog',
'take-in', 'take-out')
}
foreach ($messageType in @($evidence.safety.observedOutboundMessageTypes)) {
if ($allowedOutbound -cnotcontains [string]$messageType) {
Throw-SafeFailure 'The evidence contains an outbound message outside the selected non-write allowlist.'
}
}
}
else {
$playoutMessages = @(
'prepare-playout', 'take-in', 'next-playout', 'take-out',
'choose-background', 'toggle-background')
foreach ($messageType in @($evidence.safety.observedOutboundMessageTypes)) {
if ($playoutMessages -ccontains [string]$messageType) {
Throw-SafeFailure 'The full UI/DB evidence contains a playout message.'
}
}
}
$screenshotItem = Get-Item -LiteralPath $ScreenshotPath -Force
$screenshotHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ScreenshotPath).Hash
if ($screenshotItem.PSIsContainer -or
($screenshotItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
$screenshotItem.Length -le 0 -or
[int64]$evidence.screenshot.bytes -ne $screenshotItem.Length -or
[string]$evidence.screenshot.sha256 -cne $screenshotHash) {
Throw-SafeFailure 'The screenshot evidence does not match its sealed file.'
}
$expectedExchangeCount = Get-ExpectedWindowsInputExchangeCount `
$HarnessProfile `
$HarnessScope
$sealedExchanges = @($evidence.windowsInput.exchanges)
if ([bool]$evidence.windowsInput.required -ne $true -or
[string]$evidence.windowsInput.result -cne 'PASS' -or
[int]$evidence.windowsInput.expectedExchangeCount -ne $expectedExchangeCount -or
[int]$evidence.windowsInput.completedExchangeCount -ne $expectedExchangeCount -or
$sealedExchanges.Count -ne $expectedExchangeCount -or
-not (Test-PathEqual ([string]$evidence.windowsInput.requestPath) `
$WindowsInputRequestPath) -or
-not (Test-PathEqual ([string]$evidence.windowsInput.acknowledgementPath) `
$WindowsInputAcknowledgementPath)) {
Throw-SafeFailure 'The sealed Windows input exchange count/path contract is not exact.'
}
$request = $null
for ($exchangeIndex = 1;
$exchangeIndex -le $expectedExchangeCount;
$exchangeIndex++) {
$requestPath = Get-WindowsInputExchangePath `
$WindowsInputRequestPath `
$exchangeIndex
$acknowledgementPath = Get-WindowsInputExchangePath `
$WindowsInputAcknowledgementPath `
$exchangeIndex
$requestEvidence = Read-SmallJsonEvidenceFile `
$requestPath `
"Final Windows input request $exchangeIndex"
$acknowledgementEvidence = Read-SmallJsonEvidenceFile `
$acknowledgementPath `
"Final Windows input acknowledgement $exchangeIndex"
$sealedExchange = $sealedExchanges[$exchangeIndex - 1]
if ([int]$sealedExchange.exchangeIndex -ne $exchangeIndex -or
[string]$sealedExchange.operation -cne
[string]$requestEvidence.Value.operation -or
[string]$sealedExchange.result -cne 'PASS' -or
-not (Test-PathEqual ([string]$sealedExchange.requestPath) $requestPath) -or
-not (Test-PathEqual `
([string]$sealedExchange.acknowledgementPath) `
$acknowledgementPath) -or
[string]$sealedExchange.requestSha256 -cne $requestEvidence.Sha256 -or
[string]$sealedExchange.acknowledgementSha256 -cne
$acknowledgementEvidence.Sha256) {
Throw-SafeFailure 'A sealed Windows input exchange does not match its files.'
}
if ($exchangeIndex -eq 1) {
$request = $requestEvidence.Value
if ([string]$request.operation -cne
'row-header-drag-home-shift-range-delete-restore' -or
[string]$evidence.windowsInput.requestSha256 -cne
$requestEvidence.Sha256 -or
[string]$evidence.windowsInput.acknowledgementSha256 -cne
$acknowledgementEvidence.Sha256 -or
-not (Test-ExactStringSequence `
@($evidence.windowsInput.beforeOrder) `
@($request.beforeOrder)) -or
-not (Test-ExactStringSequence `
@($evidence.windowsInput.afterOrder) `
@($request.afterOrder)) -or
-not (Test-ExactStringSequence `
@($evidence.windowsInput.shiftRangeDeletedRowIds) `
@($request.expectedRangeSelectedRowIds)) -or
[string]$evidence.windowsInput.activeRowIdAfterHome -cne
[string]$request.expectedActiveRowIdAfterHome -or
[string]$evidence.windowsInput.finalActiveRowId -cne
[string]$request.expectedFinalActiveRowId) {
Throw-SafeFailure 'The sealed playlist Windows input evidence is not exact.'
}
}
elseif ($HarnessProfile -ceq 'DryRunPlayout' -and
$exchangeIndex -eq 2) {
if ([string]$requestEvidence.Value.operation -cne
'background-f2-picker-cancel' -or
[int]$requestEvidence.Value.f2KeyCalls -ne 1 -or
[int]$requestEvidence.Value.escapeKeyCalls -ne 1 -or
[int]$acknowledgementEvidence.Value.f2KeyCalls -ne 1 -or
[int]$acknowledgementEvidence.Value.escapeKeyCalls -ne 1 -or
$acknowledgementEvidence.Value.pickerUniqueNew -ne $true -or
$acknowledgementEvidence.Value.pickerOwnerLinked -ne $true -or
$acknowledgementEvidence.Value.pickerSameProcess -ne $true -or
[string]$acknowledgementEvidence.Value.pickerClass -cne '#32770' -or
[string]$acknowledgementEvidence.Value.pickerTitle -cne
$BackgroundPickerTitle -or
$acknowledgementEvidence.Value.pickerModal -ne $true -or
$acknowledgementEvidence.Value.pickerClosedAfterEscape -ne $true -or
$evidence.dryRunPlayout.backgroundShortcutParity.pickerOwnedAndCancelled -ne
$true) {
Throw-SafeFailure 'The sealed physical F2 picker cancellation evidence is not exact.'
}
}
elseif ($HarnessProfile -ceq 'DryRunPlayout' -and
$exchangeIndex -eq 3) {
if ([string]$requestEvidence.Value.operation -cne
'background-f3-none' -or
[int]$requestEvidence.Value.f3KeyCalls -ne 1 -or
[int]$acknowledgementEvidence.Value.f3KeyCalls -ne 1 -or
$evidence.dryRunPlayout.backgroundShortcutParity.defaultAssetAbsentFailClosed -ne
$true -or
$evidence.dryRunPlayout.backgroundShortcutParity.finalBackgroundEnabled -ne
$false -or
$evidence.dryRunPlayout.backgroundShortcutParity.finalBackgroundNoneChecked -ne
$true) {
Throw-SafeFailure 'The sealed physical F3 background-none evidence is not exact.'
}
}
elseif ([string]$requestEvidence.Value.operation -cne
'named-playlist-load-double-click' -or
[string]$requestEvidence.Value.definitionTitle -cne
[string]$evidence.fixture.namedPlaylistTitle -or
@($requestEvidence.Value.expectedPlaylistContent).Count -ne 3 -or
@($evidence.fullUiDb.databaseChecks | Where-Object {
[string]$_.name -ceq
'PList physical double-click fresh readback' -and
$_.details.contentRestored -eq $true -and
$_.details.startedUnselected -eq $true -and
[int]$_.details.selectionIntentCount -eq 1 -and
[int]$_.details.loadIntentCount -eq 1 -and
[string]$_.details.input -ceq 'Windows SendInput double-click'
}).Count -ne 1) {
Throw-SafeFailure 'The sealed PList physical double-click evidence is not exact.'
}
}
$snapshot = $evidence.finalSnapshot
$state = $snapshot.state
$playout = $state.playout
$dom = $snapshot.dom
if ($null -eq $snapshot -or $null -eq $state -or $null -eq $playout -or
$null -eq $dom -or
$state.isBusy -ne $false -or
[string]$playout.mode -cne 'dryRun' -or
[string]$playout.connectionState -cne 'dryRunReady' -or
[string]$playout.phase -cne 'idle' -or
$playout.isConnected -ne $false -or
$playout.liveTakeInAllowed -ne $false -or
$playout.isPlayCompletionPending -ne $false -or
$playout.isTakeOutCompletionPending -ne $false -or
$playout.outcomeUnknown -ne $false -or
$playout.isBusy -ne $false -or
$playout.refreshActive -ne $false -or
$null -ne $playout.preparedCode -or
$null -ne $playout.onAirCode -or
$null -ne $playout.currentEntryId -or
$snapshot.safety.postMessageWrapped -ne $true -or
@($snapshot.safety.blockedOutboundMessages).Count -ne 0 -or
@($snapshot.safety.stateViolations).Count -ne 0 -or
[string]$dom.readyState -cne 'complete' -or
[string]$dom.url -cne 'https://legacy-parity.mbn.local/index.html' -or
[string]$dom.bodyBusy -cne 'false' -or
[string]$dom.connectionText -cne 'DRY RUN' -or
$dom.legacyDialogHidden -ne $true -or
$dom.namedModalHidden -ne $true -or
$dom.namedConfirmationHidden -ne $true) {
Throw-SafeFailure 'The sealed final application state is not exact DryRun IDLE.'
}
$expectedAfterOrder = @($request.afterOrder | ForEach-Object { [string]$_ })
$expectedFinalOrder = @($expectedAfterOrder)
if ($HarnessProfile -ceq 'DryRunPlayout') {
# DryRun deliberately proves that a PROGRAM cut double-click appends one
# safe tail row. Preserve the sealed Windows drag order as the prefix and
# require that exact new opaque row id at the tail after TAKE OUT.
$expectedFinalOrder += @(
[string]$evidence.dryRunPlayout.tailAppendedEntryId)
}
$statePlaylist = @($state.playlist)
$domPlaylist = @($dom.playlist)
$statePlaylistIds = @($statePlaylist | ForEach-Object { [string]$_.rowId })
$domPlaylistIds = @($domPlaylist | ForEach-Object { [string]$_.rowId })
$selectedStateIds = @($statePlaylist | Where-Object { $_.isSelected -eq $true } |
ForEach-Object { [string]$_.rowId })
$activeStateIds = @($statePlaylist | Where-Object { $_.isActive -eq $true } |
ForEach-Object { [string]$_.rowId })
$selectedDomIds = @($domPlaylist | Where-Object { $_.selected -eq $true } |
ForEach-Object { [string]$_.rowId })
$activeDomIds = @($domPlaylist | Where-Object { $_.active -eq $true } |
ForEach-Object { [string]$_.rowId })
$dragVisualCount = @($domPlaylist | Where-Object {
$_.dragging -eq $true -or $_.dragBefore -eq $true -or $_.dragAfter -eq $true
}).Count
if (($HarnessProfile -ceq 'ReadOnly' -or
$HarnessProfile -ceq 'DryRunPlayout' -or
$HarnessProfile -ceq 'ProgramPlaylistDb') -and (
-not (Test-ExactStringSequence $statePlaylistIds $expectedFinalOrder) -or
-not (Test-ExactStringSequence $domPlaylistIds $expectedFinalOrder) -or
-not (Test-ExactStringSequence $selectedStateIds @([string]$request.sourceRowId)) -or
-not (Test-ExactStringSequence $selectedDomIds @([string]$request.sourceRowId)) -or
-not (Test-ExactStringSequence `
$activeStateIds `
@([string]$request.expectedFinalActiveRowId)) -or
-not (Test-ExactStringSequence `
$activeDomIds `
@([string]$request.expectedFinalActiveRowId)) -or
$dragVisualCount -ne 0 -or
@($dom.playlistPointerCapture.rowIds).Count -ne 0)) {
Throw-SafeFailure 'The sealed final playlist state does not match the Windows input result.'
}
$expectedFullUiDbPlaylistCount = if ($HarnessScope -ceq 'GraphE' -or
$HarnessScope -ceq 'Screens' -or $HarnessScope -ceq 'All') { 0 } else { 3 }
if ($HarnessProfile -ceq 'FullUiDb' -and (
$statePlaylist.Count -ne $expectedFullUiDbPlaylistCount -or
$domPlaylist.Count -ne $expectedFullUiDbPlaylistCount -or
$dragVisualCount -ne 0 -or
@($dom.playlistPointerCapture.rowIds).Count -ne 0)) {
Throw-SafeFailure 'The sealed full UI/DB playlist readback is not exact.'
}
return $evidence
}
catch [InvalidOperationException] {
throw
}
catch {
# Never echo evidence JSON or parser diagnostics.
Throw-SafeFailure 'The real input evidence could not be validated safely.'
}
}
if ($DefinitionsOnly) {
return
}
$recoveryRequested = -not [string]::IsNullOrEmpty($RecoverNamedPlaylistTitle)
if ($recoveryRequested -and
($RecoverNamedPlaylistTitle.Length -ne 26 -or
$RecoverNamedPlaylistTitle -cnotmatch '^CDX_P_[0-9]{13}_[0-9a-f]{6}$')) {
Throw-SafeFailure 'RecoverNamedPlaylistTitle must be one exact generated CDX_P fixture title.'
}
if ($recoveryRequested -and
($Profile -cne 'FullUiDb' -or
($FullUiDbScope -cne 'PList' -and $FullUiDbScope -cne 'All'))) {
Throw-SafeFailure 'Named-playlist recovery is allowed only for FullUiDb PList or All.'
}
$timestamp = [datetime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ')
$Output = Resolve-EvidencePath $Output "legacy-package-input-$timestamp.json"
$Screenshot = Resolve-EvidencePath $Screenshot "legacy-package-input-$timestamp.png"
$WindowsInputRequest = [IO.Path]::ChangeExtension(
$Output,
'.windows-input-request.json')
$WindowsInputAcknowledgement = [IO.Path]::ChangeExtension(
$Output,
'.windows-input-ack.json')
$ExpectedWindowsInputExchangeCount = Get-ExpectedWindowsInputExchangeCount `
$Profile `
$FullUiDbScope
$WindowsInputRequestPaths = @(1..$ExpectedWindowsInputExchangeCount | ForEach-Object {
Get-WindowsInputExchangePath $WindowsInputRequest $_
})
$WindowsInputAcknowledgementPaths = @(1..$ExpectedWindowsInputExchangeCount |
ForEach-Object {
Get-WindowsInputExchangePath $WindowsInputAcknowledgement $_
})
$NodePath = Resolve-NodeExecutable $NodePath
try {
if (-not [IO.File]::Exists($HarnessPath)) {
Throw-SafeFailure 'The tracked package input harness is missing.'
}
if (-not [string]::Equals(
[IO.Path]::GetExtension($Output), '.json', [StringComparison]::OrdinalIgnoreCase) -or
-not [string]::Equals(
[IO.Path]::GetExtension($Screenshot), '.png', [StringComparison]::OrdinalIgnoreCase)) {
Throw-SafeFailure 'Output must be .json and screenshot must be .png.'
}
$evidencePaths = @($Output, $Screenshot) +
$WindowsInputRequestPaths + $WindowsInputAcknowledgementPaths
for ($leftIndex = 0; $leftIndex -lt $evidencePaths.Count; $leftIndex++) {
for ($rightIndex = $leftIndex + 1; $rightIndex -lt $evidencePaths.Count; $rightIndex++) {
if (Test-PathEqual $evidencePaths[$leftIndex] $evidencePaths[$rightIndex]) {
Throw-SafeFailure 'All evidence paths must be different.'
}
}
}
Assert-OutputPathAvailable $Output 'Output'
Assert-OutputPathAvailable $Screenshot 'Screenshot'
for ($exchangeIndex = 1;
$exchangeIndex -le $ExpectedWindowsInputExchangeCount;
$exchangeIndex++) {
Assert-OutputPathAvailable $WindowsInputRequestPaths[$exchangeIndex - 1] `
"Windows input request $exchangeIndex"
Assert-OutputPathAvailable $WindowsInputAcknowledgementPaths[$exchangeIndex - 1] `
"Windows input acknowledgement $exchangeIndex"
}
Assert-LocalConfigurationIsDryRun
Assert-NoUnsafeEnvironment
$registration = Get-ExactDevelopmentPackage
Assert-NoExistingApplication
Assert-PortIsFree $Port
Initialize-BackgroundShortcutFixture $registration.InstallLocation
$script:Phase = 'package launch'
$script:ApplicationProcess = Start-PackagedDryRunApplication $registration $Port
$script:ApplicationWasLaunched = $true
[void](Wait-ExactLoopbackListener `
$Port `
$script:ApplicationProcess `
([datetime]::UtcNow.AddSeconds($LaunchTimeoutSeconds)))
Assert-NoTornadoConnection $script:ApplicationProcess.Id
Wait-StartupIntroDismissed `
$script:ApplicationProcess `
$registration.Executable
$script:Phase = 'real input harness'
$harnessRun = Invoke-NodeHarnessUnderTcpWatch `
$NodePath `
$HarnessPath `
$Port `
$Output `
$Screenshot `
$WindowsInputRequest `
$WindowsInputAcknowledgement `
$(if ($Profile -ceq 'FullUiDb') {
'full-ui-db'
}
elseif ($Profile -ceq 'DryRunPlayout') {
'dry-run-playout'
}
elseif ($Profile -ceq 'ProgramPlaylistDb') {
'program-playlist-db'
}
else {
'read-only'
}) `
$(if ($Profile -ceq 'FullUiDb') { $FullUiDbScope.ToLowerInvariant() } else { 'all' }) `
$RecoverNamedPlaylistTitle `
$HarnessTimeoutSeconds `
$script:ApplicationProcess `
$registration.Executable
if ($harnessRun.ExitCode -ne 0) {
Throw-SafeFailure "The real input harness failed with exit code $($harnessRun.ExitCode)."
}
if (-not [IO.File]::Exists($Output) -or
-not [IO.File]::Exists($Screenshot) -or
(Get-Item -LiteralPath $Output).Length -le 0 -or
(Get-Item -LiteralPath $Screenshot).Length -le 0 -or
@($WindowsInputRequestPaths + $WindowsInputAcknowledgementPaths |
Where-Object {
-not [IO.File]::Exists($_) -or
(Get-Item -LiteralPath $_).Length -le 0
}).Count -ne 0) {
Throw-SafeFailure 'The real input evidence files are missing or empty.'
}
$harnessEvidence = Read-AndAssertHarnessEvidence `
$Output `
$Screenshot `
$WindowsInputRequest `
$WindowsInputAcknowledgement `
$Profile `
$FullUiDbScope `
$RecoverNamedPlaylistTitle `
$Port `
$HarnessTimeoutSeconds
$wrapperWindowsInput = @($harnessRun.WindowsInput)
$sealedWindowsInput = @($harnessEvidence.windowsInput.exchanges)
if ($wrapperWindowsInput.Count -ne $ExpectedWindowsInputExchangeCount -or
$sealedWindowsInput.Count -ne $ExpectedWindowsInputExchangeCount) {
Throw-SafeFailure 'The wrapper and harness Windows input exchange counts differ.'
}
for ($exchangeIndex = 1;
$exchangeIndex -le $ExpectedWindowsInputExchangeCount;
$exchangeIndex++) {
$wrapperExchange = $wrapperWindowsInput[$exchangeIndex - 1]
$sealedExchange = $sealedWindowsInput[$exchangeIndex - 1]
if ([int]$wrapperExchange.ExchangeIndex -ne $exchangeIndex -or
[int]$sealedExchange.exchangeIndex -ne $exchangeIndex -or
[string]$wrapperExchange.Operation -cne [string]$sealedExchange.operation -or
[string]$wrapperExchange.RequestSha256 -cne
[string]$sealedExchange.requestSha256 -or
[string]$wrapperExchange.AcknowledgementSha256 -cne
[string]$sealedExchange.acknowledgementSha256) {
Throw-SafeFailure 'The wrapper and harness Windows input exchange hashes differ.'
}
}
Assert-ExactApplicationStillRunning $script:ApplicationProcess $registration.Executable
Assert-NoTornadoConnection $script:ApplicationProcess.Id
if ($Profile -cne 'FullUiDb' -and $Profile -cne 'ProgramPlaylistDb') {
$script:Phase = 'single-instance activation'
$singleInstance = Assert-SingleInstanceActivation `
$script:ApplicationProcess `
$registration.Executable
}
else {
# The read-only package smoke already owns the single-instance contract.
# Avoid launching an unrelated redirector after an authorized DB-write run.
$singleInstance = [pscustomobject]@{
ActivationCalls = 0
ProcessCount = 1
RedirectorProcessId = $null
RedirectorStartTimeUtc = $null
RedirectorExited = $null
StableOriginalMilliseconds = $null
}
}
$script:Phase = 'normal close'
Invoke-ExactNormalClose $script:ApplicationProcess $registration.Executable
Wait-PortReleased $Port ([datetime]::UtcNow.AddSeconds(15))
if (@(Get-LegacyApplicationProcesses).Count -ne 0) {
Throw-SafeFailure 'A LegacyParity process remained after normal exit.'
}
Remove-BackgroundShortcutFixture
$script:Phase = 'complete'
[pscustomobject]@{
result = [string]$harnessEvidence.result
packageFullName = $PackageFullName
packageMode = $PackageModeLabel
playoutMode = 'DryRun'
processId = $script:ApplicationProcess.Id
cdpAddress = '127.0.0.1'
cdpPort = $Port
tornadoPortConnections = 0
tornadoMonitoringScope = 'application PID from exact launch identity through normal exit'
windowsInputRequestCalls = $script:WindowsInputRequestCalls
windowsInputAcknowledgementCalls = $script:WindowsInputAcknowledgementCalls
windowsInputRequest = $WindowsInputRequest
windowsInputRequestSha256 = [string]$wrapperWindowsInput[0].RequestSha256
windowsInputAcknowledgement = $WindowsInputAcknowledgement
windowsInputAcknowledgementSha256 = `
[string]$wrapperWindowsInput[0].AcknowledgementSha256
windowsInputExchanges = @($wrapperWindowsInput)
singleInstanceActivationCalls = $singleInstance.ActivationCalls
singleInstanceProcessCount = $singleInstance.ProcessCount
singleInstanceRedirectorProcessId = $singleInstance.RedirectorProcessId
singleInstanceRedirectorStartTimeUtc = $singleInstance.RedirectorStartTimeUtc
singleInstanceRedirectorExited = $singleInstance.RedirectorExited
singleInstanceStableOriginalMilliseconds = $singleInstance.StableOriginalMilliseconds
closeMainWindowCalls = $script:CloseMainWindowCalls
primaryInvokeCalls = $script:PrimaryInvokeCalls
output = $Output
outputSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Output).Hash
screenshot = $Screenshot
screenshotSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Screenshot).Hash
}
}
catch {
$message = $_.Exception.Message
if ($script:CloseMainWindowCalls -gt 0 -or $script:PrimaryInvokeCalls -gt 0) {
Throw-SafeFailure (
"Package input smoke became ambiguous during '$($script:Phase)': $message " +
'No close retry or forced termination was attempted.')
}
$running = $false
if ($script:ApplicationWasLaunched -and $null -ne $script:ApplicationProcess) {
try {
$script:ApplicationProcess.Refresh()
$running = -not $script:ApplicationProcess.HasExited
}
catch {
$running = $false
}
}
if (-not $running) {
try {
$running = @(Get-LegacyApplicationProcesses).Count -gt 0
}
catch {
# An inability to re-enumerate cannot authorize another close attempt.
$running = $false
}
}
if ($running) {
Throw-SafeFailure (
"Package input smoke stopped during '$($script:Phase)': $message " +
'The application was intentionally left open; no close retry or forced termination was attempted.')
}
if ($script:BackgroundShortcutRootCreated) {
Remove-BackgroundShortcutFixture
}
throw
}