Bring a program to foreground or start it - powershell

For a handful of programs I use frequently, I am trying to write me some functions or aliases which would check if this program is already running and bring its window to foreground, else start this program.
Usage example with np, a handle for notepad.exe:
PS> np
checks if notepad.exe is running (Get-Process -Name "notepad.exe") if not, it would start it. When Notepad is already running but my maximized console is in the foreground, I'd like to execute the same command again, but this time I want it to bring the already running notepad process to foreground, rather than start a new one.
In order to implement this, I created this class called Program which I would instantiate for every program I want to handle like this. Then I have a HashTable $knownprograms of instances of this class, and in the end I try to define functions for every program, so that I could just type two or three letters to the console to start a program or bring its running process back to foreground.
class Program {
[string]$Name
[string]$Path
[string]$Executable
[string[]]$Arguments
Program(
[string]$n,
[string]$p,
[string]$e,
[string[]]$a
){
$this.Name = $n
$this.Path = $p
$this.Executable = $e
$this.Arguments = $a
}
[string]FullPath(){
return ("{0}\{1}" -f $this.Path, $this.Executable)
}
[void]ShowOrStart(){
try {
# Adapted from https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/bringing-window-in-the-foreground
$Process = Get-Process -Name $this.Name -ErrorAction Stop
Write-Host "Found at least one process called $this.Name"
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
$Mode = 4 # Will restore the window, not maximize it
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
} catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
Write-Host "Did not find any process called $this.Name"
Invoke-Command -ScriptBlock { & $this.FullPath() $this.Arguments }
}
}
}
$knownprograms = #{}
$knownprograms.Add("np", [Program]::new(
"np",
"$Env:SystemRoot\System32",
"notepad.exe",
#())
)
$knownprograms.Add("pt", [Program]::new(
"pt",
"$Env:SystemRoot\System32",
"mspaint.exe",
#())
)
Function np {
[cmdletbinding()]
Param()
$knownprograms.np.ShowOrStart()
}
Function pt {
[cmdletbinding()]
Param()
$knownprograms.pt.ShowOrStart()
}
The idea would be that I would source this script in my profile.ps1 and then just use the pre-factored functions. However, it seems that this code always opens a new instance of the program, rather than using its running process. Maybe I need some sort of delayed evaluation, so that the ShowOrStart() method checks at the time of invocation of np or pt whether the associated process exists. Any ideas how to accomplish this?

The process name for notepad.exe is notepad.
Update
$knownprograms.Add("np", [Program]::new(
"notepad",
"$Env:SystemRoot\System32",
"notepad.exe",
#())
)
And this works as expected.
This would be probably interesting to register $sig once for all and not on every call (which will probably raise an error).

Related

PowerShell Get process by its handle

$ActiveHandle = [UserWindows]::GetForegroundWindow()
$Process = Get-Process | ? {$_.MainWindowHandle -eq $ActiveHandle}
This code retrieves a title of the current active window. Problem is that it only filters processes by MainWindowHandle. For example, if my active handle is a popup from the same process, it doesn't return anything as the handle is not its main handle. How can I modify the code to check for ALL handles instead of just the main one? Or rather, how can I retrieve all process handles?
I do not want to use external tools like WASP.
You can use the GetWindowThreadProcessId Win32 API function for this:
# Define a type that allows us to call the relevant win32 api
$user32 = Add-Type -MemberDefinition #'
[DllImport("user32.dll", SetLastError=true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
'# -PassThru -Name user32Func
# prepare a variable to receive the target process id
$procId = 0
# call the function with, pass the handle as the first argument
$threadId = $user32::GetWindowThreadProcessId($ActiveHandle, [ref]$procId)
# If the owning thread and process was identified, the return-value will have been non-0
if($threadId) {
Write-Host "Found process $(Get-Process -Id $procId |% Name) with PID $procId"
}
else {
Write-Host "No owning process found"
}

Powershell - How to script a session refresh in powershell?

I have one ps1 script that drives the operations I want to perform.
I am using modules with class definitions in the modules that use Command pattern.
All is well and good first time I open a powershell session console and run the script.
If I change a class in any way and re-run in the same console, the console does not seem to be picking up the changed script. I have to close the powershell console and run the script fresh in order for my changes work. Otherwise I just get the script behaving the same way it does before I made the change. Clearly there is some caching going on.
I am wondering if MS has finally resolved this issue. I have read many older posts with complaints about this.
I have tried the following and none of them appears to work:
Remove-Variable * -ErrorAction SilentlyContinue;
Remove-Module *;
$error.Clear();
Clear-Host
I have even tried all of them together. Still not helping.
Is there something else can can be done to ensure the latest code in any supporting modules gets loaded? Having to close the whole console and reload is a serious productivity issue.
Example of what I am doing:6
using module .\Logger.psm1
using module .\AzurePlatformParmsDefault.psm1
using module .\AzurePlatform.psm1
[Logger] $Logger = [Logger]::Create()
[AzurePlatformParms] $AzurePlatformParms = [AzurePlatformParmsDefault]::Create( $Logger )
[AzurePlatform] $AzurePlatform = [AzurePlatform]::Create( $Logger, $AzurePlatformParms )
[bool] $Result = $AzurePlatform.Execute()
The conventional wisdom is that there isn't a way to do this natively, and creating a new runspace or process is the solution.
You can reset variables to default values and import environment variables from the user/machine scope (on windows); before clearing any jobs, events, event subscribers etc. This isn't a true session refresh though, and classes/custom types will persist.
To speed up your workflow, you may want to use a function in your $profile that can automate creating a new session, and loading in what's needed. This approach can save enough time that it is trivial to recycle an interactive session. I will include the one I use in my profile as an example. It's fairly comprehensive, but I suggest tailoring one that is suitable for your specific needs.
Example
function Start-NewSession {
[CmdletBinding(DefaultParameterSetName = 'NoChange')]
[Alias('sans')]
param(
[Alias('N')]
[switch]
$NoClose,
[Parameter(ParameterSetName = 'Elevate')]
[Parameter(ParameterSetName = 'NoChange')]
[Alias('nop')]
[switch]
$NoProfile,
[Parameter(ParameterSetName = 'Elevate')]
[Parameter(ParameterSetName = 'NoChange')]
[Alias('A')]
[switch]
$AddCommands,
[Parameter(ParameterSetName = 'Elevate')]
[Alias('E')]
[switch]
$Elevate,
[Parameter(ParameterSetName = 'DeElevate')]
[Alias('D')]
[switch]
$DeElevate
)
$PSAppPath = (Get-Process -Id $PID).Path
$SPParams = #{
Filepath = $PSAppPath
WorkingDirectory = $PWD
ArgumentList = ''
}
if ($Elevate.IsPresent) {
$SPParams['Verb'] = 'RunAs'
}
elseif ($DeElevate.IsPresent) {
$SPParams['FilePath'] = Join-Path $env:windir 'explorer.exe'
$SPParams['ArgumentList'] = $PSAppPath
}
if ($NoProfile.IsPresent) {
$SPParams['ArgumentList'] += ' -NoProfile'
}
if ($AddCommands.IsPresent) {
$ExtraCmds = Read-Host -Prompt 'Post-startup commands'
if (-not [string]::IsNullOrWhiteSpace($ExtraCmds)) {
$SPParams['ArgumentList'] +=
' -NoExit -Command "' + $ExtraCmds.Replace('"', '\"') + '"'
}
}
if ([string]::IsNullOrWhiteSpace($SPParams['ArgumentList'])) {
$SPParams.Remove('ArgumentList')
}
Start-Process #SPParams
if (-not $NoClose.IsPresent) { exit }
}
This permits typing sans to generate a new session and close the old one.

How do I put the focus on a modal dialog box using powershell?

I found a script here (see below) that allows me to select a main window from powershell and then add some keypresses. However, when the script selects the main window and not the dialog box I want to do key presses on to make it go away. Is there some way to select the dialog box instead, or use keypresses to select it?
Function SendKey{
[CMDLetBinding()]
Param(
[String]
[Parameter(Mandatory=$True,ValueFromPipelineByPropertyName=$True,Position=1)]
$WindowTitle,
[String[]]
[Parameter(Mandatory=$True,ValueFromPipelineByPropertyName=$True,Position=2)]
$Key
)
Begin{
$ErrorActionPreference = 'SilentlyContinue'
$Dlls = #'
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
'#
$WindowControl = Add-Type -MemberDefinition $Dlls -Name "Win32WindowControl" -namespace Win32Functions -passThru
}
Process{
$WindowHandle = Get-Process | Where-Object { $_.MainWindowTitle -Match $WindowTitle } | Select-Object -ExpandProperty MainWindowHandle
If($WindowHandle){
$WindowControl::SetForegroundWindow($WindowHandle)
Sleep 1
$FocusHandle = $WindowControl::GetForegroundWindow()
If($FocusHandle -eq $WindowHandle){
ForEach($Press in $Key){
[System.Windows.Forms.SendKeys]::SendWait("$Press")
}
}
}
}
}
Your code:
has an incidental problem: $ErrorActionPreference = 'SilentlyContinue' suppresses subsequent errors, so that [System.Windows.Forms.SendKeys]::SendWait("$Press") quietly fails, given that the System.Windows.Forms assembly is never loaded in your code (Add-Type -AssemblyName System.Windows.Forms)
has a fundamental problem: Using SetForegroundWindow() with a process' main window will indeed set the focus on that main window even while a modal dialog is open - as a result, keystrokes may go nowhere.
The simplest solution is to use the [Microsoft.VisualBasic.Interaction] type's static .AppActivate() method instead:
.AppActivate() properly activates whatever window belonging to the target application is frontmost - as would be activated if you alt-tabbed to the application. This may be the main window or an open modal dialog, for instance.
Function SendKey {
[CmdletBinding()]
Param(
[String]
[Parameter(Mandatory = $True, Position = 1)]
$WindowTitle,
[String[]]
[Parameter(Mandatory = $True, Position = 2)]
$Key
)
Begin {
# Load the required assemblies.
Add-Type -AssemblyName System.Windows.Forms, Microsoft.VisualBasic
}
Process {
# Find the process with the main window title of interest.
$procId = (Get-Process | Where-Object { $_.MainWindowTitle -Match $WindowTitle }).Id
If ($procId) { # Target application's process found.
# Activate it by its process ID.
[Microsoft.VisualBasic.Interaction]::AppActivate($procId)
# Send the keystrokes.
ForEach ($Press in $Key) {
[System.Windows.Forms.SendKeys]::SendWait($Press)
}
}
}
}
To test the code:
From a PowerShell window, dot-source the function above (define it in your session).
Open a Notepad instance (run notepad).
Switch to the new instance and make the file-open dialog visible (Ctrl+O).
Switch back to your PowerShell window and run SendKey Notepad '{ESC}o'
Notepad should be activated, the file-open dialog should be dismissed, and o should be typed in the main window (the document).
If there's no dialog open, then the {ESC} should have no effect and o should appear in the main window too.
Caveat: The keystrokes will be sent to whatever control inside the target window / target window's open dialog happens to have the keyboard focus.
Therefore, if you know what specific window / dialog will be open at the time you send the keystrokes, you can first send additional keystrokes that activate the specific control of interest.
For instance, if you expect the file-open dialog to be open, you could send %n - the equivalent of Alt+N - first, to ensure that the File name: text box has the input focus.
E.g., to send filename file.txt: SendKey Notepad '%nfile.txt'

Make a Powershell respond to Register-ObjectEvent events in script mode

I have a simple Powershell script that I wrote in the Powershell ISE. The gist of it is that it watches a named pipe for a write as a signal to perform an action, while at the same time monitoring its boss process. When the boss-process exits, the script exits as well. Simple.
After struggling to get the named pipe working in Powershell without crashing, I managed to get working code, which is shown below. However, while this functions great in the Powershell ISE and interactive terminals, I've been hopeless in getting this to work as a standalone script.
$bosspid = 16320
# Create the named pipe
$pipe = new-object System.IO.Pipes.NamedPipeServerStream(
-join('named-pipe-',$bosspid),
[System.IO.Pipes.PipeDirection]::InOut,
1,
[System.IO.Pipes.PipeTransmissionMode]::Byte,
[System.IO.Pipes.PipeOptions]::Asynchronous
)
# If we don't do it this way, Powershell crashes
# Brazenly stolen from github.com/Tadas/PSNamedPipes
Add-Type #"
using System;
public sealed class CallbackEventBridge
{
public event AsyncCallback CallbackComplete = delegate {};
private void CallbackInternal(IAsyncResult result)
{
CallbackComplete(result);
}
public AsyncCallback Callback
{
get { return new AsyncCallback(CallbackInternal); }
}
}
"#
$cbbridge = New-Object CallBackEventBridge
Register-ObjectEvent -InputObject $cbbridge -EventName CallBackComplete -Action {
param($asyncResult)
$pipe.EndWaitForConnection($asyncResult)
$pipe.Disconnect()
$pipe.BeginWaitForConnection($cbbridge.Callback, 1)
Host-Write('The named pipe has been written to!')
}
# Make sure to close when boss closes
$bossproc = Get-Process -pid $bosspid -ErrorAction SilentlyContinue
$exitsequence = {
$pipe.Dispose()
[Environment]::Exit(0)
}
if (-Not $bossproc) {$exitsequence.Invoke()}
Register-ObjectEvent $bossproc -EventName Exited -Action {$exitsequence.Invoke()}
# Begin watching for events until boss closes
$pipe.BeginWaitForConnection($cbbridge.Callback, 1)
The first problem is that the script terminates before doing anything meaningful. But delaying end of execution with such tricks like while($true) loops, the -NoExit flag, pause command, or even specific commands which seem made for the purpose, like Wait-Event, will cause the process to stay open, but still won't make it respond to the events.
I gave up on doing it the "proper" way and have instead reverted to using synchronous code wrapped in while-true blocks and Job control.
$bosspid = (get-process -name notepad).id
# Construct the named pipe's name
$pipename = -join('named-pipe-',$bosspid)
$fullpipename = -join("\\.\pipe\", $pipename) # fix SO highlighting: "
# This will run in a separate thread
$asyncloop = {
param($pipename, $bosspid)
# Create the named pipe
$pipe = new-object System.IO.Pipes.NamedPipeServerStream($pipename)
# The core loop
while($true) {
$pipe.WaitForConnection()
# The specific signal I'm using to let the loop continue is
# echo m > %pipename%
# in CMD. Powershell's echo will *not* work. Anything other than m
# will trigger the exit condition.
if ($pipe.ReadByte() -ne 109) {
break
}
$pipe.Disconnect()
# (The action this loop is supposed to perform on trigger goes here)
}
$pipe.Dispose()
}
# Set up the exit sequence
$bossproc = Get-Process -pid $bosspid -ErrorAction SilentlyContinue
$exitsequence = {
# While PS's echo doesn't work for passing messages, it does
# open and close the pipe which is enough to trigger the exit condition.
&{echo q > $fullpipename} 2> $null
[Environment]::Exit(0)
}
if ((-Not $bossproc) -or $bossproc.HasExited) { $exitsequence.Invoke() }
# Begin watching for events until boss closes
Start-Job -ScriptBlock $asyncloop -Name "miniloop" -ArgumentList $pipename,$bosspid
while($true) {
Start-Sleep 1
if ($bossproc.HasExited) { $exitsequence.Invoke() }
}
This code works just fine now and does the job I need.

How to execute a powershell script without stealing focus?

I made a powershell script that uses GetForegroundWindow() to identify which Window is the currently focused Window. Unfortunately, when the powershell script is executed (via Windows Task Scheduler or a hotkey), the script steals focus of the current foreground app; incorrectly using the Powershell window itself as the Foreground app, instead of the intended foreground app.
I even tried creating an EXE using PS2EXE "-noconsole" setting; however, the script still doesn't doesn't work.
Could someone please suggest a way to execute this script without changing focus of the current foreground Window?
My script code works during tests (ONLY if I add "Start-Sleep -s 5"; and, manually steal back the focus with alt-tab for the script to identify the correct foreground Window.
Add-Type #"
using System;
using System.Runtime.InteropServices;
public class Tricks {
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
}
"#
function Set-WindowStyle {
param(
[Parameter()]
[ValidateSet('FORCEMINIMIZE', 'HIDE', 'MAXIMIZE', 'MINIMIZE', 'RESTORE',
'SHOW', 'SHOWDEFAULT', 'SHOWMAXIMIZED', 'SHOWMINIMIZED',
'SHOWMINNOACTIVE', 'SHOWNA', 'SHOWNOACTIVATE', 'SHOWNORMAL')]
$Style = 'SHOW',
[Parameter()]
$MainWindowHandle = (Get-Process -Id $pid).MainWindowHandle
)
$WindowStates = #{
FORCEMINIMIZE = 11; HIDE = 0
MAXIMIZE = 3; MINIMIZE = 6
RESTORE = 9; SHOW = 5
SHOWDEFAULT = 10; SHOWMAXIMIZED = 3
SHOWMINIMIZED = 2; SHOWMINNOACTIVE = 7
SHOWNA = 8; SHOWNOACTIVATE = 4
SHOWNORMAL = 1
}
Write-Verbose ("Set Window Style {1} on handle {0}" -f $MainWindowHandle, $($WindowStates[$style]))
$Win32ShowWindowAsync = Add-Type -memberDefinition #"
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
"# -name "Win32ShowWindowAsync" -namespace Win32Functions -passThru
$Win32ShowWindowAsync::ShowWindowAsync($MainWindowHandle, $WindowStates[$Style]) | Out-Null
}
$a = [tricks]::GetForegroundWindow()
$title = get-process | ? { $_.mainwindowhandle -eq $a }
$title2 = $title | select -ExpandProperty ProcessName
if ($title2 -eq 'Kodi'){
Set-WindowStyle MINIMIZE $a;
if (Get-Process -Name Yatse2) {(Get-Process -Name Yatse2).MainWindowHandle | foreach { Set-WindowStyle MINIMIZE $_ }}
} ELSE {
$title.CloseMainWindow()
}
I know this question is old, but I've spent quite some time trying to figure out how to not lose focus from the current window for myself and found some information, so hopefully this will help future readers.
The easiest solution is just to literally simulate an Alt+Tab keypress from within your Powershell script instead of having to do it yourself. The following code comes from this StackOverflow answer:
[void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[System.Windows.Forms.SendKeys]::SendWait("%{TAB}")
# Retrieve the window handles...
...where % represents the Alt modifier and {TAB} is interpreted as the tab key.
User homersimpson's answer works, but using reflection is a bit slow. You can speed things up by adding the Windows.Forms assembly directly...
Add-Type -AssemblyName System.Windows.Forms
...other imports
#Return focus to the original window.
[System.Windows.Forms.SendKeys]::SendWait("%{TAB}")
...Your Code Here
Again, the % represents Alt and you know what TAB is. You are effectively Alt-Tabbing the new window away, returning focus to your desired one.
Create task that can be run on demand in Task Scheduler and executes your powershell script
Make sure it is set to be run whether user is logged on or not
Create a shortcut that starts the task
That allows your script to be run without visible window