powershell mouse move does not prevent idle mode - windows

[System.Windows.Forms.Cursor]::Position = `
New-Object System.Drawing.Point($pos.X, ($pos.Y - 1))
[System.Windows.Forms.Cursor]::Position = `
New-Object System.Drawing.Point($pos.X, $pos.Y)
Well, I want to move the mouse cursor every 4 minutes to prevent the screensaver from appearing (every second in the code above for testing). The code does really move the mouse every time one pixel up and then down immediately.
The thing is, the screensaver (or idle mode of windows) is still appearing.
Where is my mistake?

The solution from the blog Prevent desktop lock or screensaver with PowerShell is working for me. Here is the relevant script, which simply sends a single period to the shell:
param($minutes = 60)
$myshell = New-Object -com "Wscript.Shell"
for ($i = 0; $i -lt $minutes; $i++) {
Start-Sleep -Seconds 60
$myshell.sendkeys(".")
}

I tried a mouse move solution too, and it likewise didn't work. This was my solution, to quickly toggle Scroll Lock every 4 minutes:
Clear-Host
Echo "Keep-alive with Scroll Lock..."
$WShell = New-Object -com "Wscript.Shell"
while ($true)
{
$WShell.sendkeys("{SCROLLLOCK}")
Start-Sleep -Milliseconds 100
$WShell.sendkeys("{SCROLLLOCK}")
Start-Sleep -Seconds 240
}
I used Scroll Lock because that's one of the most useless keys on the keyboard. Also could be nice to see it briefly blink every now and then. This solution should work for just about everyone, I think.
Some people get success using $WShell.sendkeys("SCROLLLOCK") instead of $WShell.sendkeys("{SCROLLLOCK}")
See also:
https://ss64.com/vb/sendkeys.html
http://devguru.com/content/technologies/wsh/wshshell-sendkeys.html

There is an analog solution to this also. There's an android app called "Timeout Blocker" that vibrates at a set interval and you put your mouse on it. https://play.google.com/store/apps/details?id=com.isomerprogramming.application.timeoutblocker&hl=en

<# Stay Awake by Frank Poth 2019-04-16 #>
(Get-Host).UI.RawUI.WindowTitle = "Stay Awake"
[System.Console]::BufferWidth = [System.Console]::WindowWidth = 40
[System.Console]::BufferHeight = [System.Console]::WindowHeight = 10
$shell = New-Object -ComObject WScript.Shell
$start_time = Get-Date -UFormat %s <# Get the date in MS #>
$current_time = $start_time
$elapsed_time = 0
Write-Host "I am awake!"
Start-Sleep -Seconds 5
$count = 0
while($true) {
$shell.sendkeys("{NUMLOCK}{NUMLOCK}") <# Fake some input! #>
if ($count -eq 8) {
$count = 0
Clear-Host
}
if ($count -eq 0) {
$current_time = Get-Date -UFormat %s
$elapsed_time = $current_time - $start_time
Write-Host "I've been awake for "([System.Math]::Round(($elapsed_time / 60), 2))" minutes!"
} else { Write-Host "Must stay awake..." }
$count ++
Start-Sleep -Seconds 2.5
}
The part that matters is $shell.sendkeys("{NUMLOCK}{NUMLOCK}") This registers two presses on the numlock key and fools the shell into thinking input was entered. I wrote this today after searching through various scripts that didn't work for me. Hope it helps someone!

