Check multiple conditions, then if all are false do something - powershell

Bit of background...
I'm trying to write a script which will run constantly. I need to it check to see if the Queue directory has an XML file in it, if it does, then send an API call to start some servers.
I've got this part sorted and it works.
I'm having issues with the second part where I need it to send another API Call to shutdown the servers if the following conditions are met;
Queue Directory must be empty
Running Directory must be empty
The time between the servers starting and stopping must be to the nearest hour (its AWS so they charge by the hour, there's no point in stopping a server if its only been running for a few mins as we'll still be charged for the full hour. Then if we need to start again, we'll be charged another hour.)
Here is what I have so far:
$QueueDir = "D:\Test"
$RunningDir = "D:\Test\copydir"
while (!(Test-path $QueueDir\*.xml)) {Start-Sleep 10}
Write-Host "Starting Servers, API NORMALLY GOES HERE"
$Starttime = (Get-Date)
Write-Host "Started Servers # $Starttime"
Start-Sleep -Seconds 30
while (!(Test-Path $rundir\*.xml)) {Start-Sleep 10}
$now = (Get-Date)
$timespan = (New-TimeSpan -Start $Starttime -End $now)
if ( (Test-Path $QueueDir\*.xml) -or (Test-Path $RunningDir\*.xml) -or ($timespan.Minutes -gt 50 -and -lt 55) ) {
Write-Host "Stopping Servers, API NORMALLY GOES HERE"
$StopTime = (Get-Date)
Write-Host "Stopped Servers # $Stoptime"
}

This $timespan.Minutes -gt 50 -and -lt 55 is not valid PowerShell logic. You have to provide a value expression after -and. You would receive an error like this:
You must provide a value expression following the '-and' operator.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ExpectedValueExpression
So you would need to use it like:
$timespan.Minutes -gt 50 -and $timespacn.Minutes -lt 55

That's simple boolean algebra. You want to take action if neither of three conditions is true. That can be formulated like this:
!A ^ !B ^ !C # (not A) and (not B) and (not C)
The above can be transformed as follows ((!A ^ !B) ⇔ !(A v B)), since multiple negations in an expression tend to be ugly:
!(A v B v C) # not (A or B or C)
In your code that would look like this:
if (-not ((Test-Path ...) -or (Test-Path ...) -or ($timespan.Minutes ...))) {
...
}

Related

If not empty jump to somewhere in PS

Hello and good morning(:
I'm looking to see if I'm able to jump to somewhere in PS without wrapping it in a ScriptBlock; hell, I'd even be okay with that but, I'm just unsure on how to go about it.
What I'm trying to do is: add a Parameter Set to a function and if something is supplied to the parameter -GrpSelec(I know imma change it), then just skip the rest of the script and go to my $swap variable to perform the switch.
$Group1 = #("1st Group", "2nd Group")
$Group2 = #("3rd Group", "4th Group")
Function Test-Group{
param(
[ValidateSet("Group1","group2")]
[array]$GrpSelec)
if($GrpSelec){ &$swap }
$AllGroups = #("Group1", "Group2")
for($i=0; $i -lt $AllGroups.Count; $i++){
Write-Host "$($i): $($AllGroups[$i])"}
$GrpSelec = Read-Host -Prompt "Select Group(s)"
$GrpSelec = $GrpSelec -split " "
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
Foreach($Group in $swap){
"$Group"}
}
Is something like this even possible?
I've googled a couple of similar questions which point to the invocation operator &(as shown above), and/or, a foreach which is definitely not the same lol.
take it easy on me, im just experimenting(:
How about a simple if statement?
function Test-Group {
param(
[string[]]$GrpSelec
)
if(!$PSBoundParameters.ContainsKey('GrpSelect')){
# no argument was passed to -GrpSelec,
# populate $GrpSelec in here before proceeding with the rest of the script
}
# Now that $GrpSelec has been populated, let's do the work
$swap = Switch -Exact ($GrpSelec){
{1 -or "Group1"} {"$Group1"}
{2 -or "Group2"} {"$Group2"}
}
# rest of function
}

Math unexpectedly produces infinity '∞'

The goal is to have a progress bar monitoring an external process that writes the step it is on into a watch file.
If I do not have a Start-Sleep in the loop, it will produce messages about not being able to convert infinity. Why is this? I am willing to have some sleep time, but why is it needed and what is the minimum time needed to sleep?
PS C:\src\t\pb> .\Monitor-Progress2.ps1 -TotalCount 5 -WatchFile wf.txt -Verbose
New-TimeSpan : Cannot bind parameter 'Seconds'. Cannot convert value "∞" to type "System.Int32". Error: "Value was either too large or too small for an Int32."
At C:\src\t\pb\Monitor-Progress2.ps1:46 char:37
+ ... an -Seconds (($ts.TotalSeconds / $currentCount) * ($TotalCount - $cur ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-TimeSpan], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.NewTimeSpanCommand
Here is the code. The same problem occurs on PowerShell 5.1 and 6.0. All I do to run it is to ECHO>wf.txt 1, then ECHO>wf.txt 2, etc. Sometimes the error occurs on step two, but sometimes on step 3.
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true)]
[int]$TotalCount
,[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType 'Leaf'})]
[string]$WatchFile
)
$currentcount = 0
$previouscount = $currentcount
$starttimestamp = Get-Date
while ($currentcount -lt $TotalCount) {
$currentcount = [int32](Get-Content $WatchFile)
### Write-Verbose $currentcount
if (($currentcount -lt $TotalCount) -and ($currentcount -ne $previouscount)) {
$ts = $(Get-Date) - $starttimestamp
### Write-Verbose $ts.TotalSeconds
### Write-Verbose $currentcount
### Write-Verbose ($ts.TotalSeconds / $currentcount)
### Write-Verbose ($TotalCount - $currentcount)
### Write-Verbose (($ts.TotalSeconds / $currentcount) * ($TotalCount - $currentcount))
$et = New-TimeSpan -Seconds (($ts.TotalSeconds / $currentCount) * ($TotalCount - $currentcount))
$runningstatus = "Long process running for {0:%d} days {0:%h} hours {0:%m} minutes {0:%s} seconds" -f $ts
$completionstatus = "Estimated completion in {0:%d} days {0:%h} hours {0:%m} minutes {0:%s} seconds" -f $et
Write-Progress -Activity $runningstatus `
-Status $completionstatus `
-percentComplete ($currentcount / $TotalCount*100)
$previouscount = $currentcount
}
#Start-Sleep -Seconds 1
}
While your code didn't process a single entity, your $currentcount is zero, therefore your naive ETA calculation returns infinity. You should check if you can calculate ETA first by comparing current count with zero.
EDIT: You have done an implicit conversion of Get-Content which returns an array of strings, so if your file has a newline in it, you get a conversion error and your $currentcount remains zero, thus you divide by zero to get that infinity. You should either use [IO.File]::ReadAllText() method, or use only the first line (with index 0) for parsing. Or you should write to the file with -NoNewLine flag to Out-File cmdlet.

