Execute the other powershell script if the previous one is successful only - powershell

I know this sounds very common but still even searching not able to find soltion for my use case.
What i a trying to do is i am calling one powershell after another from one file say "Test"
and scripts Script A and Script B
......
ScriptA.ps1
ScriptB.ps1
.....other stuff
Now if my scriptA gets executed successfully then ScriptB should execute but if ScriptA throws any exception for which i have already put try/catch block inside ScriptA, ScriptB should not execute.
But i am not able to enforce any check from my "Test" to stop execution of ScriptB.ps1.
Looked for the exit codes but not sure how to collect back in the "Test" something like
......
ScriptA.ps1----returns exit code 1
if(exit code!=1)
{
ScriptB.ps1
}
else
{
"Cant execute due to error"
}
.....other stuff

If your .ps1 scripts communicate failure by nonzero exit code - via an exit statement - you must use the automatic $LASTEXITCODE variable to check for such an exit code:
.\ScriptA.ps1
if ($LASTEXITCODE -eq 0) {
.\ScriptB.ps1
}
# ...
In PowerShell (Core) 7+, you can more simply use &&, the pipeline-chain AND operator:
# PowerShell 7+ only.
.\ScriptA.ps1 && .\ScriptB.ps1 # Execute ScriptB only if ScriptA succeeded.

Use a loop statement (like foreach($thing in $collection){...}) to invoke each script in succession, then break out of the loop on failure - which you can assess by inspecting the $? automatic variable:
$failed = $false
foreach($scriptFile in Get-ChildItem -Filter Script*.ps1){
& $scriptFile.FullName
if(-not $?){
$failed = $true
break
}
}
if($failed){
# handle failure (exit/return or whatever is appropriate)
} else {
# success, continue here
}

Related

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

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()

How to determine if Write-Host will work for the current host