I created a PS script to check idle time and jiggle the mouse to prevent the screensaver.
There are two parameters you can control how it works.
$checkIntervalInSeconds : the interval in seconds to check if the idle time exceeds the limit
$preventIdleLimitInSeconds : the idle time limit in seconds. If the idle time exceeds the idle time limit, jiggle the mouse to prevent the screensaver
Here we go. Save the script in preventIdle.ps1. For preventing the 4-min screensaver, I
set $checkIntervalInSeconds = 30 and $preventIdleLimitInSeconds = 180.
Add-Type #'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace PInvoke.Win32 {
public static class UserInput {
[DllImport("user32.dll", SetLastError=false)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO {
public uint cbSize;
public int dwTime;
}
public static DateTime LastInput {
get {
DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
return lastInput;
}
}
public static TimeSpan IdleTime {
get {
return DateTime.UtcNow.Subtract(LastInput);
}
}
public static double IdleSeconds {
get {
return IdleTime.TotalSeconds;
}
}
public static int LastInputTicks {
get {
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return lii.dwTime;
}
}
}
}
'#
Add-Type #'
using System;
using System.Runtime.InteropServices;
namespace MouseMover
{
public class MouseSimulator
{
[DllImport("user32.dll", SetLastError = true)]
static extern uint SendInput(uint nInputs, ref INPUT pInputs, int cbSize);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT lpPoint);
[StructLayout(LayoutKind.Sequential)]
struct INPUT
{
public SendInputEventType type;
public MouseKeybdhardwareInputUnion mkhi;
}
[StructLayout(LayoutKind.Explicit)]
struct MouseKeybdhardwareInputUnion
{
[FieldOffset(0)]
public MouseInputData mi;
[FieldOffset(0)]
public KEYBDINPUT ki;
[FieldOffset(0)]
public HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
struct HARDWAREINPUT
{
public int uMsg;
public short wParamL;
public short wParamH;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
struct MouseInputData
{
public int dx;
public int dy;
public uint mouseData;
public MouseEventFlags dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[Flags]
enum MouseEventFlags : uint
{
MOUSEEVENTF_MOVE = 0x0001
}
enum SendInputEventType : int
{
InputMouse
}
public static void MoveMouseBy(int x, int y) {
INPUT mouseInput = new INPUT();
mouseInput.type = SendInputEventType.InputMouse;
mouseInput.mkhi.mi.dwFlags = MouseEventFlags.MOUSEEVENTF_MOVE;
mouseInput.mkhi.mi.dx = x;
mouseInput.mkhi.mi.dy = y;
SendInput(1, ref mouseInput, Marshal.SizeOf(mouseInput));
}
}
}
'#
$checkIntervalInSeconds = 30
$preventIdleLimitInSeconds = 180
while($True) {
if (([PInvoke.Win32.UserInput]::IdleSeconds -ge $preventIdleLimitInSeconds)) {
[MouseMover.MouseSimulator]::MoveMouseBy(10,0)
[MouseMover.MouseSimulator]::MoveMouseBy(-10,0)
}
Start-Sleep -Seconds $checkIntervalInSeconds
}
Then, open Windows PowerShell and run
powershell -ExecutionPolicy ByPass -File C:\SCRIPT-DIRECTORY-PATH\preventIdle.ps1

I had a similar situation where a download needed to stay active overnight and required a key press that refreshed my connection.
I also found that the mouse move does not work. However, using notepad and a send key function appears to have done the trick. I send a space instead of a "." because if there is a [yes/no] popup, it will automatically click the default response using the spacebar. Here is the code used.
param($minutes = 120)
$myShell = New-Object -com "Wscript.Shell"
for ($i = 0; $i -lt $minutes; $i++) {
Start-Sleep -Seconds 30
$myShell.sendkeys(" ")
}
This function will work for the designated 120 minutes (2 Hours), but can be modified for the timing desired by increasing or decreasing the seconds of the input, or increasing or decreasing the assigned value of the minutes parameter.
Just run the script in powershell ISE, or powershell, and open notepad. A space will be input at the specified interval for the desired length of time ($minutes).
Good Luck!

Try this:
(source: http://just-another-blog.net/programming/powershell-and-the-net-framework/)
Add-Type -AssemblyName System.Windows.Forms
$position = [System.Windows.Forms.Cursor]::Position
$position.X++
[System.Windows.Forms.Cursor]::Position = $position
while(1) {
$position = [System.Windows.Forms.Cursor]::Position
$position.X++
[System.Windows.Forms.Cursor]::Position = $position
$time = Get-Date;
$shorterTimeString = $time.ToString("HH:mm:ss");
Write-Host $shorterTimeString "Mouse pointer has been moved 1 pixel to the right"
#Set your duration between each mouse move
Start-Sleep -Seconds 150
}

I've added a notification that you can easily enable / disable just setting its variable to $true or $false. Also the mouse cursor moves 1 px right and then 1 px left so it basically stays in the same place even after several iterations.
# Lines needed for the notification
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
Add-Type -AssemblyName System.Windows.Forms
$isNotificationOn = $true
$secondsBetweenMouseMoves = 6
$Pos = [System.Windows.Forms.Cursor]::Position
$PosDelta = 1
$logFilename = "previousMouseMoverAction.txt"
$errorLogFilename = "mouseMoverLog.txt"
if (!(Test-Path "$PSScriptRoot\$logFilename")) {
New-Item -path $PSScriptRoot -name $logFilename -type "file" -value "right"
Write-Host "Warning: previousMouseMoverAction.txt missing, created a new one."
}
$previousPositionChangeAction = Get-Content -Path $PSScriptRoot\$logFilename
if ($previousPositionChangeAction -eq "left") {
$PosDelta = 1
Set-Content -Path $PSScriptRoot\$logFilename -Value 'right'
} else {
$PosDelta = -1
Set-Content -Path $PSScriptRoot\$logFilename -Value 'left'
}
for ($i = 0; $i -lt $secondsBetweenMouseMoves; $i++) {
[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point((($Pos.X) + $PosDelta) , $Pos.Y)
if ($isNotificationOn) {
# Sending a notification to the user
$global:balloon = New-Object System.Windows.Forms.NotifyIcon
$path = (Get-Process -id $pid).Path
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning
$balloon.BalloonTipText = 'I have just moved your cheese...'
$balloon.BalloonTipTitle = "Attention, $Env:USERNAME"
$balloon.Visible = $true
$balloon.ShowBalloonTip(3000)
}
}

I simply run alt+tab after every random second between 5 to 10 sec
Because new tools even track pattern of any key press.
Add it inside a loop and you are done.
[System.Windows.Forms.SendKeys]::SendWait("%{TAB}")
$ran=(Get-Random -Minimum 5 -Maximum 10)
echo "sleep for $ran sec"
sleep $ran

Below PowerShell script toggles scroll lock every minute, prints out current time, and clears console every 5 minutes (in case you want to keep the script going indefinitely).
$WShell = New-Object -com "Wscript.Shell"
cls
$count = 0
while ($true)
{
$count = $count + 1
if($count -eq 5) {
cls
$count=0
}
$WShell.sendkeys("{SCROLLLOCK}")
Start-Sleep -Milliseconds 100
Write-Output "Toggle Scroll at $(Get-Date -Format u)"
$WShell.sendkeys("{SCROLLLOCK}")
Start-Sleep -Seconds 60
}

Related

Powershell script to install font family

Below is my script istalling Monserrat fonts from zip file. I can't figure how to check if a font already installed. After installation I can open folder C:\Windows\Fonts\Montserrat and I see al of them. When I am running script second time, it is not recognize existance of this folder. Where is my mistake?
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($File in $allFonts)
{
If((Test-Path "C:\Windows\Fonts\Montserrat") -eq $True)
{
echo "Font $File already installed"
}
Else
{
echo "Installing $File"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($File.fullname,$CopyFlag)
}
}
Finally my script:
$Source = "Montserrat.zip"
$FontsFolder = "FontMontserrat"
Expand-Archive $Source -DestinationPath $FontsFolder -Force
$FONTS = 0x14
$CopyOptions = 4 + 16;
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.Namespace($FONTS)
$allFonts = dir $FontsFolder
foreach($font in Get-ChildItem -Path $fontsFolder -File)
{
$dest = "C:\Windows\Fonts\$font"
If(Test-Path -Path $dest)
{
echo "Font $font already installed"
}
Else
{
echo "Installing $font"
$CopyFlag = [String]::Format("{0:x}", $CopyOptions);
$objFolder.CopyHere($font.fullname,$CopyFlag)
}
}
I am running this script by following cmd:
set batchPath=%~dp0
powershell.exe -noexit -file "%batchPath%InstMontserrat.ps1"
I don't have to run it as administrator, but user have admin permissions.
Corrections of your script based on my comment assuming Windows 10:
# well-known SID for admin group
if ('S-1-5-32-544' -notin [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups) {
throw 'Script must run as admin!'
}
$source = 'Montserrat.zip'
$fontsFolder = 'FontMontserrat'
Expand-Archive -Path $source -DestinationPath $fontsFolder
foreach ($font in Get-ChildItem -Path $fontsFolder -File) {
$dest = "C:\Windows\Fonts\$font"
if (Test-Path -Path $dest) {
"Font $font already installed."
}
else {
$font | Copy-Item -Destination $dest
}
}
If you do not want to install the font on OS level but only make it available for programs to use until reboot you may want to use this script that:
Will fail/throw if it cannot register/unregister font.
Broadcasts WM_FONTCHANGE to inform all windows that fonts have changed
Does not require administrator privileges
Does not install fonts in Windows, only makes them available for all programs in current session until reboot
Has verbose mode for debugging
Does not work with font folders
Usage:
register-fonts.ps1 [-v] [-unregister <PATH>[,<PATH>...]] [-register <PATH>[,<PATH>...]] # Register and unregister at same time
register-fonts.ps1 [-v] -unregister <PATH>
register-fonts.ps1 [-v] -register <PATH>
register-fonts.ps1 [-v] <PATH> # Will register font path
Param (
[Parameter(Mandatory=$False)]
[String[]]$register,
[Parameter(Mandatory=$False)]
[String[]]$unregister
)
# Stop script if command fails https://stackoverflow.com/questions/9948517/how-to-stop-a-powershell-script-on-the-first-error
$ErrorActionPreference = "Stop"
add-type -name Session -namespace "" -member #"
[DllImport("gdi32.dll")]
public static extern bool AddFontResource(string filePath);
[DllImport("gdi32.dll")]
public static extern bool RemoveFontResource(string filePath);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam = 0, int lParam = 0);
"#
$broadcast = $False;
Foreach ($unregisterFontPath in $unregister) {
Write-Verbose "Unregistering font $unregisterFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-removefontresourcea
$success = [Session]::RemoveFontResource($unregisterFontPath)
if (!$success) {
Throw "Cannot unregister font $unregisterFontPath"
}
$broadcast = $True
}
Foreach ($registerFontPath in $register) {
Write-Verbose "Registering font $registerFontPath"
# https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea
$success = [Session]::AddFontResource($registerFontPath)
if (!$success) {
Throw "Cannot register font $registerFontPath"
}
$broadcast = $True
}
if ($broadcast) {
# HWND_BROADCAST https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$HWND_BROADCAST = New-Object IntPtr 0xffff
# WM_FONTCHANGE https://learn.microsoft.com/en-us/windows/win32/gdi/wm-fontchange
$WM_FONTCHANGE = 0x1D
Write-Verbose "Broadcasting font change"
# Broadcast will let other programs know that fonts were changed https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
$success = [Session]::PostMessage($HWND_BROADCAST, $WM_FONTCHANGE)
if (!$success) {
Throw "Cannot broadcase font change"
}
}
The script was inspired by this gist https://gist.github.com/Jaykul/d53a16ce5e7d50b13530acb4f98aaabd

Bring Powershell-Console to front from WinForms

im trying to bring my powershell console to front, even if it is minimized.
I found following code:
function Show-Process($Process, [Switch]$Maximize)
{
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
}
Show-Process -Process (Get-Process -Id $pid)
It works fine, but when i call the function from a Button Click event, the console wont show.
What is the Problem? Is there a way to bring the powershell Console to front when using a WinForms GUI?
Here is the example GUI Code:
function Show-Process($Process, [Switch]$Maximize)
{
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
'
if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
$hwnd = $process.MainWindowHandle
$null = $type::ShowWindowAsync($hwnd, $Mode)
$null = $type::SetForegroundWindow($hwnd)
}
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$Form.ClientSize = '446,266'
$Form.text = "Form"
$Form.TopMost = $false
$Button1 = New-Object system.Windows.Forms.Button
$Button1.text = "button"
$Button1.width = 60
$Button1.height = 30
$Button1.location = New-Object System.Drawing.Point(75,29)
$Button1.Font = 'Microsoft Sans Serif,10'
$Button1.Add_Click({
Show-Process -Process (Get-Process -Id $pid)
})
$Form.controls.AddRange(#($Button1))
[void]$Form.ShowDialog()
Thanks to #iRon's answer, i was able to figure it out, how i want it.
For anyone curious, the problem is, you only can get the consoles MainwindowHandle as long as ShowDialog wasn't called.
So i save the console Handle in a variable and i use the Form_Shown event to get the Form WindowHandle, since Form_Load still returns the Console Handle.
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);'
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
[IntPtr]$handleConsole = (Get-Process -Id $pid).MainWindowHandle
[void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole)
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$Form.ClientSize = '446,266'
$Form.text = "Form"
$Form.TopMost = $false
$Form.Add_Shown({
$global:handleForm = (Get-Process -Id $pid).MainWindowHandle
})
$Button1 = New-Object system.Windows.Forms.Button
$Button1.text = "Clone ad-USer"
$Button1.width = 60
$Button1.height = 30
$Button1.location = New-Object System.Drawing.Point(75,29)
$Button1.Font = 'Microsoft Sans Serif,10'
$Button1.Add_Click({
[void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole)
Read-Host -Prompt "Please Enter a Value"
[void]$type::ShowWindowAsync($global:handleForm, 4);[void]$type::SetForegroundWindow($global:handleForm)
})
$Form.controls.AddRange(#($Button1))
[void]$Form.ShowDialog()
Now, if i press the Button, to console pops up in front.
After the User enter something into the Console, the Form comes to front again.
Unfortunately, I can't completely fix it, but maybe others might help you further based on my findings:
First of all, the process within the button click event is a different process space as where the parent PowerShell host runs in. This can be easily proven but revealing the $hwhd with Write-Host $hwnd in the Show-Process function and also calling the Show-Process function prior calling the ShowDialog:
Show-Process -Process (Get-Process -Id $pid)
[void]$Form.ShowDialog()
In other words: to fix this part, you will need to the catch the parent $Pid from the PowerShell window first:
$Button1.Add_Click({
Show-Process -Process $MyProcess
})
$Form.controls.AddRange(#($Button1))
$MyProcess = Get-Process -Id $pid
Show-Process -Process $MyProcess
[void]$Form.ShowDialog()
The above snippet works, but as soon I remove (or comment out) the line Show-Process -Process $MyProcess (at the host level), it breaks again...
As you've discovered, .MainWindowHandle is not a static property (from the linked docs; emphasis added):
The main window is the window opened by the process that currently has the focus [...]
Therefore, what the value of the current process' .MainWindowHandle property changes from the console-window handle to the WinForms window while the form is being displayed.[1]
Caching the console-window handle before you display the form is definitely an option, but there's an easier way, given that you're already using Add-Member with WinAPI P/Invoke declarations: The GetConsoleWindow() WinAPI function always returns the current process' console-window handle.
Additionally, your $Forms form instance has a .Handle property, which directly returns the form's window handle - no (Get-Process -Id $pid).MainWindowHandle call needed.
The following solution therefore needs no global or script-level variables and confines querying the window handles to the button-click event handler:
# P/Invoke signatures - note the addition of GetConsoleWindow():
$sig = '
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();'
$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
Add-Type -AssemblyName System.Windows.Forms
$Form = New-Object system.Windows.Forms.Form -Property #{
ClientSize = '446,266'
text = "Form"
}
$Button1 = New-Object system.Windows.Forms.Button -Property #{
text = "Test"
location = New-Object System.Drawing.Point(75, 29)
}
$Button1.Add_Click({
# Get this form's window handle.
$handleForm = $Form.Handle # More generically: $this.FindForm().Handle
# Get the console window's handle.
$handleConsole = $type::GetConsoleWindow()
# Activate the console window and prompt the user.
$null = $type::ShowWindowAsync($handleConsole, 4); $null = $type::SetForegroundWindow($handleConsole)
Read-Host -Prompt "Please Enter a Value"
# Reactivate this form.
$null = $type::ShowWindowAsync($handleForm, 4); $null = $type::SetForegroundWindow($handleForm)
})
$Form.controls.AddRange(#($Button1))
$null = $Form.ShowDialog()
[1] Note that a cached process object doesn't dynamically update its .MainWindowHandle value; you have to call .Refresh() manually.
Because iRon's solution caches the current-process object before displaying the form, it still happens to reflect the console-window handle inside the button-click handler.

Speed up PowerShell script for Windows Registry search (currently 30 minutes)

I'm working on a script for use in Windows 7 and Windows 10 for a Windows Registry search in HKLM:\Software\Classes. So far my code works, but it's extremely slow. It takes about 30 minutes to complete.
I need to use Set-Location also to avoid an error with Get-ItemProperty, which occurs because the $path is not a valid object.
How can I speed this code up? What's wrong?
File regsearch.ps1 (Mathias R. Jessen's answer applied)
Function Get-RegItems
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
#Set Local Path and ignore wildcard (literalpath)
Set-Location -literalpath $path
$d = Get-Item -literalpath $path
# If more than one value -> process
If ($d.Valuecount -gt 0) {
$d |
# Get unkown property
Select-Object -ExpandProperty Property |
ForEach {
$val = (Get-ItemProperty -Path . -Name $_).$_
#if Filter $match found, generate ReturnObject
if (($_ -match $match) -or ($val -match $match ) -or ($path-match $match)) {
New-Object psobject -Property #{ “key”=$path; “property”=$_; “value” = $val ;}
}
}
}
} #end function Get-RegItems
Function RegSearch
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
# Expand $path if necessary to get a valid object
if ($path.Indexof("HKEY") -ne "-1" -and $path.Indexof("Registry::") -eq "-1" ) {
$path = "Microsoft.PowerShell.Core\Registry::" +$path
}
# Retrieve items of the main key
Get-RegItems -path $path -match $match
# Retrieve items of all child keys
Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue |
ForEach {
Get-RegItems -path $_.PsPath -match $match
}
} #end function RegSearch
#$search = "HKCU:\SOFTWARE\Microsoft\Office"
$searchkey = ‘HKLM:\SOFTWARE\Microsoft\Office\’
#$searchkey = "HKLM:\Software\Classes\"
$pattern = "EventSystem"
cls
$result = #()
Measure-Command {$result = Regsearch -path $searchkey -match $pattern }
# TESTING
#$t = #( "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Software\Classes",
# "HKLM:\Software\Classes\Wow6432Node\CLSID\",
# "HKCU:\SOFTWARE\Microsoft\Office\",
# "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office")
#cls
#$t |ForEach { Get-RegItems -path $_ } | fl
if ($result.Count) {
$result
"Count: {0}" -f ($result.Count-1)
}
else {
"Path: {0} `nNo Items found" -f $searchkey
}
I accepted the challenge and made it "as fast as possible".
Now it is even faster than REGEDIT or any other tool.
The below sample lasts 11 seconds to parse the complete OFFICE-key and all subkeys.
In addition, it also searches for string-matches in REG-BINARY etc.
Enjoy!
# carsten.giese#googlemail.com
# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = #'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx(
UInt32 hkey,
StringBuilder lpSubKey,
int ulOptions,
int samDesired,
out IntPtr phkResult
);
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey(
IntPtr hKey,
StringBuilder lpClass, Int32 lpCls, Int32 spare,
out int subkeys, out int skLen, int mcLen, out int values,
out int vNLen, out int mvLen, int secDesc,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue(
IntPtr hKey,
int dwIndex,
IntPtr lpValueName,
ref IntPtr lpcchValueName,
IntPtr lpReserved,
out IntPtr lpType,
IntPtr lpData,
ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx(
IntPtr hKey,
int dwIndex,
IntPtr lpName,
ref int lpcName,
IntPtr lpReserved,
IntPtr lpClass,
int lpcClass,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'#
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) {
# open the key:
[IntPtr]$hkey = 0
$result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25,[ref]$hkey)
if ($result -eq 0) {
# get details of the key:
$subKeyCount = 0
$maxSubKeyLen = 0
$valueCount = 0
$maxNameLen = 0
$maxValueLen = 0
$time = $global:time
$result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time)
if ($result -eq 0) {
$maxSubkeyLen += $maxSubkeyLen+1
$maxNameLen += $maxNameLen +1
$maxValueLen += $maxValueLen +1
}
# enumerate the values:
if ($valueCount -gt 0) {
$type = [IntPtr]0
$pName = $marshal::AllocHGlobal($maxNameLen)
$pValue = $marshal::AllocHGlobal($maxValueLen)
foreach ($index in 0..($valueCount-1)) {
$nameLen = $maxNameLen
$valueLen = $maxValueLen
$result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen)
if ($result -eq 0) {
$name = $marshal::PtrToStringUni($pName)
$value = switch ($type) {
1 {$marshal::PtrToStringUni($pValue)}
2 {$marshal::PtrToStringUni($pValue)}
3 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) {
[System.Text.Encoding]::Unicode.GetString($b)
} else {
[System.Text.Encoding]::UTF8.GetString($b)}
}
4 {$marshal::ReadInt32($pValue)}
7 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
$msz = [System.Text.Encoding]::Unicode.GetString($b)
$msz.TrimEnd(0).split(0)}
11 {$marshal::ReadInt64($pValue)}
}
if ($name -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
} elseif ($value -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
}
}
}
$marshal::FreeHGlobal($pName)
$marshal::FreeHGlobal($pValue)
}
# enumerate the subkeys:
if ($subkeyCount -gt 0) {
$subKeyList = #()
$pName = $marshal::AllocHGlobal($maxSubkeyLen)
$subkeyList = foreach ($index in 0..($subkeyCount-1)) {
$nameLen = $maxSubkeyLen
$result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time)
if ($result -eq 0) {
$marshal::PtrToStringUni($pName)
}
}
$marshal::FreeHGlobal($pName)
}
# close:
$result = $reg::RegCloseKey($hkey)
# get Tree-Size from each subkey:
$subKeyValueCount = 0
if ($subkeyCount -gt 0) {
foreach ($subkey in $subkeyList) {
$subKeyValueCount += search-RegistryTree "$path\$subkey"
}
}
return ($valueCount+$subKeyValueCount)
}
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "enterprise"
$hive = [uint32]"0x80000002" #HKLM
$subkey = "SOFTWARE\Microsoft\Office"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
The single biggest improvement you can make here is changing:
Set-Location -literalpath $path
$d= Get-Item .
to
$d = Get-Item -LiteralPath $path
Manipulating the location stack for each key in the hierarchy introduces A LOT of unnecessary overhead
User function call overhead (scriptblocks included) is extremely big (e.g. 0.1-1ms). This becomes a very serious issue when the function is executed thousands/millions of times. Surprisingly, it's not mentioned in optimization-related articles (at least I've never seen it and I googled this topic a lot).
Unfortunately, the only only real solution to this particular issue is to inline the code at the cost of duplication and reduced readability.
Optimization should include code profiling.
PowerShell doesn't have a code profiler so you'll need to do it manually with Measure-Command.
Use System.Diagnostics.Stopwatch inside loops to display the accumulated time:
# global stopwatch
$sw1 = [Diagnostics.Stopwatch]::new()
$sw2 = [Diagnostics.Stopwatch]::new()
............
forEach(....) {
........
$sw1.start()
........
$sw1.stop()
........
$sw2.start()
........
$sw2.stop()
........
}
............
echo $sw1.ElapsedMilliseconds, $sw2.ElapsedMilliseconds
Here is a faster version of you sample-script.
Lasts ca. 1 minute on my machine.
If you need it faster, then you need to work with advapi32.dll-Pinvokes, but then
it will end quite complex.
Function Get-RegItems {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
#write-host $path.Substring(30)
$key = Get-Item -literalpath $path
ForEach ($entry in $key.Property) {
$value = $key.GetValue($entry)
if (($entry -match $match) -or ($value -match $match ) -or ($path -match $match)) {
write-host "key=$path property=$entry value=$value"
}
}
}
Function RegSearch {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
Get-RegItems -path $path -match $match
ForEach ($item in get-ChildItem -literalpath $path -ea 0) {
RegSearch -path $item.PsPath -match $match
}
}
cls
Remove-Variable * -ea 0
[System.GC]::Collect()
$searchkey =‘HKLM:\SOFTWARE\Microsoft\Office’
$pattern = "EventSystem"
measure-command {
$result = RegSearch -path $searchkey -match $pattern
}
Don't use the registry drive provider, if you want it faster.
I've also read classes with static methods are faster.

File System Watcher stops working when converting Word doc/docx files to PDF

I have a Powershell script for automatic converting .doc/.docx files to *.pdf.
The script is running well for the first file. But if I put another file in the watched folder, the watcher doesn't trigger an event.
Here is the complete script. If I comment out the all $doc variables, the script is running multiple times without any problems. Did I ignore/overlook something?
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "$Env:DropboxRoot"
$watcher.Filter = "*.doc*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
Add-type -AssemblyName Microsoft.Office.Interop.Word
$action = {
$name = (get-item $Event.SourceEventArgs.FullPath).BaseName
### DON'T PROCESS WORD BACKUP FILES (START WITH A TILDE ~)
if(!($name.startsWith("~"))){
write-host Triggered event from $Event.SourceEventArgs.FullPath
$inputFilePath = $Event.SourceEventArgs.FullPath
$parentPath = (get-item $inputFilePath).Directory
$filename = (get-item $inputFilePath).BaseName
$pdfDir = "$parentPath\PDF"
if(!(Test-Path -Path $pdfDir)){
New-Item -ItemType directory -Path $pdfDir
}
###Execute PDF generate script
write-host Create word object
$word = New-Object -ComObject "Word.Application"
######define the parameters######
write-host Define parameters
$wdExportFormat =[Microsoft.Office.Interop.Word.WdExportFormat]::wdExportFormatPDF
$OpenAfterExport = $false
$wdExportOptimizeFor = [Microsoft.Office.Interop.Word.WdExportOptimizeFor]::wdExportOptimizeForOnScreen
$wdExportItem = [Microsoft.Office.Interop.Word.WdExportItem]::wdExportDocumentContent
$IncludeDocProps = $true
$KeepIRM = $false #Don't export Inormation Rights Management informations
$wdExportCreateBookmarks = [Microsoft.Office.Interop.Word.WdExportCreateBookmarks]::wdExportCreateWordBookmarks #Keep bookmarks
$DocStructureTags = $true #Add additional data for screenreaders
$BitmapMissingFonts = $true
$UseISO19005_1 = $true #Export as PDF/A
$outputFilePath = $pdfDir + "\" + $filename + ".pdf"
$doc = $word.Documents.Open($inputFilePath)
$doc.ExportAsFixedFormat($OutputFilePath,$wdExportFormat,$OpenAfterExport,`
$wdExportOptimizeFor,$wdExportRange,$wdStartPage,$wdEndPage,$wdExportItem,$IncludeDocProps,`
$KeepIRM,$wdExportCreateBookmarks,$DocStructureTags,$BitmapMissingFonts,$UseISO19005_1)
$doc.Close()
$word.Quit()
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc)
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($word)
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
}
$created = Register-ObjectEvent $watcher -EventName "Created" -Action $action
$renamed = Register-ObjectEvent $watcher -EventName "Renamed" -Action $action
while($true) {
sleep 5
}`
Your script has a few issues, that more debugging logic could find.
In some cases, (Get-Item System.Management.Automation.PSEventArgs.SourceEventArgs.FullPath) returns null. For unknown reasons, this seems to happen once for every document that gets converted. Perhaps it has to do with the "~Temp" files.
Subsequently, if(!($name.startsWith("~") will throw an exception.
When you use $inputFilePath = $Event.SourceEventArgs.FullPath, your variable is a FileInfo, and really you want to pass a string to $word.Documents.Open($inputFilePath).
Lastly, sometimes BaseName is null. Not sure why but the code could test for that or use other means to dissect the FullPath to get names and path parts.
All that said, once you get this working, my personal experience is that calling the COM object on Word to do this conversion in PowerShell is unreliable (Word hangs, ~Temp files get left behind, you have to kill Word from task manager, the COM calls in PowerShell never return). My testing shows that calling a C# console app to do the conversion is much more reliable. You could write this directory watcher and converter completely in C# and accomplish the same task.
Assuming you still want to combine the two, a PowerShell watcher, and a C# Word to PDF converter, below is a solution I came up with. The script runs for about a minute so you can test in the ISE or Console. From the Console press a key to exit. Before exiting, the script exits cleanly by unregistering the events which is quite helpful while testing in the ISE. Change this accordingly for how you intend to run the script.
PowerShell watcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "d:\test\docconvert\src"
$watcher.Filter = "*.doc*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
# copy this somehwere appropriate
# perhaps in same directory as your script
# put on a read-only share, etc.
$wordToPdf = 'd:\test\docconvert\WordToPdf\WordToPdf\bin\Debug\WordToPdf.exe'
$action = {
try
{
Write-Host "Enter action # $(Get-Date)"
$fullPathObject = (Get-Item $Event.SourceEventArgs.FullPath)
if (!($fullPathObject))
{
Write-Host "(Get-Item $Event.SourceEventArgs.FullPath) returned null."
return
}
$fullPath = ($fullPathObject).ToString()
Write-Host "Triggered event from $fullPath"
$fileName = Split-Path $FullPath -Leaf
if ($fileName -and ($fileName.StartsWith("~")))
{
Write-Host "Skipping temp file"
return
}
# put pdf in same dir as the file
# can be changed, but a lot easier to test this way
$pdfDir = Split-Path $FullPath -Parent
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$outputFilePath = Join-Path $pdfDir $($baseName + ".pdf")
Write-Host "outputFilePath is: '$outputFilePath'"
# call c# WordToPdf to do conversion because
# it is way more reliable than similar calls
# from PowerShell
& $wordToPdf $fullPath $outputFilePath
if ($LASTEXITCODE -ne 0)
{
Write-Host "Conversion result: FAIL"
}
else
{
Write-Host "Conversion result: OK"
}
}
catch
{
Write-Host "Exception from ACTION:`n$($_ | Select *)"
}
finally
{
Write-Host "Exit action # $(Get-Date)"
}
}
$created = Register-ObjectEvent $watcher -EventName "Created" -Action $action
$renamed = Register-ObjectEvent $watcher -EventName "Renamed" -Action $action
$count = 12
while($count--) {
Write-Output "run/sleep ($count)..."
sleep 5
# will exit from console, not ISE
if ([console]::KeyAvailable) {
$key = [console]::ReadKey()
break
}
}
$created | % {Unregister-Event $_.Name}
$renamed | % {Unregister-Event $_.Name}
C# WordToPdf converter
add appropriate error checking for the arguments...
add Reference to COM Microsoft.Office.Interop.Word
using System;
using Microsoft.Office.Interop.Word;
namespace WordToPdf
{
class Program
{
static int Main(string[] args)
{
Console.WriteLine($"Converting: {args[0]} to {args[1]}");
var conversion = new DocumentConversion();
bool result = conversion.WordToPdf(args[0], args[1]);
if (result)
{
return 0;
}
else {
return 1;
}
}
}
public class DocumentConversion
{
private Microsoft.Office.Interop.Word.Application Word;
private object Unknown = Type.Missing;
private object True = true;
private object False = false;
public bool WordToPdf(object Source, object Target)
{
bool ret = true;
if (Word == null) Word = new Microsoft.Office.Interop.Word.Application();
try
{
Word.Visible = false;
Word.Documents.Open(ref Source, ref Unknown,
ref True, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown);
Word.Application.Visible = false;
Word.WindowState = WdWindowState.wdWindowStateMinimize;
#if false
object saveFormat = Microsoft.Office.Interop.Word.WdSaveFormat.wdFormatPDF;
Word.ActiveDocument.SaveAs(ref Target, ref saveFormat,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown, ref Unknown,
ref Unknown, ref Unknown);
#else
Word.ActiveDocument.ExportAsFixedFormat(
(string)Target, WdExportFormat.wdExportFormatPDF,
false, WdExportOptimizeFor.wdExportOptimizeForOnScreen,
WdExportRange.wdExportAllDocument, 0, 0,
WdExportItem.wdExportDocumentContent, true, false,
WdExportCreateBookmarks.wdExportCreateWordBookmarks,
true, true, true);
#endif
}
catch (Exception e)
{
Console.WriteLine(e.Message);
ret = false;
}
finally
{
if (Word != null)
{
// close the application
Word.Quit(ref Unknown, ref Unknown, ref Unknown);
}
}
return ret;
}
}
}

