Powershell - Loop run console app >Wait for Ctrl+c - powershell

I have a windows console app that currently runs to process some files, at the end of the run, if successful, it starts a windows service and I get the output > xxx service is now running, press control_c to exit.
The console app looks at a config file to pull some parameters, I need to be able to re-run this multiple times while changing the parameters in the config file first. To do this manually I'd do the following:
change config file
run the app from powershell
wait for the message above to appear
click ctrl + c to terminate
change config file and run again
I thought it makes sense to automate this in a PS script where I can just pass the config values for all the runs, then the script loops through the values, edit the config file and run the exe.
Issue I have is the loop gets "stuck" at first run because the application is waiting for the ctrl+c command so never progresses through the loop.
what I have at the moment looks like this:
foreach ($dt in $datesarr)
{
##edit config values with stuff in $dt
$output=(<path to app here>)
while ($output[-1] -notlike "*Control-C*")
{
Start-Sleep -Seconds 10
}
}
problem I have is the script never reaches the while loop as it's just stuck after running the app awaiting for ctrl + c... What I want it to do is launch the app, wait for it to get to the ctrl + c bit then exit the loop and pick the second value in the parameter.
Any thoughts would be hugely appreciated!

Try the following approach, which is based on direct use of the following, closely related .NET APIs:
System.Diagnostics.ProcessStartInfo
System.Diagnostics.Process
Instead of trying to programmatically press Ctrl-C, the process associated with the external program is simply killed (terminated).
# Remove the next line if you don't want to see verbose output.
$VerbosePreference = 'Continue'
$psi = [System.Diagnostics.ProcessStartInfo] #{
UseShellExecute = $false
WindowStyle = 'Hidden'
FileName = '<path to app here>'
Arguments = '<arguments string here>' # only if args must be passed
RedirectStandardOutput = $true
RedirectStandardError = $true # optional - if you also want to examine stderr
}
Write-Verbose "Launching $($psi.FileName)..."
$ps = [System.Diagnostics.Process]::Start($psi)
Write-Verbose "Waiting for launched process $($ps.Id) to output the line of interest..."
$found = $false
while (
-not $ps.HasExited -and
-not ($found = ($line = $ps.StandardOutput.ReadLine()) -match 'Control-C')
) {
Write-Verbose "Stdout line received: $line"
}
if ($found) {
Write-Verbose "Line of interest received; terminating process $($ps.Id)."
# Note: If the process has already terminated, this will be a no-op.
# .Kill() kills only the target process itself.
# In .NET Core / .NET 5+, you can use .Kill($true) to also
# kill descendants of the process, i.e. all processes launched
# by it, directly and via its children.
$ps.Kill()
} else {
Write-Error "Process $($ps.Id) terminated before producing the expected output."
}
$ps.Dispose()

Related

PowerShell script lacks consistency when run through task scheduler, am I missing something or is this Windows being Windows?

