Created
December 15, 2015 15:12
-
-
Save sitano/1dbc78ecd2d3bda0e96d to your computer and use it in GitHub Desktop.
This is PowerShell script execution wrapper which can dynamically setup args and read $input. v1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
This is PowerShell script execution wrapper which can dynamically setup args and read $input. | |
.EXAMPLE | |
If you need read input. This would not work in Consul config because of "" in -Command. | |
echo 'ars {blah,tatata}' | PowerShell.exe -NoProfile -NonInteractive -Command "$input | ./Run.ps1 -ScriptName ... -Arg1 val1 -Arg2 val2 ..." | |
.EXAMPLE | |
If you do not need input. | |
echo 'ars {blah,tatata}' | PowerShell.exe -NoProfile -NonInteractive -Command ./Run.ps1 -ScriptName ... -Arg1 val1 -Arg2 val2 ... | |
.EXAMPLE | |
The only way you can use piping in consul configs (you can't use '' / "" in -Command flag). | |
"PowerShell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command Tests\\Run.ps1 -ScriptName ... -InputObject $input" | |
#> | |
[CmdletBinding(SupportsShouldProcess)] | |
Param( | |
[parameter(Mandatory, Position = 0)] | |
$ScriptName, | |
[parameter(ValueFromPipeline)] | |
$InputObject, | |
[ValidateNotNullOrEmpty()] | |
[string]$RunLog) | |
DynamicParam { | |
# This script block just extends local script arguments with the arguments from the target script | |
# by filling in $Params dict with args defs read got from metadata of $Script external-script command. | |
$Params = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary | |
$Script = Join-Path -Path $PSScriptRoot -ChildPath $ScriptName | |
$Command = Get-Command $Script -ErrorAction Stop | |
$MetaData = New-Object -TypeName System.Management.Automation.CommandMetaData -ArgumentList $Command | |
# This complex filter tries to omit argument from target script passed here which have [ValueFromPipeline] | |
# attribute setup, because this Run.ps1 script already have [parameter(ValueFromPipeline)] $InputObject, and | |
# any script can't contain more that 1 unique pipeline entry endpoints. | |
$MetaData.Parameters.Keys | ? { $_ -notin ($MyInvocation.BoundParameters.Keys) -and | |
-not($MetaData.Parameters[$_].Attributes | ? { | |
$_.TypeId -eq [System.Management.Automation.ParameterAttribute] -and | |
$_.ValueFromPipeline}) } | % { | |
# $_ name of the parameter | |
$Value = $MetaData.Parameters[$_] | |
# Just copy parameter definition from target script to local dynamic context | |
$Params.Add($_, (New-Object ` | |
-Type System.Management.Automation.RuntimeDefinedParameter ` | |
($_, $value.ParameterType, $Value.Attributes))) | |
} | |
# Remove parameters not known to target script to be able to pass all bound params. | |
# Explicitly skip -InputObject removal at this stage. | |
@() + $PSBoundParameters.Keys | ? { | |
$_ -notin $MetaData.Parameters.Keys -and $_ -ne "InputObject" | |
} | % { | |
$PSBoundParameters.Remove($_) | Out-Null | |
} | |
return $Params | |
} | |
Begin { | |
Set-StrictMode -Version 2 | |
function Get-DateForFilename { | |
Get-Date -Format yyyy.M.d_HH.mm.ss | |
} | |
function Rotate-Logs { | |
[CmdletBinding()] | |
param( | |
[ValidateNotNullOrEmpty()] | |
[string]$TempPath = "C:\Temp", | |
[parameter(Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[string]$Prefix, | |
[int]$Max = 5) | |
$Logs = Get-ChildItem (Join-Path -Path $TempPath -ChildPath "$($Prefix)_*.log") | | |
Sort-Object -Property "LastWriteTime" -Descending | Select-Object -Skip $Max | |
if ($Logs) { | |
$Count = ($Logs | measure).Count | |
Write-Verbose "Cleared $Count logs at $Prefix*.log" | |
$Logs | Remove-Item | Write-Verbose | |
} | |
} | |
Function Write-Error-Detailed { | |
[CmdletBinding()] | |
Param( | |
$E, | |
[parameter(ValueFromPipeline)] | |
$InputObject) | |
<# | |
.SYNOPSIS | |
Prints very detailed information about error | |
#> | |
Begin { | |
if ($E -is [System.Management.Automation.ErrorRecord]) { | |
$E = [System.Management.Automation.ErrorRecord]$E | |
Write-Error -Message ("$($E.Exception)`n" + | |
"CategoryInfo: $($E.CategoryInfo)`n" + | |
"ErrorDetails: $($E.ErrorDetails)`n" + | |
"FullyQualifiedErrorId: $($E.FullyQualifiedErrorId)`n" + | |
"InvocationInfo: $($E.InvocationInfo)`n" + | |
"PipelineIterationInfo: $($E.PipelineIterationInfo)`n" + | |
"PSMessageDetails: $($E.PSMessageDetails)`n" + | |
"ScriptStackTrace: $($E.ScriptStackTrace)`n" + | |
"TargetObject: $($E.TargetObject)") | |
} else { | |
Write-Error -Exception $E | |
} | |
} | |
Process { | |
if ($_) { | |
Write-Error-Detailed -E $_ | |
} | |
} | |
} | |
# Validate Modules directory included into $env:PSModulePath | |
$ModulesPath = Join-Path -Path $PSScriptRoot -ChildPath "Modules" | |
if ($env:PSModulePath -notcontains $ModulesPath) { | |
$env:PSModulePath += ";$ModulesPath" | |
} | |
# Interactive mode is the mode in which user interaction is expected. | |
# Those, when you run anything from PowerShell command line - it is interactive. | |
# When you run from cmd.exe PowerShell.exe -NonInteractive - it is not interactive, | |
# it just running the interpretor of any script. | |
# Then this is presseting of Exit Code to the Parent process - 2 which means to Consul | |
# error is set here for the case if script will fail in the middle, it should report error | |
# any way. | |
# Check for interactive mode here is to prevent exiting / closing your command line shell | |
# if you are trying to run it from PowerShell console - so you are able to run and debug fine. | |
function Test-IsNonInteractive { [bool]([Environment]::GetCommandLineArgs() -Match '-NonInteractive') } | |
$IsNonInteractive = Test-IsNonInteractive | |
if ($IsNonInteractive) { $Host.SetShouldExit(2) } | |
$Script = Join-Path -Path $PSScriptRoot -ChildPath $ScriptName | |
if (-not(Test-Path -Path $Script -PathType Leaf)) { | |
throw [System.IO.FileNotFoundException] "Script not found at $Script" | |
} | |
# We want to run target script from it local working environment so jump into there. | |
Push-Location -Path (Split-Path -Path $Script -Parent) | |
try { | |
# Get command wrapper around external script. | |
$WrappedCmd = Get-Command($Script) | |
} catch { | |
Pop-Location | |
if ($IsNonInteractive) { | |
Write-Error-Detailed $_ | |
$Host.SetShouldExit(2) | |
Exit | |
} else { | |
throw $_ | |
} | |
} | |
# Case when $InputObject is used not for piping, but was explicitly passed as arg. | |
# Resave it into another var, because original will be rewritten by Process {} | |
$InputObjectArg = $InputObject | |
# Rotate logs if requested | |
if ($RunLog) { | |
$LogPath = "c:\Temp" | |
$LogPrefix = $RunLog | |
$LogMax = 5 | |
$LogSuffix = Get-DateForFilename | |
$LogFile = Join-Path -Path $LogPath -ChildPath "$($LogPrefix)_$LogSuffix.log" | |
Rotate-Logs -TempPath $LogPath -Prefix $LogPrefix -ErrorAction SilentlyContinue | |
} | |
} | |
Process { | |
# Remove local pipeline entry endpoint as it is not know to target script too, | |
# and the pipeline will be provided through explicit piping. | |
$PSBoundParameters.Remove("InputObject") | Out-Null | |
$BoundParameters = $PSBoundParameters | |
$Executor = { | |
try { | |
# Case when $InputObject is used not for piping, but was explicitly passed as arg. | |
if ($InputObjectArg) { | |
$InputObjectArg | . $WrappedCmd @BoundParameters | |
# This complex filter tries to select right way of calling target script wrapper | |
# as in strict bind mode we can't pipe in without proper binding on remote side. | |
} elseif ($WrappedCmd.Parameters.Values | ? { $_.Attributes | ? { | |
$_.TypeId -eq [System.Management.Automation.ParameterAttribute] -and | |
$_.ValueFromPipeline} }) { | |
$_ | . $WrappedCmd @BoundParameters | |
} else { | |
& $WrappedCmd @BoundParameters | |
} | |
} catch { | |
Write-Error-Detailed -E $_ | |
throw $_ | |
} | |
} | |
try { | |
if ($RunLog -and $LogFile) { | |
$Response = & $Executor *>> $LogFile | |
} else { | |
$Response = & $Executor | |
} | |
} catch { | |
Pop-Location | |
if ($IsNonInteractive) { | |
Write-Error-Detailed -E $_ | |
$Host.SetShouldExit(2) | |
Exit | |
} else { | |
throw $_ | |
} | |
} | |
} | |
End { | |
try { | |
# Emulate out of pipe execution edge case. | |
# This is special case in PowerShell runtime pipeline processing, in which | |
# outer call was made with piping like $input | Run.ps1, but pipe was empty, | |
# and thus Process {} body was not called. | |
# So, we recall script explicitly with out pipe as we have nothing there. | |
if (-not (Get-Variable -Name "Response" -Scope "Local" -ErrorAction Ignore)) { | |
if ($InputObjectArg) { | |
throw "You are not allowed to pass InputObject pipe and bind from arg." | |
} else { | |
$BoundParameters = $PSBoundParameters | |
$Executor = { | |
try { | |
& $WrappedCmd @BoundParameters | |
Set-Variable -Name "Response" -Value $Response -Scope 1 | |
} catch { | |
Write-Error-Detailed -E $_ | |
throw $_ | |
} | |
} | |
if ($RunLog -and $LogFile) { | |
$Response = & $Executor *>> $LogFile | |
} else { | |
$Response = & $Executor | |
} | |
} | |
} | |
Pop-Location | |
if ($IsNonInteractive) { | |
if ($Response -is [int]) { | |
# If $Response is set and is [int], then hope test knows it returns to consul | |
$Host.SetShouldExit($Response) | |
} else { | |
# If not set, or not [int], then by default return Good to consul, as there were | |
# no error \ exception | |
$Host.SetShouldExit(0) | |
} | |
# Explicitly exit powershell process, do prevent hang on opened runspaces / timers / etc. | |
Exit | |
} else { | |
# If interactive mode, just return $Response (results of the script) to the user shell | |
$Response | |
} | |
} catch { | |
Pop-Location | |
if ($IsNonInteractive) { | |
Write-Error-Detailed -E $_ | |
$Host.SetShouldExit(2) | |
Exit | |
} else { | |
throw $_ | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment