How to Close Window with PowerShell Core






4.20/5 (3 votes)
PowerShell script that runs command and confirms operation by closing confirmation window
Introduction
Automation is a great approach to get rid of the manual work and PowerShell is a great scripting language. Unfortunately some applications require user input or confirmation. The post is devoted to the script written in PowerShell which runs the command and confirms an operation by the closing confirmation window.
Let’s note that script has several disadvantages.
Background
The solution uses PowerShell 7.1.3.
Problem
Let’s consider the PowerShell script that should be run in an unattended mode, but one of the commands shows the dialog window to ask a user to confirm or to cancel the operation. Obviously this window blocks the script until the user closes it.
I faced up with such an issue when try trust ASP.NET Core HTTPS development certificate by running the command:
dotnet dev-certs https --trust;
It gives the output and asks the user to confirm or to cancel the operation:
Trusting the HTTPS development certificate was requested.
A confirmation prompt will be displayed if the certificate
was not previously trusted. Click yes on the prompt to
trust the certificate.
Solution
Let’s create the script close-windows.ps1
which solves this blocking issue.
PowerShell allows to write command which seeks the window and sends keys to it. It means that the main command and the close window command should be executed in parallel as background jobs. As jobs are started, the main script continues and waits until either jobs finish or timeout is expired. Then it stops hung jobs if any and outputs execution results.
There is a listing of the script close-windows.ps1
:
param (
# command to run
[Parameter(Mandatory = $true)]
[ScriptBlock]
$Command = { Write-Host 'Run command'; },
# name of the window to close
[Parameter(Mandatory = $true)]
[string]$WindowName,
# number of attempts
[Parameter(Mandatory = $false)]
[Int16]$MaxAttempts = 10,
# delay in seconds
[Parameter(Mandatory = $false)]
[Int16]$Delay = 5
)
# script seeks for the windows and send keys
$closeWindowJob = {
param (
# name of the window to close
[Parameter(Mandatory = $true)]
[string]$WindowName,
# number of attempts
[Parameter(Mandatory = $false)]
[Int16]$MaxAttempts = 10,
# delay in seconds
[Parameter(Mandatory = $false)]
[Int16]$Delay = 5
)
Write-Host 'Creating a shell object';
$wshell = New-Object -ComObject wscript.shell;
for ($index = 0; $index -lt $MaxAttempts; $index++) {
Write-Host "#$($index). Seeking for a window";
$result = $wshell.AppActivate($WindowName);
if ($result) {
Write-Host 'Send keys';
$wshell.SendKeys('{TAB}');
$wshell.SendKeys('~');
break;
}
else {
Write-Host "Window '$WindowName' is not found";
Start-Sleep $Delay;
}
}
}
Write-Verbose 'start jobs';
$jobs = New-Object "System.Collections.ArrayList";
$job = Start-Job -Name 'command-job' -ScriptBlock $Command;
$jobs.Add($job) | Out-Null;
$job = Start-Job -Name 'close-window-job' -ScriptBlock $closeWindowJob -ArgumentList $WindowName, $MaxAttempts, $Delay;
$jobs.Add($job) | Out-Null;
Write-Host "Command job Id: $($jobs[0].Id)";
Write-Host "Close window job Id: $($jobs[1].Id)";
# get all jobs in the current session
Get-Job;
$attempt = 0;
# Wait for all jobs to complete
While ((Get-Job -State "Running") -and ($attempt -lt $MaxAttempts)) {
Write-Verbose "#$($attempt). Sleep $Delay seconds";
Start-Sleep $Delay;
++$attempt;
}
# use job Id as the current session could contain more than 1 job with the name
$job = $null;
foreach ($job in $jobs) {
$jobState = Get-Job -Id $job.Id;
if ($jobState.State -eq "Running") {
Stop-Job -Id $job.Id;
}
}
Write-Host 'Getting the information back from the jobs';
foreach ($job in $jobs) {
Get-Job -Id $job.Id; # | Receive-Job;
}
As the script accepts a script block as a parameter, the command could be set by the another script. For example close-window.example.ps1
defines script block $Command
as the required command, $WindowName
as the name of a window that should be closed, and calls close-window.ps1
:
# script runs dotnet command
$Command = {
$inputFile = """$($Env:ProgramFiles)\dotnet\dotnet.exe""";
Write-Host "Run $inputFile";
Start-Process $inputFile -ArgumentList `
'dev-certs', 'https', '--trust' -Wait | Out-Host;
}
$WindowName = 'Security Warning';
.\close-window.ps1 `
-Command $Command `
-WindowName $WindowName `
-Verbose;
Let’s note that the script has some disadvantages:
- It searches the dialog window by its name, that could produce wrong results.
- It can be interrupted by user actions with windows.
- The key sequence is hardcoded as
Tab
,Enter
. - If you need run
Start-Process
with-NoNewWindow
switch,Start-Job
could fail, so there is a workaround for it powershell – “Start-Process -NoNewWindow” within a Start-Job? – Stack Overflow.
Close-window.ps1 script
The script has the following parameters:
$Command
is the command that is run as the background job.$WindowName
is the name of the window to close, can't be null or empty string.$MaxAttempts
is the number of attempts to confirm the operation. It is the optional parameter and has default value equals 10 attempts.$Delay
is the delay in seconds between attempts. It is the optional parameter and has default value equals 5 seconds.
The close window command is defined at lines 21-52. The command block has parameters that allow to pass values from the main script as $WindowName
, $MaxAttempts
, $Delay
. It creates wscript.shell
object and seeks for a window by its name. To simulate thread synchronization the close window command runs the loop at most $MaxAttempt
times and seeks for a window. If window is not found it means that the main command is still in progress. If a window is found, the command sends key sequence, that in our case is Tab
, Enter
to move focus to Yes
button and click it.
A Windows PowerShell background job is a command that runs in the background without interacting with the current session. Typically, you use a background job to run a complex command that takes a long time to finish. For more information about background jobs in Windows PowerShell, see about_Jobs.
Two jobs are started and their ids are stored in an array because the current session could contains other jobs, possible with the same names.
command-job
, defined on the line #56, runs the command passed as the parameter$Command
.close-window-job
, defined on the line #58, runs the close window command and passes parameters from the main script.
$jobs = New-Object "System.Collections.ArrayList";
$job = Start-Job -Name 'command-job' -ScriptBlock $Command;
$jobs.Add($job) | Out-Null;
$job = Start-Job -Name 'close-window-job' -ScriptBlock $closeWindowJob -ArgumentList $WindowName, $MaxAttempts, $Delay;
$jobs.Add($job) | Out-Null;
As soon as jobs are executed in the background, the main script waits until the jobs do their work but is aware that jobs could hung. The statement Get-Job -State "Running"
returns $null
if there are no running jobs in the current session and could be used as a condition for the loop. If jobs are still running the main script waits $Delay
seconds and checks jobs once again. In order not to get an infinite loop let's run the loop no more than $MaxAttempts
times. The loop is defined at lines 68-72.
While ((Get-Job -State "Running") -and ($attempt -lt $MaxAttempts)) {
Write-Verbose "#$($attempt). Sleep $Delay seconds";
Start-Sleep $Delay;
++$attempt;
}
To clean up resources the main script checks job state for all running jobs. If it still running, the scripts stops the job at line #79.
if ($jobState.State -eq "Running") {
Stop-Job -Id $job.Id;
}
At the end of the script, the command Get-Job -Id $job.Id
returns job states.
References
There are several useful topics:
- Get-Job (Microsoft.PowerShell.Core) – PowerShell | Microsoft Docs
- multithreading – Can Powershell Run Commands in Parallel? – Stack Overflow
- telnet – SendKeys Method in Powershell – Super User
- How to perform keystroke inside powershell? – Stack Overflow
History
- 08/05/2021 - the article is published.
- 08/26/2021 - the script is extended: add parameters, timeouts, more checks, and add close-window.example.ps1 as a sample script.
Notes
- All used IP-addresses, names of servers, workstations, domains, are fictional and are used exclusively as a demonstration only.
- Information is provided «AS IS».