Executing command doesn't result in script exception - powershell

We have a powershell script that deploys a database script. However, if the database script fails, the output doesn't throw exceptions to the powershell script.
Below is an example of the .ps1 file:
function Publish-DatabaseProject
{
sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
}
function Add-Timestamp {
process {
if ($_.GetType() -eq [string]) {
"[$(Get-Date -Format o)] $_"
} else {
$_
}
}
}
function Write-LogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)] [string] $Path,
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] [object[]] $InputObject
)
begin {
$root = Split-Path -Path $Path -Parent
if ($root -and !(Test-Path -Path $root)) { New-Item -Path $root -Type Directory
| Out-Null }
}
process {
$InputObject |
Add-Timestamp |
Tee-Object -File $Path -Append
}
}
Publish-DatabaseProject -ErrorVariable DeployError 2>&1 |
Write-LogFile -Path "C:\output.log"
if ($DeployError -and $DeployError.Count -ne 0)
{
Write-Output "Failed"
} else
{
Write-Output "Succeeded"
}
The query in question is executing against a non-existent table. The text output shows:
[2015-12-11T14:42:45.1973944+00:00] Msg 4902, Level 16, State 1, Server ABDN-DEV-PC1, Line 1
[2015-12-11T14:42:45.2053944+00:00] Cannot find the object "xx" because it does not exist or you do not have permission
s.
Succeeded
I am expecting the last line to read: Failed.
If you run the sqlcmd line on its own, and follow it up with $LastExitCode, it does spit out a non-zero exit code.
> sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
Msg 4902, Level 16, State 1, Server ABDN-DEV-PC1, Line 1
Cannot find the object "xx" because it does not exist or you do not have permissions.
> $LastExitCode
1
For various reasons, we cannot use Invoke-SqlCmd, and need to stick with SQLCMD.exe.
How can we make exceptions from within SQLCMD bubble out correctly to the calling script?

Your -ErrorVariable DeployError statement would only get triggered if the Publish-DatabaseProject cmdlet itself fails to execute. As that function is mostly a wrapper around sqlcmd.exe, there isn't any intelligence to bubble up this error. We can wrap it though by using the $LastExitCode automatic variable.
function Publish-DatabaseProject
{
sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
if ($LastExitCode -ne 0)
{
Write-error $LastExitCode
throw $LastExitCode
}
}
Now, PowerShell will catch this error from the .exe, and you can use -ErrorVariable.
Update
So, since you want to keep running and not abandon ship when enountering an error, we need to wrap your Publish-DataBaseProject function with a try{}catch{} block, to catch the error we're generating, without stopping execution.
try {Publish-DatabaseProject -ErrorAction STOP -ErrorVariable DeployError 2>&1 |
Write-LogFile -Path "C:\output.log" }
catch{
Write-Output "Current job $($_), failed"
Write-Output "Exception $($Error.ExceptionID)"
}
Now we're properly generating an exception from a CMD, bubbling it up through our Function, and handling it as if it were a normal function, all using Native PowerShell. I believe this is what you were seeking to do. If not, post a gist of the whole script and I'll help you in a more targeted fashion.

Related

When running a command in powershell how can I prepend a date/time for all output on stdout/stderr?