Powershell script exits ForEach-Object loop prematurely [duplicate]

This question already has answers here:
Why does 'continue' behave like 'break' in a Foreach-Object?
(4 answers)
Closed 5 years ago.
So I've been writing a script that will take all of the data that is stored in 238 spreadsheets and copy it into a master sheet, as well as 9 high level report sheets. I'm really not sure why, but after a specific document, the script ends prematurely without any errors being posted. It's very strange. I'll post some anonymized code below so maybe someone can help me find the error of my ways here.
As far as I can tell, the document that it exits after is fine. I don't see any data errors in it, and the info is copied successfully to the master document before powershell just calls it quits on the script completely.
I've tried changing the size of the data set by limiting only to the folder that contains the problem file. It still ends after the same file with no error output. I cannot upload the file due to company policy, but I really don't see anything different about the data on that one file when compared to any other file of the same nature.
Also, apologies in advance for the crappy code. I'm not a developer and have been relearning powershell since it's the only tool available to me right now.
$StartTime = Get-Date -Format g
Write-Host $StartTime
pushd "Z:\Shared Documents\IO"
$TrackTemplate = "C:\Users\USERNAME\Desktop\IODATA\MasterTemplate.xlsx"
# Initialize the Master Spreadsheet
$xlMaster = New-Object -ComObject Excel.Application
$xlMaster.Visible = $False
$xlMaster.DisplayAlerts = $False
$MasterFilePath = "C:\Users\USERNAME\Desktop\IODATA\Master.xlsx"
Copy-Item $TrackTemplate $MasterFilePath
$wbMaster = $xlMaster.Workbooks.Open($MasterFilePath)
$wsMaster = $wbMaster.Worksheets.Item(2)
$wsMaster.Unprotect("PASSWORD")
$wsMasterRow = 3
# Initialize L4 Document Object
$xlL4 = New-Object -ComObject Excel.Application
$xlL4.Visible = $False
$xlL4.DisplayAlerts = $False
# Initialize object for input documents
$xlInput = New-Object -ComObject Excel.Application
$xlInput.Visible = $False
$xlInput.DisplayAlerts = $False
# Arrays used to create folder path names
$ArrayRoot = #("FOLDER1","FOLDER2","FOLDER3","FOLDER4","FOLDER5","FOLDER6","FOLDER7","FOLDER8","FOLDER9")
$ArrayShort = #("SUB1","SUB2","SUB3","SUB4","SUB5","SUB6","SUB7","SUB8","SUB9")
# $counter is used to iterate inside the loop over the short name array.
$counter = 0
$FileNumber = 0
$TotalFiles = 238
$ArrayRoot | ForEach-Object {
$FilePathL4 = "C:\Users\USERNAME\Desktop\IODATA\ROLLUP\" + $ArrayShort[$counter] + "_DOC_ROLLUP.xlsx"
Copy-Item $TrackTemplate $FilePathL4
$wbL4 = $xlL4.Workbooks.Open($FilePathL4)
$wsL4 = $wbL4.Worksheets.Item(2)
$wsL4.Unprotect("PASSWORD")
$wsL4Row = 3
If ($ArrayShort[$counter] -eq "SUB7") {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\" + $ArrayShort[$counter] + " - DOC v2\"}
Else {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\!" + $ArrayShort[$counter] + " - DOC v2\"}
Get-ChildItem -Path $FilePath | ForEach-Object {
If ($_.Name -eq "SPECIFIC_DOC.xlsx") {Continue}
$FileNumber += 1
Write-Host "$FileNumber / $TotalFiles $_"
$wbInput = $xlInput.Workbooks.Open($_.FullName)
$wsInput = $wbInput.Worksheets.Item(2)
$wsInputLastRow = 0
#Find the last row in the Input document
For ($i = 3; $i -le 10000; $i++) {
If ([string]::IsNullOrEmpty($wsInput.Cells.Item($i,1).Value2)) {
$wsInputLastRow = $i - 1
Break
}
Else { Continue }
}
[void]$wsInput.Range("A3:AC$wsInputLastRow").Copy()
Start-Sleep -Seconds 1
[void]$wsMaster.Range("A$wsMasterRow").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsMasterRow += $wsInputLastRow - 2
[void]$wsL4.Range("A$wsL4Row").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsL4Row += $wsInputLastRow - 2
$wbInput.Close()
$wbMaster.Save()
}
$counter += 1
$wsL4.Protect("PASSWORD")
$wbL4.Save()
$wbL4.Close()
}
$wsMaster.Protect("PASSWORD")
$wbMaster.Save()
$wbMaster.Close()
$xlMaster.Quit()
$EndTime = Get-Date -Format g
$TimeTotal = New-Timespan -Start $StartTime -End $EndTime
Write-Host $TimeTotal
To continue pipeline processing with the next input object, use return - not continue - in the script block passed to the ForEach-Object cmdlet.
The following simple example skips the 1st object output by Get-ChildItem and passes the remaining ones through:
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { return }; $_ }
There is currently (PSv5.1) no direct way to stop the processing of further input objects - for workarounds, see this answer of mine.
By contrast, as you've discovered, break and continue only work as expected in the script block of a for / foreach statement, not directly in the script block passed to the ForeEach-Object cmdlet:
For instance, the following produces no output (using break would have the same effect):
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { continue }; $_ }
The reason is that continue and break look for an enclosing for / foreach statement to continue / break out of, and since there is none, the entire command is exited; in a script, the entire script is exited if there's no enclosing for / foreach / switch statement on the call stack.

