Skip to content

Instantly share code, notes, and snippets.

@sitano
Created December 15, 2015 15:12
Show Gist options
  • Save sitano/1dbc78ecd2d3bda0e96d to your computer and use it in GitHub Desktop.
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.
<#
.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