I am a beginner in PowerShell.
I have created myself a PowerShell program to act as my alarm clock in the morning. I have task scheduler executing it on a time trigger. The problem i am having is a lack of consistency. Sometimes it will run properly without any interference, other times it will open PowerShell, error out and close immediately without executing (no error code). When i execute it myself with a double click, it seems to work just fine.
Execution Policy = All-Signed
Task Scheduler
Trigger Tab:
Trigger: Daily
Details: At 8:00 AM every Day
Status: Enabled
Action Tab:
Action: Start a Program
Program/Script: PowerShell.exe
Add arguments: -NoExit D:\Programs\AlarmClock\AlarmClockScript.ps1
Script:
#define loop start state
$Snoozevar = 'Yes'
#Import form module (for menu)
[reflection.assembly]::LoadWithPartialName("System.Windows.forms") | Out-Null
#Menu
$snoozeTxtBox = New-Object System.Windows.Forms.Button
$snoozeTxtBox.Text = 'Snooze'
$snoozeTxtBox.Location = '50,15'
$snoozeTxtBox.Size = '60,23'
$snoozeTxtBox.DialogResult = [System.Windows.Forms.DialogResult]::Yes # 'Snooze' = Yes
$quitTxtBox = New-Object System.Windows.Forms.Button
$quitTxtBox.Text = 'I`m Up'
$quitTxtBox.Location = '125,15'
$quitTxtBox.Size = '50,23'
$quitTxtBox.DialogResult = [System.Windows.Forms.DialogResult]::No # 'I`m Up' = No
$basicForm = New-Object System.Windows.Forms.Form
$basicForm.StartPosition = 'CenterScreen'
$basicForm.Size = '250,100'
$basicForm.Controls.Add($snoozeTxtBox)
$basicForm.Controls.Add($quitTxtBox)
$basicForm.TopMost = $true
while ($Snoozevar -eq 'Yes'){
Start-Process "D:\Programs\Winamp\winamp.exe" /PLAY # Start Winamp /autoplay
Start-Process D:\Programs\nircmd\nircmd.exe -ArgumentList " setsysvolume 65535" #Max Volume
$Snoozevar = $basicForm.ShowDialog() # Call Menu, assign output to $Snoozevar
$pro = Get-Process -Name "winamp" # Kill winamp post menu selection
Stop-Process -Id $pro.Id
$pro = ""
if ($Snoozevar -eq 'No'){ # Clean up powershell
$pro = Get-Process -Name powershell
Stop-Process $pro
} #end if
$rngvar = Get-Random -Minimum 540 -Maximum 720 # Time to Snooze (9-12 minutes)
Start-Sleep -Seconds $rngvar
} #end while
# SIG # Begin signature block
...
# SIG # End signature block
This is my first time asking a question here, please forgive and point out mistakes in forum standards.
Thank You in advance!
Here's a summary of the things that can be done to diagnose an inconsistend scheduled task execution.
Since your task is interactive (have a form), Run whether user is logged on or not should be left unchecked. While you'd normally want it checked most of the time, tasks that interact with the user (popup / forms / etc...) won't work properly if thus option is checked.
Add Start-Transcript -Path "Some\Path\AlarmLog_$(get-date -f 'yyyyMMdd').txt at the beginning of your file and Stop-Transcript at the end to gain more insight on when it fail
Make sure to check the Conditions tab as there are additional constraint that could affect task execution (eg: By default, task will not execute if on battery power)
If the task is running under a different user or in a different context (eg: with Highest priviledges), try to execute your script in that context to see if it fail (for instance, start Vscode / ISE using that context and run the task)
If you have multiple set of operations, you can wrap them in Try / Catch block so if one set fail, you can perform additional logging and also decide whether or not the task should be cancelled altogether or continue through. (Note: When using try/catch, you'll want to set -ErrorAction Stop on the functions that have that parameter to make sure they get caught properly in the catch block.
References
Msdocs - Start-Transcript
Msdocs - Task scheduler -Security context for running task

Powershell AcceptTcpClient() cannot be interrupted by Ctrl-C

I am writing a simple TCP/IP server using Powershell. I notice that Ctrl-C cannot interrupt the AcceptTcpClient() call. Ctrl-C works fine after the call though. I have searched around, nobody reported similar problem so far.
The problem can be repeated by the following simple code. I am using Windows 10, latest patch, with the native Powershell terminal, not Powershell ISE.
$listener=new-object System.Net.Sockets.TcpListener([system.net.ipaddress]::any, 4444)
$listener.start()
write-host "listener started at port 4444"
$tcpConnection = $listener.AcceptTcpClient()
write-host "accepted a client"
This is what happens when I run it
ps1> .\test_ctrl_c.ps1
listener started at port 4444
(Ctrl-C doesn't work here)
After getting #mklement0's answer, I gave up my original clean code. I figured out a workaround. Now Ctrl-C can interrupt my program
$listener=new-object System.Net.Sockets.TcpListener([system.net.ipaddress]::any, 4444)
$listener.start()
write-host "listener started at port 4444"
while ($true) {
if ($listener.Pending()) {
$tcpConnection = $listener.AcceptTcpClient()
break;
}
start-sleep -Milliseconds 1000
}
write-host "accepted a client"
Now Ctrl-C works
ps1> .\test_ctrl_c.ps1
listener started at port 4444
(Ctrl-C works here)
(As of PowerShell 7.0) Ctrl-C only works while PowerShell code is executing, not during execution of a .NET method.
Since most .NET method calls execute quickly, the problem doesn't usually surface.
See this GitHub issue for a discussion and background information.
As for possible workarounds:
The best approach - if possible - is the one shown in your own answer:
Run in a loop that periodically polls for a condition, sleeping between tries, and only invoke the method when the condition being met implies that the method will then execute quickly instead of blocking indefinitely.
If this is not an option (if there is no such condition you can test for), you can run the blocking method in a background job, so that it runs in a child process that can be terminated on demand by the caller; do note the limitations of this approach, however:
Background jobs are slow and resource-intensive, due to needing to run a new PowerShell instance in a hidden child process.
Since cross-process marshaling of inputs to and outputs from the job is necessary:
Inputs and output won't be live objects.
Complex objects (objects other than instances of primitive .NET types and a few well-known types) will be emulations of the original objects; in essence, objects with static copies of the property values, and no methods - see this answer for background information.
Here's a simple demonstration:
# Start the long-running, blocking operation in a background job (child process).
$jb = Start-Job -ErrorAction Stop {
# Simulate a long-running, blocking .NET method call.
[Threading.Thread]::Sleep(5000)
'Done.'
}
$completed = $false
try {
Write-Host -ForegroundColor Yellow "Waiting for background job to finish. Press Ctrl-C to abort."
# Note: The output collected won't be *live* objects, and with complex
# objects will be *emulations* of the original objects that have
# static copies of their property values and no methods.
$output = Receive-Job -Wait -Job $jb
$completed = $true
}
finally { # This block is called even when Ctrl-C has been pressed.
if (-not $completed) { Write-Warning 'Aborting due to Ctrl-C.' }
# Remove the background job.
# * If it is still running and we got here due to Ctrl-C, -Force is needed
# to forcefully terminate it.
# * Otherwise, normal job cleanup is performed.
Remove-Job -Force $jb
# If we got here due to Ctrl-C, execution stops here.
}
# Getting here means: Ctrl-C was *not* pressed.
# Show the output received from the job.
Write-Host -ForegroundColor Yellow "Job output received:"
$output
If you execute the above script and do not press Ctrl-C, you'll see:
If you do press Ctrl-C, you'll see:

Multiple io.filesystemwatchers in parallel

I have three different tasks that I wish to outsource to filesystem watchers in powershell. I have the code all set up to initialize two watchers and to check every ten seconds to make sure they are running. However the tasks that they perform last under a minute, and 5 minutes respectively. The third task I wish to outsource to a watcher takes about an hour. I am concerned that if I have all of them running simultaneously, tasks that the first two should watch for will not get done at all if the third watcher is executing its change action. Is there a way to implement or run them such that the change actions can be executed in parallel?
You can use the Start-ThreadJob cmdlet to run your file-watching tasks in parallel.
Start-ThreadJob comes with the ThreadJob module and offers a lightweight, thread-based alternative to the child-process-based regular background jobs.
It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
The following self-contained sample code:
uses thread jobs to run 2 distinct file-monitoring and processing tasks in parallel,
which neither block each other nor the caller.
Note:
Each task creates its own System.IO.FileSystemWatcher instance in the code below, though creating too many of them can put a significant load on the system, possibly resulting in events getting missed.
An alternative is to share instances, such as creating a single one in the caller's context, which the thread jobs can access (see comments in source code below).
[This is in part speculative; do tell us if I got things wrong] Direct FileSystemWatcher .NET event-handler delegates should be kept short, but subscribing to the events from PowerShell via an event job created by Register-ObjectEvent queues the events on the PowerShell side, which PowerShell then dispatches to the -Action script blocks, so that these blocks perform long-running operations below shouldn't be an immediate concern (the tasks may take a long time to catch up, though).
# Make sure that the ThreadJob module is available.
# In Windows PowerShell, it must be installed first.
# In PowerShell [Core], it is available by default.
Import-Module ThreadJob -ea Stop
try {
# Use the system's temp folder in this example.
$dir = (Get-Item -EA Ignore temp:).FullName; if (-not $dir) { $dir = $env:TEMP }
# Define the tasks as an array of custom objects that specify the dir.
# and file name pattern to monitor as well as the action script block to
# handle the events.
$tasks = # array of custom objects to describe the
[pscustomobject] #{
DirToMonitor = $dir
FileNamePattern = '*.tmp1'
Action = {
# Print status info containing the event data to the host, synchronously.
Write-Host -NoNewLine "`nINFO: Event 1 raised:`n$($EventArgs | Format-List | Out-String)"
# Sleep to simulate blocking the thread with a long-running task.
Write-Host "INFO: Event 1: Working for 4 secs."
Start-Sleep 4
# Create output, which Receive-Job can collect.
"`nEvent 1 output: " + $EventArgs.Name
}
},
[pscustomobject] #{
DirToMonitor = $dir
FileNamePattern = '*.tmp2'
Action = {
# Print status info containing the event data to the host, synchronously
Write-Host -NoNewLine "`nINFO: Event 2 raised:`n$($EventArgs | Format-List | Out-String)"
# Sleep to simulate blocking the thread with a long-running task.
Write-Host "INFO: Event 2: Working for 2 secs"
Start-Sleep 2
# Create output, which Receive-Job can collect.
"`nEvent 2 output: " + $EventArgs.Name
}
}
# Start a separate thread job for each action task.
$threadJobs = $tasks | ForEach-Object {
Start-ThreadJob -ArgumentList $_ {
param([pscustomobject] $task)
# Create and initialize a thread-specific watcher.
# Note: To keep system load low, it's generally better to use a *shared*
# watcher, if feasible. You can define it in the caller's scope
# and access here via $using:watcher
$watcher = [System.IO.FileSystemWatcher] [ordered] #{
Path = $task.DirToMonitor
Filter = $task.FileNamePattern
EnableRaisingEvents = $true # start watching.
}
# Subscribe to the watcher's Created events, which returns an event job.
# This indefinitely running job receives the output from the -Action script
# block whenever the latter is called after an event fires.
$eventJob = Register-ObjectEvent -ea stop $watcher Created -Action $task.Action
Write-Host "`nINFO: Watching $($task.DirToMonitor) for creation of $($task.FileNamePattern) files..."
# Indefinitely wait for output from the action blocks and relay it.
try {
while ($true) {
Receive-Job $eventJob
Start-Sleep -Milliseconds 500 # sleep a little
}
}
finally {
# !! This doesn't print, presumably because this is killed by the
# !! *caller* being killed, which then doesn't relay the output anymore.
Write-Host "Cleaning up thread for task $($task.FileNamePattern)..."
# Dispose of the watcher.
$watcher.Dispose()
# Remove the event job (and with it the event subscription).
$eventJob | Remove-Job -Force
}
}
}
$sampleFilesCreated = $false
$sampleFiles = foreach ($task in $tasks) { Join-Path $task.DirToMonitor ("tmp_$PID" + ($task.FileNamePattern -replace '\*')) }
Write-Host "Starting tasks...`nUse Ctrl-C to stop."
# Indefinitely wait for and display output from the thread jobs.
# Use Ctrl+C to stop.
$dtStart = [datetime]::UtcNow
while ($true) {
# Receive thread job output, if any.
$threadJobs | Receive-Job
# Sleep a little.
Write-Host . -NoNewline
Start-Sleep -Milliseconds 500
# A good while after startup, create sample files that trigger all tasks.
# NOTE: The delay must be long enough for the task event handlers to already be
# in place. How long that takes can vary.
# Watch the status output to make sure the files are created
# *after* the event handlers became active.
# If not, increase the delay or create files manually once
# the event handlers are in place.
if (-not $sampleFilesCreated -and ([datetime]::UtcNow - $dtStart).TotalSeconds -ge 10) {
Write-Host
foreach ($sampleFile in $sampleFiles) {
Write-Host "INFO: Creating sample file $sampleFile..."
$null > $sampleFile
}
$sampleFilesCreated = $true
}
}
}
finally {
# Clean up.
# Clean up the thread jobs.
Remove-Job -Force $threadJobs
# Remove the temp. sample files
Remove-Item -ea Ignore $sampleFiles
}
The above creates output such as the following (sample from a macOS machine):
Starting tasks...
Use Ctrl-C to stop.
.
INFO: Watching /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/ for creation of *.tmp1 files...
INFO: Watching /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/ for creation of *.tmp2 files...
.........
INFO: Creating sample file /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/tmp_91418.tmp1...
INFO: Creating sample file /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/tmp_91418.tmp2...
.
INFO: Event 1 raised:
ChangeType : Created
FullPath : /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/tmp_91418.tmp1
Name : tmp_91418.tmp1
INFO: Event 1: Working for 4 secs.
INFO: Event 2 raised:
ChangeType : Created
FullPath : /var/folders/19/0lxcl7hd63d6fqd813glqppc0000gn/T/tmp_91418.tmp2
Name : tmp_91418.tmp2
INFO: Event 2: Working for 2 secs
....
Event 2 output: tmp_91418.tmp2
....
Event 1 output: tmp_91418.tmp1
.................

