I want to know if a ScriptBlock executed successfully.
If I run
1/0; echo $?
I get
RuntimeException: Attempted to divide by zero.
false
but if I do
$s = { 1/0 }; Invoke-Command $s; echo $?
I get
RuntimeException: Attempted to divide by zero.
true
I assume that $? is referring to the execution of Invoke-Command, but how can I get success of the script block itself?
I don't need to use Invoke-Command, if changing to $s.Invoke() is viable then happily use that.
Context
I want to write a function for a DSL along the likes of
function at_place {
Param(
[string] $Path,
[scriptblock] $ScriptBlock
)
Push-Location $Path ;
Invoke-Command $ScriptBlock ;
# following line doesn't work
[bool] $ScriptBlockPass = $? ;
If ( $? ){
Write-Debug "success!" ;
Pop-Location ;
} Else {
Write-Error "ScriptBlock failed, remaining at $Path, please fix manually." ;
throw "ScriptBlock failed at $Path" ;
}
}
I have things in my buffer like
Push-Location ~/foo; doStuff; If ( $? ){ Pop-Location; } Else { Write-Error "Failed, fix here" }
Which I would like to write as
at_place ~/foo { doStuff; }
My actual change is about using Git to ignore a bunch of files, stash them, apply some edits, and then reignore them etc. Simplified here hopefully to be more broadly applicable and less distracting.
Your question is a bit broad and the answer is, it really depends.
Child scope invocations can only update the value of $? via $PSCmdlet using either .WriteError method or .ThrowTerminatingError method as stated in the about Automatic Variables documentation. This would imply that the script block or function is an advanced one:
$s = {
[CmdletBinding()]
param()
try {
1 / 0
}
catch {
$PSCmdlet.WriteError($_)
}
}
& $s # Writes Error
$? # Value will be False (Failure)
For a non-advanced function or script block, $? would be categorized as unreliable, one way to know if the script block succeeded (and by succeeded I mean it had no errors) and, in my opinion, the most reliable one, would be to set the error preference to Stop to target both, terminating an non terminating errors and execute the script block in a try / catch / finally:
$s = { Write-Error foo }
try {
$previousPreference = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
& $s
}
catch {
'had errors'
}
finally {
$ErrorActionPreference = $previousPreference
}
As for the new edit, I would approach your function this way:
function at_place {
[CmdletBinding()]
Param(
[string] $Path,
[scriptblock] $ScriptBlock
)
try {
$sbSuccess = $true
$ErrorActionPreference = 'Stop'
$pushSuccess = Push-Location $Path -PassThru
& $ScriptBlock
}
catch {
$sbSuccess = $false
$PSCmdlet.ThrowTerminatingError($_)
}
finally {
if($pushSuccess -and $sbSuccess) {
Pop-Location
}
}
}
Related
I am writing a powershell script that is running on Linux. The purpose of this script is to run the MS SQL Server and show its output, but when the user presses Ctrl+C or some error happens, the script takes a copy of the data folder and then after that exits.
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
BEGIN {
Write-Output "started $PSScriptRoot"
$currentErrorLevel = $ErrorActionPreference
# [console]::TreatControlCAsInput = $true
}
PROCESS {
try {
$ErrorActionPreference = 'SilentlyContinue';
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
$ErrorActionPreference = $currentErrorLevel;
Write-Error $_.Exception
}
}
End {
$ErrorActionPreference = $currentErrorLevel;
#do backup
Create-Backup "/opt/mssql/bin/data"
Write-Output "finishd!"
}
I got a couple of problems with this script:
When I press Ctrl+C it breaks the main script and it never reaches to the Create-Backup section at the bottom of the script.
If I remove -Wait then the script won't show the sql log output
So my prefered solution is to run the sql with -Wait parameter and prevent the powershell to exit the code after I press Ctrl+C, but instead Ctrl+C close the sql instance
So I'm looking for a way to achieve both.
For simplicity I'll assume that your function only needs to support a single input object, so I'm using a simple function body without begin, process and end blocks, which is equivalent to having just an end block:
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
Write-Output "started $PSScriptRoot"
# No need to save the current value, because the modified
# value is local to the function and goes out of scope on
# exiting the function.
$ErrorActionPreference = 'SilentlyContinue';
try {
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
Write-Error $_.Exception
}
finally {
# This block is *always* called - even if Ctrl-C was used.
Create-Backup "/opt/mssql/bin/data"
# CAVEAT: If Ctrl-C was used to terminate the run,
# you can no longer produce *pipeline* input at this point
# (it will be quietly ignored).
# However, you can still output to the *host*.
Write-Host "finished!"
}
If you really need to support multiple input objects, it gets more complicated.
I have a few functions that get called either from Jenkins as part of a pipeline, they also get called from a pester test or lastly they can get called from the powershell console. The issue I have really stems from Jenkins not seeming to handle write-output in the way I think it should.
So what I am doing is creating a Boolean param that will allow my to choose if I terminate my function with a exit code or a return message. The exit code will be used by my pipeline logic and the return message for the rest ?
Is there a alternate approach I should be using this seems to be a bit of a hack.
function Get-ServerPowerState
{
[CmdletBinding()]
param
(
[string[]]$ilo_ip,
[ValidateSet('ON', 'OFF')]
[string]$Status,
[boolean]$fail
)
BEGIN
{
$here = Split-Path -Parent $Script:MyInvocation.MyCommand.Path
$Credentials = IMPORT-CLIXML "$($here)\Lib\iLOCred.xml"
}
PROCESS
{
foreach ($ip in $ilo_ip)
{
New-LogEntry -Message ("Getting current powerstate " + $ip)
If (Test-Connection -ComputerName $ip.ToString() -Count 1 -Quiet)
{
$hostPower = Get-HPiLOhostpower -Server $ip -Credential
$Credentials -DisableCertificateAuthentication
}
}
}
END
{
If($fail){
New-LogEntry -Message "Script been set to fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
Exit 0
}
else {
Exit 1
}
}
else {
New-LogEntry -Message "Script been set to NOT fail with exit code" -Log Verbose
New-LogEntry -Message "The host is powered - $($HostPower.Host_Power)" -Log Verbose
If($hostPower.HOST_POWER -match $Status)
{
return 0
}
else {
return 1
}
}
}
}
Like this
function Get-Output {
param ([switch]$asint)
if ($asint) {
return 1
}
else {
write-output 'one'
}
}
Get-Output
Get-Output -asint
If you intend to use the output in the pipeline then use Write-Output. If you intend to only send it to the host process then use Write-Host. I typically use the return keyword if I want to assign a return value to a variable.
[int]$result = Get-Output -asint
Say that I have this simple function:
Function DoSomething
{
Write-Host "Doing something locally..."
Start-Sleep 5
Return 5
}
Now, say that I want to be able to execute this function on the local machine but also on another one using Powershell remoting. The issue is that Write-Host is not suited for this. Indeed, in the case where the script is executed remotely, I don't want the sentence to be displayed on the console, but I'd like to received information about the progress of the task, so I did this:
Function DoSomething
{
if ([Environment]::UserInteractive -eq $true)
{
Write-Host "Doing something locally..."
}
else
{
Write-Output "Doing something remotely..."
}
Start-Sleep 5
Return 5
}
With that, I can pipe the function like this:
Function Pipable {
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelinebyPropertyName = $true)]
$value
)
Process {
Write-Host $value
}
}
Invoke-Command -ComputerName "MACHINE" -ScriptBlock {
Function DoSomething
{ ... }
DoSomething
} | Pipable
My problem is that if I replace the call to DoSomething by something like:
$value = DoSomething
Then the sentence "Doing something remotely..." is not send through the pipe for reasons I understand but it blocks me in what I want to do:
I'd like to be able to display some information on the console if the function is executed locally but when it's executed remotely, I'd like to get information about the status that I would handle my own way on the computer that start the PSSession. Write-Output seemed useful for this, but I can't use it in a function that return a value for the reason explained above.
Is there a way to do so?
You can still use Write-Host. On remote you could do:
DoSomething *> $Env:TEMP\DoSomething
if ((gc $Env:TEMP\DoSomething) -match 'handle stuff') ...
$out = DoSomething 6> $Env:TEMP\DoSomething
if ((gc $Env:TEMP\DoSomething) -match 'handle stuff') ...
I have a PowerShell function (Add-EventLogSource) that checks if an event log source exists. If it does not exist and the shell is not elevated, I start a new, elevated shell and call the function again.
I can't seem to get the return values correct. If the event log source does not exist, and I call Add-EventLogSource, I am not getting the return value all the way back to instance that originally called Add-EventLogSource. Can anyone see the problem? The code looks like this:
Function Add-EventLogSource {
Param (
[Parameter(Mandatory=$True)]
$EventLogSource
)
# Check if $EventLogSource exists as a source. If the shell is not elevated and the check fails to access the Security log, assume the source does not exist.
Try {
$sourceExists = [System.Diagnostics.EventLog]::SourceExists("$EventLogSource")
}
Catch {
$sourceExists = $False
}
If ((([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] “Administrator”)) -AND ($sourceExists -eq $False)) { # Shell is elevated...
Try {
New-EventLog –LogName Application –Source $EventLogSource -ErrorAction Stop
}
Catch {
Return "Error"
}
Return "Created"
}
ElseIf ($sourceExists -eq $False) {
$return = Start-Process PowerShell –Verb RunAs -ArgumentList "Add-EventLogSource -EventLogSource $EventLogSource; start-sleep 5" -Wait
Return $return
}
Else {
Return "Exists"
}
}
Thanks.
I have a system in which I'm remoting into a single machine at a time and running commands, scripts, etc. It would be useful to be able to effectively return log messages from the remote script in "realtime". Some code to get an idea of what I'm trying to do.
Note that both the local Log-*Msg functions log to a database (and tee to standard out/err as appropriate). Also note that we have analogous Log-*Msg methods on the remote side (loaded from a module) that are meant to pitched back across the wire and recorded in the DB as if the local Log-*Msg function was called.
Local Methods
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
if($_ -isnot [string]) { return $_ }
if($_.StartsWith('Log-VerboseMsg:')) {
Log-VerboseMsg $_.Replace("Log-VerboseMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-WarningMsg:')) {
Log-WarningMsg $_.Replace("Log-WarningMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-UserMsg:')) {
Log-UserMsg $_.Replace("Log-UserMsg:", "") | Out-Null
return
}
else { return $_ }
}
Example Remote Method
On the remote side I have a module that gets loaded with a few logging functions, here's one such function:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
"Log-VerboseMsg:$msg"
}
For the most part it works, I can do the following
$val = Exec-Remote -Session $PSSession {
Log-VerboseMsg 'A test log message!'
return $true
}
And have it do the right thing transparently.
However, it fails in the following scenario.
$val = Exec-Remote -Session $PSSession {
function Test-Logging {
Log-VerboseMsg 'A test log message!'
return $true
}
$aVariable = Test-Logging
Do-ALongRunningOperation
return $aVariable
}
The above will not return anything until the 'long running operation' completes.
My question to you is the following.
Is there a way for me to reliably do this in Powershell? In some form, if the approach I'm using is really that terrible, feel free to lambast me and explain why.
NOTE: connecting to the DB from the remote environment and recording the log messages will not always be possible, so while that approach could work, for my specific needs it isn't sufficient.
In PowerShell v5 you can use new information stream for this. You should modify local functions as following:
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
# 6>&1 will redirect information stream to output, so Filter-RemoteLogs can process it.
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock 6>&1 | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
# Function should be advanced, so we can call $PSCmdlet.WriteInformation.
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[PSObject]$InputObject
)
if(
# If it is InformationRecord.
($InputObject -is [Management.Automation.InformationRecord]) -and
# And if it come from informational steam.
($WriteInformationStream=$InputObject.PSObject.Properties['WriteInformationStream']) -and
($WriteInformationStream.Value)
) {
# If it is our InformationRecord.
if($InputObject.Tags-contains'MyLoggingInfomation') {
# Write it to log.
&"Log-$($InputObject.MessageData.LogType)Msg" $InputObject.MessageData.Message | Out-Null
} else {
# Return not our InformationRecord to informational stream.
$PSCmdlet.WriteInformation($InputObject)
}
} else {
# Return other objects to output stream.
$PSCmdlet.WriteObject($InputObject)
}
}
And remote logging functions should write to information stream:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Verbose'}) MyLoggingInfomation
}
function Log-WarningMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Warning'}) MyLoggingInfomation
}
function Log-UserMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='User'}) MyLoggingInfomation
}