How to capture process output asynchronously in powershell?

I want to capture stdout and stderr from a process that I start in a Powershell script and display it asynchronously to the console. I've found some documentation on doing this through MSDN and other blogs.
After creating and running the example below, I can't seem to get any output to be displayed asynchronously. All of the output is only displayed when the process terminates.
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "cmd.exe"
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.Arguments = "/c echo `"hi`" `& timeout 5"
$action = { Write-Host $EventArgs.Data }
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -Action $action | Out-Null
$ps.start() | Out-Null
$ps.BeginOutputReadLine()
$ps.WaitForExit()
In this example, I was expecting to see the output of "hi" on the commandline before the end of program execution because the OutputDataReceived event should have been triggered.
I've tried this using other executables - java.exe, git.exe, etc. All of them have the same effect, so I'm left to think that there is something simple that I'm not understanding or have missed. What else needs to be done to read stdout asynchronously?
Unfortunately asynchronous reading is not that easy if you want to do it properly. If you call WaitForExit() without timeout you could use something like this function I wrote (based on C# code):
function Invoke-Executable {
# Runs the specified executable and captures its exit code, stdout
# and stderr.
# Returns: custom object.
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$sExeFile,
[Parameter(Mandatory=$false)]
[String[]]$cArgs,
[Parameter(Mandatory=$false)]
[String]$sVerb
)
# Setting process invocation parameters.
$oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
$oPsi.CreateNoWindow = $true
$oPsi.UseShellExecute = $false
$oPsi.RedirectStandardOutput = $true
$oPsi.RedirectStandardError = $true
$oPsi.FileName = $sExeFile
if (! [String]::IsNullOrEmpty($cArgs)) {
$oPsi.Arguments = $cArgs
}
if (! [String]::IsNullOrEmpty($sVerb)) {
$oPsi.Verb = $sVerb
}
# Creating process object.
$oProcess = New-Object -TypeName System.Diagnostics.Process
$oProcess.StartInfo = $oPsi
# Creating string builders to store stdout and stderr.
$oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
$oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder
# Adding event handers for stdout and stderr.
$sScripBlock = {
if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
$Event.MessageData.AppendLine($EventArgs.Data)
}
}
$oStdOutEvent = Register-ObjectEvent -InputObject $oProcess `
-Action $sScripBlock -EventName 'OutputDataReceived' `
-MessageData $oStdOutBuilder
$oStdErrEvent = Register-ObjectEvent -InputObject $oProcess `
-Action $sScripBlock -EventName 'ErrorDataReceived' `
-MessageData $oStdErrBuilder
# Starting process.
[Void]$oProcess.Start()
$oProcess.BeginOutputReadLine()
$oProcess.BeginErrorReadLine()
[Void]$oProcess.WaitForExit()
# Unregistering events to retrieve process output.
Unregister-Event -SourceIdentifier $oStdOutEvent.Name
Unregister-Event -SourceIdentifier $oStdErrEvent.Name
$oResult = New-Object -TypeName PSObject -Property ([Ordered]#{
"ExeFile" = $sExeFile;
"Args" = $cArgs -join " ";
"ExitCode" = $oProcess.ExitCode;
"StdOut" = $oStdOutBuilder.ToString().Trim();
"StdErr" = $oStdErrBuilder.ToString().Trim()
})
return $oResult
}
It captures stdout, stderr and exit code. Example usage:
$oResult = Invoke-Executable -sExeFile 'ping.exe' -cArgs #('8.8.8.8', '-a')
$oResult | Format-List -Force
For more info and alternative implementations (in C#) read this blog post.
Based on Alexander Obersht's answer I've created a function that uses timeout and asynchronous Task classes instead of event handlers.
According to Mike Adelson
Unfortunately, this method(event handlers) provides no way to know
when the last bit of data has been received. Because everything is
asynchronous, it is possible (and I have observed this) for events to
fire after WaitForExit() has returned.
function Invoke-Executable {
# from https://stackoverflow.com/a/24371479/52277
# Runs the specified executable and captures its exit code, stdout
# and stderr.
# Returns: custom object.
# from http://www.codeducky.org/process-handling-net/ added timeout, using tasks
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$sExeFile,
[Parameter(Mandatory=$false)]
[String[]]$cArgs,
[Parameter(Mandatory=$false)]
[String]$sVerb,
[Parameter(Mandatory=$false)]
[Int]$TimeoutMilliseconds=1800000 #30min
)
Write-Host $sExeFile $cArgs
# Setting process invocation parameters.
$oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
$oPsi.CreateNoWindow = $true
$oPsi.UseShellExecute = $false
$oPsi.RedirectStandardOutput = $true
$oPsi.RedirectStandardError = $true
$oPsi.FileName = $sExeFile
if (! [String]::IsNullOrEmpty($cArgs)) {
$oPsi.Arguments = $cArgs
}
if (! [String]::IsNullOrEmpty($sVerb)) {
$oPsi.Verb = $sVerb
}
# Creating process object.
$oProcess = New-Object -TypeName System.Diagnostics.Process
$oProcess.StartInfo = $oPsi
# Starting process.
[Void]$oProcess.Start()
# Tasks used based on http://www.codeducky.org/process-handling-net/
$outTask = $oProcess.StandardOutput.ReadToEndAsync();
$errTask = $oProcess.StandardError.ReadToEndAsync();
$bRet=$oProcess.WaitForExit($TimeoutMilliseconds)
if (-Not $bRet)
{
$oProcess.Kill();
# throw [System.TimeoutException] ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ")
}
$outText = $outTask.Result;
$errText = $errTask.Result;
if (-Not $bRet)
{
$errText =$errText + ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ")
}
$oResult = New-Object -TypeName PSObject -Property ([Ordered]#{
"ExeFile" = $sExeFile;
"Args" = $cArgs -join " ";
"ExitCode" = $oProcess.ExitCode;
"StdOut" = $outText;
"StdErr" = $errText
})
return $oResult
}
I couldn't get either of these examples to work with PS 4.0.
I wanted to run puppet apply from an Octopus Deploy package (via Deploy.ps1) and see the output in "real time" rather than wait for the process to finish (an hour later), so I came up with the following:
# Deploy.ps1
$procTools = #"
using System;
using System.Diagnostics;
namespace Proc.Tools
{
public static class exec
{
public static int runCommand(string executable, string args = "", string cwd = "", string verb = "runas") {
//* Create your Process
Process process = new Process();
process.StartInfo.FileName = executable;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
//* Optional process configuration
if (!String.IsNullOrEmpty(args)) { process.StartInfo.Arguments = args; }
if (!String.IsNullOrEmpty(cwd)) { process.StartInfo.WorkingDirectory = cwd; }
if (!String.IsNullOrEmpty(verb)) { process.StartInfo.Verb = verb; }
//* Set your output and error (asynchronous) handlers
process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);
//* Start process and handlers
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
//* Return the commands exit code
return process.ExitCode;
}
public static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) {
//* Do your stuff with the output (write to console/log/StringBuilder)
Console.WriteLine(outLine.Data);
}
}
}
"#
Add-Type -TypeDefinition $procTools -Language CSharp
$puppetApplyRc = [Proc.Tools.exec]::runCommand("ruby", "-S -- puppet apply --test --color false ./manifests/site.pp", "C:\ProgramData\PuppetLabs\code\environments\production");
if ( $puppetApplyRc -eq 0 ) {
Write-Host "The run succeeded with no changes or failures; the system was already in the desired state."
} elseif ( $puppetApplyRc -eq 1 ) {
throw "The run failed; halt"
} elseif ( $puppetApplyRc -eq 2) {
Write-Host "The run succeeded, and some resources were changed."
} elseif ( $puppetApplyRc -eq 4 ) {
Write-Warning "WARNING: The run succeeded, and some resources failed."
} elseif ( $puppetApplyRc -eq 6 ) {
Write-Warning "WARNING: The run succeeded, and included both changes and failures."
} else {
throw "Un-recognised return code RC: $puppetApplyRc"
}
Credit goes to T30 and Stefan Goßner
The examples here are all useful, but didn't completely suit my use case. I didn't want to invoke the command and exit. I wanted to open a command prompt, send input, read the output, and repeat. Here's my solution for that.
Create Utils.CmdManager.cs
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
namespace Utils
{
public class CmdManager : IDisposable
{
const int DEFAULT_WAIT_CHECK_TIME = 100;
const int DEFAULT_COMMAND_TIMEOUT = 3000;
public int WaitTime { get; set; }
public int CommandTimeout { get; set; }
Process _process;
StringBuilder output;
public CmdManager() : this("cmd.exe", null, null) { }
public CmdManager(string filename) : this(filename, null, null) { }
public CmdManager(string filename, string arguments) : this(filename, arguments, null) { }
public CmdManager(string filename, string arguments, string verb)
{
WaitTime = DEFAULT_WAIT_CHECK_TIME;
CommandTimeout = DEFAULT_COMMAND_TIMEOUT;
output = new StringBuilder();
_process = new Process();
_process.StartInfo.FileName = filename;
_process.StartInfo.RedirectStandardInput = true;
_process.StartInfo.RedirectStandardOutput = true;
_process.StartInfo.RedirectStandardError = true;
_process.StartInfo.CreateNoWindow = true;
_process.StartInfo.UseShellExecute = false;
_process.StartInfo.ErrorDialog = false;
_process.StartInfo.Arguments = arguments != null ? arguments : null;
_process.StartInfo.Verb = verb != null ? verb : null;
_process.EnableRaisingEvents = true;
_process.OutputDataReceived += (s, e) =>
{
lock (output)
{
output.AppendLine(e.Data);
};
};
_process.ErrorDataReceived += (s, e) =>
{
lock (output)
{
output.AppendLine(e.Data);
};
};
_process.Start();
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
_process.StandardInput.AutoFlush = true;
}
public void RunCommand(string command)
{
_process.StandardInput.WriteLine(command);
}
public string GetOutput()
{
return GetOutput(null, CommandTimeout, WaitTime);
}
public string GetOutput(string endingOutput)
{
return GetOutput(endingOutput, CommandTimeout, WaitTime);
}
public string GetOutput(string endingOutput, int commandTimeout)
{
return GetOutput(endingOutput, commandTimeout, WaitTime);
}
public string GetOutput(string endingOutput, int commandTimeout, int waitTime)
{
string tempOutput = "";
int tempOutputLength = 0;
int amountOfTimeSlept = 0;
// Loop until
// a) command timeout is reached
// b) some output is seen
while (output.ToString() == "")
{
if (amountOfTimeSlept >= commandTimeout)
{
break;
}
Thread.Sleep(waitTime);
amountOfTimeSlept += waitTime;
}
// Loop until:
// a) command timeout is reached
// b) endingOutput is found
// c) OR endingOutput is null and there is no new output for at least waitTime
while (amountOfTimeSlept < commandTimeout)
{
if (endingOutput != null && output.ToString().Contains(endingOutput))
{
break;
}
else if(endingOutput == null && tempOutputLength == output.ToString().Length)
{
break;
}
tempOutputLength = output.ToString().Length;
Thread.Sleep(waitTime);
amountOfTimeSlept += waitTime;
}
// Return the output and clear the buffer
lock (output)
{
tempOutput = output.ToString();
output.Clear();
return tempOutput.TrimEnd();
}
}
public void Dispose()
{
_process.Kill();
}
}
}
Then from PowerShell add the class and use it.
Add-Type -Path ".\Utils.CmdManager.cs"
$cmd = new-object Utils.CmdManager
$cmd.GetOutput() | Out-Null
$cmd.RunCommand("whoami")
$cmd.GetOutput()
$cmd.RunCommand("cd")
$cmd.GetOutput()
$cmd.RunCommand("dir")
$cmd.GetOutput()
$cmd.RunCommand("cd Desktop")
$cmd.GetOutput()
$cmd.RunCommand("cd")
$cmd.GetOutput()
$cmd.RunCommand("dir")
$cmd.GetOutput()
$cmd.Dispose()
Don't forget to call the Dispose() function at the end to clean up the process that is running in the background. Alternatively, you could close that process by running something like $cmd.RunCommand("exit")
I came here looking for a solution to create a wrapper that logs the process, and outputs it to screen. None of these worked for me. I made this code, which seemed to work fine.
The PSDataCollection allows you to continue out with your script, without having to wait for process to complete.
Using namespace System.Diagnostics;
Using namespace System.Management.Automation;
$Global:Dir = Convert-Path "."
$Global:LogPath = "$global:Dir\logs\mylog.log"
[Process]$Process = [Process]::New();
[ProcessStartInfo]$info = [ProcessStartInfo]::New();
$info.UseShellExecute = $false
$info.Verb = "runas"
$info.WorkingDirectory = "$Global:Dir\process.exe"
$info.FileName = "$Global:Dir\folder\process.exe"
$info.Arguments = "-myarg yes -another_arg no"
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$Process.StartInfo = $info;
$Process.EnableRaisingEvents = $true
$Global:DataStream = [PSDataCollection[string]]::New()
$Global:DataStream.add_DataAdded(
{
$line = $this[0];
[IO.File]::AppendAllLines($LogPath, [string[]]$line);
[Console]::WriteLine($line)
$this.Remove($line);
}
)
$script = {
param([Object]$sender, [DataReceivedEventArgs]$e)
$global:Datastream.Add($e.Data)
}
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'OutputDataReceived' | Out-Null
Register-ObjectEvent -InputObject $Process -Action $script -EventName 'ErrorDataReceived' | Out-Null
$Process.Start()
$Process.BeginOutputReadLine()
$Process.BeginErrorReadLine()
If you just want to dynamically dump it to the PowerShell console do this:
my.exe | Out-Default
I can't claim to have figured it out.
See the bottom of this technet post: https://social.technet.microsoft.com/Forums/windowsserver/en-US/b6691fba-0e92-4e9d-aec2-47f3d5a17419/start-process-and-redirect-output-to-powershell-window?forum=winserverpowershell
which also refers to this stackoverflow post.
$LASTEXITCODE was also populated with the exit code from my exe which was also what I needed.

Resources