Is it possible in powershell when running a script to add a date prefix to all log output?
I know that it would be possible to do something like:
Write-Host "$(Get-Date -format 'u') my log output"
But I dont want to have to call some function for each time we output a line. Instead I want to modify all output when running any script or command and have the time prefix for every line.
To insert a date in front of all output, that is stdout, stderr and the PowerShell-specific streams, you can use the redirection operator *>&1 to redirect (merge) all streams of a command or scriptblock, pipe to Out-String -Stream to format the stream objects into lines of text and then use ForEach-Object to process each line and prepend the date.
Let me start with a simple example, a more complete solution can be found below.
# Run a scriptblock
&{
# Test output to all possible streams, using various formatting methods.
# Added a few delays to test if the final output is still streaming.
"Write $($PSStyle.Foreground.BrightGreen)colored`ntext$($PSStyle.Reset) to stdout"
Start-Sleep -Millis 250
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Start-Sleep -Millis 250
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Start-Sleep -Millis 250
Write-Host 'Write to information stream'
Start-Sleep -Millis 250
Write-Warning 'Write to warning stream'
Start-Sleep -Millis 250
Write-Verbose 'Write to verbose stream' -Verbose
Start-Sleep -Millis 250
$DebugPreference = 'Continue' # To avoid prompt, needed for Windows Powershell
Write-Debug 'Write to debug stream'
} *>&1 | Out-String -Stream | ForEach-Object {
# Add date in front of each output line
$date = Get-Date -Format "yy\/MM\/dd H:mm:ss"
foreach( $line in $_ -split '\r?\n' ) {
"$($PSStyle.Reset)[$date] $line"
}
}
Output in PS 7.2 console:
Using Out-String we use the standard PowerShell formatting system to have the output look normally, as it would appear without redirection (e. g. things like tables stay intact). The -Stream parameter is crucial to keep the streaming output behaviour of PowerShell. Without this parameter, output would only be received once the whole scriptblock has completed.
While the output already looks quite nice, there are some minor issues:
The verbose, warning and debug messages are not colored as usual.
The word "text" in the 2nd line should be colored in green. This isn't working due to the use of $PSStyle.Reset. When removed, the colors of the error message leak into the date column, which looks far worse. It can be fixed, but it is not trivial.
The line wrapping isn't right (it wraps into the date column in the middle of the output).
As a more general, reusable solution I've created a function Invoke-WithDateLog that runs a scriptblock, captures all of its output, inserts a date in front of each line and outputs it again:
Function Invoke-WithDateLog {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock,
[Parameter()]
[string] $DateFormat = '[yy\/MM\/dd H:mm:ss] ',
[Parameter()]
[string] $DateStyle = $PSStyle.Foreground.BrightBlack,
[Parameter()]
[switch] $CatchExceptions,
[Parameter()]
[switch] $ExceptionStackTrace,
[Parameter()]
[Collections.ICollection] $ErrorCollection
)
# Variables are private so they are not visible from within the ScriptBlock.
$private:ansiEscapePattern = "`e\[[0-9;]*m"
$private:lastFmt = ''
& {
if( $CatchExceptions ) {
try { & $scriptBlock }
catch {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
# Write as regular output, colored like an error message.
"`n" + $PSStyle.Formatting.Error + "EXCEPTION ($($_.Exception.GetType().FullName)):`n $_" + $PSStyle.Reset
# Optionally write stacktrace. Using the -replace operator we indent each line.
Write-Debug ($_.ScriptStackTrace -replace '^|\r?\n', "`n ") -Debug:$ExceptionStackTrace
}
}
else {
& $scriptBlock
}
} *>&1 | ForEach-Object -PipelineVariable record {
# Here the $_ variable is either:
# - a string in case of simple output
# - an instance of one of the System.Management.Automation.*Record classes (output of Write-Error, Write-Debug, ...)
# - an instance of one of the Microsoft.PowerShell.Commands.Internal.Format.* classes (output of a Format-* cmdlet)
if( $_ -is [System.Management.Automation.ErrorRecord] ) {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
}
$_ # Forward current record
} | Out-String -Stream | ForEach-Object {
# Here the $_ variable is always a (possibly multiline) string of formatted output.
# Out-String doesn't add any ANSI escape codes to colorize Verbose, Warning and Debug messages,
# so we have to do it by ourselfs.
$overrideFmt = switch( $record ) {
{ $_ -is [System.Management.Automation.VerboseRecord] } { $PSStyle.Formatting.Verbose; break }
{ $_ -is [System.Management.Automation.WarningRecord] } { $PSStyle.Formatting.Warning; break }
{ $_ -is [System.Management.Automation.DebugRecord] } { $PSStyle.Formatting.Debug; break }
}
# Prefix for each line. It resets the ANSI escape formatting before the date.
$prefix = $DateStyle + (Get-Date -Format $DateFormat) + $PSStyle.Reset
foreach( $line in $_ -split '\r?\n' ) {
# Produce the final, formatted output.
$prefix + ($overrideFmt ?? $lastFmt) + $line + ($overrideFmt ? $PSStyle.Reset : '')
# Remember last ANSI escape sequence (if any) of current line, for cases where formatting spans multiple lines.
$lastFmt = [regex]::Match( $line, $ansiEscapePattern, 'RightToLeft' ).Value
}
}
}
Usage example:
# To differentiate debug and verbose output from warnings
$PSStyle.Formatting.Debug = $PSStyle.Foreground.Yellow
$PSStyle.Formatting.Verbose = $PSStyle.Foreground.BrightCyan
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace {
"Write $($PSStyle.Foreground.Green)colored`ntext$($PSStyle.Reset) to stdout"
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Write-Host 'Write to information stream'
Write-Warning 'Write to warning stream'
Write-Verbose 'Write to verbose stream' -Verbose
Write-Debug 'Write to debug stream' -Debug
throw 'Critical error'
}
Output in PS 7.2 console:
Notes:
The code requires PowerShell 7+.
The date formatting can be changed through parameters -DateFormat (see formatting specifiers) and -DateStyle (ANSI escape sequence for coloring).
Script-terminating errors such as created by throwing an exception or using Write-Error -EA Stop, are not logged by default. Instead they bubble up from the scriptblock as usual. You can pass parameter -CatchExceptions to catch exceptions and log them like regular non-terminating errors. Pass -ExceptionStackTrace to also log the script stacktrace, which is very useful for debugging.
Scripted cmdlets such as this one don't set the automatic variable $? and also don't add errors to the automatic $Error variable when an error is written via Write-Error. Neither the common parameter -ErrorVariable works. To still be able to collect error information I've added parameter -ErrorCollection which can be used like this:
$scriptErrors = [Collections.ArrayList]::new()
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace -ErrorCollection $scriptErrors {
Write-Error 'Write to stderr' -EA Continue
throw 'Critical error'
}
if( $scriptErrors ) {
# Outputs "Number of errors: 2"
"`nNumber of errors: $($scriptErrors.Count)"
}
The objects generated by Write-Host already come with a timestamp, you can use Update-TypeData to override the .ToString() Method from the InformationRecord Class and then redirect the output from the Information Stream to the Success Stream.
Update-TypeData -TypeName System.Management.Automation.InformationRecord -Value {
return $this.TimeGenerated.ToString('u') + $this.MessageData.Message.PadLeft(10)
} -MemberType ScriptMethod -MemberName ToString -Force
'Hello', 'World', 123 | Write-Host 6>&1

