I have a script that kicks off an event and waits for the user to press any key to stop the script execution. I was trying to find a way to show a timer (how long the script has been running) while waiting for user input at Read-Host. Is there a way to accomplish this?
This works
$Time = [System.Diagnostics.Stopwatch]::StartNew()
while ($true) {
$CurrentTime = $Time.Elapsed
write-host $([string]::Format("`rTime: {0:d2}:{1:d2}:{2:d2}",
$CurrentTime.hours,
$CurrentTime.minutes,
$CurrentTime.seconds)) -nonewline
sleep 1
if ($Host.UI.RawUI.KeyAvailable -and ("q" -eq $Host.UI.RawUI.ReadKey("IncludeKeyUp,NoEcho").Character)) {
Write-Host "Exiting now"
break;
}
}
This gave me the output I was after :)
$StartTime = $(get-date)
$elapsedTime = $(get-date) - $StartTime
$totalTime = "{0:HH:mm:ss}" -f ([datetime]$elapsedTime.Ticks)
From the article Measuring Elapsed Time in Powershell (archived copy):
Assuming that the variable $script:StartTime was set at the
beginning of your script, elapsed time can be determined using either
of the following methods:
$elapsedTime = new-timespan $script:StartTime $(get-date)
or
$elapsedTime = $(get-date) - $script:StartTime
Both methods work exactly the same and produce a System.TimeSpan object.
So, using the above example, you could set $script:StartTime before Read-Host and then call
$elapsedTime = $(get-date) - $script:StartTime after.
Using a timer class (RIP poshtips) gave me something like this:
$Time = [System.Diagnostics.Stopwatch]::StartNew()
while ($NoEvent) {
$CurrentTime = $Time.Elapsed
write-host $([string]::Format("`rTime: {0:d2}:{1:d2}:{2:d2}",
$CurrentTime.hours,
$CurrentTime.minutes,
$CurrentTime.seconds)) -nonewline
sleep 1
#Handle event
if(event){$NoEvent = false}
}
Where $NoEvent is your event/boolean (key press func, etc.).
I'm happy with this little function expanded on from Xelco52's answer
$startTime = $(get-date)
write-host "Elapsed:00:00:00"
$NoEvent = $true
While ($NoEvent)
{
Start-Sleep 1
$elapsedTime = new-timespan $startTime $(get-date)
write-host "Elapsed:$($elapsedTime.ToString("hh\:mm\:ss"))"
#Handle event
if(event){$NoEvent = $false}
}
TLDR answer
$StartTime = $(get-date)
#...do something...
$elapsedTime = $(get-date) - $StartTime
Most of the times $elapsedTime.TotalSeconds or $elapsedTime.TotalMilliseconds is what you want.
Related
super new to scripting and this script was found online and has been edited slightly.
I figured I'd try to make it print the time after each cycle, but can't make it print an updated time; it just repeats the first printed time.
$myshell = New-Object -com "Wscript.Shell"
$today = Get-Date -format t
while ($true)
{
$myshell.sendkeys("{F15}")
Write-Host "Last Run $today"
Start-Sleep -Seconds 300
}
I've tried "Write-Output" as well but no change.
Picture of Write-Host
Get-Date is only called once, before your loop so that's why it always displays the same date time.
If you modify your code like this:
$myshell = New-Object -com "Wscript.Shell"
$today = Get-Date -format t
while ($true)
{
$myshell.sendkeys("{F15}")
Write-Host "Last Run $(Get-Date -format t)"
Start-Sleep -Seconds 300
}
It will call Get-Date from within the loop.
I want to keep running forever the appending function to file , but I aslo want to add a second true while that runs x stuff every 10s , but how can I handle that?
I have tried doing while($true){while ($true) {
write-host 'do stuff...'
Start-Sleep -Seconds 60
} } , but I dindt have a luck.
while ($true) {
Start-Sleep -Milliseconds 40
# scan all ASCII codes above 8
for ($ascii = 9; $ascii -le 254; $ascii++) {
# get current key state
$state = $API::GetAsyncKeyState($ascii)
# is key pressed?
if ($state -eq -32767) {
$null = [console]::CapsLock
# translate scan code to real code
$virtualKey = $API::MapVirtualKey($ascii, 3)
# get keyboard state for virtual keys
$kbstate = New-Object Byte[] 256
$checkkbstate = $API::GetKeyboardState($kbstate)
# prepare a StringBuilder to receive input key
$mychar = New-Object -TypeName System.Text.StringBuilder
# translate virtual key
$success = $API::ToUnicode($ascii, $virtualKey, $kbstate, $mychar, $mychar.Capacity, 0)
if ($success)
{
# add key to logger file
[System.IO.File]::AppendAllText($Path, $mychar, [System.Text.Encoding]::Unicode)
}
}
}
}
I expect my first while true append every log to the file forever , but I also want to keep running every 10s to the second while true , but how can I do this?
Have separate scripts for both actions. One with infinite while loop another with the delay and call them as background jobs.
$AppendJob = Start-Job -FilePath c:\Temp\FirstWhile.ps1 -Name AppenJob
$DelayJob = Start-Job -FilePath c:\Temp\SecondWhile.ps1 -Name DelayJob
Another possible solution not yet mentionned would be to keep you main loop and have a timer event registered for the 10 seconds "loop".
$timer = new-object timers.timer
$action = {
# Edit me... put whatever you want to be done every 10 seconds here instead.
write-host "Timer Elapse Event: $(get-date -Format ‘HH:mm:ss’)" -ForegroundColor Cyan
}
$timer.Interval = 10000 #10 seconds
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier '10secTimer' -Action $action
$timer.start()
while ($true) {
Write-Host 'Append to file...'
Start-Sleep -Seconds 3
}
#to stop run
$timer.stop()
#cleanup
Unregister-Event 10secTimer
You could also use one while loop and make use of a Stopwatch object to check when 10 seconds elapsed and perform another action then.
$StopWatch = New-Object -TypeName 'System.Diagnostics.Stopwatch'
$StopWatch.Start()
while ($true) {
Write-Host 'Do stuff...' -ForegroundColor Cyan
Write-Host "$($StopWatch.Elapsed.Seconds) seconds elapsed"
Start-Sleep -Milliseconds 1000
if ($StopWatch.Elapsed.Seconds -ge 10) {
# Put everything for your 10 seconds here.
Write-Host 'Do other stuff...' -ForegroundColor Green
$StopWatch.Reset()
$StopWatch.Start()
}
}
$StopWatch.Stop()
To answer the "two true on the same while", if you wanted to use 2 While loop imbricated like your example, you'd need to add a break statement in the inner loop if you ever want to return to the outer loop.
while ($true) {
Write-Host 'Outer loop...'
Start-Sleep -Seconds 3
While ($true) {
Write-Host 'Inner loop...'
break; # If you don't break, you'll never exit the inner loop.
}
}
Use the modulus operator % it will calculate the reminder after dividing by a number:
PS > 5 % 2
1
=> 5 /2 = 4 rest 1
PS > 250 % 250
0
Use $I++ In your function to track how often it was executed and use if with %
to execute it every 10 seconds (10 seconds / 0.040 Seconds = 250)
$I = 0
$I++
If (($I % 250) -eq 0) { do x }
$startTime = $(get-date)
write-host "`rElapsed:00:00:00"
$NoEvent = $true
While ($NoEvent)
{
Start-Sleep 1
$elapsedTime = new-timespan $startTime $(get-date)
write-host "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))"
#Handle event
if(event){$NoEvent = $false}
}
I've tried running this in the ISE as well as through the regular console. The returns are never output.
I eventually got it working using -NoNewLine switch
write-host -NoNewLine "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))"
`r only issues a CR, not a CR+LF (which is probably what you want). Use `n (‘newline’) instead.
I don't think there is any PS cmdlet that can help with overwriting text from the same line unless you are clearing the entire window with clear-host or cls, but PowerShell has a built in write-progress cmdlet if that is something else you would want to consider.
You can try:
$startTime = $(get-date)
$NoEvent = $true
While ($NoEvent)
{
for ($a=1; $a -lt 100; $a++) {
Start-Sleep 1
$elapsedTime = new-timespan $startTime $(get-date)
Write-Progress -Activity "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))" -PercentComplete $a -CurrentOperation "$a% Processed" -Status "Please wait."
#Handle event
if(event){$NoEvent = $false}
}
}
See: https://learn.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Utility/Write-Progress?view=powershell-5.1 for info on write-progress
and here is a related question: PowerShell - Overwriting line written with Write-Host
Since, I am little forgetful, I am making a simple Powershell script, which will remind me of certain specified things at given time. Below is my code, which goes into infinite loops.
$whsehll = New-Object -ComObject Wscript.shell
$Reminder = Read-Host "What time do you want a reminder of?"
$Subject = Read-Host "What do you want me to remind you?"
$Time = Get-Date -Format hh:mm
do {
Start-Sleep -Seconds 1
}while (($Reminder -ge $Time))
$whsehll.Popup("Hello, I am here to remind you of $Subject")
I have done all wayouts, but I am unable to work it out.
P.S. I ran a Get-date command first on powershell, and Got the time pattern as 03:00 instead 15:00hrs, so I assume that userinput should also be in 12 hrs clock pattern?
P.P.S. I did try other work-arounds like toggling with comparing options -gt -ge -lt -le but none of it works. Please help.
Change the following
$Time = Get-Date -Format hh:mm
do {
Start-Sleep -Seconds 1
to
do {
Start-Sleep -Seconds 1
$Time = Get-Date -Format hh:mm
and it will work.
You should take the current time each Loop, and not compare to the Initial time, when entered the loop.
Or use HH:mm for 24h format, as AgentK stated.
Note, that if $reminder ist 02:34, it will notify you at 02:35, because of -ge instead of -gt (or 14:34/35 when used HH:mm as Format).
For me it is easier to read/understand to just use a while Loop:
$wshshell = New-Object -ComObject Wscript.shell
$Reminder = Read-Host "What time do you want a reminder of?"
$Subject = Read-Host "What do you want me to remind you?"
while ($Reminder -gt (Get-Date -Format HH:mm))
{
Start-Sleep -Seconds 1
}
$wshshell.Popup("Hello, I am here to remind you of $Subject")
Fixed up your script a little and now it seems to be working:
$whsehll = New-Object -ComObject Wscript.shell
[datetime]$Reminder = Read-Host "What time do you want a reminder of?"
$Subject = Read-Host "What do you want me to remind you?"
$Time = Get-Date
do {
Start-Sleep -Seconds 1
}
while ($Reminder -ge $Time)
$whsehll.Popup("Hello, I am here to remind you of $Subject")
PS. If you using 24H format time you want to use capital H, like HH:mm
I want to set a time limit on a PowerShell (v2) script so it forcibly exits after that time limit has expired.
I see in PHP they have commands like set_time_limit and max_execution_time where you can limit how long the script and even a function can execute for.
With my script, a do/while loop that is looking at the time isn't appropriate as I am calling an external code library that can just hang for a long time.
I want to limit a block of code and only allow it to run for x seconds, after which I will terminate that code block and return a response to the user that the script timed out.
I have looked at background jobs but they operate in a different thread so won't have kill rights over the parent thread.
Has anyone dealt with this or have a solution?
Thanks!
Something like this should work too...
$job = Start-Job -Name "Job1" -ScriptBlock {Do {"Something"} Until ($False)}
Start-Sleep -s 10
Stop-Job $job
Here's my solution, inspired by this blog post. It will finish running when all has been executed, or time runs out (whichever happens first).
I place the stuff I want to execute during a limited time in a function:
function WhatIWannaDo($param1, $param2)
{
# Do something... that maybe takes some time?
Write-Output "Look at my nice params : $param1, $param2"
}
I have another funtion that will keep tabs on a timer and if everything has finished executing:
function Limit-JobWithTime($Job, $TimeInSeconds, $RetryInterval=5)
{
try
{
$timer = [Diagnostics.Stopwatch]::StartNew()
while (($timer.Elapsed.TotalSeconds -lt $TimeInSeconds) -and ('Running' -eq $job.JobStateInfo.State)) {
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds,0)
$tsString = $("{0:hh}:{0:mm}:{0:ss}" -f [timespan]::fromseconds($totalSecs))
Write-Progress "Still waiting for action $($Job.Name) to complete after [$tsString] ..."
Start-Sleep -Seconds ([math]::Min($RetryInterval, [System.Int32]($TimeInSeconds-$totalSecs)))
}
$timer.Stop()
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds,0)
$tsString = $("{0:hh}:{0:mm}:{0:ss}" -f [timespan]::fromseconds($totalSecs))
if ($timer.Elapsed.TotalSeconds -gt $TimeInSeconds -and ('Running' -eq $job.JobStateInfo.State)) {
Stop-Job $job
Write-Verbose "Action $($Job.Name) did not complete before timeout period of $tsString."
} else {
if('Failed' -eq $job.JobStateInfo.State){
$err = $job.ChildJobs[0].Error
$reason = $job.ChildJobs[0].JobStateInfo.Reason.Message
Write-Error "Job $($Job.Name) failed after with the following Error and Reason: $err, $reason"
}
else{
Write-Verbose "Action $($Job.Name) completed before timeout period. job ran: $tsString."
}
}
}
catch
{
Write-Error $_.Exception.Message
}
}
... and then finally I start my function WhatIWannaDo as a background job and pass it on to the Limit-JobWithTime (including example of how to get output from the Job):
#... maybe some stuff before?
$job = Start-Job -Name PrettyName -Scriptblock ${function:WhatIWannaDo} -argumentlist #("1st param", "2nd param")
Limit-JobWithTime $job -TimeInSeconds 60
Write-Verbose "Output from $($Job.Name): "
$output = (Receive-Job -Keep -Job $job)
$output | %{Write-Verbose "> $_"}
#... maybe some stuff after?
I know this is an old post, but I have used this in my scripts.
I am not sure if its the correct use of it, but the System.Timers.Timer that George put up gave me an idea and it seems to be working for me.
I use it for servers that sometimes hang on a WMI query, the timeout stops it getting stuck.
Instead of write-host I then output the message to a log file so I can see which servers are broken and fix them if needed.
I also don't use a guid I use the servers hostname.
I hope this makes sense and helps you.
$MyScript = {
Get-WmiObject -ComputerName MyComputer -Class win32_operatingsystem
}
$JobGUID = [system.Guid]::NewGuid()
$elapsedEventHandler = {
param ([System.Object]$sender, [System.Timers.ElapsedEventArgs]$e)
($sender -as [System.Timers.Timer]).Stop()
Unregister-Event -SourceIdentifier $JobGUID
Write-Host "Job $JobGUID removed by force as it exceeded timeout!"
Get-Job -Name $JobGUID | Remove-Job -Force
}
$timer = New-Object System.Timers.Timer -ArgumentList 3000 #just change the timeout here
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $elapsedEventHandler -SourceIdentifier $JobGUID
$timer.Start()
Start-Job -ScriptBlock $MyScript -Name $JobGUID
Here is an example of using a Timer. I haven't tried it personally, but I think it should work:
function Main
{
# do main logic here
}
function Stop-Script
{
Write-Host "Called Stop-Script."
[System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.CloseAsync()
}
$elapsedEventHandler = {
param ([System.Object]$sender, [System.Timers.ElapsedEventArgs]$e)
Write-Host "Event handler invoked."
($sender -as [System.Timers.Timer]).Stop()
Unregister-Event -SourceIdentifier Timer.Elapsed
Stop-Script
}
$timer = New-Object System.Timers.Timer -ArgumentList 2000 # setup the timer to fire the elapsed event after 2 seconds
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Elapsed -Action $elapsedEventHandler
$timer.Start()
Main
How about something like this:
## SET YOUR TIME LIMIT
## IN THIS EXAMPLE 1 MINUTE, BUT YOU CAN ALSO USE HOURS/DAYS
# $TimeSpan = New-TimeSpan -Days 1 -Hours 2 -Minutes 30
$TimeSpan = New-TimeSpan -Minutes 1
$EndTime = (Get-Date).AddMinutes($TimeSpan.TotalMinutes).ToString("HH:mm")
## START TIMED LOOP
cls
do
{
## START YOUR SCRIPT
Write-Warning "Test-Job 1...2...3..."
Start-Sleep 3
Write-Warning "End Time = $EndTime`n"
}
until ($EndTime -eq (Get-Date -Format HH:mm))
## TIME REACHED AND END SCRIPT
Write-Host "End Time reached!" -ForegroundColor Green
When using hours or days as a timer, make sure you adjust the $TimeSpan.TotalMinutes
and the HH:mm format, since this does not facilitate the use of days in the example.
I came up with this script.
Start-Transcript to log all actions and save them to a file.
Store the current process ID value in the variable $p then write it to screen.
Assign the current date to the $startTime variable.
Afterwards I assign it again and add the extra time to the current date to the var $expiration.
The updateTime function return what time there is left before the application closes. And writes it to console.
Start looping and kill process if the timer exceeds the expiration time.
That's it.
Code:
Start-Transcript C:\Transcriptlog-Cleanup.txt #write log to this location
$p = Get-Process -Id $pid | select -Expand id # -Expand selcts the string from the object id out of the current proces.
Write-Host $p
$startTime = (Get-Date) # set start time
$startTime
$expiration = (Get-Date).AddSeconds(20) #program expires at this time
# you could change the expiration time by changing (Get-Date).AddSeconds(20) to (Get-Date).AddMinutes(10)or to hours whatever you like
#-----------------
#Timer update function setup
function UpdateTime
{
$LeftMinutes = ($expiration) - (Get-Date) | Select -Expand minutes # sets minutes left to left time
$LeftSeconds = ($expiration) - (Get-Date) | Select -Expand seconds # sets seconds left to left time
#Write time to console
Write-Host "------------------------------------------------------------------"
Write-Host "Timer started at : " $startTime
Write-Host "Current time : " (Get-Date)
Write-Host "Timer ends at : " $expiration
Write-Host "Time on expire timer : "$LeftMinutes "Minutes" $LeftSeconds "Seconds"
Write-Host "------------------------------------------------------------------"
}
#-----------------
do{ #start loop
Write-Host "Working"#start doing other script stuff
Start-Sleep -Milliseconds 5000 #add delay to reduce spam and processing power
UpdateTime #call upadate function to print time
}
until ($p.HasExited -or (Get-Date) -gt $expiration) #check exit time
Write-Host "done"
Stop-Transcript
if (-not $p.HasExited) { Stop-Process -ID $p -PassThru } # kill process after time expires