Files
Invoke-VirtIODrivers/Toolkit/Functions/Get-InstalledSoftware.ps1
gsadmin 8889f41c7c Refactor to script template and toolkit flow with PS7 compatibility
- Adopt the standardized Toolkit layout (Toolkit.ps1 + Functions/Modules/Libraries/Tools)
- Replace the legacy NLog-era Registry.dll with Eric Zimmerman Registry 2026.5.0 (netstandard2.0) for offline hive parsing without mounting; libraries are loaded into memory in dependency order (byte load on Windows PowerShell 5.1, dedicated AssemblyLoadContext stream load on PowerShell 7)
- Rewrite Invoke-RegistryHiveAction with a case-insensitive ValueTable contract for direct value name indexing
- Rewrite Invoke-FileDownloadWithProgress with automatic system proxy detection and progress reporting
- Convert Copy-ItemWithProgress, Get-InstalledSoftware, and the MSI helper functions to the toolkit logging and error handling patterns
- Add opt-in QEMU guest agent installation from the mounted ISO (full operating system only, version-aware, REBOOT=ReallySuppress)
- Remove the task sequence variable parameters from the script surface while retaining the toolkit functionality
- Update README with structure, usage, and OS deployment guidance (DeployR/MDT/SCCM two-pass flow, pwsh.exe for DeployR boot images)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:38:29 -04:00

599 lines
42 KiB
PowerShell

