How to close window with PowerShell Core

Source code

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

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 command 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 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.
Security Warning dialog to install a 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 close-windows.ps1 has some disadvantages:

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.

  1. command-job, defined on the line #56, runs the command passed as the parameter $Command;
  2. 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:


1. All used IP-addresses, names of servers, workstations, domains, are fictional and are used exclusively as a demonstration only.
2. Information is provided «AS IS».

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.