I have a script that uses Write-Verbose calls on it. I want to create a wrapper - override it, add another call, and then invoke the regular Write-Verbose command.
Something like:
Function global:Write-Verbose ($msg) {
MyLogger $msg
Real-Write-Verbose $msg
}
How can I do it?
Possible with $ExecutionContext.InvokeCommand.GetCommand:
function Write-Verbose {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[Alias('Msg')]
[AllowEmptyString()]
[System.String]
${Message})
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Write-Verbose', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process {
try {
MyLogger $Message
$steppablePipeline.Process($_)
} catch {
throw
}
}
end {
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
Credit goes to Joel Bennett#Poshcode.
If you just want to override it in your script scope (but not in any called scripts or functions), you can do it like this:
function Private:Write-Verbose ($msg) {
MyLogger $msg
&{Write-Verbose $msg}
}
Related
I am working on a script logging solution for an install script that has many tasks and long processing times, and I am trying to address the issue of network dropout. I am also moving to a StreamWriter based approach vs my old Add-Content approach for performance reasons.
The problem I am having is that once the network drops out, the StreamWriter doesn't reconnect. So, first question is, CAN I reconnect, or is this a limitation of StreamWriter? The fact that the StreamWriter has a cache that can be Flushed makes me think I may be doing a bunch of work to recreate functionality that is already there.
And second, I am starting to think a simpler/better solution is simply to write the log to a local folder, so the log is always complete, then simply attempt to copy that to the network location for progress review. I had been thinking about implementing parallel logs, so a loss of the network would still leave the local copy complete. Curious if anyone else looks at this and says "Well, YEAH, dufus, obviously."
function Get-PxLogFile {
return $script:pxLogFile
}
function Set-PxLogFile {
param (
[string]$path
)
if (Test-Path $path) {
Remove-Item $path -force
}
[string]$script:pxLogFile= $path
}
function Get-PxLogWriter {
$logFile = Get-PxLogFile
if (-not $script:pxFileStream) {
$script:pxFileStream = New-Object io.fileStream $logFile, 'Append', 'Write', 'Read'
$script:pxlogWriter = New-Object io.streamWriter $script:pxFileStream
} elseif ($script:pxFileStream.name -ne $logFile) {
$script:pxFileStream = New-Object io.fileStream $logFile, 'Append', 'Write', 'Read'
$script:pxlogWriter = New-Object io.streamWriter $script:pxFileStream
}
return $script:pxlogWriter
}
function Dispose-PxLogFile {
$script:pxlogWriter.Dispose()
$script:pxFileStream.Dispose()
$script:pxFileStream = $null
$script:pxlogWriter = $null
}
# Shared
function Get-PxDeferredLog {
return ,$script:deferredLog # , keeps PS from unrolling a single item array into a string
}
function Set-PxDeferredLog {
param (
[collections.arrayList]$deferredLog
)
[collections.arrayList]$script:deferredLog = $deferredLog
}
function Get-PxDeferredLogTimes {
return ($script:deferredLogTimes -join ', ')
}
function Finalize-PxLogFile {
$logWriter = Get-PxLogWriter
if (($deferredLog = Get-PxDeferredLog).count -gt 0) {
$abandonTime = (Get-Date) + (New-TimeSpan -seconds:3)
$logWriter = Get-PxLogWriter
:lastChanceLogWindow do {
$deferredLog = Get-PxDeferredLog
:deferredItemsWrite do {
try {
$logWriter.WriteLine($deferredLog[0])
if ($deferredLog.count -gt 0) {
$deferredLog.RemoveAt(0)
} else {
break :lastChanceWindow
}
} catch {
break :deferredItemsWrite
}
} while ($deferredLog.count -gt 0)
Set-PxDeferredLog $deferredLog
if ($deferredLog.count -eq 0) {
break :lastChanceWindow
}
if ((Get-Date) -gt $abandonTime) {
break :lastChanceLogWindow
}
} while ((Get-Date) -lt $abandonTime)
if ($deferredLog) {
Write-Host "Failed to write all log entries"
}
}
$script:deferredLogItems = $script:deferredLogTimes = $null
}
function Add-PxLogFileContent {
param (
[string]$string
)
$logWriter = Get-PxLogWriter
# Nested Functions
function Add-PxDeferredLogItem {
param (
[string]$item
)
if (-not $script:deferredLog) {
[collections.arrayList]$script:deferredLog = New-Object collections.arrayList
}
if ($script:deferredLog.count -eq 0) {
Start-PxDeferredLogTime
}
[void]$script:deferredLog.Add($item)
}
function Start-PxDeferredLogTime {
if (-not $script:deferredLogTimes) {
[collections.arrayList]$script:deferredLogTimes = New-Object collections.arrayList
}
if ((-not $script:deferredLogTimes) -or (-not $script:deferredLogTimes[-1].EndsWith('-'))) {
[void]$script:deferredLogTimes.Add("$((Get-Date).ToString('T'))-")
}
}
function Stop-PxDeferredLogTime {
if ($script:deferredLogTimes[-1].EndsWith('-')) {
$script:deferredLogTimes[-1] = "$($script:deferredLogTimes[-1])$((Get-Date).ToString('T'))"
}
}
$deferredLogProcessed = $false
if ([collections.arrayList]$deferredLog = Get-PxDeferredLog) {
$deferredLogProcessed = $true
:deferredItemsWrite do {
try {
$logWriter.WriteLine($deferredLog[0])
$logWriter.Flush()
$deferredLog.RemoveAt(0)
} catch {
break :deferredItemsWrite
}
} while ($deferredLog.count -gt 0)
if ($deferredLog.count -eq 0) {
$deferredLogPending = $false
} else {
$deferredLogPending = $true
}
Set-PxDeferredLog $deferredLog
} else {
$deferredLogPending = $false
}
if (-not $deferredLogPending) {
try {
if ($logWriter.WriteLine($string)) {
$logWriter.Flush()
}
if ($deferredLogProcessed) {Stop-PxDeferredLogTime}
} catch {
Add-PxDeferredLogItem $string
Write-Host "Failed: $(Get-Date)`n$($_.Exception.Message)"
}
} else {
Add-PxDeferredLogItem $string
}
}
### MAIN
Clear-Host
$script:deferredLogItems = $script:deferredLogTimes = $null
$logPath = '\\px\Content'
Write-Host 'logTest1.txt'
$startTime = Get-Date
$endTime = $startTime + (New-TimeSpan -minutes:5)
#Set-PxLogFile "$([System.IO.Path]::GetFullPath($env:TEMP))\logTest1.txt"
Set-PxLogFile "$logPath\logTest1.txt"
do {
Add-PxLogFileContent "logged: $(Get-Date)"
Start-SLeep -s:10
} while ((Get-Date) -lt $endTime)
Finalize-PxLogFile
Write-Host 'logTest2.txt'
$startTime = Get-Date
$endTime = $startTime + (New-TimeSpan -minutes:5)
#Set-PxLogFile "$([System.IO.Path]::GetFullPath($env:TEMP))\logTest2.txt"
Set-PxLogFile "$logPath\logTest2.txt"
do {
Add-PxLogFileContent "logged: $(Get-Date)"
Start-SLeep -s:10
} while ((Get-Date) -lt $endTime)
if ([string]$deferredLogTimes = Get-PxDeferredLogTimes) {
Add-PxLogFileContent "Deferred logging time ranges: $deferredLogTimes"
}
Finalize-PxLogFile
Dispose-PxLogFile
I have a module which has the following two functions, which are almost identical:
<#
.SYNOPSIS
Retrieves a VApp from VCloud.
#>
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_.Get(); }
}
}
End
{
#
}
}
and
<#
.SYNOPSIS
Retrieves a VAppRecord from VCloud.
#>
Function Get-VAppRecord
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_; }
}
}
End
{
#
}
}
Essentially, Get-VApp is like Get-VAppRecord, except that the former calls a Get() method on the returned object. This seems wasteful. If I wasn't bothering with pipelines, it would be easy:
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true)]
[string[]] $VAppName
)
Get-VAppRecord $Session $VAppName |
ForEach-Object {
$_.Get();
}
}
But obviously the pipeline messes things up. I don't call the code in the Begin block multiple times for efficiency, and I would like to find a way to "play nice" with the pipeline without having to batch up records.
The SteppablePipeline class is designed for wrapping pipeline-enabled commands without messing up their pipeline support.
You don't even need to know how to set it up, ProxyCommand.Create() will generate the scaffolding for it!
So let's start out by creating a proxy function for Get-VAppRecord:
$GetVAppRecordCommand = Get-Command Get-VAppRecord
$GetVAppRecordCommandMetadata = [System.Management.Automation.CommandMetadata]::new($GetVAppRecordCommand)
# returns the body of the new proxy functions
[System.Management.Automation.ProxyCommand]::Create($GetVAppRecordCommandMetadata)
... and then we just need to add the Get() call in the process block of it:
function Get-VApp {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[System.Xml.XmlElement]
${Session},
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]]
${VAppName})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-VAppRecord', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($MyInvocation.ExpectingInput) # Many examples use $PSCmdlet; however setting this ensures that $steppablePipeline.Process() returns the output of the inner function.
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_) |ForEach-Object {
# call Get() on the record
$_.Get()
}
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
How do I download files from a remote server over a PSSession? I'm aware that PS5 introduced Copy-Item -FromSession, but both local and remote may be not running PS5. My files are also quite large so a simple Get-Content may be problematic.
You can read file on remote side as sequence of byte arrays, stream them thru remoting, and then assemble them back to file locally:
function DownloadSingleFile {
param(
[System.Management.Automation.Runspaces.PSSession] $FromSession,
[string] $RemoteFile,
[string] $LocalFile,
[int] $ChunkSize = 1mb
)
Invoke-Command -Session $FromSession -ScriptBlock {
param(
[string] $FileName,
[int] $ChunkSize
)
$FileInfo = Get-Item -LiteralPath $FileName
$FileStream = $FileInfo.OpenRead()
try {
$FileReader = New-Object System.IO.BinaryReader $FileStream
try {
for() {
$Chunk = $FileReader.ReadBytes($ChunkSize)
if($Chunk.Length) {
,$Chunk
} else {
break;
}
}
} finally {
$FileReader.Close();
}
} finally {
$FileStream.Close();
}
} -ArgumentList $RemoteFile, $ChunkSize | ForEach-Object {
$FileName = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LocalFile)
$FileStream = [System.IO.File]::Create($FileName)
} {
$FileStream.Write($_, 0, $_.Length)
} {
$FileStream.Close()
}
}
The Vester project creates pester tests that follow a specific pattern of Describe, It, Try, Catch, Remdiate to test and then fix issues with a VMWare environment, Ex. Update-DNS.Tests.ps1.
In semi-pseudo code, the core of the algorithm is the following:
Param(
[switch]$Remediate = $false
)
Process {
Describe -Name "Test Group Name" -Tag #("Tag") -Fixture {
#Some code to load up state for the test group
foreach ($Thing in $Things) {
It -name "Name of first test" -test {
#Some code to load up state for the test
try {
#Conditional tests using Pester syntax
} catch {
if ($Remediate) {
Write-Warning -Message $_
Write-Warning -Message "Remediation Message"
#Code to remediate issues
} else {
throw $_
}
}
}
}
}
}
I would like to be able to write the code that would allow for the following Pester like DSL syntax:
Param(
[switch]$Remediate = $false
)
CheckGroup "AD User Checks" {
ForEach($Aduser in (Get-aduser -Filter * -Properties HomeDirectory)) {
Check "Home directory path exists" {
Condition {
if ($Aduser.HomeDirectory) {
Test-path $Aduser.HomeDirectory | Should be $true
}
}
Remdiation "Create home directory that doesn't exist" {
New-Item -ItemType Directory -Path $Aduser.HomeDirectory
}
}
}
}
Running this would result in something like the following actually being run:
Describe -Name "AD User Checks" -Fixture {
ForEach($Aduser in (Get-aduser -Filter * -Properties HomeDirectory)) {
It -name "Home directory path exists" -test {
try {
if ($Aduser.HomeDirectory) {
Test-path $Aduser.HomeDirectory | Should be $true
}
} catch {
if ($Remediate) {
Write-Warning -Message $_
Write-Warning -Message "Create home directory that doesn't exist"
New-Item -ItemType Directory -Path $Aduser.HomeDirectory
} else {
throw $_
}
}
}
}
}
How can I go about implementing this DSL designed specifically for performing checks and remediations?
Here is some of the code that I have written to try to accomplish this:
Function CheckGroup {
param (
[Parameter(Mandatory, Position = 0)][String]$Name,
[Parameter(Position = 1)]$CheckGroupScriptBlock
)
Describe $CheckGroupName -Fixture $CheckGroupScriptBlock
}
Function Check {
param (
[Parameter(Mandatory, Position = 0)][String]$Name,
$ConditionScriptBlock,
$RemediationScriptBlock
)
It -name $Name -test {
try {
& $ConditionScriptBlock
} catch {
& $RemediationScriptBlock
}
}
}
Function Condition {
[CmdletBinding()]
param (
[Parameter(Position = 1)]$ScriptBlock
)
& $ScriptBlock
}
Function Remediation {
[CmdletBinding()]
param (
$Name,
[Parameter(Position = 1)]$ScriptBlock,
[bool]$Remediate = $false
)
if ($Remediate) {
Write-Verbose $_
Write-Verbose $Name
& $ScriptBlock
} else {
throw $_
}
}
For the function Check I really need to be able to take in a single script block as a parameter but somehow find the Condition and Remediation function calls inside the script block passed and split them out of the script block and blend them into the appropriate spot in the Try {} Catch {} inside the It in the Check function.
I am trying to rename a few of my cmdlets and want to do it without breaking existing scripts. I want to do it without using Set-Alias/New-Alias because I do not want the Aliases to show up when we do Get-Command from the powershell prompt and I thought it might be possible to use exported functions to achieve the same thing that aliasing cmdlets would do.
Here is an example of what I want to do
Old cmdlet - Add-Foo
Renamed cmdlet - Add-FooBar
Expectation - Scripts using Add-Foo should continue to work the same way as it used to
I am thinking of introducing the following function
function Add-Foo()
{
# Delegate parameter inputs to cmdlet Add-FooBar
}
I have a simple version of it but I am not sure if it would work in more complex cases.
function Add-Foo()
{
$cmd = "Add-FooBar"
if ($arguments.Length -eq 0){
Invoke-Expression $cmd;
}
else{
# Concatentate cmdlet and arguments into an expression
$expr = "$($cmd) $($args)";
Write-Debug $expr;
Invoke-Expression $expr;
}
}
I am not sure if my function is going to be 100% compatible with existing usages. Can the function Add-Foo be made such that it behaves well with parameter attributes (pipeline binding) and any other possible usages? Essentially I want the function to take the arguments as is and pass it to the underlying renamed cmdlet.
Any help is appreciated.
Thanks
PowerShell has a built-in feature for this: Proxy commands.
The [System.Management.Automation.ProxyCommand] class has several static methods to help out with this. Below is a template you can use to generate a proxy command and add a condition choosing whether or not to call the original command.
function New-ProxyCommand($command)
{
$cmd = Get-Command $command
$blocks = #{
CmdletBinding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($cmd)
Params = [System.Management.Automation.ProxyCommand]::GetParamBlock($cmd)
Begin = [System.Management.Automation.ProxyCommand]::GetBegin($cmd)
Process = [System.Management.Automation.ProxyCommand]::GetProcess($cmd)
End = [System.Management.Automation.ProxyCommand]::GetEnd($cmd)
}
# Indent
filter Indent($indent=' ') { $_ | foreach { ($_ -split "`r`n" | foreach { "${indent}$_" }) -join "`r`n" } }
[array]$blocks.Keys | foreach { $blocks[$_] = $blocks[$_] | Indent }
#"
function $command
{
$($blocks.CmdletBinding)
param
($($blocks.Params)
)
begin
{
`$Reroute = `$false ### Put your conditions here ###
if (`$Reroute) { return }
$($blocks.Begin)}
process
{
if (`$Reroute) { return }
$($blocks.Process)}
end
{
if (`$Reroute) { return }
$($blocks.End)}
}
"#
}
Example:
PS> New-ProxyCommand Get-Item
function Get-Item
{
[CmdletBinding(DefaultParameterSetName='Path', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113319')]
param
(
[Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath},
[string]
${Filter},
[string[]]
${Include},
[string[]]
${Exclude},
[switch]
${Force},
[Parameter(ValueFromPipelineByPropertyName=$true)]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential}
)
begin
{
$Reroute = $false ### Put your conditions here ###
if ($Reroute) { return }
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
if ($Reroute) { return }
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
if ($Reroute) { return }
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
One option is to use a private function:
function Private:Add-Foo
{
Add-Foobar $args
}
Add-Foo will only call this function in the current scope. The function will not be visible within any child scope (like a called script), and they will use the Add-Foo cmdlet instead.