## Microsoft Function Naming Convention: http://msdn.microsoft.com/en-us/library/ms714428(v=vs.85).aspx
#region Function Get-InstalledSoftware
Function Get-InstalledSoftware
{
<#
.SYNOPSIS
Retrieves a list of software installed on the current device.
.DESCRIPTION
Supports filtering the resulting list of software by using regular expressions.
.PARAMETER FilterInclusionExpression
Includes software based on their display name.
.PARAMETER FilterInclusionExpression
Excludes software based on their display name.
.PARAMETER PropertyList
Retrieves the value of each specified value name for a piece software. This will make the returned objects uniform.
By default, the following properties are retrieved by default. Additional non specified properties are also added to the returned objects for easier locating of software outside of this function.
.PARAMETER AllProperties
Retrieves the value of each value name that is detected for a piece of software. This will make the returned objects non-uniform because of how software vendors have not standardized this process.
.PARAMETER AllUserProfiles
When this function is executed with the appropriate permissions, all user profile(s) can be searched for software by using their SIDs. The user profile(s) will be dynamically determined and associated with the pieces of software that are located.
.PARAMETER AdditionalRegistryHiveObjects
Allows for searching additional registry hives as necessary in order to extend the search. See examples below.
.PARAMETER ContinueOnError
Continues processing even if an error has occured.
.EXAMPLE
Get-InstalledSoftware -AllProperties
.EXAMPLE
$GetInstalledSoftwareParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetInstalledSoftwareParameters.FilterExpression = {($_.DisplayName -imatch '(^.*Google.*Chrome.*$)') -and ([Version]$_.DisplayVersion -ge [Version]'75.0.0.0')}
$GetInstalledSoftwareParameters.AllProperties = $False
$GetInstalledSoftwareParameters.AllUserProfiles = $True
$GetInstalledSoftwareParameters.ContinueOnError = $False
$GetInstalledSoftwareParameters.Verbose = $True
$GetInstalledSoftwareResult = Get-InstalledSoftware @GetInstalledSoftwareParameters
Write-Output -InputObject ($GetInstalledSoftwareResult)
.EXAMPLE
$GetInstalledSoftwareParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetInstalledSoftwareParameters.FilterExpression = {($_.DisplayName -imatch '(^.*$)') -and ($_.DisplayName -inotmatch '(^.{0,0}$)')}
$GetInstalledSoftwareParameters.PropertyList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$GetInstalledSoftwareParameters.PropertyList.Add('DisplayName')
$GetInstalledSoftwareParameters.PropertyList.Add('DisplayVersion')
$GetInstalledSoftwareParameters.PropertyList.Add('UninstallString')
$GetInstalledSoftwareParameters.PropertyList.Add('InstallLocation')
$GetInstalledSoftwareParameters.PropertyList.Add('Publisher')
$GetInstalledSoftwareParameters.PropertyList.Add('InstallDate')
$GetInstalledSoftwareParameters.ContinueOnError = $False
$GetInstalledSoftwareParameters.Verbose = $False
$GetInstalledSoftwareResult = Get-InstalledSoftware @GetInstalledSoftwareParameters
Write-Output -InputObject ($GetInstalledSoftwareResult)
.EXAMPLE
$GetInstalledSoftwareParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$GetInstalledSoftwareParameters.FilterExpression = {($_.DisplayName -imatch '(^.*$)') -and ($_.DisplayName -inotmatch '(^.{0,0}$)')}
$GetInstalledSoftwareParameters.AllProperties = $True
$GetInstalledSoftwareParameters.AdditionalRegistryHiveObjects = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
$RegistryHiveProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RegistryHiveProperties.Add('Type', [Microsoft.Win32.RegistryHive]::Users)
$RegistryHiveProperties.Add('KeyList', (New-Object -TypeName 'System.Collections.Generic.List[String]'))
$RegistryHiveProperties.KeyList.Add("SID\Software\Microsoft\Windows\CurrentVersion\Uninstall")
Switch ([System.Environment]::Is64BitOperatingSystem)
{
{($_ -eq $True)}
{
$RegistryHiveProperties.KeyList.Add("SID\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
}
$RegistryHiveObject = New-Object -TypeName 'PSObject' -Property ($RegistryHiveProperties)
$GetInstalledSoftwareParameters.AdditionalRegistryHiveObjects.Add($RegistryHiveObject)
$GetInstalledSoftwareParameters.ContinueOnError = $False
$GetInstalledSoftwareParameters.Verbose = $True
$GetInstalledSoftwareResult = Get-InstalledSoftware @GetInstalledSoftwareParameters
Write-Output -InputObject ($GetInstalledSoftwareResult)
#>
[CmdletBinding(DefaultParameterSetName = 'PropertyList')]
Param
(
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('IE')]
[ScriptBlock]$FilterExpression,
[Parameter(Mandatory=$False, ParameterSetName = 'PropertyList')]
[Alias('PL')]
[String[]]$PropertyList,
[Parameter(Mandatory=$False, ParameterSetName = 'AllProperties')]
[Alias('AP')]
[Switch]$AllProperties,
[Parameter(Mandatory=$False)]
[Alias('AUP')]
[Switch]$AllUserProfiles,
[Parameter(Mandatory=$False)]
[ValidateNotNullOrEmpty()]
[Alias('ARH')]
[PSObject[]]$AdditionalRegistryHiveObjects,
[Parameter(Mandatory=$False)]
[Alias('COE')]
[Switch]$ContinueOnError
)
Begin
{
Try
{
#Determine the date and time we executed the function
$FunctionStartTime = (Get-Date)
[String]$FunctionName = $MyInvocation.MyCommand
$WriteLogMessage.Invoke(0, @("Function `'$($FunctionName)`' is beginning. Please Wait..."))
#Define Default Action Preferences
$ErrorActionPreference = 'Stop'
[String[]]$SuppliedScriptParameters = $PSBoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key):$($_.Value.GetType().Name)"}
$WriteLogMessage.Invoke(0, @("Supplied Function Parameter(s) = $($SuppliedScriptParameters -Join ', ')"))
$WriteLogMessage.Invoke(0, @("Execution of $($FunctionName) began on $($FunctionStartTime.ToString($DateTimeLogFormat))"))
$RegularExpressionTable = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RegularExpressionTable.Base64 = '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$' -As [Regex]
$RegularExpressionTable.GUID = '(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}' -As [Regex]
$RegexOptionList = New-Object -TypeName 'System.Collections.Generic.List[System.Text.RegularExpressions.RegexOptions[]]'
$RegexOptionList.Add([System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$RegexOptionList.Add([System.Text.RegularExpressions.RegexOptions]::Multiline)
#region Set Default Parameter Values
Switch ($True)
{
{($Null -ieq $PropertyList) -or ($PropertyList.Count -eq 0)}
{
$OutputObjectValueList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$OutputObjectValueList.Add('DisplayName')
$OutputObjectValueList.Add('DisplayVersion')
$OutputObjectValueList.Add('UninstallString')
$OutputObjectValueList.Add('QuietUninstallString')
$OutputObjectValueList.Add('InstallLocation')
$OutputObjectValueList.Add('Publisher')
$OutputObjectValueList.Add('InstallDate')
[String[]]$PropertyList = $OutputObjectValueList.ToArray()
}
}
#endregion
#Create a table for the conversion of dates
$DateTimeProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$DateTimeProperties.FormatList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$DateTimeProperties.FormatList.AddRange(([System.Globalization.DateTimeFormatInfo]::CurrentInfo.GetAllDateTimePatterns()))
$DateTimeProperties.FormatList.AddRange(([System.Globalization.DateTimeFormatInfo]::InvariantInfo.GetAllDateTimePatterns()))
$DateTimeProperties.FormatList.Add('yyyyMM')
$DateTimeProperties.FormatList.Add('yyyyMMdd')
$DateTimeProperties.Culture = $Null
$DateTimeProperties.Styles = New-Object -TypeName 'System.Collections.Generic.List[System.Globalization.DateTimeStyles]'
$DateTimeProperties.Styles.Add([System.Globalization.DateTimeStyles]::AssumeLocal)
$DateTimeProperties.Styles.Add([System.Globalization.DateTimeStyles]::AllowWhiteSpaces)
#Create an object that will contain the functions output.
$OutputObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
}
Catch
{
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
}
Finally
{
}
}
Process
{
Try
{
$RegistryHiveList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
$RegistryHiveProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RegistryHiveProperties.Add('Type', [Microsoft.Win32.RegistryHive]::LocalMachine)
$RegistryHiveProperties.Add('KeyList', (New-Object -TypeName 'System.Collections.Generic.List[String]'))
$RegistryHiveProperties.KeyList.Add('Software\Microsoft\Windows\CurrentVersion\Uninstall')
Switch ([System.Environment]::Is64BitOperatingSystem)
{
{($_ -eq $True)}
{
$RegistryHiveProperties.KeyList.Add('Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
}
}
$RegistryHiveObject = New-Object -TypeName 'PSObject' -Property ($RegistryHiveProperties)
$RegistryHiveList.Add($RegistryHiveObject)
Switch ($AllUserProfiles.IsPresent)
{
{($_ -eq $True)}
{
$UserProfilePropertyList = New-Object -TypeName 'System.Collections.Generic.List[Object]'
$UserProfilePropertyList.Add('LocalPath')
$UserProfilePropertyList.Add('SID')
$UserProfilePropertyList.Add(@{Name = 'NTAccount'; Expression = {Try {(New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList @($_.SID)).Translate([System.Security.Principal.NTAccount]).Value} Catch {$Null}}})
$UserProfilePropertyList.Add('Special')
$UserProfilePropertyList.Add('Loaded')
$UserProfileList = Get-CIMInstance -Namespace 'root\CIMv2' -Class 'Win32_UserProfile' | Where-Object {($_.Special -eq $False)} | Select-Object -Property ($UserProfilePropertyList) | Where-Object {([String]::IsNullOrEmpty($_.NTAccount) -eq $False) -and ([String]::IsNullOrWhiteSpace($_.NTAccount) -eq $False)}
ForEach ($UserProfile In $UserProfileList)
{
$RegistryHiveProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$RegistryHiveProperties.Add('Type', [Microsoft.Win32.RegistryHive]::Users)
$RegistryHiveProperties.Add('NTAccount', $UserProfile.NTAccount)
$RegistryHiveProperties.Add('SID', $UserProfile.SID)
$RegistryHiveProperties.Add('KeyList', (New-Object -TypeName 'System.Collections.Generic.List[String]'))
$RegistryHiveProperties.KeyList.Add("$($RegistryHiveProperties.SID)\Software\Microsoft\Windows\CurrentVersion\Uninstall")
$WriteLogMessage.Invoke(1, @("Attempting to add user profile `"$($RegistryHiveProperties.NTAccount)`" [SID: $($RegistryHiveProperties.SID)] to the search for installed software. Please Wait..."))
Switch ([System.Environment]::Is64BitOperatingSystem)
{
{($_ -eq $True)}
{
$RegistryHiveProperties.KeyList.Add("$($RegistryHiveProperties.SID)\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
}
$RegistryHiveObject = New-Object -TypeName 'PSObject' -Property ($RegistryHiveProperties)
$RegistryHiveList.Add($RegistryHiveObject)
}
}
}
ForEach ($AdditionalRegistryHiveObject In $AdditionalRegistryHiveObjects) {$RegistryHiveList.Add($AdditionalRegistryHiveObject)}
For ($RegistryHiveListIndex = 0; $RegistryHiveListIndex -lt $RegistryHiveList.Count; $RegistryHiveListIndex++)
{
$RegistryHive = $RegistryHiveList[$RegistryHiveListIndex]
$RegistryHivePropertyList = $RegistryHive.PSObject.Properties
[Char[]]$HiveNameCharacterArray = ForEach ($Character in $RegistryHive.Type.ToString().ToCharArray())
{
If ([Char]::IsUpper($Character))
{
'_'
}
$Character
}
$HiveName = 'HKEY' + ($HiveNameCharacterArray -Join '').Trim().ToUpper()
$RegistryHiveObject = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive.Type, [Microsoft.Win32.RegistryView]::Default)
For ($KeyListIndex = 0; $KeyListIndex -lt $RegistryHive.KeyList.Count; $KeyListIndex++)
{
$RegistryKey = $RegistryHive.KeyList[$KeyListIndex]
$WriteLogMessage.Invoke(1, @("Attempting to search registry key path `"$($HiveName)\$($RegistryKey)`". Please Wait..."))
$RegistryKeyObject = $RegistryHiveObject.OpenSubKey($RegistryKey)
Switch ($Null -ine $RegistryKeyObject)
{
{($_ -eq $True)}
{
$SubKeyNameList = $RegistryKeyObject.GetSubKeyNames() | Sort-Object
For ($SubKeyNameListIndex = 0; $SubKeyNameListIndex -lt $SubKeyNameList.Count; $SubKeyNameListIndex++)
{
Try
{
$SubKeyName = $SubKeyNameList[$SubKeyNameListIndex]
$SubKeyObject = $RegistryKeyObject.OpenSubKey($SubKeyName)
$SubKeyObjectSegments = $SubKeyObject.Name.Split('\')
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$OutputObjectProperties.RegistryPath = $SubKeyObject.Name
$OutputObjectProperties.RegistryHive = $RegistryHive.Type.value__
$OutputObjectProperties.RegistryHiveName = $HiveName
$OutputObjectProperties.RegistryLocation = ($SubKeyObjectSegments[1..$($SubKeyObjectSegments.GetUpperBound(0))]) -Join '\'
ForEach ($OutputObjectValueName In $PropertyList)
{
$OutputObjectProperties.$($OutputObjectValueName) = $Null
}
Switch ($AllProperties.IsPresent)
{
{($_ -eq $True)}
{
$ValueNameList = $SubKeyObject.GetValueNames() | Sort-Object
}
Default
{
$ValueNameList = $PropertyList
}
}
$WriteLogMessage.Invoke(1, @("Now processing entry `"$($OutputObjectProperties.RegistryPath)`" [ValueCount: $($ValueNameList.Count)]. Please Wait..."))
Switch ($ValueNameList.Count -gt 0)
{
{($_ -eq $True)}
{
For ($ValueNameListIndex = 0; $ValueNameListIndex -lt $ValueNameList.Count; $ValueNameListIndex++)
{
Try
{
$ValueName = $ValueNameList[$ValueNameListIndex]
$ValueKind = $SubKeyObject.GetValueKind($ValueName)
Switch ($ValueKind)
{
{($_ -ieq 'PlaceHolder')}
{
}
Default
{
$Value = $SubKeyObject.GetValue($ValueName)
}
}
$OutputObjectProperties.$($ValueName) = $Value
}
Catch
{
$OutputObjectProperties.$($ValueName) = $Null
}
Finally
{
}
}
$ProductCodePropertyList = New-Object -TypeName 'System.Collections.Generic.List[String]'
$ProductCodePropertyList.Add('UninstallString')
$ProductCodePropertyList.Add('QuietUninstallString')
$ProductCodePropertyList.Add('ModifyPath')
$ProductCodePropertyList.Add('RegistryLocation')
$ProductCodePropertyList.Add('RegistryPath')
$OutputObjectProperties.ProductCode = $Null
:ProductCodePropertyLoop ForEach ($ProductCodeProperty In $ProductCodePropertyList)
{
$ProductCodePropertyValue = $OutputObjectProperties.$($ProductCodeProperty)
$ProductCodeMatch = [Regex]::Match($ProductCodePropertyValue, $RegularExpressionTable.GUID.ToString(), $RegexOptionList.ToArray())
Switch ($ProductCodeMatch.Success)
{
{($_ -eq $True)}
{
Switch (([String]::IsNullOrEmpty($OutputObjectProperties.ProductCode) -eq $True) -or ([String]::IsNullOrWhiteSpace($OutputObjectProperties.ProductCode) -eq $True))
{
{($_ -eq $True)}
{
$OutputObjectProperties.ProductCode = Try {"`{$($ProductCodeMatch.Value.Trim().ToUpper() -ireplace '(\{)|(\})', '')`}"} Catch {$Null}
Break ProductCodePropertyLoop
}
}
}
}
}
Switch ($Null -ine $FilterExpression)
{
{($_ -eq $True)}
{
$FilterExpressionResult = [Boolean]($OutputObjectProperties | Where-Object -FilterScript $FilterExpression)
}
Default
{
$FilterExpressionResult = $True
}
}
Switch ($FilterExpressionResult)
{
{($_ -eq $True)}
{
ForEach ($OutputObjectProperty In ($OutputObjectProperties.Keys | Sort-Object))
{
$OutputObjectPropertyName = $OutputObjectProperty
$OutputObjectPropertyValue = $OutputObjectProperties.$($OutputObjectPropertyName)
Switch ($OutputObjectPropertyName)
{
{($_ -iin @('DisplayVersion'))}
{
$ParsedVersion = New-Object -TypeName 'System.Version'
$ParseVersionResult = [Version]::TryParse($OutputObjectPropertyValue, [Ref]$ParsedVersion)
Switch ($ParseVersionResult)
{
{($_ -eq $True)}
{
$OutputObjectPropertyValue = $ParsedVersion
}
}
}
{($_ -iin @('InstallDate'))}
{
$DateTime = New-Object -TypeName 'DateTime'
$DateTimeProperties.Input = $OutputObjectPropertyValue
$DateTimeProperties.Successful = [DateTime]::TryParseExact($DateTimeProperties.Input, $DateTimeProperties.FormatList, $DateTimeProperties.Culture, $DateTimeProperties.Styles.ToArray(), [Ref]$DateTime)
$DateTimeProperties.DateTime = $DateTime
$DateTimeObject = New-Object -TypeName 'PSObject' -Property ($DateTimeProperties)
Switch ($DateTimeObject.Successful)
{
{($_ -eq $True)}
{
$OutputObjectPropertyValue = $DateTimeObject.DateTime
}
}
}
{($_ -iin @('UninstallString'))}
{
Switch (([String]::IsNullOrEmpty($OutputObjectPropertyValue) -eq $True) -or ([String]::IsNullOrWhiteSpace($OutputObjectPropertyValue) -eq $True))
{
{($_ -eq $True)}
{
Switch (([String]::IsNullOrEmpty($OutputObjectProperties.ProductCode) -eq $False) -and ([String]::IsNullOrWhiteSpace($OutputObjectProperties.ProductCode) -eq $False))
{
{($_ -eq $True)}
{
$OutputObjectPropertyValue = "msiexec.exe /x `"$($OutputObjectProperties.ProductCode)`""
}
}
}
}
}
{($_ -iin @('QuietUninstallString'))}
{
Switch (([String]::IsNullOrEmpty($OutputObjectPropertyValue) -eq $True) -or ([String]::IsNullOrWhiteSpace($OutputObjectPropertyValue) -eq $True))
{
{($_ -eq $True)}
{
Switch (([String]::IsNullOrEmpty($OutputObjectProperties.ProductCode) -eq $False) -and ([String]::IsNullOrWhiteSpace($OutputObjectProperties.ProductCode) -eq $False))
{
{($_ -eq $True)}
{
$OutputObjectPropertyValue = "msiexec.exe /x `"$($OutputObjectProperties.ProductCode)`" /qn /norestart REBOOT=ReallySuppress /L*v `"$($Env:Windir)\Temp\$(($OutputObjectProperties.DisplayName.Split([System.IO.Path]::GetInvalidFileNameChars()) -Join '') -ireplace '(\s+)', '_')_Removal.log`""
}
}
}
}
}
Default
{
Switch ($OutputObjectPropertyValue)
{
{($_ -imatch '(^\d+$)')}
{
$OutputObjectPropertyValue = $OutputObjectPropertyValue -As [Int32]
}
}
}
}
$OutputObjectProperties.Remove($OutputObjectPropertyName)
$OutputObjectProperties.$($OutputObjectPropertyName) = $OutputObjectPropertyValue
}
Switch (($RegistryHivePropertyList.Name -icontains 'NTAccount') -and ($RegistryHivePropertyList.Name -icontains 'SID'))
{
{($_ -eq $True)}
{
$OutputObjectProperties.ProfileNTAccount = $RegistryHive.NTAccount -As [String]
$OutputObjectProperties.ProfileSID = $RegistryHive.SID -As [String]
}
Default
{
$OutputObjectProperties.ProfileNTAccount = $Null -As [String]
$OutputObjectProperties.ProfileSID = $Null -As [String]
}
}
$OutputObject = New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)
$OutputObjectList.Add($OutputObject)
}
}
}
}
Try {$Null = $SubKeyObject.Close()} Catch {}
}
Catch
{
}
Finally
{
}
}
}
}
Try {$Null = $RegistryKeyObject.Close()} Catch {}
}
Try {$Null = $RegistryHiveObject.Close()} Catch {}
}
$WriteLogMessage.Invoke(0, @("Software Detection Filter: $($FilterExpression)"))
$WriteLogMessage.Invoke(0, @("Found $($OutputObjectList.Count) installed software entries matching the specified criteria."))
}
Catch
{
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
}
Finally
{
}
}
End
{
Try
{
#Determine the date and time the function completed execution
$FunctionEndTime = (Get-Date)
$WriteLogMessage.Invoke(0, @("Execution of $($FunctionName) ended on $($FunctionEndTime.ToString($DateTimeLogFormat))"))
#Log the total script execution time
$FunctionExecutionTimespan = New-TimeSpan -Start ($FunctionStartTime) -End ($FunctionEndTime)
$WriteLogMessage.Invoke(0, @("Function execution took $($FunctionExecutionTimespan.Hours.ToString()) hour(s), $($FunctionExecutionTimespan.Minutes.ToString()) minute(s), $($FunctionExecutionTimespan.Seconds.ToString()) second(s), and $($FunctionExecutionTimespan.Milliseconds.ToString()) millisecond(s)"))
$WriteLogMessage.Invoke(0, @("Function `'$($FunctionName)`' is completed."))
}
Catch
{
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
}
Finally
{
$OutputObjectList = $OutputObjectList.ToArray()
Write-Output -InputObject ($OutputObjectList)
}
}
}
#endregion