Stop a process running longer than an hour

I posted a question a couple ago, I needed a powershell script that would start a service if it was stopped, stop the process if running longer than an hour then start it again, and if running less than an hour do nothing. I was given a great script that really helped, but I'm trying to convert it to a "process". I have the following code (below) but am getting the following error
Error
"cmdlet Start-Process at command pipeline position 3
Supply values for the following parameters:
FilePath: "
Powershell
# for debugging
$PSDefaultParameterValues['*Process:Verbose'] = $true
$str = Get-Process -Name "Chrome"
if ($str.Status -eq 'stopped') {
$str | Start-Process
} elseif ($str.StartTime -lt (Get-Date).AddHours(-1)) {
$str | Stop-Process -PassThru | Start-Process
} else {
'Chrome is running and StartTime is within the past hour!'
}
# other logic goes here
Your $str is storing a list of all processes with the name "Chrome", so I imagine you want a single process. You'll need to specify an ID in Get-Process or use $str[0] to single out a specific process in the list.
When you store a single process in $str, if you try to print your $str.Status, you'll see that it would output nothing, because Status isn't a property of a process. A process is either running or it doesn't exist. That said, you may want to have your logic instead check if it can find the process and then start the process if it can't, in which case it needs the path to the executable to start the process. More info with examples can be found here: https://technet.microsoft.com/en-us/library/41a7e43c-9bb3-4dc2-8b0c-f6c32962e72c?f=255&MSPPError=-2147217396
If you're using Powershell ISE, try storing the process in a variable in the terminal, type the variable with a dot afterwards, and Intellisense (if it's on) should give a list of all its available properties.

