# -IncludeTheme also exercises theme writes. Those broadcast WM_SETTINGCHANGE to # every window on the desktop and have been observed to wedge apps that react to # it, so they stay opt-in. param([switch] $IncludeTheme) $ErrorActionPreference = 'Stop' $script = Join-Path $PSScriptRoot 'win32-drive.ps1' if (-not (Test-Path $script)) { throw "Missing script: $script" } Add-Type -AssemblyName System.Drawing function Invoke-Raw($arguments, [switch] $AllowFail) { $raw = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script @arguments $code = $LASTEXITCODE if (-not $AllowFail -and $code -ne 0) { throw "Command failed: $($arguments -join ' ')`n$raw" } [pscustomobject]@{ Code = $code; Json = ($raw | ConvertFrom-Json); Raw = $raw } } function Invoke-Json($arguments) { (Invoke-Raw $arguments).Json } function Assert($condition, $message) { if (-not $condition) { throw $message } } function Assert-File($path) { Assert (Test-Path $path) "missing file: $path" } function Assert-Err($arguments, $pattern) { $r = Invoke-Raw $arguments -AllowFail Assert ($r.Code -ne 0) "expected failure: $($arguments -join ' ')" Assert ($r.Json.ok -eq $false) 'failure JSON must have ok=false' Assert ($r.Json.error -match $pattern) "error '$($r.Json.error)' does not match '$pattern'" } function Wait-Title($hwnd, [string] $Expected, [int] $TimeoutMs = 4000) { $deadline = (Get-Date).AddMilliseconds($TimeoutMs) do { $win = @(Invoke-Json @('list')).windows | Where-Object hwnd -eq $hwnd | Select-Object -First 1 if ($win -and $win.title -eq $Expected) { return $win.title } Start-Sleep -Milliseconds 120 } while ((Get-Date) -lt $deadline) throw "window title never became '$Expected' (last: '$($win.title)')" } function Start-TestWindow($title) { $app = Join-Path $env:TEMP ("win32-drive-app-" + [guid]::NewGuid().ToString('N') + '.ps1') @" `$ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms,System.Drawing [Windows.Forms.Application]::EnableVisualStyles() `$form = New-Object Windows.Forms.Form `$form.Text = '$title' `$form.StartPosition = 'Manual' `$form.SetBounds(80,80,420,260) `$form.BackColor = [Drawing.Color]::FromArgb(0,120,215) `$box = New-Object Windows.Forms.TextBox `$box.Multiline = `$true `$box.Name = 'InputBox' `$box.SetBounds(20,20,360,140) `$button = New-Object Windows.Forms.Button `$button.Name = 'ClickMe' `$button.Text = 'ClickMe' `$button.SetBounds(20,175,100,32) `$button.Add_Click({ `$form.Text = '$title clicked' }) `$box.Add_TextChanged({ `$form.Text = '$title text:' + `$box.Text }) `$form.Controls.Add(`$box) `$form.Controls.Add(`$button) `$form.Add_Shown({ `$box.Focus() }) [Windows.Forms.Application]::Run(`$form) "@ | Set-Content -Encoding UTF8 $app $p = Start-Process powershell.exe -ArgumentList @('-STA','-NoProfile','-ExecutionPolicy','Bypass','-File',$app) -PassThru $deadline = (Get-Date).AddSeconds(8) do { Start-Sleep -Milliseconds 150 $wins = @(Invoke-Json @('list')).windows | Where-Object { $_.pid -eq $p.Id -and $_.title -like "$title*" } } while ($wins.Count -eq 0 -and (Get-Date) -lt $deadline) if ($wins.Count -eq 0) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue; throw 'test window did not appear' } Start-Sleep -Milliseconds 500 [pscustomobject]@{ Process = $p; Script = $app; Window = $wins[0]; Title = $title } } $created = @() $originalClipboard = $null try { try { $originalClipboard = (Invoke-Json @('clipboard')).text } catch { $originalClipboard = $null } $help = Invoke-Json @('help') foreach ($cmd in 'help','list','shot','pixel','uia','uia-invoke','uia-set','cursor','focus','idle','clipboard','overlay','click','type','key','window','theme') { Assert ($help.commands.PSObject.Properties.Name -contains $cmd) "help missing $cmd" } foreach ($cmd in 'click','type','key','window','theme','uia-invoke','uia-set') { Assert ($help.commands.$cmd.racing -eq $true) "$cmd must be racing" } foreach ($cmd in 'list','shot','pixel','uia','cursor','focus','idle','overlay') { Assert ($help.commands.$cmd.racing -eq $false) "$cmd must be nonracing" } Assert ($help.dpiAwareness -in @('per-monitor-v2','per-monitor','system','unaware')) 'help must report dpi awareness' Assert ($help.dpiAwareness -ne 'unaware') 'process must be dpi aware' Assert ($help.nativeLoad -in @('cache','compiled','loaded')) 'help must report how the native assembly loaded' Assert ($help.cmd -eq 'help' -and $help.elapsedMs -ge 0) 'every result needs cmd and elapsedMs' # batch: one process start, ordered steps, per-step envelopes. # Steps go through a file because shells mangle embedded quotes. function Invoke-Batch([string] $StepsJson, [switch] $AllowFail, [string[]] $Extra = @()) { $path = Join-Path $env:TEMP ("win32-drive-test-batch-" + [guid]::NewGuid().ToString('N') + '.json') Set-Content -LiteralPath $path -Value $StepsJson -Encoding UTF8 try { (Invoke-Raw (@('batch','-File',$path) + $Extra) -AllowFail:$AllowFail) } finally { Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue } } $batch = (Invoke-Batch '[{"cmd":"focus"},{"cmd":"wait","ms":50},{"cmd":"idle"}]').Json Assert ($batch.ok -and $batch.steps -eq 3 -and $batch.ran -eq 3) 'batch must run every step' Assert ($batch.results[0].cmd -eq 'focus' -and $batch.results[0].step -eq 0) 'batch results need cmd and step' Assert ($batch.results[1].waitedMs -eq 50) 'batch wait step failed' Assert ($null -eq $batch.failedAt) 'successful batch must not report failedAt' $batchFail = Invoke-Batch '[{"cmd":"idle"},{"cmd":"nonsense"},{"cmd":"idle"}]' -AllowFail Assert ($batchFail.Json.ok -eq $false -and $batchFail.Json.failedAt -eq 1 -and $batchFail.Json.ran -eq 2) 'batch must stop at the failing step' Assert ($batchFail.Json.results[1].code -eq 'unknown-command') 'batch failures must carry the error code' $batchContinue = Invoke-Batch '[{"cmd":"nonsense"},{"cmd":"idle"}]' -AllowFail -Extra @('-ContinueOnError') Assert ($batchContinue.Json.ran -eq 2) 'batch -ContinueOnError must keep going' $batchBogus = Invoke-Batch '[{"cmd":"idle","Bogus":1}]' -AllowFail Assert ($batchBogus.Json.code -eq 'unknown-option' -and $batchBogus.Json.error -match 'step 0') 'batch must reject unknown step options' # structured errors carry codes, hints, and context $noWindow = (Invoke-Raw @('list','-Hwnd','1') -AllowFail).Json Assert ($noWindow.count -eq 0) 'list with an unmatched selector returns no windows' $stale = (Invoke-Raw @('window','-Hwnd','999999999','-Action','min','-IdleMs','0') -AllowFail).Json Assert ($stale.code -eq 'stale-window' -and $stale.hint) 'stale handles need code stale-window plus a hint' $missing = (Invoke-Raw @('shot','-TitleContains',('no-window-' + [guid]::NewGuid().ToString('N'))) -AllowFail).Json Assert ($missing.code -eq 'no-window' -and $missing.candidates.Count -gt 0) 'no-window must list candidates' $consent = (Invoke-Raw @('click','-X','10','-Y','10','-IdleMs','0') -AllowFail).Json Assert ($consent.code -eq 'pointer-consent') 'click without -Pointer must fail with pointer-consent' $list = Invoke-Json @('list') Assert ($list.windows.Count -gt 0) 'list should return visible windows' Assert ($list.count -eq $list.windows.Count -and $null -ne $list.foreground -and $null -ne $list.idleMs) 'list must report count, foreground, and idle time' Assert (($list.windows | Where-Object { $_.focused }).Count -le 1) 'at most one window can be focused' Assert ($list.windows[0].z -eq 0) 'list must report z order' $idle = Invoke-Json @('idle') Assert ($idle.idleMs -is [int] -or $idle.idleMs -is [long]) 'idleMs must be numeric' $cursor = Invoke-Json @('cursor') Assert ($cursor.x -is [int] -and $cursor.y -is [int]) 'cursor must have x/y ints' Assert ((Invoke-Json @('overlay','-DurationMs','1')).ok) 'overlay failed' Assert ((Invoke-Json @('overlay','-X','10','-Y','10','-DurationMs','1')).ok) 'overlay target failed' $px = Invoke-Json @('pixel','-X','0','-Y','0') Assert ($px.hex -match '^#[0-9A-F]{6}$' -and $px.r -ge 0 -and $px.r -le 255 -and $px.g -ge 0 -and $px.g -le 255 -and $px.b -ge 0 -and $px.b -le 255) 'pixel failed' $shotPath = Join-Path $env:TEMP ("win32-drive-test-region-" + [guid]::NewGuid().ToString('N') + '.png') $created += $shotPath $shot = Invoke-Json @('shot','-X','0','-Y','0','-Width','8','-Height','8','-Out',$shotPath) Assert-File $shot.path Assert ($shot.width -eq 8 -and $shot.height -eq 8 -and $shot.mode -eq 'region') 'region shot metadata wrong' $app = Start-TestWindow ("Win32DriveTest-" + [guid]::NewGuid().ToString('N')) $created += $app.Script $hwnd = [string]$app.Window.hwnd $winShotPath = Join-Path $env:TEMP ("win32-drive-test-window-" + [guid]::NewGuid().ToString('N') + '.png') $created += $winShotPath $winShot = Invoke-Json @('shot','-Hwnd',$hwnd,'-Out',$winShotPath) Assert-File $winShot.path Assert ($winShot.hwnd -eq $app.Window.hwnd -and $winShot.width -gt 0 -and $winShot.height -gt 0) 'window shot metadata wrong' Assert ($winShot.width -eq $app.Window.width -and $winShot.height -eq $app.Window.height) 'window shot must match the frame bounds reported by list' Assert ((Invoke-Json @('uia','-Depth','0')).ok) 'focused uia failed' $uia = Invoke-Json @('uia','-Hwnd',$hwnd,'-Depth','1') Assert ($uia.tree.name -like 'Win32DriveTest-*') 'uia root title wrong' Assert ($uia.tree.children.Count -gt 0) 'uia children missing' Assert ($null -eq $uia.tree.error) 'uia root must not carry an error' Assert ((Invoke-Json @('uia','-Hwnd',$hwnd,'-Depth','0')).tree.truncated -eq $true) 'uia must flag truncated subtrees' # Capture origin must be the frame origin reported by list/shot: a control's # screen rect minus that origin must hit the same control inside the image. # This catches the PrintWindow shadow offset without depending on z-order. $edge = $uia.tree.children | Where-Object { $_.name -ne 'ClickMe' -and $_.width -gt 200 } | Select-Object -First 1 Assert ($null -ne $edge) 'uia did not expose the text box' $geometryShotPath = Join-Path $env:TEMP ("win32-drive-test-geometry-" + [guid]::NewGuid().ToString('N') + '.png') $created += $geometryShotPath $geometryShot = Invoke-Json @('shot','-Hwnd',$hwnd,'-Out',$geometryShotPath) $bitmap = [Drawing.Bitmap]::FromFile($geometryShot.path) try { $inside = $bitmap.GetPixel([int]($edge.x - $geometryShot.x + 8), [int]($edge.y - $geometryShot.y + 8)) $outside = $bitmap.GetPixel([int]($edge.x - $geometryShot.x - 4), [int]($edge.y - $geometryShot.y - 4)) } finally { $bitmap.Dispose() } Assert ($inside.R -gt 200 -and $inside.G -gt 200 -and $inside.B -gt 200) "capture offset: inside text box is $($inside.R),$($inside.G),$($inside.B), expected white (mode=$($geometryShot.mode) shot=$($geometryShot.x),$($geometryShot.y) $($geometryShot.width)x$($geometryShot.height) edge=$($edge.x),$($edge.y) $($edge.width)x$($edge.height) file=$($geometryShot.path))" Assert ($outside.B -gt $outside.R) "capture offset: outside text box is $($outside.R),$($outside.G),$($outside.B), expected the blue form background" # Accessibility actions must change the app without touching focus or pointer. $foregroundBefore = Invoke-Json @('focus') $invoked = Invoke-Json @('uia-invoke','-Hwnd',$hwnd,'-Name','ClickMe','-IdleMs','0','-TimeoutMs','1000') Assert ($invoked.pattern -in @('Invoke','MsaaDoDefaultAction','BM_CLICK')) "unexpected invoke pattern: $($invoked.pattern)" Wait-Title $app.Window.hwnd "$($app.Title) clicked" | Out-Null $uiaText = 'uia-set-' + [guid]::NewGuid().ToString('N') $valued = Invoke-Json @('uia-set','-Hwnd',$hwnd,'-ClassName','*EDIT*','-Value',$uiaText,'-IdleMs','0','-TimeoutMs','1000') Assert ($valued.pattern -in @('Value','WM_CHAR')) "unexpected value pattern: $($valued.pattern)" Wait-Title $app.Window.hwnd "$($app.Title) text:$uiaText" | Out-Null Assert ((Invoke-Json @('focus')).hwnd -eq $foregroundBefore.hwnd) 'accessibility actions must not change the foreground window' Assert-Err @('uia-invoke','-Hwnd',$hwnd,'-IdleMs','0','-TimeoutMs','1000') 'need -Name, -AutomationId, -ClassName, or -ControlType' Assert-Err @('uia-invoke','-Name','ClickMe','-IdleMs','0','-TimeoutMs','1000') 'need a window selector' Assert-Err @('uia-set','-Hwnd',$hwnd,'-ClassName','*EDIT*','-IdleMs','0','-TimeoutMs','1000') 'uia-set needs -Value' Assert-Err @('uia-invoke','-Hwnd',$hwnd,'-Name','definitely-no-such-control','-IdleMs','0','-TimeoutMs','1000') 'No matching element' # pointer input needs explicit consent Assert ((Invoke-Raw @('click','-Hwnd',$hwnd,'-IdleMs','0','-TimeoutMs','1000') -AllowFail).Json.code -eq 'pointer-consent') 'click without -Pointer must fail with pointer-consent' Assert ((Invoke-Raw @('type','-Hwnd',$hwnd,'-Text','x','-Focus','auto','-IdleMs','0','-TimeoutMs','1000') -AllowFail).Json.code -eq 'pointer-consent') '-Focus auto without -Pointer must fail with pointer-consent' $theme = Invoke-Json @('theme') Assert ($theme.apps -in @('dark','light') -and $theme.system -in @('dark','light')) 'theme read failed' Assert-Err @('theme','-Mode','blue','-IdleMs','0','-TimeoutMs','1000') 'Mode must be dark or light' Assert-Err @('theme','-Mode',$theme.apps,'-Scope','wat','-IdleMs','0','-TimeoutMs','1000') 'Scope must be apps, system, or both' Assert-Err @('theme','-Mode',$theme.apps,'-Refresh','wat','-IdleMs','0','-TimeoutMs','1000') 'Refresh must be broadcast or full' if ($IncludeTheme) { $themeSet = Invoke-Json @('theme','-Mode',$theme.apps,'-Scope','apps','-IdleMs','0','-TimeoutMs','1000') Assert ($themeSet.apps -eq $theme.apps) 'theme apps set failed' Assert ($themeSet.refresh -eq 'broadcast') 'theme must default to the broadcast refresh' Assert ($themeSet.previous.apps -in @('dark','light')) 'theme set must report previous values' Assert ((Invoke-Json @('theme','-Mode',$theme.system,'-Scope','system','-IdleMs','0','-TimeoutMs','1000')).system -eq $theme.system) 'theme system set failed' } else { Write-Host 'skipping theme writes (pass -IncludeTheme to exercise them)' } $clipValue = 'win32-drive-clip-' + [guid]::NewGuid().ToString('N') Assert ((Invoke-Json @('clipboard','-Set',$clipValue,'-IdleMs','0','-TimeoutMs','1000')).ok) 'clipboard set failed' Assert ((Invoke-Json @('clipboard')).text -eq $clipValue) 'clipboard read after set failed' # click must land on the control, focus the target, and leave the cursor untouched $button = $uia.tree.children | Where-Object { $_.name -eq 'ClickMe' } | Select-Object -First 1 Assert ($null -ne $button) 'uia did not expose the ClickMe button' $buttonX = [int]($button.x + $button.width / 2 - $winShot.x) $buttonY = [int]($button.y + $button.height / 2 - $winShot.y) $before = Invoke-Json @('cursor') $click = Invoke-Json @('click','-Hwnd',$hwnd,'-X',[string]$buttonX,'-Y',[string]$buttonY,'-Pointer','-Focus','auto','-Return','keep','-NoCountdown','-IdleMs','0','-TimeoutMs','1000') Assert ($click.foreground.hwnd -eq $app.Window.hwnd) 'click must report the target window as foreground' Assert ($click.target.hwnd -eq $app.Window.hwnd -and $click.target.title -like 'Win32DriveTest-*') 'click must echo the resolved target window' Assert ($click.return -eq 'keep' -and $null -eq $click.returnedTo) '-Return keep must leave focus on the target' Assert ($click.focus -eq 'auto' -and $click.cursor -eq 'restore') 'click focus/cursor reporting wrong' Wait-Title $app.Window.hwnd "$($app.Title) clicked" | Out-Null $after = Invoke-Json @('cursor') # Tolerates the human moving the mouse mid-run: the regression is a cursor # parked on the click target, not a cursor that simply moved. Assert (($after.x -eq $before.x -and $after.y -eq $before.y) -or $after.x -ne $click.x -or $after.y -ne $click.y) 'click must restore the cursor position' Assert ((Invoke-Json @('click','-Hwnd',$hwnd,'-X',[string]$buttonX,'-Y',[string]$buttonY,'-Pointer','-Cursor','keep','-Focus','skip','-NoCountdown','-IdleMs','0','-TimeoutMs','1000')).cursor -eq 'keep') 'click -Cursor keep failed' Assert-Err @('click','-Hwnd',$hwnd,'-Pointer','-Focus','wat','-IdleMs','0','-TimeoutMs','1000') 'Focus must be auto, require, or skip' Assert-Err @('click','-Hwnd',$hwnd,'-Pointer','-Cursor','wat','-IdleMs','0','-TimeoutMs','1000') 'Cursor must be restore or keep' # type must reach the focused control verbatim, including non-ASCII # WinForms controls surface through the MSAA bridge, so match the text box by # elimination instead of by AutomationId (which is just the raw HWND here). $textbox = $uia.tree.children | Where-Object { $_.name -ne 'ClickMe' -and $_.width -gt 200 } | Select-Object -First 1 Assert ($null -ne $textbox) 'uia did not expose the text box' Assert ((Invoke-Json @('click','-Hwnd',$hwnd,'-X',[string]([int]($textbox.x + $textbox.width / 2 - $winShot.x)),'-Y',[string]([int]($textbox.y + $textbox.height / 2 - $winShot.y)),'-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000')).ok) 'textbox click failed' $unicode = 'Grusse-ae-' + [char] 0x00E4 + [char] 0x20AC Assert ((Invoke-Json @('key','-Hwnd',$hwnd,'-Keys','^a','-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000')).ok) 'select all before typing failed' $typed = Invoke-Json @('type','-Hwnd',$hwnd,'-Text',$unicode,'-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000') Assert ($typed.chars -eq $unicode.Length) 'type failed' Assert ($typed.method -eq 'unicode') 'type must default to the unicode method on 64-bit' Assert ($typed.foreground.hwnd -eq $app.Window.hwnd) 'type must report the target window as foreground' Wait-Title $app.Window.hwnd "$($app.Title) text:$unicode" | Out-Null Assert ((Invoke-Json @('key','-Hwnd',$hwnd,'-Keys','^a','-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000')).foreground.hwnd -eq $app.Window.hwnd) 'key failed' $sendkeys = 'sk+^%~(){}[]' Assert ((Invoke-Json @('type','-Hwnd',$hwnd,'-Text',$sendkeys,'-Method','sendkeys','-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000')).method -eq 'sendkeys') 'type -Method sendkeys failed' Wait-Title $app.Window.hwnd "$($app.Title) text:$sendkeys" | Out-Null Assert-Err @('type','-Hwnd',$hwnd,'-Text','x','-Method','wat','-Pointer','-IdleMs','0','-TimeoutMs','1000') 'Method must be unicode or sendkeys' # focusing must fail loudly instead of driving whatever happens to be in front Assert ((Invoke-Json @('window','-Hwnd',$hwnd,'-Action','min','-IdleMs','0','-TimeoutMs','1000')).ok) 'window min failed' Start-Sleep -Milliseconds 250 Assert-Err @('type','-Hwnd',$hwnd,'-Text','must-not-arrive','-Pointer','-Focus','auto','-NoCountdown','-IdleMs','0','-TimeoutMs','1000') 'minimized' Assert ((Invoke-Json @('window','-Hwnd',$hwnd,'-Action','restore','-IdleMs','0','-TimeoutMs','1000')).ok) 'window restore failed' Start-Sleep -Milliseconds 250 $movedResult = Invoke-Json @('window','-Hwnd',$hwnd,'-Action','move','-X','100','-Y','100','-Width','430','-Height','270','-IdleMs','0','-TimeoutMs','1000') Assert ($movedResult.ok) 'window move failed' Assert ($movedResult.window.width -gt 350 -and $movedResult.window.height -gt 200) 'window must return the updated rect without a second query' Start-Sleep -Milliseconds 150 $moved = @(Invoke-Json @('list')).windows | Where-Object hwnd -eq $app.Window.hwnd | Select-Object -First 1 Assert ($moved.width -gt 350 -and $moved.height -gt 200) 'window move/list update failed' foreach ($action in 'min','restore','max','restore') { Assert ((Invoke-Json @('window','-Hwnd',$hwnd,'-Action',$action,'-IdleMs','0','-TimeoutMs','1000')).action -eq $action) "window $action failed" Start-Sleep -Milliseconds 150 } Assert ((Invoke-Json @('window','-Hwnd',$hwnd,'-Action','close','-IdleMs','0','-TimeoutMs','1000')).ok) 'window close failed' $app.Process.WaitForExit(5000) | Out-Null Assert ($app.Process.HasExited) 'test window did not close' # the takeover countdown must be vetoable by real user input $abortJob = Start-Job -ScriptBlock { param($scriptPath) & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scriptPath overlay -Text 'abort probe' -DurationMs 3000 } -ArgumentList $script Start-Sleep -Milliseconds 1500 Invoke-Json @('key','-Keys','{F13}','-Focus','skip','-NoCountdown','-IdleMs','0','-TimeoutMs','1000') | Out-Null $abort = (Receive-Job -Job $abortJob -Wait -AutoRemoveJob) | ConvertFrom-Json Assert ($abort.completed -eq $false) 'user input during the countdown must cancel the overlay' Assert-Err @('wat') 'Unknown command' Assert-Err @('shot','-Bogus','x') 'Unknown option' Assert-Err @('shot','-X') 'Missing value' Assert-Err @('shot','-TitleContains',('definitely-no-window-' + [guid]::NewGuid().ToString('N'))) 'No matching window' # an option value equal to the script name must not swallow the command line Assert-Err @('shot','-TitleContains','win32-drive.ps1') 'No matching window' Assert-Err @('window','-Hwnd','999999999','-Action','move','-IdleMs','0','-TimeoutMs','1000') 'Invalid window|empty bounds|failed' $bad = Start-TestWindow ("Win32DriveBadAction-" + [guid]::NewGuid().ToString('N')) $created += $bad.Script Assert-Err @('window','-Hwnd',([string]$bad.Window.hwnd),'-Action','wat','-IdleMs','0','-TimeoutMs','1000') 'Unknown window action' Stop-Process -Id $bad.Process.Id -Force -ErrorAction SilentlyContinue 'OK' } finally { if ($originalClipboard -ne $null) { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script clipboard -Set $originalClipboard -IdleMs 0 -TimeoutMs 1000 | Out-Null } if ($app -and -not $app.Process.HasExited) { Stop-Process -Id $app.Process.Id -Force -ErrorAction SilentlyContinue } foreach ($path in $created) { if ($path -and (Test-Path $path)) { Remove-Item $path -Force -ErrorAction SilentlyContinue } } }