redirect stdout, stderr from powershell script as admin through start-process

Inside a powershell script, I'm running a command which starts a new powershell as admin (if I'm not and if needed, depending on $arg) and then runs the script.
I'm trying to redirect stdout and stderr to the first terminal.
Not trying to make things easier, there are arguments too.
param([string]$arg="help")
if($arg -eq "start" -Or $arg -eq "stop")
{
if(![bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
{
Start-Process powershell -Verb runas -ArgumentList " -file servicemssql.ps1 $arg"
exit
}
}
$Services = "MSSQLSERVER", "SQLSERVERAGENT", "MSSQLServerOLAPService", "SSASTELEMETRY", "SQLBrowser", `
"SQLTELEMETRY", "MSSQLLaunchpad", "SQLWriter", "MSSQLFDLauncher"
function startsql {
"starting SQL services"
Foreach ($s in $Services) {
"starting $s"
Start-Service -Name "$s"
}
}
function stopsql {
"stopping SQL services"
Foreach ($s in $Services) {
"stopping $s"
Stop-Service -Force -Name "$s"
}
}
function statussql {
"getting SQL services status"
Foreach ($s in $Services) {
Get-Service -Name "$s"
}
}
function help {
"usage: StartMssql [status|start|stop]"
}
Switch ($arg) {
"start" { startsql }
"stop" { stopsql }
"status" { statussql }
"help" { help }
"h" { help }
}
Using the following answers on SO doesn't work:
Capturing standard out and error with Start-Process
Powershell: Capturing standard out and error with Process object
How to deal with the double quote inside double quote while preserving the variable ($arg) expansion ?
PowerShell's Start-Process cmdlet:
does have -RedirectStandardOut and -RedirectStandardError parameters,
but syntactically they cannot be combined with -Verb Runas, the argument required to start a process elevated (with administrative privileges).
This constraint is also reflected in the underlying .NET API, where setting the .UseShellExecute property on a System.Diagnostics.ProcessStartInfo instance to true - the prerequisite for being able to use .Verb = "RunAs" in order to run elevated - means that you cannot use the .RedirectStandardOutput and .RedirectStandardError properties.
Overall, this suggests that you cannot directly capture an elevated process' output streams from a non-elevated process.
A pure PowerShell workaround is not trivial:
param([string] $arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Invoke the script via -Command rather than -File, so that
# a redirection can be specified.
$passThruArgs = '-command', '&', 'servicemssql.ps1', $arg, '*>', "`"$PSScriptRoot\out.txt`""
Start-Process powershell -Wait -Verb RunAs -ArgumentList $passThruArgs
# Retrieve the captured output streams here:
Get-Content "$PSScriptRoot\out.txt"
exit
}
}
# ...
Instead of -File, -Command is used to invoke the script, because that allows appending a redirection to the command: *> redirects all output streams.
#soleil suggests using Tee-Object as an alternative so that the output produced by the elevated process is not only captured, but also printed to the (invariably new window's) console as it is being produced:
..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""
Caveat: While it doesn't make a difference in this simple case, it's important to know that arguments are parsed differently between -File and -Command modes; in a nutshell, with -File, the arguments following the script name are treated as literals, whereas the arguments following -Command form a command that is evaluated according to normal PowerShell rules in the target session, which has implications for escaping, for instance; notably, values with embedded spaces must be surrounded with quotes as part of the value.
The $PSScriptRoot\ path component in output-capture file $PSScriptRoot\out.txt ensures that the file is created in the same folder as the calling script (elevated processes default to $env:SystemRoot\System32 as the working dir.)
Similarly, this means that script file servicemssql.ps1, if it is invoked without a path component, must be in one of the directories listed in $env:PATH in order for the elevated PowerShell instance to find it; otherwise, a full path is also required, such as $PSScriptRoot\servicemssql.ps1.
-Wait ensures that control doesn't return until the elevated process has exited, at which point file $PSScriptRoot\out.txt can be examined.
As for the follow-up question:
To go even further, could we have a way to have the admin shell running non visible, and read the file as we go with the Unix equivalent of tail -f from the non -privileged shell ?
It is possible to run the elevated process itself invisibly, but note that you'll still get the UAC confirmation prompt. (If you were to turn UAC off (not recommended), you could use Start-Process -NoNewWindow to run the process in the same window.)
To also monitor output as it is being produced, tail -f-style, a PowerShell-only solution is both nontrivial and not the most efficient; to wit:
param([string]$arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Delete any old capture file.
$captureFile = "$PSScriptRoot\out.txt"
Remove-Item -ErrorAction Ignore $captureFile
# Start the elevated process *hidden and asynchronously*, passing
# a [System.Diagnostics.Process] instance representing the new process out, which can be used
# to monitor the process
$passThruArgs = '-noprofile', '-command', '&', "servicemssql.ps1", $arg, '*>', $captureFile
$ps = Start-Process powershell -WindowStyle Hidden -PassThru -Verb RunAs -ArgumentList $passThruArgs
# Wait for the capture file to appear, so we can start
# "tailing" it.
While (-not $ps.HasExited -and -not (Test-Path -LiteralPath $captureFile)) {
Start-Sleep -Milliseconds 100
}
# Start an aux. background that removes the capture file when the elevated
# process exits. This will make Get-Content -Wait below stop waiting.
$jb = Start-Job {
# Wait for the process to exit.
# Note: $using:ps cannot be used directly, because, due to
# serialization/deserialization, it is not a live object.
$ps = (Get-Process -Id $using:ps.Id)
while (-not $ps.HasExited) { Start-Sleep -Milliseconds 100 }
# Get-Content -Wait only checks once every second, so we must make
# sure that it has seen the latest content before we delete the file.
Start-Sleep -Milliseconds 1100
# Delete the file, which will make Get-Content -Wait exit (with an error).
Remove-Item -LiteralPath $using:captureFile
}
# Output the content of $captureFile and wait for new content to appear
# (-Wait), similar to tail -f.
# `-OutVariable capturedLines` collects all output in
# variable $capturedLines for later inspection.
Get-Content -ErrorAction SilentlyContinue -Wait -OutVariable capturedLines -LiteralPath $captureFile
Remove-Job -Force $jb # Remove the aux. job
Write-Verbose -Verbose "$($capturedLines.Count) line(s) captured."
exit
}
}
# ...