Is there any sane, reliable contract that dictates whether Write-Host is supported in a given PowerShell host implementation, in a script that could be run against any reasonable host implementation?
(Assume that I understand the difference between Write-Host and Write-Output/Write-Verbose and that I definitely do want Write-Host semantics, if supported, for this specific human-readable text.)
I thought about trying to interrogate the $Host variable, or $Host.UI/$Host.UI.RawUI but the only pertinent differences I am spotting are:
in $Host.Name:
The Windows powershell.exe commandline has $Host.Name = 'ConsoleHost'
ISE has $Host.Name = 'Windows PowerShell ISE Host'
SQL Server Agent job steps have $Host.Name = 'Default Host'
I have none of the non-Windows versions installed, but I expect they are different
in $Host.UI.RawUI:
The Windows powershell.exe commandline returns values for all properties of $Host.UI.RawUI
ISE returns no value (or $null) for some properties of $Host.UI.RawUI, e.g. $Host.UI.RawUI.CursorSize
SQL Server Agent job steps return no values for all of $Host.UI.RawUI
Again, I can't check in any of the other platforms
Maintaining a list of $Host.Name values that support Write-Host seems like it would be bit of a burden, especially with PowerShell being cross-platform now. I would reasonably want the script to be able to be called from any host and just do the right thing.
Background
I have written a script that can be reasonably run from within the PowerShell command prompt, from within the ISE or from within a SQL Server Agent job. The output of this script is entirely textual, for human reading. When run from the command prompt or ISE, the output is colorized using Write-Host.
SQL Server jobs can be set up in two different ways, and both support capturing the output into the SQL Server Agent log viewer:
via a CmdExec step, which is simple command-line execution, where the Job Step command text is an executable and its arguments, so you invoke the powershell.exe executable. Captured output is the stdout/sterr of the process:
powershell.exe -Command x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
via a PowerShell step, where the Job Step command text is raw PS script interpreted by its own embedded PowerShell host implementation. Captured output is whatever is written via Write-Output or Write-Error:
#whatever
Do-WhateverPowershellCommandYouWant
x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
Due to some other foibles of the SQL Server host implementation, I find that you can emit output using either Write-Output or Write-Error, but not both. If the job step fails (i.e. if you throw or Write-Error 'foo' -EA 'Stop'), you only get the error stream in the log and, if it succeeds, you only get the output stream in the log.
Additionally, the embedded PS implementation does not support Write-Host. Up to at least SQL Server 2016, Write-Host throws a System.Management.Automation.Host.HostException with the message A command that prompts the user failed because the host program or the command type does not support user interaction.
To support all of my use-cases, so far, I took to using a custom function Write-Message which was essentially set up like (simplified):
$script:can_write_host = $true
$script:has_errors = $false
$script:message_stream = New-Object Text.StringBuilder
function Write-Message {
Param($message, [Switch]$iserror)
if ($script:can_write_host) {
$private:color = if ($iserror) { 'Red' } else { 'White' }
try { Write-Host $message -ForegroundColor $private:color }
catch [Management.Automation.Host.HostException] { $script:can_write_host = $false }
}
if (-not $script:can_write_host) {
$script:message_stream.AppendLine($message) | Out-Null
}
if ($iserror) { $script:has_errors = $true }
}
try {
<# MAIN SCRIPT BODY RUNS HERE #>
}
catch {
Write-Message -Message ("Unhandled error: " + ($_ | Format-List | Out-String)) -IsError
}
finally {
if (-not $script:can_write_host) {
if ($script:has_errors) { Write-Error ($script:message_stream.ToString()) -EA 'Stop' }
else { Write-Output ($script:message_stream.ToString()) }
}
}
As of SQL Server 2019 (perhaps earlier), it appears Write-Host no longer throws an exception in the embedded SQL Server Agent PS host, but is instead a no-op that emits nothing to either output or error streams. Since there is no exception, my script's Write-Message function can no longer reliably detect whether it should use Write-Host or StringBuilder.AppendLine.
The basic workaround for SQL Server Agent jobs is to use the more-mature CmdExec step type (where Write-Output and Write-Host both get captured as stdout), but I do prefer the PowerShell step type for (among other reasons) its ability to split the command reliably across multiple lines, so I am keen to see if there is a more-holistic, PowerShell-based approach to solve the problem of whether Write-Host does anything useful for the host I am in.
Just check if your host is UserInteractive or an service type environment.
$script:can_write_host = [Environment]::UserInteractive
Another way to track the output of a script in real time is to push that output to a log file and then monitor it in real time using trace32. This is just a workaround, but it might work out for you.
Add-Content -Path "C:\Users\username\Documents\PS_log.log" -Value $variablewithvalue

Script stops execution when faces an error

I have a master script master.ps1 which calls two scripts One.ps1 and Two.ps1 like:
&".\One.ps1"
&".\Two.ps1"
When the One.ps1 script has an error, the execution gets stopped without continuing the execution of Two.ps1
How to continue execution of Two.ps1 even if there is an error in One.ps1?
You have to set the $ErrorActionPreference to continue:
Determines how Windows PowerShell responds to a non-terminating
error (an error that does not stop the cmdlet processing) at the
command line or in a script, cmdlet, or provider, such as the
generated by the Write-Error cmdlet.
You can also use the ErrorAction common parameter of a cmdlet to
override the preference for a specific command.
Source.
$ErrorActionPreference = 'continue'
Note: As a best practice I would recommend to first determine the current error action preference, store it in a variable and reset it after your script:
$currentEAP = $ErrorActionPreference
$ErrorActionPreference = 'continue'
&".\One.ps1"
&".\Two.ps1"
$ErrorActionPreference = $currentEAP
#Martin is correct assuming that the success or failure of .\One.ps1 does not impact .\Two.ps1 and if you don't care about logging or otherwise dealing with the error. but if you would prefer to handle the error rather than just continue past it you could also use a Try{}Catch{} block as below to log the error (or take any other action you would like in the Catch{})
Try{
&".\One.ps1"
} Catch {
$error | Out-File "OneError.txt"
}
Try{
&".\Two.ps1"
} Catch {
$error | Out-File "TwoError.txt"
}
Other ways to format this but you get the idea.

Execute code if a PowerShell script is terminated