Piping array elements into New-TimeSpan returning method not supported

I have a script that checks the last write times of some files, and if they're outside the time parameter I want them to be in, they send me a text. Here is the (working) way of how I used to do it:
$lastupdate = (Get-ChildItem N:\BYUI-played.xml).LastWriteTime
$currentdate = get-date
$difference =NEW-TIMESPAN –Start $lastupdate –End $currentdate
IF ($difference -gt "0.0:31:0.0") {
$Sesherror = 1
Log-Write "$currentdate [ERROR] BYUI-JustPlayed is down"
}
Six times...and then an if at the bottom of the program would check to see the value of of $Sesherror and...well you can probably figure out the rest. Obviously cumbersome and not very well written. So, I decided to refactor it, and I came up with what I thought was slick:
$lastupdate = #((Get-ChildItem N:\BYUI-played.xml).LastWriteTime,(Get-ChildItem N:\byui-now.xml).LastWriteTime,(Get-ChildItem N:\KBYR-played.xml).LastWriteTime,(Get-ChildItem N:\KBYR-now.xml).LastWriteTime,(Get-ChildItem N:\KBYI-played.xml).LastWriteTime,(Get-ChildItem N:\KBYI-now.xml).LastWriteTime)
Try {
$lastupdate | % {
if (NEW-TIMESPAN –Start $lastupdate –End $currentdate -gt "0.1:0:0.0") {
$Sesherror = 1
Log-Write "$currentdate [ERROR] $lastupdate is down"
}}
}
Catch {
Write-Warning "Something went wrong with the algorithim"
Write-Host "$ERROR" -foregroundcolor Red
Write-Warning "Terminating error. Shutting down."
cmd /c pause
exit
}
Again, polling the $Sesherror variable at the bottom. But, when that runs, I get six instances of this error message:
Cannot convert 'System.Object[]' to the type 'System.DateTime' required by parameter 'Start'. Specified method is not supported.
So I have to assume it has something to do with the array being piped in as a "Time" type. But, I've echoed the array, and they all output a correct time type. So I guess the question is two fold:
1. Why won't the foreach loop accept an array?
2. If it never will, what is the best way to do what I am trying to accomplish?
Your immediate problem is that you're using the array-valued $lastupdate variable in the ForEach-Object (%) script block rather than the automatic iteration variable $_.
Therefore you're passing an array to New-TimeSpan's -Start parameter, which fails.
Additionally, several optimizations can be made to your script:
$files = 'N:\BYUI-played.xml', 'N:\byui-now.xml', 'N:\KBYR-played.xml', 'N:\KBYR-now.xml', 'N:\KBYI-played.xml', 'N:\KBYI-now.xml'
$lastupdate = #(Get-Item $files | % { $_.LastWriteTime })
$currentdate = get-date
Try {
$lastupdate | % {
if (($_ – $currentdate) -gt "0.1:0:0.0") {
$Sesherror = 1
Log-Write "$currentdate [ERROR] $lastupdate is down"
}
}
} Catch {
Write-Warning "Something went wrong with the algorithm"
Write-Host "$ERROR" -foregroundcolor Red
Write-Warning "Terminating error. Shutting down."
cmd /c pause
exit
}
Get-ChildItem / Get-Item accept an array of paths, so there's no need for a separate invocation for each file.
Subtracting two [datetime] instances from one another implicitly returns a [timespan] instance - no need for New-TimeSpan.
Additionally, you could construct a [timespan] instance based on "0.1:0:0.0" outside the loop and store it in a variable for use in the -gt comparison, so as to avoid having to convert the string value to a [timespan] instance in each loop iteration, though the real-world impact of this optimization may be negligible.