Silent installation by using Powershell scripting

I am trying to install one client's software by using PowerShell silent scripting. Below is the script which I have created and its not working and throwing errors like below:
Cannot validate argument on parameter 'ArgumentList'. The argument is null, empty, or an element of the argument collection contains a null value. Supply a collection that does not contain any null values and then try the command again
Is the below line correct or any mistake in this Here I am getting issue.
$Args = #("/S", "/L1033", -INSTALL_TYPE=PRESERVE_VERSION, START_MENU=AStartMenuFolder\Software\production\)
Full script:
$INSTALLDIR = "C:\Software\Software.exe"
$Args = #("/S", "/L1033", -INSTALL_TYPE=PRESERVE_VERSION, START_MENU=AStartMenuFolder\Software\production\)
$logfile = "D:\BACKUP\Install_Logfile.txt"
$ErrorActionPreference = 'Stop'
try {
$exitcode = (Start-Process $Installer -ArgumentList $Args -NoNewWindow -Wait -Passthru).ExitCode
if ($exitcode -eq 0) {
[System.Windows.MessageBox]::Show('Installation Completed Successfully')
} else {
[System.Windows.MessageBox]::Show('Installation Failled')
}
} catch {
"$_" | Out-File $logfile -Append
{[System.Windows.MessageBox]::Show('Installation Failled')}
}
Edit:
$Installer = "C:\OTE\OTE.exe"
$params = #("/S", "/L1033", "-INSTALL_TYPE=PRESERVE_VERSION", "-START_MENU=AStartMenuFolder\OTE\production\")
$logfile = "C:\Install_Logfile.txt"
$ErrorActionPreference = 'Stop'
& $Installer #params
if ($LastExitCode -eq 0) {
[Windows.MessageBox]::Show('Installation Completed Successfully')
} else {
"$_" | out-file $logfile -append
[Windows.MessageBox]::Show('Installation Failled')
}
In the above script I am getting the error like below,
Cannot validate argument on parameter 'ArgumentList'. The argument is null, empty, or an element of the argument collection contains a null value. Supply a collection that does not contain any null values and then try the command again.
There are several things you may want to fix in your code:
$args is an automatic variable. Don't try to overwrite it. Use a different variable name, e.g. $params. As others have already mentioned, put the parameters in quotes when defining the array.
Unless you have specific reasons to use Start-Process it's easier to use the call operator and splatting.
External programs don't throw PowerShell exceptions, so using try/catch on them is pointless.
PowerShell automatically records the exit code of an external program in the automatic variable $LastExitCode.
$installer = 'C:\Software\Software.exe'
$params = '/S', '/L1033', '-INSTALL_TYPE=PRESERVE_VERSION',
'START_MENU=AStartMenuFolder\Software\production'
& $Installer #params
if ($LastExitCode -eq 0) {
[Windows.MessageBox]::Show('Installation Completed Successfully')
} else {
[Windows.MessageBox]::Show('Installation Failled')
}
Your items need to be within quotes when creating an array:
$Args=#("/S", "/L1033", "-INSTALL_TYPE=PRESERVE_VERSION", "START_MENU=AStartMenuFolder\Software\production\")

