From a4daf1fa08742ed9b4e29a8ac3ec5b84fcd3df02 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Fri, 6 Dec 2024 14:55:58 -0600 Subject: [PATCH 01/14] Moved Add-ScriptBlockInjection to Shared --- Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 | 2 +- .../Write/Write-LargeDataObjectsOnMachine.ps1 | 3 ++- .../ScriptBlock}/Add-ScriptBlockInjection.ps1 | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename {Diagnostics/ExchangeLogCollector/Helpers => Shared/ScriptBlock}/Add-ScriptBlockInjection.ps1 (99%) diff --git a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 index 1f55a62ed4..9ece2d3afe 100644 --- a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 +++ b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 @@ -1,12 +1,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -. $PSScriptRoot\Add-ScriptBlockInjection.ps1 . $PSScriptRoot\Enter-YesNoLoopAction.ps1 . $PSScriptRoot\PipelineFunctions.ps1 . $PSScriptRoot\Start-JobManager.ps1 . $PSScriptRoot\..\RemoteScriptBlock\Get-FreeSpace.ps1 . $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Add-ScriptBlockInjection.ps1 function Test-DiskSpace { param( [Parameter(Mandatory = $true)][array]$Servers, diff --git a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 index 02c9b3ce70..99722452ea 100644 --- a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 +++ b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 @@ -3,13 +3,14 @@ . $PSScriptRoot\..\ExchangeServerInfo\Get-DAGInformation.ps1 . $PSScriptRoot\..\ExchangeServerInfo\Get-ExchangeBasicServerObject.ps1 -. $PSScriptRoot\..\Helpers\Add-ScriptBlockInjection.ps1 + . $PSScriptRoot\..\Helpers\PipelineFunctions.ps1 . $PSScriptRoot\..\Helpers\Start-JobManager.ps1 . $PSScriptRoot\..\RemoteScriptBlock\Get-ExchangeInstallDirectory.ps1 . $PSScriptRoot\..\RemoteScriptBlock\IO\Compress-Folder.ps1 . $PSScriptRoot\..\RemoteScriptBlock\IO\Save-DataToFile.ps1 . $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Add-ScriptBlockInjection.ps1 #This function job is to write out the Data that is too large to pass into the main script block #This is for mostly Exchange Related objects. #To handle this, we export the data locally and copy the data over the correct server. diff --git a/Diagnostics/ExchangeLogCollector/Helpers/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 similarity index 99% rename from Diagnostics/ExchangeLogCollector/Helpers/Add-ScriptBlockInjection.ps1 rename to Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index 14924ae5ff..3c51219eb9 100644 --- a/Diagnostics/ExchangeLogCollector/Helpers/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -. $PSScriptRoot\..\..\..\Shared\Invoke-CatchActionError.ps1 +. $PSScriptRoot\..\Invoke-CatchActionError.ps1 # Injects Verbose and Debug Preferences and other passed variables into the script block # It will also inject any additional script blocks into the main script block. From 77e905dc80bf1d45656d22307ac57c9ec696b529 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 11 Dec 2024 08:31:48 -0600 Subject: [PATCH 02/14] Add comment based help --- .../ScriptBlock/Add-ScriptBlockInjection.ps1 | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index 3c51219eb9..d3696d1462 100644 --- a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -3,9 +3,37 @@ . $PSScriptRoot\..\Invoke-CatchActionError.ps1 -# Injects Verbose and Debug Preferences and other passed variables into the script block -# It will also inject any additional script blocks into the main script block. -# This allows for an Invoke-Command to work as intended if multiple functions/script blocks are required. +<# +.SYNOPSIS + Takes a script block and injects additional functions that you might want to have included in remote or job script block. + This prevents duplicate code from being written to bloat the script size. +.DESCRIPTION + By default, it will inject the Verbose and Debug Preferences and other passed variables into the script block with "using" in the correct usage. + Within this project, we accounted for Invoke-Command to fail due to WMI issues, therefore we would fallback and execute the script block locally, + if that the server we wanted to run against. Therefore, if you are use '$Using:VerbosePreference' it would cause a failure. + So we account for that here as well. +.PARAMETER PrimaryScriptBlock + This is the main script block that we will be injecting everything inside of. + This is the one that you will be passing your arguments to if there are any and will be executing. +.PARAMETER IncludeUsingParameter + TODO change this parameter name to IncludeUsingVariable + Add any additional variables that we wish to provide to the script block with the "$using:" status. + These are for things that are not included in the passed arguments and are likely script scoped variables in functions that are being injected. +.PARAMETER IncludeScriptBlock + Additional script blocks that need to be included. The most common ones are going to be like Write-Verbose and Write-Host. + This then allows the remote script block to manipulate the data that is in Write-Verbose and be returned to the pipeline so it can be logged to the main caller. +.PARAMETER CatchActionFunction + The script block to be executed if we have an exception while trying to create the injected script block. +.NOTES + Supported Script Block Creations are: + [ScriptBlock]::Create(string) and ${Function:Write-Verbose} + Supported ways to write the function of the script block are defined in the Pester testing file. + Supported ways of using the return script block: + Invoke-Command + Invoke-Command -AsJob + Start-Job + & $scriptBlock @params +#> function Add-ScriptBlockInjection { [CmdletBinding()] [OutputType([string])] From d142584436f173d7da55c5bc878625154ff02504 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 11 Dec 2024 14:33:41 -0600 Subject: [PATCH 03/14] Return Script Block vs String --- Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index d3696d1462..73736079d0 100644 --- a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -36,7 +36,6 @@ #> function Add-ScriptBlockInjection { [CmdletBinding()] - [OutputType([string])] param( [Parameter(Mandatory = $true)] [ScriptBlock]$PrimaryScriptBlock, @@ -163,8 +162,7 @@ function Add-ScriptBlockInjection { $scriptBlockFinalized += $_.ToString() + [System.Environment]::NewLine } - #Need to return a string type otherwise run into issues. - return $scriptBlockFinalized + return ([ScriptBlock]::Create($scriptBlockFinalized)) } catch { Write-Verbose "Failed to add to the script block" Invoke-CatchActionError $CatchActionFunction From ee45b8de313adddfdcd3dc57468c195c4b6b16a3 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 12 Dec 2024 15:50:23 -0600 Subject: [PATCH 04/14] Update Elc for Add-ScriptBlockInjection returning ScriptBlock type --- .../Helpers/Test-DiskSpace.ps1 | 14 ++++++++------ .../Write/Write-LargeDataObjectsOnMachine.ps1 | 10 ++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 index 9ece2d3afe..f4c30d0fd7 100644 --- a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 +++ b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 @@ -58,12 +58,14 @@ function Test-DiskSpace { Write-Verbose("Getting Get-FreeSpace string to create Script Block") SetWriteRemoteVerboseAction "New-VerbosePipelineObject" - $getFreeSpaceString = Add-ScriptBlockInjection -PrimaryScriptBlock ${Function:Get-FreeSpace} ` - -IncludeScriptBlock @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) ` - -IncludeUsingParameter "WriteRemoteVerboseDebugAction" ` - -CatchActionFunction ${Function:Invoke-CatchActions} - Write-Verbose("Creating Script Block") - $getFreeSpaceScriptBlock = [ScriptBlock]::Create($getFreeSpaceString) + $scriptBlockInjectParams = @{ + PrimaryScriptBlock = ${Function:Get-FreeSpace} + IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) + IncludeUsingParameter = "WriteRemoteVerboseDebugAction" + CatchActionFunction = ${Function:Invoke-CatchActions} + } + $getFreeSpaceScriptBlock = Add-ScriptBlockInjection @scriptBlockInjectParams + Write-Verbose("Successfully Created Script Block") $serversData = Start-JobManager -ServersWithArguments $serverArgs -ScriptBlock $getFreeSpaceScriptBlock ` -NeedReturnData $true ` -JobBatchName "Getting the free space for test disk space" ` diff --git a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 index 99722452ea..0185ef01b4 100644 --- a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 +++ b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 @@ -428,15 +428,13 @@ function Write-LargeDataObjectsOnMachine { $scriptBlockInjectParams = @{ IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) IncludeUsingParameter = "WriteRemoteVerboseDebugAction" + PrimaryScriptBlock = ${Function:Get-ExchangeInstallDirectory} + CatchActionFunction = ${Function:Invoke-CatchActions} } #Setup all the Script blocks that we are going to use. Write-Verbose("Getting Get-ExchangeInstallDirectory string to create Script Block") - $getExchangeInstallDirectoryString = Add-ScriptBlockInjection @scriptBlockInjectParams ` - -PrimaryScriptBlock ${Function:Get-ExchangeInstallDirectory} ` - -CatchActionFunction ${Function:Invoke-CatchActions} - Write-Verbose("Creating Script Block") - $getExchangeInstallDirectoryScriptBlock = [ScriptBlock]::Create($getExchangeInstallDirectoryString) - + $getExchangeInstallDirectoryScriptBlock = Add-ScriptBlockInjection @scriptBlockInjectParams + Write-Verbose("Successfully Created Script Block") Write-Verbose("New-Item create Script Block") $newFolderScriptBlock = { param($path) New-Item -ItemType Directory -Path $path -Force | Out-Null } From 7e22305ceda33668e14c6a97f2bae6ac645aa6be Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 18 Dec 2024 08:00:30 -0600 Subject: [PATCH 05/14] Add-ScriptBlockInjection to wrap command into function name This is required in order to get Invoke-Command to work --- .../ScriptBlock/Add-ScriptBlockInjection.ps1 | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index 73736079d0..0faa6cf26d 100644 --- a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -57,7 +57,6 @@ function Add-ScriptBlockInjection { $scriptBlockFinalized = [string]::Empty $adjustedScriptBlock = $PrimaryScriptBlock $injectedLinesHandledInBeginBlock = $false - $adjustInject = $false if ($null -ne $IncludeUsingParameter) { $lines = @() @@ -91,17 +90,20 @@ function Add-ScriptBlockInjection { # Here you need to find the ParamBlock and add it to the inject lines to be at the top of the script block. # Then you need to recreate the adjustedScriptBlock to be where the ParamBlock ended. - if ($null -ne $PrimaryScriptBlock.Ast.ParamBlock) { - Write-Verbose "Ast ParamBlock detected" - $adjustLocation = $PrimaryScriptBlock.Ast - } elseif ($null -ne $PrimaryScriptBlock.Ast.Body.ParamBlock) { - Write-Verbose "Ast Body ParamBlock detected" - $adjustLocation = $PrimaryScriptBlock.Ast.Body - } + # adjust the location of the adjustedScriptBlock if required here. + if ($null -ne $PrimaryScriptBlock.Ast.ParamBlock -or + $null -ne $PrimaryScriptBlock.Ast.Body.ParamBlock) { - $adjustInject = $null -ne $PrimaryScriptBlock.Ast.ParamBlock -or $null -ne $PrimaryScriptBlock.Ast.Body.ParamBlock + if ($null -ne $PrimaryScriptBlock.Ast.ParamBlock) { + Write-Verbose "Ast ParamBlock detected" + $adjustLocation = $PrimaryScriptBlock.Ast + } elseif ($null -ne $PrimaryScriptBlock.Ast.Body.ParamBlock) { + Write-Verbose "Ast Body ParamBlock detected" + $adjustLocation = $PrimaryScriptBlock.Ast.Body + } else { + throw "Unknown adjustLocation" + } - if ($adjustInject) { $scriptBlockInjectLines += $adjustLocation.ParamBlock.ToString() $startIndex = $adjustLocation.ParamBlock.Extent.EndOffSet - $adjustLocation.Extent.StartOffset $adjustedScriptBlock = [ScriptBlock]::Create($PrimaryScriptBlock.ToString().Substring($startIndex)) @@ -162,6 +164,13 @@ function Add-ScriptBlockInjection { $scriptBlockFinalized += $_.ToString() + [System.Environment]::NewLine } + # In order to fully use Invoke-Command, we need to wrap everything in it's own function name again. + if (-not [string]::IsNullOrEmpty($PrimaryScriptBlock.Ast.Name)) { + Write-Verbose "Wrapping into function name" + $scriptBlockFinalized = "function $($PrimaryScriptBlock.Ast.Name) { $([System.Environment]::NewLine)" + + "$scriptBlockFinalized $([System.Environment]::NewLine) } $([System.Environment]::NewLine) $($PrimaryScriptBlock.Ast.Name) @args" + } + return ([ScriptBlock]::Create($scriptBlockFinalized)) } catch { Write-Verbose "Failed to add to the script block" From 89c78c891324e78828797e86c2941ef610d30ff5 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 18 Dec 2024 15:51:57 -0600 Subject: [PATCH 06/14] for using variable, need to be using Script scope when setting --- Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index 0faa6cf26d..7514b684a9 100644 --- a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -62,7 +62,7 @@ function Add-ScriptBlockInjection { $lines = @() $lines += 'if ($PSSenderInfo) {' $IncludeUsingParameter | ForEach-Object { - $lines += '$name=$Using:name'.Replace("name", "$_") + $lines += '$Script:name=$Using:name'.Replace("name", "$_") } $lines += "}" + [System.Environment]::NewLine $usingLines = $lines -join [System.Environment]::NewLine From 30c3a93a1897440dce26511109a70ea739587265 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 11 Dec 2024 08:52:06 -0600 Subject: [PATCH 07/14] Creating Pester tests for Add-ScriptBlockInjection --- .../Tests/Add-ScriptBlockInjection.Tests.ps1 | 501 ++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 diff --git a/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 new file mode 100644 index 0000000000..72d915e65a --- /dev/null +++ b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 @@ -0,0 +1,501 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Pester testing file')] +[CmdletBinding()] +param() +BeforeAll { + $Script:parentPath = (Split-Path -Parent $PSScriptRoot) + . $Script:parentPath\Add-ScriptBlockInjection.ps1 +} + +# Only testing with Start-Job and operator, due to the fact that Invoke-Command should be doing a similar operation as operator. +# Invoke-Command is going to be using WMI, which shouldn't be an issue. +# We can't use Invoke-Command as a Job or against the machine within Pester because it requires to be run as Admin, which can't be done in the pipeline. +Describe "Generic Testing" { + BeforeAll { + $sb = [ScriptBlock]::Create("Get-Process") + $Script:results = Add-ScriptBlockInjection -PrimaryScriptBlock $sb + } + + It "Making sure it is a ScriptBlock type" { + $Script:results.GetType().Name | Should -Be "ScriptBlock" + } + + It "Can be converted to script block" { + $Script:results = [ScriptBlock]::Create($Script:results) + $Script:results.GetType().Name | Should -Be "ScriptBlock" + } +} + +Describe "Supported Primary Script Block Types" { + BeforeAll { + <# + Primary Script Block Types that are supported. + #> + function GenFunction { + param($a, $b) + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + + # This is currently not supported. + function GenFunction2 ($a, $b) { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + + function GenFunction3 { + [CmdletBinding()] + param( + [string]$a, + [string]$b + ) + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + + function GenFunction4 { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = "Hello" + B = "World" + } + } + + # This is currently not supported. + # Don't use process without using params + function ProcessFunction { + process { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = "Hello" + B = "World" + } + } + } + function ProcessFunctionParamEmpty { + param() + process { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = "Hello" + B = "World" + } + } + } + + function ProcessFunction1 { + param( + [string]$a, + [string]$b + ) + process { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + } + + function ProcessFunction2 { + param( + [string]$a, + [string]$b + ) + begin { + Write-Verbose "Test Begin" + } + process { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + } + + function ProcessFunction3 { + param( + [string]$a, + [string]$b + ) + begin { + Write-Verbose "Test Begin" + } + process { + Write-Verbose "Testing" + [PSCustomObject]@{ + A = $a + B = $b + } + } + end { + Write-Verbose "Test End" + } + } + + function ProcessFunction4 { + param( + [string]$a, + [string]$b + ) + process { + Write-Verbose "Testing" + } + end { + [PSCustomObject]@{ + A = $a + B = $b + } + Write-Verbose "Test End" + } + } + + $stringScriptBlock = "param(`$a, `$b) + Write-Verbose `"Testing`" + [PSCustomObject]@{ + A = `$a + B = `$b + } + " + $Script:scriptBlock = [ScriptBlock]::Create($stringScriptBlock) + + <# + End of Primary Script Block Types that are supported. + #> + + function InvokePesterGeneralTests { + param( + [ScriptBlock]$GenScriptBlock, + [object[]]$ArgumentList, + [string[]]$VerboseMockMatches + ) + Mock Write-Verbose { param ($Message ) } + $sb = Add-ScriptBlockInjection -PrimaryScriptBlock $GenScriptBlock + + $arguments = @{ + ScriptBlock = $sb + } + + if ($null -ne $ArgumentList) { + $arguments["ArgumentList"] = $ArgumentList + } + + # Start Job + $job = Start-Job @arguments + $r = Receive-Job $job -Wait -AutoRemoveJob + $r.A | Should -Be $ArgumentList[0] + $r.B | Should -Be $ArgumentList[1] + + # operator + try { + if ($PSSenderInfo) { + $PSSenderInfoOriginal = $PSSenderInfo + $PSSenderInfo = $null + $restPSSenderInfo = $true + } + + if ( $null -eq $ArgumentList) { + $r = & $sb + $r.A | Should -Be "Hello" + $r.B | Should -Be "World" + } else { + $r = & $sb @ArgumentList + $r.A | Should -Be $ArgumentList[0] + $r.B | Should -Be $ArgumentList[1] + } + + foreach ($match in $VerboseMockMatches) { + Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $match } -Exactly 1 + } + } finally { + if ($restPSSenderInfo) { + $PSSenderInfo = $PSSenderInfoOriginal + } + } + } + } + + It "GenFunction Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:GenFunction} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } + + It "GenFunction2 Testing" { + #TODO this should work, but not going to try to fix it now. + # need to fix Add-ScriptBlockInjection to address this + # InvokePesterGeneralTests -GenScriptBlock ${Function:GenFunction2} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + $true | Should -Be $true + } + + It "GenFunction3 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:GenFunction3} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } + + It "GenFunction4 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:GenFunction4} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } + + It "ProcessFunction Testing" { + #TODO adjust pester testing maybe here to account for the unsupported scenario. + $true | Should -Be $true + } + + It "ProcessFunction1 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:ProcessFunctionParamEmpty} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } + + It "ProcessFunction1 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:ProcessFunction1} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } + + It "ProcessFunction2 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:ProcessFunction2} -ArgumentList "Hello", "World" -VerboseMockMatches "Test Begin", "Testing" + } + + It "ProcessFunction3 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:ProcessFunction3} -ArgumentList "Hello", "World" -VerboseMockMatches "Test Begin", "Testing", "Test End" + } + + It "ProcessFunction4 Testing" { + InvokePesterGeneralTests -GenScriptBlock ${Function:ProcessFunction4} -ArgumentList "Hello", "World" -VerboseMockMatches "Testing", "Test End" + } + + It "Script Block Testing" { + InvokePesterGeneralTests -GenScriptBlock $Script:scriptBlock -ArgumentList "Hello", "World" -VerboseMockMatches "Testing" + } +} + +Describe "Supported Additional Parameters" { + BeforeAll { + + <# + Primary Script Block Types that are supported. + #> + function GenFunction { + param($a, $b) + Write-Verbose "Test in GenFunction" + Write-Verbose "Using $myUsing" + InjectProcessFunction $a $b + } + + function GenFunction3 { + [CmdletBinding()] + param( + [string]$a, + [string]$b + ) + Write-Verbose "Test in GenFunction3" + Write-Verbose "Using $myUsing" + $params = @{ + A = $a + B = $b + } + InjectProcessFunction @params + } + + function GenFunction4 { + Write-Verbose "Test in GenFunction4" + Write-Verbose "Using $myUsing" + InjectProcessFunction "Hello" "Bill" + } + + function ProcessFunction1 { + param( + [string]$First, + [string]$Second + ) + process { + Write-Verbose "Test in process of ProcessFunction1" + Write-Verbose "Using $myUsing" + InjectProcessFunction $First $Second + } + } + + function ProcessFunction2 { + param( + [string]$a, + [string]$b + ) + begin { + Write-Verbose "Test in begin of ProcessFunction2" + $params = @{ + A = $a + B = $b + } + Write-Verbose "Using $myUsing" + } + process { + Write-Verbose "Test in process of ProcessFunction2" + InjectProcessFunction @params + } + } + + function ProcessFunction3 { + param( + [string]$First, + [string]$Second + ) + begin { + Write-Verbose "Test in begin of ProcessFunction3" + Write-Verbose "Using $myUsing" + $First = "$First $man1" + } + process { + Write-Verbose "Test in process of ProcessFunction3" + $Second = "$Second $man2" + InjectProcessFunction $First $Second + } + end { + Write-Verbose "Test in end of ProcessFunction3" + } + } + + function ProcessFunction4 { + param( + [string]$a, + [string]$b + ) + process { + Write-Verbose "Test in process of ProcessFunction4" + Write-Verbose "Using $myUsing" + } + end { + InjectProcessFunction $a $b + Write-Verbose "Test in end of ProcessFunction4" + } + } + + <# + End of Primary Script Block Types that are supported. + #> + + function InjectProcessFunction { + param( + [string]$a, + [string]$b + ) + process { + Write-Verbose "Testing in InjectProcessFunction" + [PSCustomObject]@{ + A = $a + B = $b + } + } + } + + function InvokePesterGeneralTests2 { + param( + [ScriptBlock]$PScriptBlock, + [string[]]$UsingVariables, + [object[]]$ArgumentList, + [string[]]$VerboseMockMatches + ) + $Script:VerboseCounter = 0 + Mock Write-Verbose { param ($Message) $Script:VerboseCounter++ } + $sb = Add-ScriptBlockInjection -PrimaryScriptBlock $PScriptBlock -IncludeUsingParameter $UsingVariables -IncludeScriptBlock ${Function:InjectProcessFunction} + $verboseFromScriptBlock = $Script:VerboseCounter + + $arguments = @{ + ScriptBlock = $sb + } + + if ($null -ne $ArgumentList) { + $arguments["ArgumentList"] = $ArgumentList + } + + # Start Job + $job = Start-Job @arguments + $r = Receive-Job $job -Wait -AutoRemoveJob + if ($UsingVariables.Count -eq 1) { + $r.A | Should -Be $ArgumentList[0] + $r.B | Should -Be $ArgumentList[1] + } else { + $r.A | Should -Be "$($ArgumentList[0]) $((Get-Variable $UsingVariables[1]).Value)" + $r.B | Should -Be "$($ArgumentList[1]) $((Get-Variable $UsingVariables[2]).Value)" + } + + try { + if ($PSSenderInfo) { + $PSSenderInfoOriginal = $PSSenderInfo + $PSSenderInfo = $null + $restPSSenderInfo = $true + } + + # operator + if ( $null -eq $ArgumentList) { + $r = & $sb + $r.A | Should -Be "Hello" + $r.B | Should -Be "World" + } else { + $r = & $sb @ArgumentList + if ($UsingVariables.Count -eq 1) { + $r.A | Should -Be $ArgumentList[0] + $r.B | Should -Be $ArgumentList[1] + } else { + $r.A | Should -Be "$($ArgumentList[0]) $((Get-Variable $UsingVariables[1]).Value)" + $r.B | Should -Be "$($ArgumentList[1]) $((Get-Variable $UsingVariables[2]).Value)" + } + } + + foreach ($match in $VerboseMockMatches) { + Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $match } -Exactly 1 + } + + # Not sure why, but we need to add 1 here. + Assert-MockCalled Write-Verbose -Exactly ($verboseFromScriptBlock + $VerboseMockMatches.Count + 1) + } finally { + if ($restPSSenderInfo) { + $PSSenderInfo = $PSSenderInfoOriginal + } + } + } + } + + It "GenFunction Test Injection" { + $Script:myUsing = "Wild" + InvokePesterGeneralTests2 -PScriptBlock ${Function:GenFunction} -UsingVariables @("myUsing") -ArgumentList "Contoso", "Lab" -VerboseMockMatches "Test in GenFunction", "Using Wild" + } + + It "GenFunction3 Test Injection" { + $Script:myUsing = "Crazy" + InvokePesterGeneralTests2 -PScriptBlock ${Function:GenFunction3} -UsingVariables @("myUsing") -ArgumentList "Hi", "Lab" -VerboseMockMatches "Test in GenFunction3", "Using Crazy" + } + + It "GenFunction4 Test Injection" { + $Script:myUsing = "Crazy" + InvokePesterGeneralTests2 -PScriptBlock ${Function:GenFunction4} -UsingVariables @("myUsing") -ArgumentList "Hello", "Bill" -VerboseMockMatches "Test in GenFunction4", "Using Crazy" + } + + It "ProcessFunction1 Test Injection" { + $Script:myUsing = "Wild" + InvokePesterGeneralTests2 -PScriptBlock ${Function:ProcessFunction1} -UsingVariables @("myUsing") -ArgumentList "Hi", "Lab" -VerboseMockMatches "Test in process of ProcessFunction1", "Using Wild" + } + + It "ProcessFunction2 Test Injection" { + $Script:myUsing = "Wild" + InvokePesterGeneralTests2 -PScriptBlock ${Function:ProcessFunction2} -UsingVariables @("myUsing") -ArgumentList "Hi", "Lab" -VerboseMockMatches "Test in process of ProcessFunction2", "Using Wild", "Test in process of ProcessFunction2" + } + + It "ProcessFunction3 Test Injection" { + $Script:myUsing = "Wild" + $Script:man1 = "Crazy" + $Script:man2 = "Man" + InvokePesterGeneralTests2 -PScriptBlock ${Function:ProcessFunction3} -UsingVariables @("myUsing", "man1", "man2") -ArgumentList "Hi", "Lab" -VerboseMockMatches "Test in process of ProcessFunction3", "Using Wild", "Test in end of ProcessFunction3", "Test in begin of ProcessFunction3" + } + + It "ProcessFunction4 Test Injection" { + $Script:myUsing = "Tiny" + InvokePesterGeneralTests2 -PScriptBlock ${Function:ProcessFunction4} -UsingVariables @("myUsing") -ArgumentList "Hi", "Lab" -VerboseMockMatches "Test in process of ProcessFunction4", "Using Tiny", "Test in process of ProcessFunction4" + } +} From b83979c65683107a5f831b76aacf76b35409d77a Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 19 Dec 2024 12:43:51 -0600 Subject: [PATCH 08/14] Parameter change from IncludeUsingParameter to IncludeUsingVariableName --- .../ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 | 8 ++++---- .../Write/Write-LargeDataObjectsOnMachine.ps1 | 8 ++++---- Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 | 12 +++++------- .../Tests/Add-ScriptBlockInjection.Tests.ps1 | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 index f4c30d0fd7..09434b1f85 100644 --- a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 +++ b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 @@ -59,10 +59,10 @@ function Test-DiskSpace { Write-Verbose("Getting Get-FreeSpace string to create Script Block") SetWriteRemoteVerboseAction "New-VerbosePipelineObject" $scriptBlockInjectParams = @{ - PrimaryScriptBlock = ${Function:Get-FreeSpace} - IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) - IncludeUsingParameter = "WriteRemoteVerboseDebugAction" - CatchActionFunction = ${Function:Invoke-CatchActions} + PrimaryScriptBlock = ${Function:Get-FreeSpace} + IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) + IncludeUsingVariableName = "WriteRemoteVerboseDebugAction" + CatchActionFunction = ${Function:Invoke-CatchActions} } $getFreeSpaceScriptBlock = Add-ScriptBlockInjection @scriptBlockInjectParams Write-Verbose("Successfully Created Script Block") diff --git a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 index 0185ef01b4..5748949a95 100644 --- a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 +++ b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 @@ -426,10 +426,10 @@ function Write-LargeDataObjectsOnMachine { # Set remote version action to be able to return objects on the pipeline to log and handle them. SetWriteRemoteVerboseAction "New-VerbosePipelineObject" $scriptBlockInjectParams = @{ - IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) - IncludeUsingParameter = "WriteRemoteVerboseDebugAction" - PrimaryScriptBlock = ${Function:Get-ExchangeInstallDirectory} - CatchActionFunction = ${Function:Invoke-CatchActions} + IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) + IncludeUsingVariableName = "WriteRemoteVerboseDebugAction" + PrimaryScriptBlock = ${Function:Get-ExchangeInstallDirectory} + CatchActionFunction = ${Function:Invoke-CatchActions} } #Setup all the Script blocks that we are going to use. Write-Verbose("Getting Get-ExchangeInstallDirectory string to create Script Block") diff --git a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 index 7514b684a9..c06bf36c6e 100644 --- a/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 +++ b/Shared/ScriptBlock/Add-ScriptBlockInjection.ps1 @@ -15,8 +15,7 @@ .PARAMETER PrimaryScriptBlock This is the main script block that we will be injecting everything inside of. This is the one that you will be passing your arguments to if there are any and will be executing. -.PARAMETER IncludeUsingParameter - TODO change this parameter name to IncludeUsingVariable +.PARAMETER IncludeUsingVariableName Add any additional variables that we wish to provide to the script block with the "$using:" status. These are for things that are not included in the passed arguments and are likely script scoped variables in functions that are being injected. .PARAMETER IncludeScriptBlock @@ -40,12 +39,11 @@ function Add-ScriptBlockInjection { [Parameter(Mandatory = $true)] [ScriptBlock]$PrimaryScriptBlock, - [string[]]$IncludeUsingParameter, + [string[]]$IncludeUsingVariableName, [ScriptBlock[]]$IncludeScriptBlock, - [ScriptBlock] - $CatchActionFunction + [ScriptBlock]$CatchActionFunction ) process { try { @@ -58,10 +56,10 @@ function Add-ScriptBlockInjection { $adjustedScriptBlock = $PrimaryScriptBlock $injectedLinesHandledInBeginBlock = $false - if ($null -ne $IncludeUsingParameter) { + if ($null -ne $IncludeUsingVariableName) { $lines = @() $lines += 'if ($PSSenderInfo) {' - $IncludeUsingParameter | ForEach-Object { + $IncludeUsingVariableName | ForEach-Object { $lines += '$Script:name=$Using:name'.Replace("name", "$_") } $lines += "}" + [System.Environment]::NewLine diff --git a/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 index 72d915e65a..298aed3488 100644 --- a/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 +++ b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 @@ -403,7 +403,7 @@ Describe "Supported Additional Parameters" { ) $Script:VerboseCounter = 0 Mock Write-Verbose { param ($Message) $Script:VerboseCounter++ } - $sb = Add-ScriptBlockInjection -PrimaryScriptBlock $PScriptBlock -IncludeUsingParameter $UsingVariables -IncludeScriptBlock ${Function:InjectProcessFunction} + $sb = Add-ScriptBlockInjection -PrimaryScriptBlock $PScriptBlock -IncludeUsingVariableName $UsingVariables -IncludeScriptBlock ${Function:InjectProcessFunction} $verboseFromScriptBlock = $Script:VerboseCounter $arguments = @{ From 036bf86f2b3b1d82c0733c65de4e82bb01ba5738 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 19 Dec 2024 13:41:14 -0600 Subject: [PATCH 09/14] Created Get-DefaultSBInjectionContext --- .../Get-DefaultSBInjectionContext.ps1 | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 diff --git a/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 b/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 new file mode 100644 index 0000000000..7c7f9679bc --- /dev/null +++ b/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 @@ -0,0 +1,105 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\OutputOverrides\Write-Verbose.ps1 +. $PSScriptRoot\..\OutputOverrides\Write-Progress.ps1 +. $PSScriptRoot\Add-ScriptBlockInjection.ps1 +. $PSScriptRoot\RemoteSBLoggingFunctions.ps1 + +<# +.SYNOPSIS + This function utilized Add-ScriptBlockInjection and will automatically include the required variables + and script blocks needed within the project to handle logging in a remote script block. +.DESCRIPTION + By default this will include, if they are overwritten, the script blocks for the functions: + Write-Verbose + Write-Progress + Write-Host + The reason why these are overwritten is to allow the code to be execute by itself and still work correctly. + So instead of creating a function called Write-VerboseAndLog to Write-Verbose to the screen and log out the information, you can just overwrite Write-Verbose to do this for you. + The problem comes in when you would like to debug a remote execution in a log to determine a problem. In your overwritten functions, you can account for this and have the caller handle this. +.PARAMETER PrimaryScriptBlock + This is the main script block that we will be injecting everything inside of. + This is the one that you will be passing your arguments to if there are any and will be executing. +.PARAMETER IncludeUsingVariableName + Add any additional variables that we wish to provide to the script block with the "$using:" status. + These are for things that are not included in the passed arguments and are likely script scoped variables in functions that are being injected. +.PARAMETER IncludeScriptBlock + Additional script blocks that need to be included. The most common ones are going to be like Write-Verbose and Write-Host. + This then allows the remote script block to manipulate the data that is in Write-Verbose and be returned to the pipeline so it can be logged to the main caller. +.PARAMETER CatchActionFunction + The script block to be executed if we have an exception while trying to create the injected script block. +#> +function Get-DefaultSBInjectionContext { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ScriptBlock]$PrimaryScriptBlock, + + [string[]]$IncludeUsingVariableName, + + [ScriptBlock[]]$IncludeScriptBlock, + + [ScriptBlock]$CatchActionFunction + ) + process { + + function Get-DefaultManipulatorVerboseMessage { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 1, ValueFromPipeline)] + [string]$Message + ) + process { + return "[[$([System.DateTime]::Now)] $env:COMPUTERNAME - Remote Logging] : $Message" + } + } + + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + $defaultScriptBlocks = @(${Function:Write-Verbose}, ${Function:Write-Host}, ${Function:Write-Progress}, + ${Function:New-RemoteLoggingPipelineObject}, ${Function:New-RemoteVerbosePipelineObject}, ${Function:Invoke-RemotePipelineHandler}, + ${Function:New-RemoteHostPipelineObject}, ${Function:New-RemoteProgressPipelineObject}) + $includeScriptBlockList = New-Object System.Collections.Generic.List[ScriptBlock] + $includeUsingVariableNameList = New-Object System.Collections.Generic.List[string] + + foreach ($sb in $defaultScriptBlocks) { + if ($null -eq $sb) { + continue + } + + # Functions should be here as we are loading them as a dependency. + if ($sb.Ast.Name -eq "Write-Verbose") { + Write-Verbose "Set and Add WriteRemoteVerboseDebugAction/WriteVerboseRemoteManipulateMessageAction to using list" + SetWriteRemoteVerboseAction "New-RemoteVerbosePipelineObject" + SetWriteVerboseRemoteManipulateMessageAction "Get-DefaultManipulatorVerboseMessage" + $includeUsingVariableNameList.Add("WriteVerboseRemoteManipulateMessageAction") + $includeUsingVariableNameList.Add("WriteRemoteVerboseDebugAction") + $includeScriptBlockList.Add(${Function:Get-DefaultManipulatorVerboseMessage}) + } + + if ($sb.Ast.Name -eq "Write-Progress") { + Write-Verbose "Set and Add SetWriteRemoteProgressAction to using list" + SetWriteRemoteProgressAction "New-RemoteProgressPipelineObject" + $includeUsingVariableNameList.Add("WriteRemoteProgressDebugAction") + } + $includeScriptBlockList.Add($sb) + } + + foreach ($sb in $IncludeScriptBlock) { + $includeScriptBlockList.Add($sb) + } + + foreach ($var in $IncludeUsingVariableName) { + $includeUsingVariableNameList.Add($var) + } + + $params = @{ + PrimaryScriptBlock = $PrimaryScriptBlock + IncludeUsingVariableName = $includeUsingVariableNameList + IncludeScriptBlock = $includeScriptBlockList + CatchActionFunction = $CatchActionFunction + } + return (Add-ScriptBlockInjection @params) + } +} From bed490c6a44a8721b2cd2dcea4918e3328e6a698 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 19 Dec 2024 14:05:34 -0600 Subject: [PATCH 10/14] Created RemoteSBLoggingFunctions --- .../ScriptBlock/RemoteSBLoggingFunctions.ps1 | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 Shared/ScriptBlock/RemoteSBLoggingFunctions.ps1 diff --git a/Shared/ScriptBlock/RemoteSBLoggingFunctions.ps1 b/Shared/ScriptBlock/RemoteSBLoggingFunctions.ps1 new file mode 100644 index 0000000000..1ddacbfb49 --- /dev/null +++ b/Shared/ScriptBlock/RemoteSBLoggingFunctions.ps1 @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# + Collection of functions that handles how to properly add + Write-Verbose, Write-Host, Write-Progress to the pipeline as an object for logging, and how to pull them off it. +#> +function New-RemoteLoggingPipelineObject { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state change.')] + [CmdletBinding()] + param( + [object]$Object, + [ValidateSet("Verbose", "Host", "Progress")] + [string]$Type + ) + process { + [PSCustomObject]@{ + RemoteLoggingValue = $Object + RemoteLoggingType = $Type + } + } +} + +<# + After calling the remote script block, you need to the log the information locally. + This loops through all the logging objects that was returned, then log everything with Write-Verbose. + Then proceeds to place the other returned objects back onto the pipeline to be handled. +#> +function Invoke-RemotePipelineLoggingLocal { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object[]]$Object + ) + process { + # Store everything into a list so we can just write it to the log once and makes it easier for log review. + $logToVerbose = New-Object System.Collections.Generic.List[string] + $logToVerbose.Add("") + $logToVerbose.Add("") + $logToVerbose.Add("------------------- Remote Pipeline Logging -------------------------") + foreach ($instance in $Object) { + $type = $instance.RemoteLoggingType + + if ($type -match "Verbose|Host|Progress") { + # Follow the process for logging locally with Write-Verbose for everything. + # These values should have been manipulated already. + $logToVerbose.Add(($instance.RemoteLoggingValue)) + } else { + # Place the other object back onto the pipeline to be handled. + $instance + } + } + $logToVerbose.Add("----------------- End Remote Pipeline Logging -----------------------") + $logToVerbose | Out-String | Write-Verbose + } +} + +<# + This function is used for when you are in a remote context to still be able to have + debug logging within a secondary function that you just called and returning a object from that function. + This then prevents all the objects from New-RemoteLoggingPipelineObject to also be stored in your variable. +#> +function Invoke-RemotePipelineHandler { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $true)] + [object[]]$Object, + + [Parameter(Mandatory = $true)] + [ref]$Result + ) + process { + $nonLoggingInfo = New-Object System.Collections.Generic.List[object] + foreach ($instance in $Object) { + $type = $instance.RemoteLoggingType + + if ($type -match "Verbose|Progress|Host") { + #place it back onto the pipeline + $instance + } else { + $nonLoggingInfo.Add($instance) + } + } + $Result.Value = $nonLoggingInfo + } +} + +function New-RemoteVerbosePipelineObject { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state change.')] + [CmdletBinding()] + param( + [Parameter(Position = 1)] + [string]$Message + ) + process { + New-RemoteLoggingPipelineObject $Message "Verbose" + } +} + +function New-RemoteHostPipelineObject { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state change.')] + [CmdletBinding()] + param( + [Parameter(Position = 1)] + [object]$Object + ) + process { + New-RemoteLoggingPipelineObject $Object "Host" + } +} + +function New-RemoteProgressPipelineObject { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state change.')] + [CmdletBinding()] + param( + [Parameter(Position = 1)] + [object]$Object + ) + process { + New-RemoteLoggingPipelineObject $Object "Progress" + } +} From 628bdc503e4c02b1f70c4824f2f7810b0058476d Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 23 Jan 2025 14:39:11 -0600 Subject: [PATCH 11/14] Write-Verbose to have remote manipulate option --- Shared/OutputOverrides/Write-Verbose.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Shared/OutputOverrides/Write-Verbose.ps1 b/Shared/OutputOverrides/Write-Verbose.ps1 index e023a70827..606e28237d 100644 --- a/Shared/OutputOverrides/Write-Verbose.ps1 +++ b/Shared/OutputOverrides/Write-Verbose.ps1 @@ -15,6 +15,11 @@ function Write-Verbose { $Message = & $Script:WriteVerboseManipulateMessageAction $Message } + if ($PSSenderInfo -and + $null -ne $Script:WriteVerboseRemoteManipulateMessageAction) { + $Message = & $Script:WriteVerboseRemoteManipulateMessageAction $Message + } + Microsoft.PowerShell.Utility\Write-Verbose $Message if ($null -ne $Script:WriteVerboseDebugAction) { @@ -40,3 +45,7 @@ function SetWriteRemoteVerboseAction ($DebugAction) { function SetWriteVerboseManipulateMessageAction ($DebugAction) { $Script:WriteVerboseManipulateMessageAction = $DebugAction } + +function SetWriteVerboseRemoteManipulateMessageAction ($DebugAction) { + $Script:WriteVerboseRemoteManipulateMessageAction = $DebugAction +} From 5e4add352d92ea4f3c6dabe21a3d4ad6c6a99e3c Mon Sep 17 00:00:00 2001 From: David Paulson Date: Fri, 17 Jan 2025 17:35:24 -0600 Subject: [PATCH 12/14] Exchange Log Collector to use new Remote Logging --- .../Helpers/PipelineFunctions.ps1 | 47 ------------------- .../Helpers/Test-DiskSpace.ps1 | 25 +++++----- .../Write/Write-LargeDataObjectsOnMachine.ps1 | 30 +++++------- 3 files changed, 23 insertions(+), 79 deletions(-) delete mode 100644 Diagnostics/ExchangeLogCollector/Helpers/PipelineFunctions.ps1 diff --git a/Diagnostics/ExchangeLogCollector/Helpers/PipelineFunctions.ps1 b/Diagnostics/ExchangeLogCollector/Helpers/PipelineFunctions.ps1 deleted file mode 100644 index 6cce729ad7..0000000000 --- a/Diagnostics/ExchangeLogCollector/Helpers/PipelineFunctions.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -function New-PipelineObject { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Caller knows that this is an action')] - [CmdletBinding()] - param( - [object]$Object, - [string]$Type - ) - process { - return [PSCustomObject]@{ - Object = $Object - Type = $Type - } - } -} - -function Invoke-PipelineHandler { - [CmdletBinding()] - param( - [object[]]$Object - ) - process { - foreach ($instance in $Object) { - if ($instance.Type -eq "Verbose") { - Write-Verbose "$($instance.PSComputerName) - $($instance.Object)" - } elseif ($instance.Type -eq "Host") { - Write-Host $instance.Object - } else { - return $instance - } - } - } -} - -function New-VerbosePipelineObject { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Caller knows that this is an action')] - [CmdletBinding()] - param( - [Parameter(Position = 1)] - [string]$Message - ) - process { - New-PipelineObject $Message "Verbose" - } -} diff --git a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 index 09434b1f85..37ece1865f 100644 --- a/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 +++ b/Diagnostics/ExchangeLogCollector/Helpers/Test-DiskSpace.ps1 @@ -2,11 +2,11 @@ # Licensed under the MIT License. . $PSScriptRoot\Enter-YesNoLoopAction.ps1 -. $PSScriptRoot\PipelineFunctions.ps1 . $PSScriptRoot\Start-JobManager.ps1 . $PSScriptRoot\..\RemoteScriptBlock\Get-FreeSpace.ps1 . $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 -. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Add-ScriptBlockInjection.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Get-DefaultSBInjectionContext.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\RemoteSBLoggingFunctions.ps1 function Test-DiskSpace { param( [Parameter(Mandatory = $true)][array]$Servers, @@ -57,19 +57,16 @@ function Test-DiskSpace { } Write-Verbose("Getting Get-FreeSpace string to create Script Block") - SetWriteRemoteVerboseAction "New-VerbosePipelineObject" - $scriptBlockInjectParams = @{ - PrimaryScriptBlock = ${Function:Get-FreeSpace} - IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) - IncludeUsingVariableName = "WriteRemoteVerboseDebugAction" - CatchActionFunction = ${Function:Invoke-CatchActions} - } - $getFreeSpaceScriptBlock = Add-ScriptBlockInjection @scriptBlockInjectParams + $getFreeSpaceScriptBlock = Get-DefaultSBInjectionContext -PrimaryScriptBlock ${Function:Get-FreeSpace} Write-Verbose("Successfully Created Script Block") - $serversData = Start-JobManager -ServersWithArguments $serverArgs -ScriptBlock $getFreeSpaceScriptBlock ` - -NeedReturnData $true ` - -JobBatchName "Getting the free space for test disk space" ` - -RemotePipelineHandler ${Function:Invoke-PipelineHandler} + $params = @{ + ServersWithArguments = $serverArgs + ScriptBlock = $getFreeSpaceScriptBlock + NeedReturnData = $true + JobBatchName = "Getting the free space for test disk space" + RemotePipelineHandler = ${Function:Invoke-RemotePipelineLoggingLocal} + } + $serversData = Start-JobManager @params $passedServers = @() foreach ($server in $Servers) { diff --git a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 index 5748949a95..8754778928 100644 --- a/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 +++ b/Diagnostics/ExchangeLogCollector/Write/Write-LargeDataObjectsOnMachine.ps1 @@ -3,14 +3,13 @@ . $PSScriptRoot\..\ExchangeServerInfo\Get-DAGInformation.ps1 . $PSScriptRoot\..\ExchangeServerInfo\Get-ExchangeBasicServerObject.ps1 - -. $PSScriptRoot\..\Helpers\PipelineFunctions.ps1 . $PSScriptRoot\..\Helpers\Start-JobManager.ps1 . $PSScriptRoot\..\RemoteScriptBlock\Get-ExchangeInstallDirectory.ps1 . $PSScriptRoot\..\RemoteScriptBlock\IO\Compress-Folder.ps1 . $PSScriptRoot\..\RemoteScriptBlock\IO\Save-DataToFile.ps1 . $PSScriptRoot\..\..\..\Shared\ErrorMonitorFunctions.ps1 -. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Add-ScriptBlockInjection.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\Get-DefaultSBInjectionContext.ps1 +. $PSScriptRoot\..\..\..\Shared\ScriptBlock\RemoteSBLoggingFunctions.ps1 #This function job is to write out the Data that is too large to pass into the main script block #This is for mostly Exchange Related objects. #To handle this, we export the data locally and copy the data over the correct server. @@ -423,17 +422,9 @@ function Write-LargeDataObjectsOnMachine { Write out the Exchange Server Object Data and copy them over to the correct server #> - # Set remote version action to be able to return objects on the pipeline to log and handle them. - SetWriteRemoteVerboseAction "New-VerbosePipelineObject" - $scriptBlockInjectParams = @{ - IncludeScriptBlock = @(${Function:Write-Verbose}, ${Function:New-PipelineObject}, ${Function:New-VerbosePipelineObject}) - IncludeUsingVariableName = "WriteRemoteVerboseDebugAction" - PrimaryScriptBlock = ${Function:Get-ExchangeInstallDirectory} - CatchActionFunction = ${Function:Invoke-CatchActions} - } #Setup all the Script blocks that we are going to use. Write-Verbose("Getting Get-ExchangeInstallDirectory string to create Script Block") - $getExchangeInstallDirectoryScriptBlock = Add-ScriptBlockInjection @scriptBlockInjectParams + $getExchangeInstallDirectoryScriptBlock = Get-DefaultSBInjectionContext -PrimaryScriptBlock ${Function:Get-ExchangeInstallDirectory} Write-Verbose("Successfully Created Script Block") Write-Verbose("New-Item create Script Block") $newFolderScriptBlock = { param($path) New-Item -ItemType Directory -Path $path -Force | Out-Null } @@ -460,16 +451,19 @@ function Write-LargeDataObjectsOnMachine { } Write-Verbose ("Calling job for Get Exchange Install Directory") - $serverInstallDirectories = Start-JobManager -ServersWithArguments $serverArgListExchangeInstallDirectory ` - -ScriptBlock $getExchangeInstallDirectoryScriptBlock ` - -NeedReturnData $true ` - -JobBatchName "Exchange Install Directories for Write-LargeDataObjectsOnMachine" ` - -RemotePipelineHandler ${Function:Invoke-PipelineHandler} + $params = @{ + ServersWithArguments = $serverArgListExchangeInstallDirectory + ScriptBlock = $getExchangeInstallDirectoryScriptBlock + NeedReturnData = $true + JobBatchName = "Exchange Install Directories for Write-LargeDataObjectsOnMachine" + RemotePipelineHandler = ${Function:Invoke-RemotePipelineLoggingLocal} + } + $serverInstallDirectories = Start-JobManager @params Write-Verbose("Calling job for folder creation") Start-JobManager -ServersWithArguments $serverArgListDirectoriesToCreate -ScriptBlock $newFolderScriptBlock ` -JobBatchName "Creating folders for Write-LargeDataObjectsOnMachine" ` - -RemotePipelineHandler ${Function:Invoke-PipelineHandler} + -RemotePipelineHandler ${Function:Invoke-RemotePipelineLoggingLocal} #Now do the rest of the actions foreach ($serverData in $exchangeServerData) { From daa1c69870265d0f60413975e0111097dc751544 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 28 Jan 2025 10:12:41 -0600 Subject: [PATCH 13/14] Use correct wording of override vs overwritten --- Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 b/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 index 7c7f9679bc..b775e119d7 100644 --- a/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 +++ b/Shared/ScriptBlock/Get-DefaultSBInjectionContext.ps1 @@ -11,13 +11,13 @@ This function utilized Add-ScriptBlockInjection and will automatically include the required variables and script blocks needed within the project to handle logging in a remote script block. .DESCRIPTION - By default this will include, if they are overwritten, the script blocks for the functions: + By default this will include, if they are overridden, the script blocks for the functions: Write-Verbose Write-Progress Write-Host - The reason why these are overwritten is to allow the code to be execute by itself and still work correctly. - So instead of creating a function called Write-VerboseAndLog to Write-Verbose to the screen and log out the information, you can just overwrite Write-Verbose to do this for you. - The problem comes in when you would like to debug a remote execution in a log to determine a problem. In your overwritten functions, you can account for this and have the caller handle this. + The reason why these are overridden is to allow the code to be execute by itself and still work correctly. + So instead of creating a function called Write-VerboseAndLog to Write-Verbose to the screen and log out the information, you can just override Write-Verbose to do this for you. + The problem comes in when you would like to debug a remote execution in a log to determine a problem. In your overridden functions, you can account for this and have the caller handle this. .PARAMETER PrimaryScriptBlock This is the main script block that we will be injecting everything inside of. This is the one that you will be passing your arguments to if there are any and will be executing. From ea02f0f787aa33240bdd7b697ac79ca2659219ad Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 28 Jan 2025 10:13:10 -0600 Subject: [PATCH 14/14] Use -BeOfType option --- Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 index 298aed3488..0362e83f61 100644 --- a/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 +++ b/Shared/ScriptBlock/Tests/Add-ScriptBlockInjection.Tests.ps1 @@ -19,12 +19,12 @@ Describe "Generic Testing" { } It "Making sure it is a ScriptBlock type" { - $Script:results.GetType().Name | Should -Be "ScriptBlock" + $Script:results | Should -BeOfType "ScriptBlock" } It "Can be converted to script block" { $Script:results = [ScriptBlock]::Create($Script:results) - $Script:results.GetType().Name | Should -Be "ScriptBlock" + $Script:results | Should -BeOfType "ScriptBlock" } }