How to configure a timeout for Read-Host in PowerShell

Like I said, this code works in PowerShell version 2, but not in PowerShell version 5.
function wait
{
$compte = 0
Write-Host "To continue installation and ignore configuration warnings type [y], type any key to abort"
While(-not $Host.UI.RawUI.KeyAvailable -and ($compte -le 20))
{
$compte++
Start-Sleep -s 1
}
if ($compte -ge 20)
{
Write-Host "Installation aborted..."
break
}
else
{
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyup")
}
if ($key.character -eq "y")
{Write-Host "Ignoring configuration warnings..."}
else
{Write-Host "Installation aborted..."
}}
The official documentation or Read-Host -? will tell that it's not possible to use Read-Host in that manner. There is no possible parameter to tell it to run with some kind of timeout.
But there are various other questions detailing how to do this in PowerShell (usually utilizing C#).
The idea seems to be to check whenever the user pressed a key using $Host.UI.RawUI.KeyAvailable and check that for the duration of your timeout.
A simple working example could be the following:
$secondsRunning = 0;
Write-Output "Press any key to abort the following wait time."
while( (-not $Host.UI.RawUI.KeyAvailable) -and ($secondsRunning -lt 5) ){
Write-Host ("Waiting for: " + (5-$secondsRunning))
Start-Sleep -Seconds 1
$secondsRunning++
}
You could use $host.UI.RawUI.ReadKey to get the key that was pressed. This solution probably would not be acceptable if you need more complex input than a simple button press. See also:
Windows PowerShell Tip of the Week - Pausing a Script Until the User Presses a Key
PowerTip: Use PowerShell to Wait for a Key Press (Hey, Scripting Guy!)
Seth, thank you for your solution. I expanded on the example you provided and wanted to give that back to the community.
The use case is a bit different here - I have a loop checking if an array of VMs can be migrated and if there are any failures to that check the operator can either remediate those until the checks clear or they can opt to "GO" and have those failing VMs excluded from the operation. If something other than GO is typed state remains within the loop.
One downside to this is if the operator inadvertently presses a key the script will be blocked by Read-Host and may not be immediately noticed. If that's a problem for anyone I'm sure they can hack around that ;-)
Write-Host "Verifying all VMs have RelocateVM_Task enabled..."
Do {
$vms_pivoting = $ph_vms | Where-Object{'RelocateVM_Task' -in $_.ExtensionData.DisabledMethod}
if ($vms_pivoting){
Write-Host -ForegroundColor:Red ("Some VMs in phase have method RelocateVM_Task disabled.")
$vms_pivoting | Select-Object Name, PowerState | Format-Table -AutoSize
Write-Host -ForegroundColor:Yellow "Waiting until this is resolved -or- type GO to continue without these VMs:" -NoNewline
$secs = 0
While ((-not $Host.UI.RawUI.KeyAvailable) -and ($secs -lt 15)){
Start-Sleep -Seconds 1
$secs++
}
if ($Host.UI.RawUI.KeyAvailable){
$input = Read-Host
Write-Host ""
if ($input -eq 'GO'){
Write-Host -ForegroundColor:Yellow "NOTICE: User prompted to continue migration without the blocked VM(s)"
Write-Host -ForegroundColor:Yellow "Removing the following VMs from the migration list"
$ph_vms = $ph_vms | ?{$_ -notin $vms_pivoting} | Sort-Object -Property Name
}
}
} else {
Write-Host -ForegroundColor:Green "Verified all VMs have RelocateVM_Task method enabled."
}
} Until(($vms_pivoting).Count -eq 0)
Also note that all this $Host.UI stuff doesn't work from the Powershell ISE.
To find out from within a script you could test for $Host.Name -eq "ConsoleHost". When true you can use the code from this topic. Otherwise you could use $Host.UI.PromptForChoice or any other way of showing a dialog box. With System.Windows.Forms.Timer you can then set a timer, and code to close the dialog box or form can be run when it expires.