How to write old VB Commands in Powershell

I want to write some old VB into PS, however there are some old DOS commands here that I am struggling to write into powershell. For instance, I want to delete files from an Archive, and the way its written in VB is:
command=(%comspec% /C DEL C:\\MyFile.txt",0,True)
If result <> 0 then
txtFile.writeline "ERROR"
txtFile.writeline "File does not exist.."
result = 0
Else
txtFile.writeline "Success"
End if
In Powershell, The /C DEL is the line I'm having trouble writing. How would I write this command in powershell or would I just ignore it completely and just carry on with my IF statement?
Thanks,
In powershell, just use Remove-Item. This mimics your VB pretty well:
try {
Remove-Item 'C:\MyFile.txt' -Force -ErrorAction 'Stop'
Write-Host 'Success'
} catch [System.Management.Automation.ItemNotFoundException] {
Write-Host 'ERROR'
Write-Host 'File does not exist..'
}
Of course, there's a dozen ways this cat can be skinned:
if (Test-Path 'C:\MyFile.txt') {
Remove-Item 'C:\MyFile.txt' -Force
Write-Host 'Success'
} else {
Write-Host 'ERROR'
Write-Host 'File does not exist..'
}
There's definitely more options out there ...

How do I clear $error and $LASTEXITCODE set by an external cmdlet or executable?

I have a custom module wrapping an external command (csrun.exe), and parses the output so I can use it in PowerShell.
Everything just about works except if the external command writes to stderror, and clearing the error in my cmdlet doesn't seem to fully work. It will clear (i.e. $error.count is 0 and $lasterrorcode is 0, but once I return to the script that is calling my cmdlet, $error and $lasterrorcode are no longer clear and the error in $error references the underlying exception for the external command
System.Management.Automation.RemoteException: The compute emulator is not running.
I've attempted, try-catches, clearing the mentioned variables. Regardless, the calling script retains a reference to the error.
CustomModule.psm1
$__azureEmulatorPath = "C:\Program Files\Microsoft SDKs\Azure\Emulator\"SDKs\Azure\Emulator\"
$__azureEmulator = __azureEmulatorPath + "csrun.exe"
function Get-EmulatorStatus() {
[OutputType([ComputeEmulatorStatus])]
[cmdletbinding()]
param()
$output = (& $__azureEmulator /status | Out-String)
if ($error.Count -gt 0 -or $LASTEXITCODE -ne 0) {
Write-Host ($Error | Format-List -Force | Out-String)
Write-Host Clearing Error and Continuing
$error.Clear()
$LASTEXITCODE = 0
}
#error from command cleared here
return $output
}
export-modulemember -function *
Test.ps1
import-module "CustomModule.psm1" # definew cmdlet Get-EmulatorStatus
$status = Get-EmulatorStatus
# even though error cleared in cmdlet, still here
Write-Host Write-Host Error $LASTEXITCODE, $Error.Count
Write-Host ($Error | Format-List -Force | Out-String)
Try using one of two options:
use exit from your cmdlet, e.g. exit 0 (preferred).
use a global scope when setting the codes explicitly, E.g.
$global:LASTEXITCODE
I ran into this calling robocopy that sets non-zero exit codes even on success, and interfered with Jenkin's automation.