[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 windows = new List(); 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( 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( AddressFamilyInterNetworkV6, delegate(MibTcp6RowOwnerPid row) { return row.OwningProcessId == (uint)processId && (DecodePort(row.LocalPort) == port || DecodePort(row.RemotePort) == port); }); } private static bool ScanTcpTable(int addressFamily, Func 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 }