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') ...
Related
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
}
}
}
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 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
}
Is it possible in Powershell to dot-source or re-use somehow script functions without it being executed? I'm trying to reuse the functions of a script, without executing the script itself. I could factor out the functions into a functions only file but I'm trying to avoid doing that.
Example dot-sourced file:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Example consuming file:
. ".\reuseme.ps1"
Write-Host "consume.ps1 main."
doA
Execution results:
reuseme.ps1 main.
consume.ps1 main.
DoAMethod
Desired result:
consume.ps1 main.
DoAMethod
You have to execute the function definitions to make them available. There is no way around it.
You could try throwing the PowerShell parser at the file and only executing function definitions and nothing else, but I guess the far easier approach would be to structure your reusable portions as modules or simply as scripts that don't do anything besides declaring functions.
For the record, a rough test script that would do exactly that:
$file = 'foo.ps1'
$tokens = #()
$errors = #()
$result = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)
$tokens | %{$s=''; $braces = 0}{
if ($_.TokenFlags -eq 'Keyword' -and $_.Kind -eq 'Function') {
$inFunction = $true
}
if ($inFunction) { $s += $_.Text + ' ' }
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'LCurly') {
$braces++
}
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'RCurly') {
$braces--
if ($braces -eq 0) {
$inFunction = $false;
}
}
if (!$inFunction -and $s -ne '') {
$s
$s = ''
}
} | iex
You will have problems if functions declared in the script reference script parameters (as the parameter block of the script isn't included). And there are probably a whole host of other problems that can occur that I cannot think of right now. My best advice is still to distinguish between reusable library scripts and scripts intended to be invoked.
After your function, the line Write-Host "reuseme.ps1 main." is known as "procedure code" (i.e., it is not within the function). You can tell the script not to run this procedure code by wrapping it in an IF statement that evaluates $MyInvocation.InvocationName -ne "."
$MyInvocation.InvocationName looks at how the script was invoked and if you used the dot (.) to dot-source the script, it will ignore the procedure code. If you run/invoke the script without the dot (.) then it will execute the procedure code. Example below:
function doA
{
Write-Host "DoAMethod"
}
If ($MyInvocation.InvocationName -ne ".")
{
Write-Host "reuseme.ps1 main."
}
Thus, when you run the script normally, you will see the output. When you dot-source the script, you will NOT see the output; however, the function (but not the procedure code) will be added to the current scope.
The best way to re-use code is to put your functions in a PowerShell module. Simply create a file with all your functions and give it a .psm1 extension. You then import the module to make all your functions available. For example, reuseme.psm1:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Then, in whatever script you want to use your module of functions,
# If you're using PowerShell 2, you have to set $PSScriptRoot yourself:
# $PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module -Name (Join-Path $PSScriptRoot reuseme.psm1 -Resolve)
doA
While looking a bit further for solutions for this issue, I came across a solution which is pretty much a followup to the hints in Aaron's answer. The intention is a bit different, but it can be used to achieve the same result.
This is what I found:
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
It needed it for some testing with Pester where I wanted to avoid changing the structure of the file before having written any tests for the logic.
It works quite well, and gives me the confidence to write some tests for the logic first, before refactoring the structure of the files so I no longer have to dot-source the functions.
Describe "SomeFunction" {
# Import the ‘SomeFunction’ function into the current scope
. (Get-FunctionDefinition –Path $scriptPath –Function SomeFunction)
It "executes the function without executing the script" {
SomeFunction | Should Be "fooBar"
}
}
And the code for Get-FunctionDefinition
#Requires -Version 3
<#
.SYNOPSIS
Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
Returns a function's source definition as a Powershell ScriptBlock from an
external .ps1 file or existing ScriptBlock. This module is primarily
intended to be used to test private/nested/internal functions with Pester
by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
The source function's name to return as a [ScriptBlock].
.PARAMETER Path
Path to a Powershell script file that contains the source function's
definition.
.PARAMETER LiteralPath
Literal path to a Powershell script file that contains the source
function's definition.
.PARAMETER ScriptBlock
A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
If the following functions are defined in a file named 'PrivateFunction.ps1'
function PublicFunction {
param ()
function PrivateFunction {
param ()
Write-Output 'InnerPrivate'
}
Write-Output (PrivateFunction)
}
The 'PrivateFunction' function can be tested with Pester by dot-sourcing
the required function in the either the 'Describe', 'Context' or 'It'
scopes.
Describe "PrivateFunction" {
It "tests private function" {
## Import the 'PrivateFunction' definition into the current scope.
. (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
PrivateFunction | Should BeExactly 'InnerPrivate'
}
}
.LINK
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
#>
function Get-FunctionDefinition {
[CmdletBinding(DefaultParameterSetName='Path')]
[OutputType([System.Management.Automation.ScriptBlock])]
param (
[Parameter(Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName='Path')]
[ValidateNotNullOrEmpty()]
[Alias('PSPath','FullName')]
[System.String] $Path = (Get-Location -PSProvider FileSystem),
[Parameter(Position = 0,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'LiteralPath')]
[ValidateNotNullOrEmpty()]
[System.String] $LiteralPath,
[Parameter(Position = 0,
ValueFromPipeline = $true,
ParameterSetName = 'ScriptBlock')]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.ScriptBlock] $ScriptBlock,
[Parameter(Mandatory = $true,
Position =1,
ValueFromPipelineByPropertyName = $true)]
[Alias('Name')]
[System.String] $Function
)
begin {
if ($PSCmdlet.ParameterSetName -eq 'Path') {
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
}
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
## Set $Path reference to the literal path(s)
$Path = $LiteralPath;
}
} # end begin
process {
$errors = #();
$tokens = #();
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors);
}
else {
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors);
}
[System.Boolean] $isFunctionFound = $false;
$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true);
foreach ($f in $functions) {
if ($f.Name -eq $Function) {
Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text));
$isFunctionFound = $true;
}
} # end foreach function
if (-not $isFunctionFound) {
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$errorMessage = 'Function "{0}" not defined in script block.' -f $Function;
}
else {
$errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path;
}
Write-Error -Message $errorMessage;
}
} # end process
} #end function Get-Function
I was reading this post about getting functions passed into a scriptblock for use with jobs:
Powershell start-job -scriptblock cannot recognize the function defined in the same file?
I get how that works by passing the function in as variable and it works for the simple example. What about a real world solution though, is there a more elegant way of handling this?
I have script I'm using to deploy changes to vendor software. It reads an xml that tells it how to navigate the environment and performs the various tasks, ie: map drives, stop services, call a perl installation script. I would like to provide a parameter to the script to allow it to run concurrently, this way if the perl script takes 5 minutes (not uncommon) and you're rolling out to 11 servers you're not waiting for the script to run for an hour.
I'm just going to post some snippets since the full script is a little lengthy. A log function:
function Log
{
Param(
[parameter(ValueFromPipeline=$true)]
$InputObject,
[parameter()]
[alias("v")]
$verbosity = $debug
)
$messageIndex = [array]::IndexOf($verbosityArray, $verbosity)
$verbosityIndex = [array]::IndexOf($verbosityArray, $loggingVerbosity)
if($messageIndex -ge $VerbosityIndex)
{
switch($verbosity)
{
$debug {Write-Host $verbosity ": " $InputObject}
$info {Write-Host $verbosity ": " $InputObject}
$warn {Write-Host $verbosity ": " $InputObject -ForegroundColor yellow}
$error {Write-Host $verbosity ": " $InputObject -ForegroundColor red}
}
}
}
Here's another function that calls the log function:
function ExecuteRollout
{
param(
[parameter(Mandatory=$true)]
[alias("ses")]
$session,
[parameter(Mandatory=$true)]
$command
)
#invoke command
Invoke-Command -session $session -ScriptBlock {$res = cmd /v /k `"$args[0]`"} -args $command
#get the return code from the remote session
$res = Invoke-Command -session $session {$res}
Log ("Command Output: "+$res)
$res = [string] $res
$exitCode = $res.substring($res.IndexOf("ExitCode:"), 10)
$exitCode = $exitCode.substring(9,1)
Log ("Exit code: "+$exitCode)
return $exitCode
}
And lastly a snippet from my main so you can get an idea of what's going on. $target.Destinations.Destination will contain all the servers and relevant information about them that the deployment will go to. I removed some variable setup and logging to make this more compact so yes you'll see variables referenced that are never defined:
#Execute concurrently
$target.Destinations.Destination | %{
$ScriptBlock = {
$destination = $args[0]
Log -v $info ("Starting remote session on: "+$destination.Server)
$session = New-PSSession -computerName $destination.Server
$InitializeRemote -session $session -destination $destination
#Gets a little tricky here, we need to keep the cmd session so it doesn't lose the sys vars set by env.bat
#String everything together with &'s
$cmdString = $destDrive + ": & call "+$lesDestDir+"data\env.bat & cd "+$rolloutDir+" & perl ..\JDH-rollout-2010.pl "+$rollout+" NC,r:\les & echo ExitCode:!errorlevel!"
Log ("cmdString: "+$cmdString)
Log -v $info ("Please wait, executing the rollout now...")
$exitCode = $ExecuteRollout -session $session -command $cmdString
Log ("ExitCode: "+$exitCode)
#respond to return code from rollout script
$HandleExitCode -session $session -destination $destination -exitCode $exitCode
$CleanUpRemote -session $session -destination $destination
}
Start-Job $ScriptBlock -Args $_
}
So if i go with the approach in the link I'd be converting all my functions to variables and passing them in to the script block. Currently, my log function will by default log in DEBUG unless the verbosity parameter is explicitly passed as a different verbosity. If I convert my functins to variables however powershell doesn't seem to like this syntax:
$Log ("Print this to the log")
So I think I'd need to use the parameter all the time now:
$Log ("Print this to the log" -v $debug
So bottom line it looks like I just need to pass all my functions as variables to the script block and change some formatting when I call them. It's not a huge effort, but I'd like to know if there's a better way before I start hacking my script up. Thanks for the input and for looking, I know this is quite a long post.
I started another post about passing parameters to functions stored as variables, the answer to that also resolves this issue. That post can be found here:
Powershell: passing parameters to functions stored in variables
The short answer is you can use the initializationscript parameter of Start-Job to feed all your functions in if you wrap them in a block and store that in a variable.
Example:
# concurrency
$func = {
function Logx
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
$scriptBlock =
{
Logx $args[0]
Start-Sleep 9
}
Write-Host "Processing: " $_
Start-Job -InitializationScript $func -scriptblock $scriptBlock -args $_
}
Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *