Powershell ForEach - new loops starting before current loops ending - sql-server

I have a quick and dirty Powershell script for running sql scripts against many servers sequentially. I have the servers stored in an array, and loop through them with ForEach. The ForEach roughly looks like this:
ForEach ($Server in $ServerList) {
Write-Host "Executing $Script against $Server..."
Invoke-SqlCmd ........
}
But the problem I have is my output looks something like this:
Executing script.sql against Server1
Executing script.sql against Server2
Executing script.sql against Server3
<Output from Server1>
<Output from Server2>
<Output from Server3>
Executing script.sql against Server4
<Output from Server4>
Executing script.sql against Server5
<Output from Server5>
...you get the idea. Is there any way to marry up the outputs so that the output appears under the message depicting which server is currently being executed on? It would help with using output for debugging etc. Executing on PS7 by the way.

What you're observing here is not that the next iteration of the foreach loops starts before the last one has ended - foreach (the loop statement, not the cmdlet) only invokes the loop body in series, never concurrently.
That doesn't mean the next iteration won't start before the formatting subsystem in the host application (eg. powershell.exe or powershell_ise.exe or pwsh.exe) has written any output from the loop body to the screen buffer.
The default host applications usually waits a few 100 milliseconds to see if there's more than one output object of the same type in the output stream - which will then inform how to format the output (table vs list view etc.).
Write-Host on the other hand is an instruction to bypass all that and instead write a message straight to the host application.
So this differentiated delay makes it look like the Write-Host statement at the top is being executed before the code from 2 iterations back - but what you're observing is actually an intentional decoupling of output vs rendering/presentation.
As zett42 notes, you can force the host application to synchronously render the output you want displayed in order by piping it to Out-Host:
ForEach ($Server in $ServerList) {
Write-Host "Executing $Script against $Server..."
Invoke-SqlCmd ........ |Out-Host
}
Since PowerShell must now fulfill your request to render the output before it can move on with the next statement/iteration, it'll no longer delay it for formatting purposes :)

Related

How to Add the Results of a Function to an Array in Powershell

So im pretty new to powershell and i have multiple questions on how to write scripts using it but right now my main issue is that i want to be able to ping all of the computers on my network and take the names of the computers that ping back and put them into an array so that i can push updates to the computers that are on the network just using one array containing the current computers on the network. here is what i am working with right now.
foreach ($c in $204computernames)
{if(test-connection -computername $c.name -count 1 -quiet)
{write-host $c.name
}
}
currently this string of code runs a foreach loop so that $c(the computers) run against $204computernames(the known computers on the network)then it tests the positive connections through the if statement and finally at the end it outputs the names of the computers that are on the network. Now the problem im understanding from this is when i use a foreach loop that it will run against all of the known computers and as each computer is put through the loop it is replaced by the preceding one, but i want each and every one that comes back on the network to be stored in an array. Is there a way that i can do this with the current format or is there a certain way that i can tweak what i have so that i can get the output i want?
The idiomatic solution to "how do I filter a list against a condition and save the results" would be to pipe the input to Where-Object:
$onlineComputers = $204computernames |Where-Object { Test-Connection $_.name -Count 1 -Quiet }

How to run SQL Server Agent Powershell script with output and exit code