InvocationStateChanged Event Raised Twice in PowerShell Script

I have been writing a script in PowerShell V4 on Windows 8.1 which makes use of background processes and events. I have found something which is a little strange. Rather than post the 2,500 lines or so of my script I have included a much shorter program which exhibits the odd behaviour. I expect it is something I am doing wrong but I cannot see what the problem is. The code is as follows:
`
# Scriptblocks to simulate the background task and the action to
# take when an event is raised
[scriptblock] $MyScript = {
for ($i = 0;$i -lt 30;$i++)
{
[console]::writeline("This is a test - $i")
start-sleep -m 500
}
}
[scriptblock] $StateChanged = {
[console]::writeline("The state changed")
}
# Create a runspace pool
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, [int] $env:NUMBER_OF_PROCESSORS + 1)
$RunspacePool.ApartmentState = "MTA"
$RunspacePool.Open()
# Create and start the background task to run
$PS = [powershell]::Create()
[void] $PS.AddScript($MyScript)
$PS.RunspacePool = $RunspacePool
$Asyncresult = $PS.BeginInvoke()
# Register an interest in the InvocationStateChanged event for
# the background task. Should the event happen (which it will)
# run the $StateChanged scriptblock
Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action $StateChanged
# The loop that simulates the main purpose of the script
[int] $j = 0
while ($PS.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running)
{
if ($j -eq 2)
{
[void] $PS.BeginStop($NULL, $NULL)
}
"Running: $j" | out-host
sleep -m 400
$j = $j + 1
}
sleep 10
`
Essentially all it does is create a runspace to run a powershell scriptblock and while that is running something else happens in the foreground. I simulate someone pressing a button or, for whatever reason, a beginstop method being executed to stop the background process. That all works and the background process duly stops. However, I have registered an event for the background powershell script which runs a scriptblock when the background job changes state. The strange thing is that the scriptblock gets invoked twice and I cannot work out why.
Here is some output from running the script:
E:\Test Programs>powershell -file .\strange.ps1
This is a test - 0
Running: 0
This is a test - 1
Running: 1
The state changed
Running: 2
The state changed
E:\Test Programs>
As you can see it displays "The state changed" twice. They are a fraction of a second apart. I put a sleep 10 at the end to eliminate the possibility that it is the script stopping that is causing the second "The state changed" message.
If anyone can explain what is wrong I would be very grateful.
The InvocationStateChanged event is likely being called when on Running and Completed
If you change $StateChanged to also include the $Sender.InvocationStateInfo.State automatic variable properties, like this:
[scriptblock] $StateChanged = {
[console]::writeline("The state changed to: $($Sender.InvocationStateInfo.State)")
}
Your output will probably look like:
This is a test - 0
Running: 0
This is a test - 1
Running: 1
The state changed to: Running
Running: 2
The state changed: Completed
I am very sorry that I didn't reply a year and a half ago as I should have done. For some reason I never had a notification that there was an answer and, to be honest, I just forgot to check.
Anyway, thanks for the answer. I still have my little test script although the thing I was actually writing has been dumped and rewritten but I did check with my little test script and what I get is:
Running: 0
Running: 1
This is a test - 1
The state changed to: Stopped
The state changed to: Stopped
Running: 2
This is now on Windows 10.
Oh well, thanks for the suggestion anyway. As I said I've rewritten the original script so this is now just for interest.
Best wishes........
Colin