Powershell to run PostMessage Windows API function failing - powershell

I need to write a script to install Java runtime of a specific version, add to the system path variable then go on to run the Java program. I want to do this all in one go without having to restart the target computer.
I found this so question and thought I could try to change to my needs.
SendMessage is causing script to hang
So I tried with PostMessage as per the answer. Obviously I needed to change the message.
So I tried code like this:
echo Start of experiment
$NewPath = (Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Session Manager\Environment" -Name Path).Path
$NewPath = "%JAVA_BIN%;" + $NewPath
$RegKey ="HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
Set-ItemProperty -Path $RegKey -Name Path -Value $NewPath
echo Updated Path environment variable
# Next bit which isn't working
$HWND_BROADCAST = 0xffff
$WM_SETTINGCHANGE = 0x001A
$ENVIRON = "Environment"
#Store the C# signature of the PostMessage function.
$signature = #"
[DllImport("user32.dll")]
public static extern int PostMessage(int hWnd, int hMsg, int wParam, int lParam);
"#
#Add the SendMessage function as a static method of a class
$PostMessage = Add-Type -MemberDefinition $signature -Name "Win32PostMessage" -Namespace Win32Functions -PassThru
#Invoke the PostMessage Function
$PostMessage::PostMessage($HWND_BROADCAST, $WM_SETTINGCHANGE, 0, $ENVIRON)
echo end of experiment
This is my output:
Start
of
experiment
Cannot convert argument "3", with value: "Environment", for "PostMessage" to type "System.Int32": "Cannot convert value "Environment" to type "System.Int32".
Error: "Input string was not in a correct format.""
At C:\share\tvm_drivers\PED\setenv.ps1:43 char:26
+ $PostMessage::PostMessage <<<< ($HWND_BROADCAST, $WM_SETTINGCHANGE, 0, $ENVIRON)
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
end
of
experiment
I am running this on Windows 7.
Can anyone help?
For a bit of background, if you reboot the target PC after setting the path env variable, then the setting works. But if you don't reboot, it is necessary to send a broadcast message as above to tell any other Window, including the command prompt, about the change. That is why I need the message.

This is the powershell that successfully updates all the open windows about the env change.
# Notifies other processes that the global environment block has changed.
# This lets other processes see changes to ENV: without having to reboot
# or logoff/logon. A non-zero result from SendMessageTimeout indicates success.
if (-not ("win32.nativemethods" -as [type])) {
# import sendmessagetimeout from win32
add-type -Namespace Win32 -Name NativeMethods -MemberDefinition #"
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,
uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
"#
}
$HWND_BROADCAST = [intptr]0xffff;
$WM_SETTINGCHANGE = 0x1a;
$result = [uintptr]::zero
# notify all windows of environment block change
[win32.nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,
[uintptr]::Zero, "Environment", 2, 5000, [ref]$result);

Related

PowerShell: AppActivate with PID not working

Under Windows 10, I would like to send a keypress to an existing and running windows application. The sending of the keypress works fine but the AppActivate via the PID of the windows application does not work. Here my code:
Function SendCommandToExistingProcess([int] $processId, [string] $processName, [string] $command)
{
$functName = 'SendCommandToExistingProcess()' # function name for log
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
[Microsoft.VisualBasic.Interaction]::AppActivate($processId)
[System.Windows.Forms.SendKeys]::SendWait($command)
WriteLogEntry -severity $sevInfo -functName $functName `
-entryText ("Command '" + $command + "' sent to process '" + `
$processName + "' with id '" + $processId)
}
$processId contains the PID of the windows application to set focus to
$command contains the keypress to send ('p')
For AppActivate I use the PID instead of the application windows title because the application title contains two special characters (similar to the copyright sign). It seems that AppActivate only works with the title (tested this successfully) but not with the PID though the AppActivate documentation shows an AppActivate overlay that acceptes a PID. I tried to set focus to the windows calculator by typing its PID as a number directly in AppActivate; did not work.
*** Update ***
The PID is obtained the following way:
Function SendCommandToProcess([string] $processName, [string] $command)
{
$result = $false # initialise to process does not exist
$functName = 'SendCommandToProcess()' # function name for log
$processId = (Get-Process -Name $processName -erroraction 'silentlycontinue').Id
if ($processId.Count -gt 0) # procss(es) exist(s)
{ # normally just one process but could be serveral
Foreach ($id IN $processId)
{ SendCommandToExistingProcess -processId $id -processName $processName -command $command }
# send command to each of them
$result = $true # command sent to specified process
}
else
{
WriteLogEntry -severity $sevWarning -functName $functName `
-entryText ("Process '" + $processName + "' not found (not running)")
}
return $result
}
$processName contains the string 'Prepar3D'
When I run the above code in PowerShell with admin rights I get the following error message:
Ausnahme beim Aufrufen von "AppActivate" mit 1 Argument(en): "Der Prozess {0} wurde nicht gefunden."
English: Exception when calling "AppActivate' with 1 argument(s): "The process {0} could not be found"
What is fooling me? Thanks for your help
Hannes
I have the same problem as I want to focus on an application called aces.exe. The solution that worked for me was following this guide:
https://powershell.one/powershell-internals/extending-powershell/vbscript-and-csharp#c-to-the-rescue
In this, Dr. Tobias Weltner, talks about 3 ways to focus on application windows. The one that will work for you is the Chapter: C# to the Rescue.
I used the admin ISE-PowerShell and copied the code from the link above. (Chapter: C# to the Rescue)
DO NOT COPY THIS CODE BUT FOLLOW THE INSTRUCTION AT THE LINKED PAGE.
This is what code I copied and used:
PS C:\Windows\system32> $code = #'
using System;
using System.Runtime.InteropServices;
namespace API
{
public class FocusWindow
{
[DllImport("User32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(IntPtr idAttach, IntPtr idAttachTo, bool fAttach);
[DllImport("User32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("User32.dll")]
private static extern IntPtr GetWindowThreadProcessId(IntPtr hwnd, IntPtr lpdwProcessId);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32")]
private static extern int BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
private const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
private const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
private const int SPIF_SENDCHANGE = 0x2;
private const int SW_HIDE = 0;
private const int SW_SHOWNORMAL = 1;
private const int SW_NORMAL = 1;
private const int SW_SHOWMINIMIZED = 2;
private const int SW_SHOWMAXIMIZED = 3;
private const int SW_MAXIMIZE = 3;
private const int SW_SHOWNOACTIVATE = 4;
private const int SW_SHOW = 5;
private const int SW_MINIMIZE = 6;
private const int SW_SHOWMINNOACTIVE = 7;
private const int SW_SHOWNA = 8;
private const int SW_RESTORE = 9;
private const int SW_SHOWDEFAULT = 10;
private const int SW_MAX = 10;
public static void Focus(IntPtr windowHandle)
{
IntPtr blockingThread = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
IntPtr ownThread = GetWindowThreadProcessId(windowHandle, IntPtr.Zero);
if (blockingThread == ownThread || blockingThread == IntPtr.Zero)
{
SetForegroundWindow(windowHandle);
ShowWindow(windowHandle, 3);
}
else
{
if (AttachThreadInput(ownThread, blockingThread, true))
{
BringWindowToTop(windowHandle);
SetForegroundWindow(windowHandle);
ShowWindow(windowHandle, SW_MAXIMIZE);
AttachThreadInput(ownThread, blockingThread, false);
}
}
if (GetForegroundWindow() != windowHandle)
{
IntPtr Timeout = IntPtr.Zero;
SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, Timeout, 0);
SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, IntPtr.Zero, SPIF_SENDCHANGE);
BringWindowToTop(windowHandle);
SetForegroundWindow(windowHandle);
ShowWindow(windowHandle, SW_MAXIMIZE);
SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, Timeout, SPIF_SENDCHANGE);
}
}
}
}
'#
# remove -PassThru in production. It is used only to
# expose the added types:
Add-Type -PassThru -TypeDefinition $code
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False FocusWindow System.Object
PS C:\Windows\system32> Add-Type -PassThru -TypeDefinition $code |
Where-Object IsPublic |
Select-Object -Property FullName
FullName
--------
API.FocusWindow
PS C:\Windows\system32> # get the main window handle for the process
# you want to switch to the foreground:
# in this example, the first instance of notepad is used
# (make sure notepad runs)
$process = Get-Process -Name aces -ErrorAction SilentlyContinue |
Select-Object -First 1
$mainWindowHandle = $process.MainWindowHandle
if (!$mainWindowHandle)
{
Write-Host "Window may be minimized, or process may not be running" -Foreground Red
}
# focus application window (and maximize it)
[API.FocusWindow]::Focus($mainWindowHandle)
PS C:\Windows\system32>
I also tried to use the nuget pack "PSOneApplicationWindow" that is in the first chapter in the link provided and that did not work to get focus on the aces.exe.
I use the Main Window Handle to solve this:
Note I have a com object built named $ie
$app = Get-Process | ?{$_.MainWindowHandle -eq $ie.hwnd}
[Microsoft.VisualBasic.Interaction]::AppActivate($app.Id)
sleep -Milliseconds 50
So build the object, search the process by the hwnd property, pass that as the object and make that the active app. Don't forget to add the sleep to give the app a moment to pop up before moving on to the next command. It makes the call much more stable.

Empty recycle bin with Powershell V2

Is there a way to empty the recycle bin using Powershell 2.0.
I do not want to update Powershell.
You could clear recycle bin via com object. Like so:
$Shell= New-Object -ComObject Shell.Application
$Bin = $Shell.NameSpace(10)
foreach ($Item in #($Bin.Items())){Remove-item $Item.Path -Force}
You could also directly call SHEmptyRecycleBin Win32 function:
$definition = #'
[DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
public static extern uint SHEmptyRecycleBin(IntPtr hwnd, string pszRootPath, uint dwFlags);
'#
$winApi = Add-Type -MemberDefinition $definition -Name WinAPI -Namespace Extern -PassThru
$winApi::SHEmptyRecycleBin(0, $null, 7)
All recycle bins are deleted, no confirmation message is shown, no progress bar, no sound.

Alternative to time delay while installing exe through PowerShell?

I have a software exe which I am trying to install via PowerShell. It's working fine. I am using SendKeys to navigate through the installation GUI. I have given delay between two SendKeys commands, because software takes some time between two steps, but that installation time varies from computer to computer.
My question is how can I bypass this time delay dependency in SendKeys? I have tried AppActivate but its of no use for me. Is there any alternative to delay?
Sure.
I've converted Nitesh's C# function to a Powershell script
$signature_user32_GetForegroundWindow = #"
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
"#
$signature_user32_GetWindowText = #"
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
"#
$foo = `
Add-Type -MemberDefinition $signature_user32_GetForegroundWindow `
-Name 'user32_GetForegroundWindow' `
-Namespace 'Win32' `
-PassThru
$bar = `
Add-Type -MemberDefinition $signature_user32_GetWindowText `
-Name 'user32_GetWindowText' `
-Namespace 'Win32' `
-Using System.Text `
-PassThru
[int]$nChars = 256
[System.IntPtr] $handle = New-object 'System.IntPtr'
[System.Text.StringBuilder] $Buff = New-Object 'System.Text.StringBuilder' `
-ArgumentList $nChars
$handle = $foo::GetForegroundWindow()
$title_character_count = $bar::GetWindowText($handle, $Buff, $nChars)
If ($title_character_count -gt 0) { Write-Output $Buff.ToString() }
There is a lot going on here. Lemme explain a little of what I did.
I've created two method signatures (the bit in the here-string); one for each function we're calling.
I use those signatures to create corresponding types. Again, one for each method.
For the GetWindowType (which passes the title back in a string and needs a reference to System.Text), I pass in the System.Text namespace in the -Using parameter.
Behind the scenes, PowerShell adds references to the System and System.Runtime.InteropServices so no need to worry about those.
I create my string size ($nChars), window pointer ($handle), and window title buffer ($Buff)
I call the functions through the type-pointer: $foo... and $bar...
Here is what I get when I run all this...
Whenever I have to call the Windows API (which isn't really my thing), I reference the following two articles:
PowerShell P/Invoke Walkthrough
Use PowerShell to Interact with the Windows API: Part 1
I hope this helps!

Maximizing Lync Window Using Powershell?

I've created a script that will automatically initiate a video call with a user of my choice.
When ran, the script leaves the video call docked, with the lync video call window flashing.
How would I be able to get this window to maximize and go to full screen when the script is ran?
Thank you so much for your help.
Below is my code
$assemblyPath = “C:\Program Files (x86)\Microsoft Office 2013\LyncSDK\Assemblies\Desktop\Microsoft.Lync.Model.DLL”
Import-Module $assemblyPath
$LyncClient = [Microsoft.Lync.Model.LyncClient]::GetClient()
$StartVideoCallMethod = {
$Conversation = $this.ConversationManager.AddConversation();
$contact = $LyncClient.ContactManager.GetContactByUri("useremailhere")
[void]$Conversation.AddParticipant($contact);
[void]$Conversation.Modalities['AudioVideo'].BeginConnect({}, 0);
};
Add-Member -InputObject $LyncClient -MemberType ScriptMethod -Name StartVideoCall -Value $StartVideoCallMethod -Force;
# Initiate the video call
$Conversation = $LyncClient.StartVideoCall();
I don't have Lync, but something like this should work. I'm using the process name (or what I'm guessing it is) to get the MainWindowHandle for the Lync window, then sending that a command to maximize (cmd=3, see here for the full list of values: https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548%28v=vs.85%29.aspx).
This code may break if more than one process matches by name, but it should get you started; if you can get the PID or some other, better unique identifier, use that. Just mess around with the output of Get-Process and you should see a number of options, and remember you can always use a Where clause to filter the output. Or of course if there's some way to get the MainWindowHandle directly from $LyncClient, even better.
$w = Get-Process -Name "Lync"
$Win32ShowWindowAsync = Add-Type –memberDefinition `
'[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' `
-name “Win32ShowWindowAsync” -namespace Win32Functions –passThru
$Win32ShowWindowAsync::ShowWindowAsync($w.MainWindowHandle,3) | Out-Null
Here the code that I have so far.
Still needs some tweaks to perfect it but it does the job.
Tweaks would be specifying which window to maximize as it will sometimes maximize the lync contacts window.
Code
$assemblyPath = “C:\Program Files (x86)\Microsoft Office 2013\LyncSDK\Assemblies\Desktop\Microsoft.Lync.Model.DLL”
Import-Module $assemblyPath
$exePath = "C:\Program Files\Microsoft Office 15\root\office15\lync.exe"
if(!(get-process | ?{$_.path -eq $exePath})){
Start-Process -FilePath $exePath -WindowStyle Maximized
Start-Sleep -s 10
}
$LyncClient = [Microsoft.Lync.Model.LyncClient]::GetClient()
$StartVideoCallMethod = {
$Conversation = $this.ConversationManager.AddConversation();
$contact = $LyncClient.ContactManager.GetContactByUri("ernesto.gomila#quirchfoods.com")
[void]$Conversation.AddParticipant($contact);
[void]$Conversation.Modalities['AudioVideo'].BeginConnect({}, 0);
};
Add-Member -InputObject $LyncClient -MemberType ScriptMethod -Name StartVideoCall -Value $StartVideoCallMethod -Force;
# Initiate the video call
$Conversation = $LyncClient.StartVideoCall();
#Maximize window
$w = Get-Process -Name "lync"
$Win32ShowWindowAsync = Add-Type –memberDefinition #"
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
"# -name “Win32ShowWindowAsync” -namespace Win32Functions –passThru
Start-Sleep -s 2
$Win32ShowWindowAsync::ShowWindowAsync($w.MainWindowHandle,3) | Out-Null

Can this be made to work on a remote computer?

I've been killing myself trying to get this to work on a remote computer, is it even possible? If so, can someone point me in the right direction?
Here's the code:
Function Lock-WorkStation {
#Requires -Version 2.0
$signature = #"
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
"#
$LockWorkStation = Add-Type -memberDefinition $signature -name "Win32LockWorkStation" -namespace Win32Functions -passthru
$LockWorkStation::LockWorkStation() | Out-Null
}
I can't test here, but for me it can NOT work because, as you can read in Microsoft documentation, the LockWorkStation function is callable only by processes running on the interactive desktop. In addition, the user must be logged on.
So when you connect to a remote computer using PSSession as far as I understand you are not in the interactive session.
Nothing to do with this, but it can help in Windows Vista/7 2008/R2, you can use the command tsdiscon.exe to lock a Remote Desktop session or your workstation.
Here is a sample where, logged as adminstrator domain on my computer, I first list, then lock the console session on my server.
PS> query session /server:WM2008R2ENT
SESSION UTILISATEUR ID ÉTAT TYPE PÉRIPHÉRIQUE
services 0 Déco
console jpb 2 Actif
PS> tsdiscon 2 /server:WM2008R2ENT
It's possible. But you need a workaround to connect to the interactive session.
Download the PowerShellPack and install it. You only need one module called "TaskScheduler".
I've tested the following code:
Function Lock-Workstation
{
param(
$Computername,
$Credential
)
if(!(get-module taskscheduler)){Import-Module TaskScheduler}
New-task -ComputerName $Computername -credential:$Credential |
Add-TaskTrigger -In (New-TimeSpan -Seconds 30) |
Add-TaskAction -Script `
{
$signature = #"
[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();
"#
$LockWorkStation = Add-Type -memberDefinition $signature `
-name "Win32LockWorkStation" `
-namespace Win32Functions `
-passthru
$LockWorkStation::LockWorkStation() | Out-Null
} | Register-ScheduledTask TestTask -ComputerName $Computername `
-credential:$Credential
}
You can use it like this:
Lock-Workstation "NameOfTheComputer" (Get-Credential)
or like this:
Lock-Workstation "NameOfTheComputer"
If you receive an error in Connect-ToTaskScheduler when specifying a credential, it's because there is a typo in the module (edit Connect-ToTaskScheduler.ps1 and replace "$NetworkCredentail.Domain," with "$NetworkCredential.Domain,"