I'm trying to run a PowerShell script as a SQL Server 2016 Agent job.
As my Powershell script runs, I'm generating several lines of output using "Write-Output". I'd like to save this output to the job history, and I only want the job to continue to the next step if the step running the PowerShell script completes with an exit code of 0.
I'm using the "PowerShell" step type in my agent job. The Command text looks like this..
# Does some stuff that eventually sets the $resultState variable...
Write-Output ("Job complete with result '" + $resultState + "'")
if($resultState -eq "SUCCESS") {
[Environment]::Exit(0);
}
else {
[Environment]::Exit(1);
}
Under the "Advanced" settings, "Include step output in history" is checked. If I remove the final "if" statement from the PowerShell script, then I can see the output in the history, but the job step is always successful and moves on to the next step. If I include the if/else statements, the job step fails if $resultState does not equal "SUCCESS" (which is what I want), but I don't see my output anymore in the history for the job step.
Any suggestions?
I worked around this by saving all of my output lines to a single variable, and using Write-Error with -ErrorAction Stop if my result wasn't what I wanted. This isn't quite what I was trying to do at first, because this doesn't use the exit codes, but SQL Agent will correctly detect if the job step succeeded or not, and my output can still show up in the job history because it will be included in the error message.
Updated code:
# Does some stuff that sets the $resultState and saves output lines to $output...
$output += "`r`nJob complete with result '$resultState'"
if($resultState -eq "SUCCESS") {
Write-Output ($output)
}
else {
Write-Error ("`r`n" + $output) -ErrorAction Stop
}
I struggled with this. The logged output from the Sql Job Powershell steps is pretty useless. I found it better to use a CmdExex step instead that calls Powershell.exe.
In a Sql 2019 CmdExec job step you can just call your powershell script like this:
Powershell "F:\Temp\TestScript.ps1"
... and you'll get all the output (and you can log it to a file if you like). And if there's an error, the job stops properly.
In some earlier versions of SQL Server, Powershell errors would get logged but the job would continue to run (see here https://stackoverflow.com/a/53732722/22194 ), so you need to wrap your script in a try/catch to bubble up the error in a way SQL can deal with it:
Powershell.exe -command "try { & 'F:\Temp\TestScript.ps1'} catch { throw $_ }"
Note that if your script path has spaces in it you might get different problems, see here https://stackoverflow.com/a/45762288/22194

Running Powershell Script from SQL Server Agent

How different should the programming be when you execute a powershell script from SQL Server Agent. I have been seeing very weird behavior.
Any object call fails
Can't we use powershell functions in these scripts. The parameters go empty if we call function through an object parameter
Some commands just don't pring messages, even though I use a variable hard print or I use Write-Output.
I just wanted to know why this is too different. I have a big script that automated and helped big manual task, which works with no errors at all when done manually.
Please help me on this.
Object: $agobj = $PrimarySQLConnObj.AvailabilityGroups[$AGName]
Error from Agent History:
The corresponding line is ' Add-SqlAvailabilityDatabase -InputObject $agobj -Database "$db" -ErrorAction Stop '. Correct the script and reschedule the job. The error information returned by PowerShell is: 'Cannot bind parameter 'InputObject'. Cannot convert the "[AG]" value of type "Microsoft.SqlServer.Management.Smo.AvailabilityGroup" to type "Microsoft.SqlServer.Management.Smo.AvailabilityGroup"

How to add an element back into an array

First off, I am new to Powershell, so please excuse any poor coding here.
I'm trying to write a script that queues commands. We have a piece of equipment that can only handle 32 commands at one time. What I want to do is build a command file that has many more than 32 commands and then it automatically queues any command after 32 until another free slot opens up. Everything seems to be working except when I go into my 'else' statement. I attempt to add the current $_ back into the array so it doesn't get lost and gets reprocessed, but this does not seem to be working.
I build my array from a text file:
$commands = #(Get-Content C:\scripts\temp\commands.txt)
When I try to add the current $_ value back into the array during the 'else' pause statement, it never adds back in, so that one particular command never enters back into the array. So if my script has to pause 15 times, 15 commands will never get ran as they just get processed by the 'else' statement and get thrown out of my 'if/else' loop.
Here's my if/else loop:
$commands | ForEach-Object {
If (Test-Path C:\scripts\temp\active_migrations.txt){
Remove-Item C:\scripts\temp\active_migrations.txt
}
CMD.EXE /C "ssh $user#$cluster -pw $PlainPassword lsmigrate" > C:\scripts\temp\active_migrations.txt
$count = Get-Content C:\scripts\temp\active_migrations.txt
$number_of_migrations = $count.Length / 6
IF ($number_of_migrations -lt 32)
{
Write-Host "Migrations are currently at $number_of_migrations. Proceeding with next vdisk migration."
Write-Host "Issuing command $_"
Write-Host "There are still $migrations_left migrations to execute of $total_migrations total."
Write-Host ""
CMD.EXE /C "ssh $user#$cluster -pw $PlainPassword $_"
SLEEP 2
$migrations_left--
}
ELSE
{
Write-Host "Migrations are currently at $number_of_migrations. Sleeping for 1 minute"
$commands = #($commands + $_)
Write-Host "There are still $migrations_left migrations to execute of $total_migrations total."
SLEEP 60
}}
}
Any help is much appreciated.
Thank you Paul. I ended up dumping things during the else statement to a temp array and it works! Appreciate the assistance.
function commands_temp
{
if (Test-Path C:\scripts\temp\commands_temp.txt)
{
$commands=#()
$commands=#(Get-Content C:\scripts\temp\commands_temp.txt)
Remove-Item C:\scripts\temp\commands_temp.txt
Proceed
}
Else
{
Write-Host "All migrations are either complete or successfully executed. Check the GUI or run lsmigrate CLI command for full status."

osql vs Invoke-Sqlcmd-- redirecting output of the latter

We're moving from a batch file that calls osql to a Powershell script which uses the Invoke-Sqlcmd cmdlet.
Would anyone know what the equivalent steps are for redirecting the output in the latter case, to using the -o flag in osql? We have some post-processing steps that look at the osql output file and act accordingly (report an error if those logs are greater than X bytes). I would very much like it if Invoke-Sqlcmd could duplicate the same output information given the same SQL commands going in.
Right now in my script I'm planning to call Invoke-Sqlcmd <...> | Out-file -filepath myLog.log. Anyone know if this is ok or makes sense?
From the documentation for the cmdlet itself:
Invoke-Sqlcmd -InputFile "C:\MyFolder\TestSQLCmd.sql" | Out-File -filePath "C:\MyFolder\TestSQLCmd.rpt"
The above is an example of calling Invoke-Sqlcmd, specifying an input file and piping the output to a file. This is similar to specifying sqlcmd with the -i and -o options.
http://technet.microsoft.com/en-us/library/cc281720.aspx
I think you'll find it's difficult to reproduce the same behavior in invoke-sqlcmd as I have.
osql and sqlcmd.exe will send T-SQL PRINT and RAISERROR and errors to the output file.
Using Powershell you can redirect standard error to standard output with the standard error redirection technique (2>&1):
Invoke-Sqlcmd <...> 2>&1 | Out-file -filepath myLog.log
However this still won't catch everything. For example RAISERROR and PRINT statements only output in Invoke-sqlcmd when using the -verbose parameter as documented in help invoke-sqlcmd. In Powershell V2 you can't redirect verbose output. Although you can with Powershell V3 using 4>
For these reason and others (like trying to recreate all the many different options in sqlcmd) I switched back to using sqlcmd.exe for scheduled job in my environment. Since osql.exe is deprecated, I would suggest switching to sqlcmd.exe which supports the same options as osql.
You can still call osql from PowerShell. I would continue to do just that. Invoke-SqlCmd returns objects representing each of the rows in your result set. If you aren't going to do anything with those objects, there's no reason to upgrade to Invoke-SqlCmd.

Resources