Is it possible to force the execution of some code if a PowerShell script is forcefully terminated? I have tried try..finally and Traps, but they both don't seem to work, at least when I press Ctrl-C from PowerShell ISE.
Basically, I have a Jenkins build that executes a PowerShell script. If for any reason I want to stop the build from within Jenkins, I don't want any subprocess to lock the files, hence keeping my build project in a broken state until an admin manually kill the offending processes (nunit-agent.exe in my case). So I want to be able to force the execution of a code that terminates nunit-agent.exe if this happens.
UPDATE: As #Frode suggested below, I tried to use try..finally:
$sleep = {
try {
Write-Output "In the try block of the job."
Start-Sleep -Seconds 10
}
finally {
Write-Output "In the finally block of the job."
}
}
try {
$sleepJob = Start-Job -ScriptBlock $sleep
Start-Sleep -Seconds 5
}
finally {
Write-Output "In the finaly block of the script."
Stop-Job $sleepJob
Write-Output "Receiving the output from the job:"
$content = Receive-Job $sleepJob
Write-Output $content
}
Then when I executed this and broke the process using Ctrl-C, I got no output. I thought that what I should got is:
In the finally block of the script.
Receiving the output from the job:
In the try block of the job.
In the finally block of the job.
I use try {} finally {} for this. The finally-block runs when try is done or if you use ctrl+c, so you need to either run commands that are safe to run either way, ex. it doesn't matter if you kill a process that's already dead..
Or you could add a test to see if the last command was a success using $?, ex:
try {
Write-Host "Working"
Start-Sleep -Seconds 100
} finally {
if(-not $?) { Write-Host "Cleanup on aisle 5" }
Write-Host "Done"
}
Or create your own test (just in case the last command in try failed for some reason):
try {
$IsDone = $false
Write-Host "Working"
Start-Sleep -Seconds 100
#.....
$IsDone = $true
} finally {
if(-not $IsDone) { Write-Host "Cleanup on aisle 5" }
Write-Host "Done"
}
UPDATE: The finally block will not work for output as the pipeline is stopped on CTRL+C.
Note that pressing CTRL+C stops the pipeline. Objects that are sent to
the pipeline will not be displayed as output. Therefore, if you
include a statement to be displayed, such as "Finally block has run",
it will not be displayed after you press CTRL+C, even if the Finally
block ran.
Source: about_Try_Catch_Finally
However, if you save the output from Receive-Job to a global variable like $global:content = Receive-Job $sleepJob you can read it after the finally-block. The variable is normally created in a different local scope and lost after the finally-block.

How to confirm completion of previous command in powershell

I have a simple powershell script that gets ran daily to compress and move some log files. How can i test that the command completes successfully before deleting the original log file.
set-location $logpath1
& $arcprg $pram $dest_file $source_file
Move-Item $dest_file $arcdir
If the Move-Item completes ok i want to remove-item $source_file
The completion status of the previous command can be accessed via the special variable $?.
Note that this works best with non-terminating errors (like you would get from Move-Item). Terminating errors are the result of a direct throw or an exception getting thrown in .NET and they alter the flow of your code. Best to use a trap or try/catch statement to observe those type of errors.
One other thing to watch out for WRT $? and console exes is that PowerShell assumes an exit code of 0 means success (i.e. $? is set to $true) and anything else means failure ($? set to $false). Unfortunately not all console exe's observe that exit code convention e.g. there may be multiple success codes and a single failure code (0). For those exes that don't follow the exit code rules, use $LastExitCode as pointed out in the comments to determine success or failure.
Depending on how parnoid you are and what component you are using for archiving, you can check the archive to confirm the file exixts. We use DotNetZip component to zip our archive log files (http://dotnetzip.codeplex.com/).
$zipFileObj = new-object Ionic.Zip.ZipFile($zipName);
[void] $zipFileObj.UpdateFile( "$fileName", "" ) # adds file if doesn't already exist
trap #catch an zip errors and Stop processing
{
write-error "Caught a system exception. Execution stopped"
write-error $("TRAPPED: " + $_.Exception.Message);
exit
}
if ( $zipFileObj.ContainsEntry( $fileName) )
{
remove-item $pathFile # delete file from file-system
}
else
{
# throw error
}