PSFramework.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSFramework.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = $false if ($psframework_dotsourcemodule) { $script:doDotSource = $true } if (($PSVersionTable.PSVersion.Major -lt 6) -or ($PSVersionTable.OS -like "*Windows*")) { if ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "DoDotSource" -ErrorAction Ignore).DoDotSource) { $script:doDotSource = $true } if ((Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "DoDotSource" -ErrorAction Ignore).DoDotSource) { $script:doDotSource = $true } } <# Note on Resolve-Path: All paths are sent through Resolve-Path in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = $false if ($PSFramework_importIndividualFiles) { $importIndividualFiles = $true } if (($PSVersionTable.PSVersion.Major -lt 6) -or ($PSVersionTable.OS -like "*Windows*")) { if ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "ImportIndividualFiles" -ErrorAction Ignore).ImportIndividualFiles) { $script:doDotSource = $true } if ((Get-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\System" -Name "ImportIndividualFiles" -ErrorAction Ignore).ImportIndividualFiles) { $script:doDotSource = $true } } if (Test-Path (Join-Path (Resolve-Path -Path "$($script:ModuleRoot)\..") '.git')) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) try { if ($doDotSource) { . (Resolve-Path $Path) } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path).ProviderPath))), $null, $null) } } catch { throw (New-Object System.Exception("Failed to import $(Resolve-Path $Path) : $_", $_.Exception)) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$($script:ModuleRoot)\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$($script:ModuleRoot)\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code #region Paths $script:path_RegistryUserDefault = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" $script:path_RegistryUserEnforced = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" $script:path_RegistryMachineDefault = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" $script:path_RegistryMachineEnforced = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" $psVersionName = "WindowsPowerShell" if ($PSVersionTable.PSVersion.Major -ge 6) { $psVersionName = "PowerShell" } #region User Local if ($IsLinux -or $IsMacOs) { # Defaults to $Env:XDG_CONFIG_HOME on Linux or MacOS ($HOME/.config/) $fileUserLocal = $Env:XDG_CONFIG_HOME if (-not $fileUserLocal) { $fileUserLocal = Join-Path $HOME .config/ } $script:path_FileUserLocal = Join-Path (Join-Path $fileUserLocal $psVersionName) "PSFramework/" } else { # Defaults to $Env:LocalAppData on Windows $script:path_FileUserLocal = Join-Path $Env:LocalAppData "$psVersionName\PSFramework\Config" if (-not $script:path_FileUserLocal) { $script:path_FileUserLocal = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) "$psVersionName\PSFramework\Config" } } #endregion User Local #region User Shared if ($IsLinux -or $IsMacOs) { # Defaults to the first value in $Env:XDG_CONFIG_DIRS on Linux or MacOS (or $HOME/.local/share/) $fileUserShared = @($Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator))[0] if (-not $fileUserShared) { $fileUserShared = Join-Path $HOME .local/share/ } $script:path_FileUserShared = Join-Path (Join-Path $fileUserShared $psVersionName) "PSFramework/" } else { # Defaults to $Env:AppData on Windows $script:path_FileUserShared = Join-Path $Env:AppData "$psVersionName\PSFramework\Config" if (-not $Env:AppData) { $script:path_FileUserShared = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Config" } } #endregion User Shared #region System if ($IsLinux -or $IsMacOs) { # Defaults to /etc/xdg elsewhere $XdgConfigDirs = $Env:XDG_CONFIG_DIRS -split ([IO.Path]::PathSeparator) | Where-Object { $_ -and (Test-Path $_) } if ($XdgConfigDirs.Count -gt 1) { $basePath = $XdgConfigDirs[1] } else { $basePath = "/etc/xdg/" } $script:path_FileSystem = Join-Path $basePath "$psVersionName/PSFramework/" } else { # Defaults to $Env:ProgramData on Windows $script:path_FileSystem = Join-Path $Env:ProgramData "$psVersionName\PSFramework\Config" if (-not $script:path_FileSystem) { $script:path_FileSystem = Join-Path ([Environment]::GetFolderPath("CommonApplicationData")) "$psVersionName\PSFramework\Config" } } #endregion System #region Special Paths if ($IsLinux -or $IsMacOs) { $script:path_Logging = Join-Path (Split-Path $script:path_FileUserShared) "Logs/" $script:path_typedata = Join-Path $script:path_FileUserShared "TypeData/" } else { # Defaults to $Env:AppData on Windows $script:path_Logging = Join-Path $Env:AppData "$psVersionName\PSFramework\Logs" $script:path_typedata = Join-Path $Env:AppData "$psVersionName\PSFramework\TypeData" if (-not $Env:AppData) { $script:path_Logging = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Logs" $script:path_typedata = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\TypeData" } } #endregion Special Paths #endregion Paths # Determine Registry Availability $script:NoRegistry = $false if (($PSVersionTable.PSVersion.Major -ge 6) -and ($PSVersionTable.OS -notlike "*Windows*")) { $script:NoRegistry = $true } if (-not ([PSFramework.Message.LogHost]::LoggingPath)) { [PSFramework.Message.LogHost]::LoggingPath = $script:path_Logging } [PSFramework.PSFCore.PSFCoreHost]::ModuleRoot = $script:ModuleRoot # Run the library initialization logic # Needed before the configuration system loads [PSFramework.PSFCore.PSFCoreHost]::Initialize() function Convert-PsfConfigValue { <# .SYNOPSIS Converts a persisted configuration's value back to its data type. .DESCRIPTION Converts a persisted configuration's value back to its data type. Can be used for either registry-based or json-file-based items. .PARAMETER Value The full value item to decode (must include the original type identifier). Example: "bool:true" .EXAMPLE PS C:\> Convert-PsfConfigValue -Value "bool:true" Will return a boolean $true #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] Param ( [string] $Value ) begin { } process { $index = $Value.IndexOf(":") if ($index -lt 1) { throw "No type identifier found!" } $type = $Value.Substring(0, $index).ToLower() $content = $Value.Substring($index + 1) switch ($type) { "bool" { if ($content -eq "true") { return $true } if ($content -eq "1") { return $true } if ($content -eq "false") { return $false } if ($content -eq "0") { return $false } throw "Failed to interpret as bool: $content" } "int" { return ([int]$content) } "double" { return [double]$content } "long" { return [long]$content } "string" { return $content } "timespan" { return (New-Object System.TimeSpan($content)) } "datetime" { return (New-Object System.DateTime($content)) } "consolecolor" { return ([System.ConsoleColor]$content) } "array" { if ($content -eq "") { return, @() } $tempArray = @() foreach ($item in ($content -split "þþþ")) { $tempArray += Convert-PsfConfigValue -Value $item } return, $tempArray } default { throw "Unknown type identifier" } } } end { } } function Read-PsfConfigFile { <# .SYNOPSIS Reads a configuration file and parses it. .DESCRIPTION Reads a configuration file and parses it. .PARAMETER Path The path to the file to parse. .PARAMETER WebLink The link to a website to download straight as raw json. .PARAMETER RawJson Raw json data to interpret. .EXAMPLE PS C:\> Read-PsfConfigFile -Path config.json Reads the config.json file and returns interpreted configuration objects. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'Weblink')] [string] $Weblink, [Parameter(Mandatory = $true, ParameterSetName = 'RawJson')] [string] $RawJson ) #region Utility Function function New-ConfigItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $FullName, $Value, $Type, [switch] $KeepPersisted, [switch] $Enforced, [switch] $Policy ) [pscustomobject]@{ FullName = $FullName Value = $Value Type = $Type KeepPersisted = $KeepPersisted Enforced = $Enforced Policy = $Policy } } function Get-WebContent { [CmdletBinding()] param ( [string] $WebLink ) $webClient = New-Object System.Net.WebClient $webClient.Encoding = [System.Text.Encoding]::UTF8 $webClient.DownloadString($WebLink) } #endregion Utility Function if ($Path) { if (-not (Test-Path $Path)) { return } $data = Get-Content -Path $Path -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop } if ($Weblink) { $data = Get-WebContent -WebLink $Weblink | ConvertFrom-Json -ErrorAction Stop } if ($RawJson) { $data = $RawJson | ConvertFrom-Json -ErrorAction Stop } foreach ($item in $data) { #region No Version if (-not $item.Version) { New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type)) } #endregion No Version #region Version One if ($item.Version -eq 1) { if ((-not $item.Style) -or ($item.Style -eq "Simple")) { New-ConfigItem -FullName $item.FullName -Value $item.Data } else { if (($item.Type -eq "Object") -or ($item.Type -eq 12)) { New-ConfigItem -FullName $item.FullName -Value $item.Value -Type "Object" -KeepPersisted } else { New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type)) } } } #endregion Version One } } function Read-PsfConfigPersisted { <# .SYNOPSIS Reads configurations from persisted file / registry. .DESCRIPTION Reads configurations from persisted file / registry. .PARAMETER Scope Where to read from. .PARAMETER Module Load module specific data. Use this to load on-demand configuration only when the module is imported. Useful when using the config system as cache. .PARAMETER ModuleVersion The configuration version of the module-settings to load. .PARAMETER Hashtable Rather than returning results, insert them into this hashtable. .PARAMETER Default When inserting into a hashtable, existing values are overwritten by default. Enabling this setting will cause it to only insert values if the key does not exist yet. .EXAMPLE Read-PsfConfigPersisted -Scope 127 Read all persisted default configuration items in the default mandated order. #> [OutputType([System.Collections.Hashtable])] [CmdletBinding()] Param ( [PSFramework.Configuration.ConfigScope] $Scope, [string] $Module, [int] $ModuleVersion = 1, [System.Collections.Hashtable] $Hashtable, [switch] $Default ) begin { #region Helper Functions function New-ConfigItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $FullName, $Value, $Type, [switch] $KeepPersisted, [switch] $Enforced, [switch] $Policy ) [pscustomobject]@{ FullName = $FullName Value = $Value Type = $Type KeepPersisted = $KeepPersisted Enforced = $Enforced Policy = $Policy } } function Read-Registry { [CmdletBinding()] param ( $Path, [switch] $Enforced ) if (-not (Test-Path $Path)) { return } $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider' foreach ($item in ((Get-ItemProperty -Path $Path -ErrorAction Ignore).PSObject.Properties | Where-Object Name -NotIn $common)) { if ($item.Value -like "Object:*") { $data = $item.Value.Split(":", 2) New-ConfigItem -FullName $item.Name -Type $data[0] -Value $data[1] -KeepPersisted -Enforced:$Enforced -Policy } else { try { New-ConfigItem -FullName $item.Name -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value)) -Policy } catch { Write-PSFMessage -Level Warning -Message "Failed to load configuration from Registry: $($item.Name)" -ErrorRecord $_ -Target "$Path : $($item.Name)" } } } } #endregion Helper Functions if (-not $Hashtable) { $results = @{ } } else { $results = $Hashtable } if ($Module) { $filename = "$($Module.ToLower())-$($ModuleVersion).json" } else { $filename = "psf_config.json" } } process { #region File - Computer Wide if ($Scope -band 64) { foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileSystem $filename))) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion File - Computer Wide #region Registry - Computer Wide if (($Scope -band 4) -and (-not $script:NoRegistry)) { foreach ($item in (Read-Registry -Path $script:path_RegistryMachineDefault)) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion Registry - Computer Wide #region File - User Shared if ($Scope -band 32) { foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileUserShared $filename))) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion File - User Shared #region Registry - User Shared if (($Scope -band 1) -and (-not $script:NoRegistry)) { foreach ($item in (Read-Registry -Path $script:path_RegistryUserDefault)) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion Registry - User Shared #region File - User Local if ($Scope -band 16) { foreach ($item in (Read-PsfConfigFile -Path (Join-Path $script:path_FileUserLocal $filename))) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion File - User Local #region Registry - User Enforced if (($Scope -band 2) -and (-not $script:NoRegistry)) { foreach ($item in (Read-Registry -Path $script:path_RegistryUserEnforced -Enforced)) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion Registry - User Enforced #region Registry - System Enforced if (($Scope -band 8) -and (-not $script:NoRegistry)) { foreach ($item in (Read-Registry -Path $script:path_RegistryMachineEnforced -Enforced)) { if (-not $Default) { $results[$item.FullName] = $item } elseif (-not $results.ContainsKey($item.FullName)) { $results[$item.FullName] = $item } } } #endregion Registry - System Enforced } end { $results } } function Write-PsfConfigFile { <# .SYNOPSIS Handles config export to file. .DESCRIPTION Handles config export to file. .PARAMETER Config The configuration items to export. .PARAMETER Path The path to export to. Needs to point to the specific file to export to. Will create the folder structure if needed. .PARAMETER Replace Completely replaces previous file contents. By default, it will integrate settings into one coherent configuration file. .EXAMPLE PS C:\> Write-PsfConfigFile -Config $items -Path .\file.json Exports all settings stored in $items to .\file.json. If the file already exists, the new settings will be merged into the existing file. #> [CmdletBinding()] Param ( [PSFramework.Configuration.Config[]] $Config, [string] $Path, [switch] $Replace ) begin { $parent = Split-Path -Path $Path if (-not (Test-Path $parent)) { $null = New-Item $parent -ItemType Directory -Force } $data = @{ } if ((Test-Path $Path) -and (-not $Replace)) { foreach ($item in (Get-Content -Path $Path -Encoding UTF8 | ConvertFrom-Json)) { $data[$item.FullName] = $item } } } process { foreach ($item in $Config) { $datum = @{ Version = 1 FullName = $item.FullName } if ($item.SimpleExport) { $datum["Data"] = $item.Value } else { $persisted = [PSFramework.Configuration.ConfigurationHost]::ConvertToPersistedValue($item.Value) $datum["Value"] = $persisted.PersistedValue $datum["Type"] = $persisted.PersistedType $datum["Style"] = "default" } $data[$item.FullName] = [pscustomobject]$datum } } end { $data.Values | ConvertTo-Json | Set-Content -Path $Path -Encoding UTF8 -ErrorAction Stop } } function Import-LocalizedString { <# .SYNOPSIS Imports a set of localized strings from a PowerShell data file. .DESCRIPTION Imports a set of localized strings from a PowerShell data file. This is used to feed the localized string feature set. Always import for all languages, do not select by current language - the system handles language selection. Strings are process wide, so loading additional languages can be offloaded into a background task. .PARAMETER Path The path to the psd1 file to import as strings file. .PARAMETER Module The module for which to import the strings. .PARAMETER Language The language of the specific strings file. Defaults to en-US. .EXAMPLE PS C:\> Import-LocalizedString -Path '$moduleRoot\strings.psd1' -Module 'MyModule' Imports the strings stored in strings.psd1 for the module MyModule as 'en-US' language strings. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Module, [string] $Language = 'en-US' ) begin { try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem } catch { Stop-PSFFunction -Message "Failed to resolve path: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ } } process { foreach ($pathItem in $resolvedPath) { $data = Import-PowerShellDataFile -Path $pathItem foreach ($key in $data.Keys) { [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key]) } } } } function Convert-PsfMessageException { <# .SYNOPSIS Transforms the Exception input to the message system. .DESCRIPTION Transforms the Exception input to the message system. If there is an exception running a transformation scriptblock, it will log the error in the transform error queue and return the original object instead. .PARAMETER Exception The input Exception object, that might have to be transformed (may not either) .PARAMETER FunctionName The function writing the message .PARAMETER ModuleName The module, that the function writing the message is part of .EXAMPLE PS C:\> Convert-PsfMessageException -Exception $Exception -FunctionName 'Get-Test' -ModuleName 'MyModule' Checks internal storage for definitions that require a Exception transform, and either returns the original object or the transformed object. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Exception, [Parameter(Mandatory = $true)] [string] $FunctionName, [Parameter(Mandatory = $true)] [string] $ModuleName ) if ($null -eq $Exception) { return } $typeName = $Exception.GetType().FullName.ToLower() if ([PSFramework.Message.MessageHost]::ExceptionTransforms.ContainsKey($typeName)) { $scriptBlock = [PSFramework.Message.MessageHost]::ExceptionTransforms[$typeName] try { $tempException = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($scriptBlock.ToString())), $null, $Exception) return $tempException } catch { [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Exception, "Exception", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId)) return $Exception } } if ($transform = [PSFramework.Message.MessageHost]::ExceptionTransformList.Get($typeName, $ModuleName, $FunctionName)) { try { $tempException = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($transform.ScriptBlock.ToString())), $null, $Exception) return $tempException } catch { [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Exception, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId)) return $Exception } } return $Exception } function Convert-PsfMessageLevel { <# .SYNOPSIS Processes the effective message level of a message .DESCRIPTION Processes the effective message level of a message - Applies level decrements - Applies message level modifiers .PARAMETER OriginalLevel The level the message was originally written to .PARAMETER FromStopFunction Whether the message was passed through Stop-PSFFunction first. This is used to increment the automatic message level decrement counter by 1 (so it ignores the fact, that it was passed through Stop-PSFFunction). The automatic message level decrement functionality allows users to make nested commands' messages be less verbose. .PARAMETER Tags The tags that were added to the message .PARAMETER FunctionName The function that wrote the message. .PARAMETER ModuleName The module the function writing the message comes from. .EXAMPLE Convert-PsfMessageLevel -OriginalLevel $Level -FromStopFunction $fromStopFunction -Tags $Tag -FunctionName $FunctionName -ModuleName $ModuleName This will convert the original level of $Level based on the transformation rules for levels. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [PSFramework.Message.MessageLevel] $OriginalLevel, [Parameter(Mandatory = $true)] [bool] $FromStopFunction, [Parameter(Mandatory = $true)] [AllowNull()] [string[]] $Tags, [Parameter(Mandatory = $true)] [string] $FunctionName, [Parameter(Mandatory = $true)] [string] $ModuleName ) $number = $OriginalLevel.value__ if ([PSFramework.Message.MessageHost]::NestedLevelDecrement -gt 0) { $depth = (Get-PSCallStack).Count - 3 if ($FromStopFunction) { $depth = $depth - 1 } $number = $number + $depth * ([PSFramework.Message.MessageHost]::NestedLevelDecrement) } foreach ($modifier in [PSFramework.Message.MessageHost]::MessageLevelModifiers.Values) { if ($modifier.AppliesTo($FunctionName, $ModuleName, $Tags)) { $number = $number + $modifier.Modifier } } # Finalize number and return if ($number -lt 1) { $number = 1 } if ($number -gt 9) { $number = 9 } return ([PSFramework.Message.MessageLevel]$number) } function Convert-PsfMessageTarget { <# .SYNOPSIS Transforms the target input to the message system. .DESCRIPTION Transforms the target input to the message system. If there is an exception running a transformation scriptblock, it will log the error in the transform error queue and return the original object instead. .PARAMETER Target The input target object, that might have to be transformed (may not either) .PARAMETER FunctionName The function writing the message .PARAMETER ModuleName The module, that the function writing the message is part of .EXAMPLE PS C:\> Convert-PsfMessageTarget -Target $Target -FunctionName 'Get-Test' -ModuleName 'MyModule' Checks internal storage for definitions that require a target transform, and either returns the original object or the transformed object. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Target, [Parameter(Mandatory = $true)] [string] $FunctionName, [Parameter(Mandatory = $true)] [string] $ModuleName ) if ($null -eq $Target) { return } $typeName = $Target.GetType().FullName.ToLower() if ([PSFramework.Message.MessageHost]::TargetTransforms.ContainsKey($typeName)) { $scriptBlock = [PSFramework.Message.MessageHost]::TargetTransforms[$typeName] try { $tempTarget = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($scriptBlock.ToString())), $null, $Target) return $tempTarget } catch { [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Target, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId)) return $Target } } if ($transform = [PSFramework.Message.MessageHost]::TargetTransformlist.Get($typeName, $ModuleName, $FunctionName)) { try { $tempTarget = $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create($transform.ScriptBlock.ToString())), $null, $Target) return $tempTarget } catch { [PSFramework.Message.MessageHost]::WriteTransformError($_, $FunctionName, $ModuleName, $Target, "Target", ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId)) return $Target } } return $Target } function global:New-PSFTeppCompletionResult { <# .SYNOPSIS Generates a completion result for dbatools internal tab completion. .DESCRIPTION Generates a completion result for dbatools internal tab completion. .PARAMETER CompletionText The text to propose. .PARAMETER ToolTip The tooltip to show in tooltip-aware hosts (ISE, mostly) .PARAMETER ListItemText ??? .PARAMETER CompletionResultType The type of object that is being completed. By default it generates one of type paramter value. .PARAMETER NoQuotes Whether to put the result in quotes or not. .EXAMPLE New-PSFTeppCompletionResult -CompletionText 'master' -ToolTip 'master' Returns a CompletionResult with the text and tooltip 'master' #> param ( [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true, ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [string] $CompletionText, [Parameter(Position = 1, ValueFromPipelineByPropertyName = $true)] [string] $ToolTip, [Parameter(Position = 2, ValueFromPipelineByPropertyName = $true)] [string] $ListItemText, [System.Management.Automation.CompletionResultType] $CompletionResultType = [System.Management.Automation.CompletionResultType]::ParameterValue, [switch] $NoQuotes ) process { $toolTipToUse = if ($ToolTip -eq '') { $CompletionText } else { $ToolTip } $listItemToUse = if ($ListItemText -eq '') { $CompletionText } else { $ListItemText } # If the caller explicitly requests that quotes # not be included, via the -NoQuotes parameter, # then skip adding quotes. if ($CompletionResultType -eq [System.Management.Automation.CompletionResultType]::ParameterValue -and -not $NoQuotes) { # Add single quotes for the caller in case they are needed. # We use the parser to robustly determine how it will treat # the argument. If we end up with too many tokens, or if # the parser found something expandable in the results, we # know quotes are needed. $tokens = $null $null = [System.Management.Automation.Language.Parser]::ParseInput("echo $CompletionText", [ref]$tokens, [ref]$null) if ($tokens.Length -ne 3 -or ($tokens[1] -is [System.Management.Automation.Language.StringExpandableToken] -and $tokens[1].Kind -eq [System.Management.Automation.Language.TokenKind]::Generic)) { $CompletionText = "'$CompletionText'" } } return New-Object System.Management.Automation.CompletionResult($CompletionText, $listItemToUse, $CompletionResultType, $toolTipToUse.Trim()) } } (Get-Item Function:\New-PSFTeppCompletionResult).Visibility = "Private" function Invoke-PSFCommand { <# .SYNOPSIS An Invoke-Command wrapper with integrated session management. .DESCRIPTION This wrapper command around Invoke-Command allows conveniently calling remote calls. - It uses the PSFComputer parameter class, and is thus a lot more flexible in accepted input - It automatically reuses sessions specified for input - It automatically establishes new sessions, tracks usage and retires sessions that have timed out. Using this command, it is no longer necessary to first establish a connection and then manually handle the session object. Just point the command at the computer and it will remember. It also reuses sessions across multiple commands that call it. Note: Special connection conditions (like a custom application name, alternative authentication schemes, etc.) are not supported and require using New-PSSession to establish the connection. Once that session has been established, the session object can be used with this command and will be used for command invocation. .PARAMETER ComputerName The computer(s) to invoke the command on. Accepts all kinds of things that legally point at a computer, including DNS names, ADComputer objects, IP Addresses, SQL Server connection strings, CimSessions or PowerShell Sessions. It will reuse PSSession objects if specified (and not include them in its session management). .PARAMETER ScriptBlock The code to execute. .PARAMETER ArgumentList The arguments to pass into the scriptblock. .PARAMETER Credential Credentials to use when establishing connections. Note: These will be ignored if there already exists an established connection. .PARAMETER HideComputerName Indicates that this cmdlet omits the computer name of each object from the output display. By default, the name of the computer that generated the object appears in the display. .PARAMETER ThrottleLimit Specifies the maximum number of concurrent connections that can be established to run this command. If you omit this parameter or enter a value of 0, the default value, 32, is used. .EXAMPLE PS C:\> Invoke-PSFCommand -ScriptBlock $ScriptBlock Runs the $scriptblock against the local computer. .EXAMPLE PS C:\> Invoke-PSFCommand -ScriptBlock $ScriptBlock (Get-ADComputer -Filter "name -like 'srv-db*'") Runs the $scriptblock against all computers in AD with a name that starts with "srv-db". #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Invoke-PSFCommand')] param ( [PSFComputer[]] [Alias('Session')] $ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [object[]] $ArgumentList, [System.Management.Automation.CredentialAttribute()] [System.Management.Automation.PSCredential] $Credential, [switch] $HideComputerName, [int] $ThrottleLimit = 32 ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' #region Clean up broken sessions [array]$broken = $psframework_pssessions.GetBroken() foreach ($sessionInfo in $broken) { Write-PSFMessage -Level Debug -Message "Removing broken session to $($sessionInfo.ComputerName)" Remove-PSSession -Session $sessionInfo.Session -ErrorAction Ignore $null = $psframework_pssessions.Remove($sessionInfo.ComputerName) } #endregion Clean up broken sessions #region Invoke Command Splats $paramInvokeCommand = @{ ScriptBlock = $ScriptBlock ArgumentList = $ArgumentList HideComputerName = $HideComputerName ThrottleLimit = $ThrottleLimit } $paramInvokeCommandLocal = @{ ScriptBlock = $ScriptBlock ArgumentList = $ArgumentList } #endregion Invoke Command Splats } process { #region Collect list of sessions to process $sessionsToInvoke = @() $managedSessions = @() foreach ($computer in $ComputerName) { if ($computer.Type -eq "PSSession") { $sessionsToInvoke += $computer.InputObject } elseif ($sessionObject = $computer.InputObject -as [System.Management.Automation.Runspaces.PSSession]) { $sessionsToInvoke += $sessionObject } else { #region Handle localhost if ($computer.IsLocalHost) { Write-PSFMessage -Level Verbose -Message "Executing command against localhost" -Target $computer Invoke-Command @paramInvokeCommandLocal continue } #endregion Handle localhost #region Already have a cached session if ($session = $psframework_pssessions[$computer.ComputerName]) { $sessionsToInvoke += $session.Session $managedSessions += $session $session.ResetTimestamp() } #endregion Already have a cached session #region Establish new session and add to management else { Write-PSFMessage -Level Verbose -Message "Establishing connection to $computer" -Target $computer try { if ($Credential) { $pSSession = New-PSSession -ComputerName $computer -Credential $Credential -ErrorAction Stop } else { $pSSession = New-PSSession -ComputerName $computer -ErrorAction Stop } } catch { Write-PSFMessage -Level Warning -Message "Failed to connect to $computer" -ErrorRecord $_ -Target $computer 3>$null Write-Error -ErrorRecord $_ continue } $session = New-Object PSFramework.ComputerManagement.PSSessioninfo($pSSession) $psframework_pssessions[$session.ComputerName] = $session $sessionsToInvoke += $session.Session $managedSessions += $session } #endregion Establish new session and add to management } } #endregion Collect list of sessions to process if ($sessionsToInvoke) { Write-PSFMessage -Level VeryVerbose -Message "Invoking command against $($sessionsToInvoke.ComputerName -join ', ' )" Invoke-Command -Session $sessionsToInvoke @paramInvokeCommand } #region Refresh timestamp foreach ($session in $managedSessions) { $session.ResetTimestamp() } #endregion Refresh timestamp } end { #region Cleanup expired sessions [array]$expired = $psframework_pssessions.GetExpired() foreach ($sessionInfo in $expired) { Write-PSFMessage -Level Debug -Message "Removing expired session to $($sessionInfo.ComputerName)" Remove-PSSession -Session $sessionInfo.Session -ErrorAction Ignore $null = $psframework_pssessions.Remove($sessionInfo.ComputerName) } #endregion Cleanup expired sessions } } function New-PSFSessionContainer { <# .SYNOPSIS Creates an object containing multiple session objects to the same computer. .DESCRIPTION Creates an object containing multiple session objects to the same computer. Using this, a single object can be used to point at a computer while containing session objects for multiple protocols inside. Only session types registered via Reigster-PSSessionObjectType are supported. .PARAMETER ComputerName The name of the computer to connect to .PARAMETER Session The session objects that are a live connection to the host. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> New-PSFSessionContainer -ComputerName "server1" -Session $pssession, $cimsession, $smosession Create a session container containing three different kinds of session objects #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSFComputer] $ComputerName, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]] $Session, [switch] $EnableException ) begin { $container = New-Object PSFramework.ComputerManagement.SessionContainer $container.ComputerName = $ComputerName } process { foreach ($sessionItem in $Session) { if ($null -eq $sessionItem) { continue } if (-not ($sessionName = [PSFramework.ComputerManagement.ComputerManagementHost]::KnownSessionTypes[$sessionItem.GetType()])) { Stop-PSFFunction -String 'New-PSFSessionContainer.UnknownSessionType' -StringValues $sessionItem.GetType().Name, $sessionItem -Continue -EnableException $EnableException } $container.Connections[$sessionName] = $sessionItem } } end { $container } } function Register-PSFSessionObjectType { <# .SYNOPSIS Registers a new type as a live session object. .DESCRIPTION Registers a new type as a live session object. This is used in the session container object, used to pass through multiple types of connection objects to a single PSFComputer parameterclassed parameter. .PARAMETER DisplayName The display name for the type. Pick anything that intuitively points at what the object is. .PARAMETER TypeName The full name of the type. .EXAMPLE PS C:\> Register-PSFSessionObjectType -DisplayName 'PSSession' -TypeName 'System.Management.Automation.Runspaces.PSSession' Registers the type 'System.Management.Automation.Runspaces.PSSession' under the name of 'PSSession'. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DisplayName, [Parameter(Mandatory = $true)] [string] $TypeName ) process { [PSFramework.ComputerManagement.ComputerManagementHost]::KnownSessionTypes[$TypeName] = $DisplayName } } function Export-PSFConfig { <# .SYNOPSIS Exports configuration items to a Json file. .DESCRIPTION Exports configuration items to a Json file. .PARAMETER FullName Select the configuration objects to export by filtering by their full name. .PARAMETER Module Select the configuration objects to export by filtering by their module name. .PARAMETER Name Select the configuration objects to export by filtering by their name. .PARAMETER Config The configuration object(s) to export. Returned by Get-PSFConfig. .PARAMETER ModuleName Exports all configuration pertinent to a module to a predefined path. Exported configuration items include all settings marked as 'ModuleExport' that have been changed from the default value. .PARAMETER ModuleVersion The configuration version of the module-settings to write. .PARAMETER Scope Which predefined path to write module specific settings to. Only file scopes are considered. By default it writes to the suer profile. .PARAMETER OutPath The path (filename included) to export to. Will fail if the folder does not exist, will overwrite the file if it exists. .PARAMETER SkipUnchanged If set, configuration objects whose value was not changed from its original value will not be exported. (Note: Settings that were updated with the same value as the original default will still be considered changed) .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Get-PSFConfig | Export-PSFConfig -OutPath '~/export.json' Exports all current settings to json. .EXAMPLE Export-PSFConfig -Module MyModule -OutPath '~/export.json' -SkipUnchanged Exports all settings of the module 'MyModule' that are no longer the original default values to json. #> [CmdletBinding(DefaultParameterSetName = 'FullName', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Export-PSFConfig')] Param ( [Parameter(ParameterSetName = "FullName", Position = 0, Mandatory = $true)] [string] $FullName, [Parameter(ParameterSetName = "Module", Position = 0, Mandatory = $true)] [string] $Module, [Parameter(ParameterSetName = "Module", Position = 1)] [string] $Name = "*", [Parameter(ParameterSetName = "Config", Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [PSFramework.Configuration.Config[]] $Config, [Parameter(ParameterSetName = "ModuleName", Mandatory = $true)] [string] $ModuleName, [Parameter(ParameterSetName = "ModuleName")] [int] $ModuleVersion = 1, [Parameter(ParameterSetName = "ModuleName")] [PSFramework.Configuration.ConfigScope] $Scope = "FileUserShared", [Parameter(Position = 1, Mandatory = $true, ParameterSetName = 'Config')] [Parameter(Position = 1, Mandatory = $true, ParameterSetName = 'FullName')] [Parameter(Position = 2, Mandatory = $true, ParameterSetName = 'Module')] [string] $OutPath, [switch] $SkipUnchanged, [switch] $EnableException ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $items = @() if (($Scope -band 15) -and ($ModuleName)) { Stop-PSFFunction -Message "Cannot export modulecache to registry! Please pick a file scope for your export destination" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'scope', 'registry' return } } process { if (Test-PSFFunctionInterrupt) { return } if (-not $ModuleName) { foreach ($item in $Config) { $items += $item } if ($FullName) { $items = Get-PSFConfig -FullName $FullName } if ($Module) { $items = Get-PSFConfig -Module $Module -Name $Name } } } end { if (Test-PSFFunctionInterrupt) { return } if (-not $ModuleName) { try { Write-PsfConfigFile -Config ($items | Where-Object { -not $SkipUnchanged -or -not $_.Unchanged } ) -Path $OutPath -Replace } catch { Stop-PSFFunction -Message "Failed to export to file" -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'export' return } } else { if ($Scope -band 16) { Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileUserLocal "$($ModuleName.ToLower())-$($ModuleVersion).json") } if ($Scope -band 32) { Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileUserShared "$($ModuleName.ToLower())-$($ModuleVersion).json") } if ($Scope -band 64) { Write-PsfConfigFile -Config (Get-PSFConfig -Module $ModuleName -Force | Where-Object ModuleExport | Where-Object Unchanged -NE $true) -Path (Join-Path $script:path_FileSystem "$($ModuleName.ToLower())-$($ModuleVersion).json") } } } } function Get-PSFConfig { <# .SYNOPSIS Retrieves configuration elements by name. .DESCRIPTION Retrieves configuration elements by name. Can be used to search the existing configuration list. .PARAMETER FullName Default: "*" Search for configurations using the full name .PARAMETER Name Default: "*" The name of the configuration element(s) to retrieve. May be any string, supports wildcards. .PARAMETER Module Default: "*" Search configuration by module. .PARAMETER Force Overrides the default behavior and also displays hidden configuration values. .EXAMPLE PS C:\> Get-PSFConfig 'Mail.To' Retrieves the configuration element for the key "Mail.To" .EXAMPLE PS C:\> Get-PSFConfig -Force Retrieve all configuration elements from all modules, even hidden ones. #> [OutputType([PSFramework.Configuration.Config])] [CmdletBinding(DefaultParameterSetName = "FullName", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFConfig')] Param ( [Parameter(ParameterSetName = "FullName", Position = 0)] [string] $FullName = "*", [Parameter(ParameterSetName = "Module", Position = 1)] [string] $Name = "*", [Parameter(ParameterSetName = "Module", Position = 0)] [string] $Module = "*", [switch] $Force ) switch ($PSCmdlet.ParameterSetName) { "Module" { $Name = $Name.ToLower() $Module = $Module.ToLower() [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { ($_.Name -like $Name) -and ($_.Module -like $Module) -and ((-not $_.Hidden) -or ($Force)) } | Sort-Object Module, Name } "FullName" { [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { ("$($_.Module).$($_.Name)" -like $FullName) -and ((-not $_.Hidden) -or ($Force)) } | Sort-Object Module, Name } } } function Get-PSFConfigValue { <# .SYNOPSIS Returns the configuration value stored under the specified name. .DESCRIPTION Returns the configuration value stored under the specified name. It requires the full name (<Module>.<Name>) and is usually only called by functions. .PARAMETER FullName The full name (<Module>.<Name>) of the configured value to return. .PARAMETER Fallback A fallback value to use, if no value was registered to a specific configuration element. This basically is a default value that only applies on a "per call" basis, rather than a system-wide default. .PARAMETER NotNull By default, this function returns null if one tries to retrieve the value from either a Configuration that does not exist or a Configuration whose value was set to null. However, sometimes it may be important that some value was returned. By specifying this parameter, the function will throw an error if no value was found at all. .EXAMPLE PS C:\> Get-PSFConfigValue -FullName 'System.MailServer' Returns the configured value that was assigned to the key 'System.MailServer' .EXAMPLE PS C:\> Get-PSFConfigValue -FullName 'Default.CoffeeMilk' -Fallback 0 Returns the configured value for 'Default.CoffeeMilk'. If no such value is configured, it returns '0' instead. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFConfigValue')] Param ( [Alias('Name')] [Parameter(Mandatory = $true)] [string] $FullName, [object] $Fallback, [switch] $NotNull ) $FullName = $FullName.ToLower() $temp = $null $temp = [PSFramework.Configuration.ConfigurationHost]::Configurations[$FullName].Value if ($temp -eq $null) { $temp = $Fallback } if ($NotNull -and ($temp -eq $null)) { Stop-PSFFunction -Message "No Configuration Value available for $FullName" -EnableException $true -Category InvalidData -Target $FullName } else { return $temp } } function Import-PSFConfig { <# .SYNOPSIS Imports a json configuration file into the configuration system. .DESCRIPTION Imports a json configuration file into the configuration system. .PARAMETER Path The path to the json file to import. .PARAMETER ModuleName Import configuration items specific to a module from the default configuration paths. .PARAMETER ModuleVersion The configuration version of the module-settings to load. .PARAMETER Scope Where to import the module specific configuration items form. Only file-based scopes are supported for this. By default, all locations are queried, with user settings beating system settings. .PARAMETER Schema The configuration schema to use for import. Use Register-PSFConfigSchema to extend the way input content can be laid out. .PARAMETER IncludeFilter If specified, only elements with names that are similar (-like) to names in this list will be imported. .PARAMETER ExcludeFilter Elements that are similar (-like) to names in this list will not be imported. .PARAMETER Peek Rather than applying the setting, return the configuration items that would have been applied. .PARAMETER AllowDelete Configurations that have been imported will be flagged as deletable. This allows to purge them at a later time using Remove-PSFConfig. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Import-PSFConfig -Path '.\config.json' Imports the configuration stored in '.\config.json' .EXAMPLE PS C:\> Import-PSFConfig -ModuleName mymodule Imports all the module specific settings that have been persisted in any of the default file system paths. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding(DefaultParameterSetName = "Path", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFConfig')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Path")] [string[]] $Path, [Parameter(ParameterSetName = "ModuleName", Mandatory = $true)] [string] $ModuleName, [Parameter(ParameterSetName = "ModuleName")] [int] $ModuleVersion = 1, [Parameter(ParameterSetName = "ModuleName")] [PSFramework.Configuration.ConfigScope] $Scope = "FileUserLocal, FileUserShared, FileSystem", [Parameter(ParameterSetName = "Path")] [PsfValidateSet(TabCompletion = 'PSFramework-Config-Schema')] [string] $Schema = "Default", [Parameter(ParameterSetName = "Path")] [string[]] $IncludeFilter, [Parameter(ParameterSetName = "Path")] [string[]] $ExcludeFilter, [Parameter(ParameterSetName = "Path")] [switch] $Peek, [Parameter(ParameterSetName = 'Path')] [switch] $AllowDelete, [switch] $EnableException ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $settings = @{ IncludeFilter = $IncludeFilter ExcludeFilter = $ExcludeFilter Peek = $Peek.ToBool() AllowDelete = $AllowDelete.ToBool() EnableException = $EnableException.ToBool() Cmdlet = $PSCmdlet Path = (Get-Location).Path } $schemaScript = [PSFramework.Configuration.ConfigurationHost]::Schemata[$Schema] } process { #region Explicit Path foreach ($item in $Path) { try { $resolvedItem = Resolve-PSFPath -Path $item -Provider FileSystem } catch { $resolvedItem = $item } foreach ($rItem in $resolvedItem) { & $schemaScript $rItem $settings } } #endregion Explicit Path #region ModuleName if ($ModuleName) { $data = Read-PsfConfigPersisted -Module $ModuleName -Scope $Scope -ModuleVersion $ModuleVersion foreach ($value in $data.Values) { if (-not $value.KeepPersisted) { Set-PSFConfig -FullName $value.FullName -Value $value.Value -EnableException:$EnableException} else { Set-PSFConfig -FullName $value.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($value.Value, $value.Type)) -EnableException:$EnableException } } } #endregion ModuleName } } function Register-PSFConfig { <# .SYNOPSIS Registers an existing configuration object in registry. .DESCRIPTION Registers an existing configuration object in registry. This allows simple persisting of settings across powershell consoles. It also can be used to generate a registry template, which can then be used to create policies. .PARAMETER Config The configuration object to write to registry. Can be retrieved using Get-PSFConfig. .PARAMETER FullName The full name of the setting to be written to registry. .PARAMETER Module The name of the module, whose settings should be written to registry. .PARAMETER Name Default: "*" Used in conjunction with the -Module parameter to restrict the number of configuration items written to registry. .PARAMETER Scope Default: UserDefault Who will be affected by this export how? Current user or all? Default setting or enforced? Legal values: UserDefault, UserMandatory, SystemDefault, SystemMandatory .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Get-PSFConfig psframework.message.* | Register-PSFConfig Retrieves all configuration items that that start with psframework.message. and registers them in registry for the current user. .EXAMPLE PS C:\> Register-PSFConfig -FullName "psframework.developer.mode.enable" -Scope SystemDefault Retrieves the configuration item "psframework.developer.mode.enable" and registers it in registry as the default setting for all users on this machine. .EXAMPLE PS C:\> Register-PSFConfig -Module MyModule -Scope SystemMandatory Retrieves all configuration items of the module MyModule, then registers them in registry to enforce them for all users on the current system. #> [CmdletBinding(DefaultParameterSetName = "Default", HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFConfig')] Param ( [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] [PSFramework.Configuration.Config[]] $Config, [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] [string[]] $FullName, [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)] [string] $Module, [Parameter(ParameterSetName = "Name", Position = 1)] [string] $Name = "*", [PSFramework.Configuration.ConfigScope] $Scope = "UserDefault", [switch] $EnableException ) begin { if ($script:NoRegistry -and ($Scope -band 14)) { Stop-PSFFunction -Message "Cannot register configurations on non-windows machines to registry. Please specify a file-based scope" -Tag 'NotSupported' -Category NotImplemented return } # Linux and MAC default to local user store file if ($script:NoRegistry -and ($Scope -eq "UserDefault")) { $Scope = [PSFramework.Configuration.ConfigScope]::FileUserLocal } # Linux and MAC get redirection for SystemDefault to FileSystem if ($script:NoRegistry -and ($Scope -eq "SystemDefault")) { $Scope = [PSFramework.Configuration.ConfigScope]::FileSystem } $parSet = $PSCmdlet.ParameterSetName function Write-Config { [CmdletBinding()] Param ( [PSFramework.Configuration.Config] $Config, [PSFramework.Configuration.ConfigScope] $Scope, [bool] $EnableException, [string] $FunctionName = (Get-PSCallStack)[0].Command ) if (-not $Config -or ($Config.RegistryData -eq "<type not supported>")) { Stop-PSFFunction -Message "Invalid Input, cannot export $($Config.FullName), type not supported" -EnableException $EnableException -Category InvalidArgument -Tag "config", "fail" -Target $Config -FunctionName $FunctionName -ModuleName "PSFramework" return } try { Write-PSFMessage -Level Verbose -Message "Registering $($Config.FullName) for $Scope" -Tag "Config" -Target $Config -FunctionName $FunctionName -ModuleName "PSFramework" #region User Default if (1 -band $Scope) { Ensure-RegistryPath -Path $script:path_RegistryUserDefault -ErrorAction Stop Set-ItemProperty -Path $script:path_RegistryUserDefault -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop } #endregion User Default #region User Mandatory if (2 -band $Scope) { Ensure-RegistryPath -Path $script:path_RegistryUserEnforced -ErrorAction Stop Set-ItemProperty -Path $script:path_RegistryUserEnforced -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop } #endregion User Mandatory #region System Default if (4 -band $Scope) { Ensure-RegistryPath -Path $script:path_RegistryMachineDefault -ErrorAction Stop Set-ItemProperty -Path $script:path_RegistryMachineDefault -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop } #endregion System Default #region System Mandatory if (8 -band $Scope) { Ensure-RegistryPath -Path $script:path_RegistryMachineEnforced -ErrorAction Stop Set-ItemProperty -Path $script:path_RegistryMachineEnforced -Name $Config.FullName -Value $Config.RegistryData -ErrorAction Stop } #endregion System Mandatory } catch { Stop-PSFFunction -Message "Failed to export $($Config.FullName), to scope $Scope" -EnableException $EnableException -Tag "config", "fail" -Target $Config -ErrorRecord $_ -FunctionName $FunctionName -ModuleName "PSFramework" return } } function Ensure-RegistryPath { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] Param ( [string] $Path ) if (-not (Test-Path $Path)) { $null = New-Item $Path -Force } } # For file based persistence $configurationItems = @() } process { if (Test-PSFFunctionInterrupt) { return } #region Registry Based if ($Scope -band 15) { switch ($parSet) { "Default" { foreach ($item in $Config) { Write-Config -Config $item -Scope $Scope -EnableException $EnableException } foreach ($item in $FullName) { if ([PSFramework.Configuration.ConfigurationHost]::Configurations.ContainsKey($item.ToLower())) { Write-Config -Config ([PSFramework.Configuration.ConfigurationHost]::Configurations[$item.ToLower()]) -Scope $Scope -EnableException $EnableException } } } "Name" { foreach ($item in ([PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name)) { Write-Config -Config $item -Scope $Scope -EnableException $EnableException } } } } #endregion Registry Based #region File Based else { switch ($parSet) { "Default" { foreach ($item in $Config) { if ($configurationItems.FullName -notcontains $item.FullName) { $configurationItems += $item } } foreach ($item in $FullName) { if (($configurationItems.FullName -notcontains $item) -and ([PSFramework.Configuration.ConfigurationHost]::Configurations.ContainsKey($item.ToLower()))) { $configurationItems += [PSFramework.Configuration.ConfigurationHost]::Configurations[$item.ToLower()] } } } "Name" { foreach ($item in ([PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object Module -EQ $Module | Where-Object Name -Like $Name)) { if ($configurationItems.FullName -notcontains $item.FullName) { $configurationItems += $item } } } } } #endregion File Based } end { #region Finish File Based Persistence if ($Scope -band 16) { Write-PsfConfigFile -Config $configurationItems -Path (Join-Path $script:path_FileUserLocal "psf_config.json") } if ($Scope -band 32) { Write-PsfConfigFile -Config $configurationItems -Path (Join-Path $script:path_FileUserShared "psf_config.json") } if ($Scope -band 64) { Write-PsfConfigFile -Config $configurationItems -Path (Join-Path $script:path_FileSystem "psf_config.json") } #endregion Finish File Based Persistence } } function Register-PSFConfigSchema { <# .SYNOPSIS Register new schemas for ingersting configuration data. .DESCRIPTION Register new schemas for ingersting configuration data. This can be used to dynamically extend the configuration system and add new file types as supported input. .PARAMETER Name The name of the Schema to register. .PARAMETER Schema The Schema Code to register. .EXAMPLE PS C:\> Register-PSFConfigSchema -Name Default -Schema $scriptblock Registers the scriptblock stored in $scriptblock under 'Default' #> [CmdletBinding()] Param ( [string] $Name, [ScriptBlock] $Schema ) process { [PSFramework.Configuration.ConfigurationHost]::Schemata[$Name] = $Schema } } function Register-PSFConfigValidation { <# .SYNOPSIS Registers a validation scriptblock for use with the configuration system. .DESCRIPTION Registers a validation scriptblock for use with the configuration system. The scriptblock must be designed according to a few guidelines: - It must not throw exceptions - It must accept a single parameter (the value to be tested) - It must return an object with two properties: 'Message', 'Value' and 'Success'. The Success property should be boolean and indicate whether the value is valid. The Value property contains the validated input. The scriptblock may legally convert the input (For example from string to int in case of integer validation) The message contains a string that will be passed along to an exception in case the input is NOT valid. .PARAMETER Name The name under which to register the validation scriptblock .PARAMETER ScriptBlock The scriptblock to register .EXAMPLE PS C:\> Register-PSFConfigValidation -Name IntPositive -ScriptBlock $scriptblock Registers the scriptblock stored in $scriptblock as validation with the name IntPositive #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFConfigValidation')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock ) [PSFramework.Configuration.ConfigurationHost]::Validation[$Name.ToLower()] = $ScriptBlock } function Remove-PSFConfig { <# .SYNOPSIS Removes configuration items from memory. .DESCRIPTION This command removes configuration items from memory. However, not all settings can just be deleted! A configuration item must be flagged as deletable. This can be done using Set-PSFConfig -AllowDelete or Import-PSFConfig -AllowDelete. Certain schema versions of configuration json may also support defining this in the file. Limitations to flagging configuration as deletable: > Once a configuration item has been initialized, its deletable status is frozen. The last time it is possible to change the deletable status is during initialization. > A setting that has been set as mandated by policy cannot be removed. Reason for this limit: The configuration system is designed for multiple scenarios. Deleting settings makes sense in some, while in others it is actually detrimental. Initialization is especially designed for the module scenario, where the module's configuration is its options menu. In this scenario, having a user deleting settings could lead to broken execution and unintended code paths, that might be at odds with policies defined. .PARAMETER Config The configuration object to remove from memory. Can be retrieved using Get-PSFConfig. .PARAMETER FullName The full name of the setting to be removed from memory. .PARAMETER Module The name of the module, whose settings should be removed from memory. .PARAMETER Name Default: "*" Used in conjunction with the -Module parameter to restrict the number of configuration items deleted from memory. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Remove-PSFConfig -FullName 'Phase1.Step1.Server' -Confirm:$false Deletes the setting 'Phase1.Step1.Server' from memory, assuming it exists and supports deletion. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] [PSFramework.Configuration.Config[]] $Config, [Parameter(ParameterSetName = "Default", Position = 0, ValueFromPipeline = $true)] [string[]] $FullName, [Parameter(Mandatory = $true, ParameterSetName = "Name", Position = 0)] [string] $Module, [Parameter(ParameterSetName = "Name", Position = 1)] [string] $Name = "*" ) process { switch ($PSCmdlet.ParameterSetName) { "Default" { #region Try removing all items specified foreach ($item in $Config) { if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $item.FullName)) { continue } try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($item.FullName) } catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $item.FullName -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ } if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName } else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced } } # Since configuration items will also bind to string, if any were included, break the switch if (Test-PSFParameterBinding -ParameterName Config) { break } #endregion Try removing all items specified #region Try removing all full names specified foreach ($nameItem in $FullName) { if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $nameItem)) { continue } try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($nameItem) } catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $nameItem -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ } $item = Get-PSFConfig -FullName $nameItem if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName } else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced } } #endregion Try removing all full names specified } "Name" { #region Try removing by filter foreach ($item in (Get-PSFConfig -Module $Module -Name $Name)) { if (-not (Test-PSFShouldProcess -ActionString 'PSFramework.Configuration.Remove-PSFConfig.ShouldRemove' -Target $item.FullName)) { continue } try { $result = [PSFramework.Configuration.ConfigurationHost]::DeleteConfiguration($item.FullName) } catch { Stop-PSFFunction -String Configuration.Remove-PSFConfig.InvalidConfiguration -StringValues $item.FullName -EnableException ($ErrorActionPreference -eq 'Stop') -Continue -Cmdlet $PSCmdlet -ErrorRecord $_ } if ($result) { Write-PSFMessage -Level InternalComment -String Configuration.Remove-PSFConfig.DeleteSuccessful -StringValues $item.FullName } else { Write-PSFMessage -Level Warning -String Configuration.Remove-PSFConfig.DeleteFailed -StringValues $item.FullName, $item.AllowDelete, $item.PolicyEnforced } } #endregion Try removing by filter } } } } function Reset-PSFConfig { <# .SYNOPSIS Reverts a configuration item to its default value. .DESCRIPTION This command can be used to revert a configuration item to the value it was initialized with. Generally, this amounts to reverting it to its default value. In order for a reset to be possible, two conditions must be met: - The setting must have been initialized. - The setting cannot have been enforced by policy. .PARAMETER ConfigurationItem A configuration object as returned by Get-PSFConfig. .PARAMETER FullName The full name of the setting to reset, offering the maximum of precision. .PARAMETER Module The name of the module, from which configurations should be reset. Used in conjunction with the -Name parameter to filter a specific set of items. .PARAMETER Name Used in conjunction with the -Module parameter to select which settings to reset using wildcard comparison. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Reset-PSFConfig -Module MyModule Resets all configuration items of the MyModule to default. .EXAMPLE PS C:\> Get-PSFConfig | Reset-PSFConfig Resets ALL configuration items to default. .EXAMPLE PS C:\> Reset-PSFConfig -FullName MyModule.Group.Setting1 Resets the configuration item named 'MyModule.Group.Setting1'. #> [CmdletBinding(DefaultParameterSetName = 'Pipeline', SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Reset-PSFConfig')] param ( [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')] [PSFramework.Configuration.Config[]] $ConfigurationItem, [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')] [string[]] $FullName, [Parameter(Mandatory = $true, ParameterSetName = 'Module')] [string] $Module, [Parameter(ParameterSetName = 'Module')] [string] $Name = "*", [switch] $EnableException ) process { #region By configuration Item foreach ($item in $ConfigurationItem) { if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -Action 'Reset to default value') { try { $item.ResetValue() } catch { Stop-PSFFunction -Message "Failed to reset the configuration item." -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue } } } #endregion By configuration Item #region By FullName foreach ($nameItem in $FullName) { # The configuration items themselves can be cast to string, so they need to be filtered out, # otherwise on bind they would execute for this code-path as well. if ($nameItem -ceq "PSFramework.Configuration.Config") { continue } foreach ($item in (Get-PSFConfig -FullName $nameItem)) { if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -Action 'Reset to default value') { try { $item.ResetValue() } catch { Stop-PSFFunction -Message "Failed to reset the configuration item." -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue } } } } #endregion By FullName if ($Module) { foreach ($item in (Get-PSFConfig -Module $Module -Name $Name)) { if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $item.FullName -Action 'Reset to default value') { try { $item.ResetValue() } catch { Stop-PSFFunction -Message "Failed to reset the configuration item." -ErrorRecord $_ -Cmdlet $PSCmdlet -Continue } } } } } } function Unregister-PSFConfig { <# .SYNOPSIS Removes registered configuration settings. .DESCRIPTION Removes registered configuration settings. This function can be used to remove settings that have been persisted for either user or computer. Note: This command has no effect on configuration setings currently in memory. .PARAMETER ConfigurationItem A configuration object as returned by Get-PSFConfig. .PARAMETER FullName The full name of the configuration setting to purge. .PARAMETER Module The module, amongst which settings should be unregistered. .PARAMETER Name The name of the setting to unregister. For use together with the module parameter, to limit the amount of settings that are unregistered. .PARAMETER Scope Settings can be set to either default or enforced, for user or the entire computer. By default, only DefaultSettings for the user are unregistered. Use this parameter to choose the actual scope for the command to process. .EXAMPLE PS C:\> Get-PSFConfig | Unregister-PSFConfig Completely removes all registered configurations currently loaded in memory. In most cases, this will mean removing all registered configurations. .EXAMPLE PS C:\> Unregister-PSFConfig -Scope SystemDefault -FullName 'MyModule.Path.DefaultExport' Unregisters the setting 'MyModule.Path.DefaultExport' from the list of computer-wide defaults. Note: Changing system wide settings requires running the console with elevation. .EXAMPLE PS C:\> Unregister-PSFConfig -Module MyModule Unregisters all configuration settings for the module MyModule. #> [CmdletBinding(DefaultParameterSetName = 'Pipeline', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Unregister-PSFConfig')] param ( [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')] [PSFramework.Configuration.Config[]] $ConfigurationItem, [Parameter(ValueFromPipeline = $true, ParameterSetName = 'Pipeline')] [string[]] $FullName, [Parameter(Mandatory = $true, ParameterSetName = 'Module')] [string] $Module, [Parameter(ParameterSetName = 'Module')] [string] $Name = "*", [PSFramework.Configuration.ConfigScope] $Scope = "UserDefault" ) begin { if (($PSVersionTable.PSVersion.Major -ge 6) -and ($PSVersionTable.OS -notlike "*Windows*") -and ($Scope -band 15)) { Stop-PSFFunction -Message "Cannot unregister configurations from registry on non-windows machines." -Tag 'NotSupported' -Category ResourceUnavailable return } #region Initialize Collection $registryProperties = @() if ($Scope -band 1) { if (Test-Path $script:path_RegistryUserDefault) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryUserDefault } } if ($Scope -band 2) { if (Test-Path $script:path_RegistryUserEnforced) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryUserEnforced } } if ($Scope -band 4) { if (Test-Path $script:path_RegistryMachineDefault) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryMachineDefault } } if ($Scope -band 8) { if (Test-Path $script:path_RegistryMachineEnforced) { $registryProperties += Get-ItemProperty -Path $script:path_RegistryMachineEnforced } } $pathProperties = @() if ($Scope -band 16) { $fileUserLocalSettings = @() if (Test-Path (Join-Path $script:path_FileUserLocal "psf_config.json")) { $fileUserLocalSettings = Get-Content (Join-Path $script:path_FileUserLocal "psf_config.json") -Encoding UTF8 | ConvertFrom-Json } if ($fileUserLocalSettings) { $pathProperties += [pscustomobject]@{ Path = (Join-Path $script:path_FileUserLocal "psf_config.json") Properties = $fileUserLocalSettings Changed = $false } } } if ($Scope -band 32) { $fileUserSharedSettings = @() if (Test-Path (Join-Path $script:path_FileUserShared "psf_config.json")) { $fileUserSharedSettings = Get-Content (Join-Path $script:path_FileUserShared "psf_config.json") -Encoding UTF8 | ConvertFrom-Json } if ($fileUserSharedSettings) { $pathProperties += [pscustomobject]@{ Path = (Join-Path $script:path_FileUserShared "psf_config.json") Properties = $fileUserSharedSettings Changed = $false } } } if ($Scope -band 64) { $fileSystemSettings = @() if (Test-Path (Join-Path $script:path_FileSystem "psf_config.json")) { $fileSystemSettings = Get-Content (Join-Path $script:path_FileSystem "psf_config.json") -Encoding UTF8 | ConvertFrom-Json } if ($fileSystemSettings) { $pathProperties += [pscustomobject]@{ Path = (Join-Path $script:path_FileSystem "psf_config.json") Properties = $fileSystemSettings Changed = $false } } } #endregion Initialize Collection $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider' } process { if (Test-PSFFunctionInterrupt) { return } # Silently skip since no action necessary if (-not ($pathProperties -or $registryProperties)) { return } foreach ($item in $ConfigurationItem) { # Registry foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -eq $item.FullName })) { Remove-ItemProperty -Path $hive.PSPath -Name $item.FullName } # Prepare file foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -contains $item.FullName })) { $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NE $item.FullName $fileConfig.Changed = $true } } foreach ($item in $FullName) { # Ignore string-casted configurations if ($item -ceq "PSFramework.Configuration.Config") { continue } # Registry foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -eq $item })) { Remove-ItemProperty -Path $hive.PSPath -Name $item } # Prepare file foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -contains $item })) { $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NE $item $fileConfig.Changed = $true } } if ($Module) { $compoundName = "{0}.{1}" -f $Module, $Name # Registry foreach ($hive in ($registryProperties | Where-Object { $_.PSObject.Properties.Name -like $compoundName })) { foreach ($propName in $hive.PSObject.Properties.Name) { if ($propName -in $common) { continue } if ($propName -like $compoundName) { Remove-ItemProperty -Path $hive.PSPath -Name $propName } } } # Prepare file foreach ($fileConfig in ($pathProperties | Where-Object { $_.Properties.FullName -like $compoundName })) { $fileConfig.Properties = $fileConfig.Properties | Where-Object FullName -NotLike $compoundName $fileConfig.Changed = $true } } } end { if (Test-PSFFunctionInterrupt) { return } foreach ($fileConfig in $pathProperties) { if (-not $fileConfig.Changed) { continue } if ($fileConfig.Properties) { $fileConfig.Properties | ConvertTo-Json | Set-Content -Path $fileConfig.Path -Encoding UTF8 } else { Remove-Item $fileConfig.Path } } } } function Get-PSFFeature { <# .SYNOPSIS Returns a list of all registered features. .DESCRIPTION Returns a list of all registered features. .PARAMETER Name The name to filter by. .EXAMPLE PS C:\> Get-PSFFeature Returns all features registered. #> [CmdletBinding()] param ( [string] $Name = "*" ) process { [PSFramework.Feature.FeatureHost]::Features.Values | Where-Object Name -Like $Name } } function Register-PSFFeature { <# .SYNOPSIS Registers a feature for use in the PSFramework Feature Flag System. .DESCRIPTION Registers a feature for use in the PSFramework Feature Flag System. This allows offering a common interface for enabling and disabling features on-demand. Typical use-cases: - Experimental Features - Reverting breaking behavior on a per-module basis. .PARAMETER Name The name of the feature to register. Feature names are scoped globally, so please prefix by your own module's name. .PARAMETER Description A description of the feature, so users can discover what it is about. .PARAMETER NotGlobal Disables global flags for this feature. By default, features can be enabled or disabled on a global scope. .PARAMETER NotModuleSpecific Disables module specific feature flags. By default, individual modules can override the global settings either way. This may not really be applicable for all features however. .PARAMETER Owner The name of the module owning the feature. Autodiscovery is attempted, but it is recommended to explicitly specify the owning module's name. .EXAMPLE PS C:\> Register-PSFFeature -Name 'MyModule.DividebyZeroExp' -Description 'Attempt to divide by zero' -Owner MyModule Registers the feature under its owning module and adds a nice description. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [string] $Description, [switch] $NotGlobal, [switch] $NotModuleSpecific, [string] $Owner = (Get-PSCallStack)[1].InvocationInfo.MyCommand.ModuleName ) begin { $featureObject = New-Object PSFramework.Feature.FeatureItem -Property @{ Name = $Name Owner = $Owner Global = (-not $NotGlobal) ModuleSpecific = (-not $NotModuleSpecific) Description = $Description } } process { [PSFramework.Feature.FeatureHost]::Features[$Name] = $featureObject } } function Set-PSFFeature { <# .SYNOPSIS Toggles a feature on or off. .DESCRIPTION Toggles a feature on or off. This controls the flags for optional features a module might offer. Features can be controlled globally or specific to a module that tries to consume it. Module specific settings can override global settings, if a feature supports both global and module flags. .PARAMETER Name The name of the feature to set. .PARAMETER Value The value to set it to. .PARAMETER ModuleName The module it should apply to. Specifying this parameter sets the flag only for the module specified. .EXAMPLE PS C:\> Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName SPReplicator This sets the flag for the Enable Exception Inheritance Name to $true, but only applies to the module SPReplicator. .EXAMPLE PS C:\> Set-PSFFeature -Name 'MyModule.Feierabend' -Value $true This enables the global flag for the MyModule.Feierabend feature. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidateSet(TabCompletion = 'PSFramework.Feature.Name')] [string] $Name, [Parameter(Mandatory = $true)] [bool] $Value, [string] $ModuleName ) process { foreach ($featureItem in $Name) { if ($ModuleName) { [PSFramework.Feature.FeatureHost]::WriteModuleFlag($ModuleName, $Name, $Value) } else { [PSFramework.Feature.FeatureHost]::WriteGlobalFlag($Name, $Value) } } } } function Test-PSFFeature { <# .SYNOPSIS Tests whether a given feature has been enabled. .DESCRIPTION Tests whether a given feature has been enabled. Use this within the feature-owning module to determine, whether a feature should be enabled or not. .PARAMETER Name The feature to test for. .PARAMETER ModuleName The name of the module that seeks to use the feature. Must be specified in order to determine module-specific flags. .EXAMPLE PS C:\> Test-PSFFeature -Name PSFramework.InheritEnableException -ModuleName SPReplicator Tests whether the module SPReplicator has enabled the Enable Exception Inheritance feature. #> [OutputType([bool])] [CmdletBinding()] param ( [PsfValidateSet(TabCompletion = 'PSFramework.Feature.Name')] [parameter(Mandatory = $true)] [string] $Name, [string] $ModuleName ) begin { $featureItem = Get-PSFFeature -Name $Name } process { if (-not $featureItem.Global) { [PSFramework.Feature.FeatureHost]::ReadModuleFlag($Name, $ModuleName) } else { [PSFramework.Feature.FeatureHost]::ReadFlag($Name, $ModuleName) } } } function Get-PSFUserChoice { <# .SYNOPSIS Prompts the user to choose between a set of options. .DESCRIPTION Prompts the user to choose between a set of options. Returns the index of the choice picked as a number. .PARAMETER Options The options the user may pick from. The user selects a choice by specifying the letter associated with a choice. The letter assigned to a choice is picked from the character after the first '&' in any specified string. If there is no '&', the system will automatically pick the first letter as choice letter: "This &is an example" will have the character "i" bound for the choice. "This is &an example" will have the character "a" bound for the choice. "This is an example" will have the character "T" bound for the choice. This parameter takes both strings and hashtables (in any combination). A hashtable is expected to have two properties, 'Label' and 'Help'. Label is the text shown in the initial prompt, help what the user sees when requesting help. .PARAMETER Caption The title of the question, so the user knows what it is all about. .PARAMETER Message A message to offer to the user. Be more specific about the implications of this choice. .PARAMETER DefaultChoice The index of the choice made by default. By default, the first option is selected as default choice. .EXAMPLE PS C:\> Get-PSFUserChoice -Options "1) Create a new user", "2) Disable a user", "3) Unlock an account", "4) Get a cup of coffee", "5) Exit" -Caption "User administration menu" -Message "What operation do you want to perform?" Prompts the user for what operation to perform from the set of options provided #> [OutputType([System.Int32])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object[]] $Options, [string] $Caption, [string] $Message, [int] $DefaultChoice = 0 ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $choices = @() foreach ($option in $Options) { if ($option -is [hashtable]) { $label = $option.Keys -match '^l' | Select-Object -First 1 [string]$labelValue = $option[$label] $help = $option.Keys -match '^h' | Select-Object -First 1 [string]$helpValue = $option[$help] } else { $labelValue = "$option" $helpValue = "$option" } if ($labelValue -match "&") { $choices += New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList $labelValue, $helpValue } else { $choices += New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($labelValue.Trim())", $helpValue } } } process { # Will error on one option so we just return the value 0 (which is the result of the only option the user would have) # This is for cases where the developer dynamically assembles options so that they don't need to ensure a minimum of two options. if ($Options.Count -eq 1) { return 0 } $Host.UI.PromptForChoice($Caption, $Message, $choices, $DefaultChoice) } } function Stop-PSFFunction { <# .SYNOPSIS Function that interrupts a function. .DESCRIPTION Function that interrupts a function. This function is a utility function used by other functions to reduce error catching overhead. It is designed to allow gracefully terminating a function with a warning by default and also allow opt-in into terminating errors. It also allows simple integration into loops. Note: When calling this function with the intent to terminate the calling function in non-ExceptionEnabled mode too, you need to add a return below the call. For a more detailed explanation - including commented full-scale implementation examples - see the associated help article: Get-Help about_psf_flowcontrol .PARAMETER Message A message to pass along, explaining just what the error was. .PARAMETER String A stored string to use to write the log. Used in combination with the localization component. For more details see the help on Import-PSFLocalizedString and Get-PSFLocalizedString. .PARAMETER StringValues Values to format into the localized string referred to in the -String parameter. .PARAMETER EnableException Replaces user friendly yellow warnings with bloody red exceptions of doom! Use this if you want the function to throw terminating errors you want to catch. .PARAMETER Category What category does this termination belong to? Is automatically set when passing an error record. Helps with differentiating exceptions without having to resort to text parsing. .PARAMETER ErrorRecord An option to include an inner exception in the error record (and in the exception thrown, if one is thrown). Use this, whenever you call Stop-PSFFunction in a catch block. Note: Pass the full error record, not just the exception. .PARAMETER Tag Tags to add to the message written. This allows filtering and grouping by category of message, targeting specific messages. .PARAMETER FunctionName The name of the function to crash. This parameter is very optional, since it automatically selects the name of the calling function. The function name is used as part of the errorid. That in turn allows easily figuring out, which exception belonged to which function when checking out the $error variable. .PARAMETER ModuleName The name of the module, the function to be crashed is part of. This parameter is very optional, since it automatically selects the name of the calling function. .PARAMETER File The file in which Stop-PSFFunction was called. Will be automatically set, but can be overridden when necessary. .PARAMETER Line The line on which Stop-PSFFunction was called. Will be automatically set, but can be overridden when necessary. .PARAMETER Exception Allows specifying an inner exception as input object. This will be passed on to the logging and used for messages. When specifying both ErrorRecord AND Exception, Exception wins, but ErrorRecord is still used for record metadata. .PARAMETER OverrideExceptionMessage Disables automatic appending of exception messages. Use in cases where you already have a speaking message interpretation and do not need the original message. .PARAMETER Target The object that was processed when the error was thrown. For example, if you were trying to process a Database Server object when the processing failed, add the object here. This object will be in the error record (which will be written, even in non-silent mode, just won't show it). If you specify such an object, it becomes simple to actually figure out, just where things failed at. .PARAMETER Continue This will cause the function to call continue while not running with exceptions enabled (-EnableException). Useful when mass-processing items where an error shouldn't break the loop. .PARAMETER SilentlyContinue This will cause the function to call continue while running with exceptions enabled (-EnableException). Useful when mass-processing items where an error shouldn't break the loop. .PARAMETER ContinueLabel When specifying a label in combination with "-Continue" or "-SilentlyContinue", this function will call continue with this specified label. Helpful when trying to continue on an upper level named loop. .PARAMETER Cmdlet The $PSCmdlet object of the calling command. Used to write exceptions in a more hidden manner, avoiding exposing internal script text in the default message display. .PARAMETER StepsUpward When not throwing an exception and not calling continue, Stop-PSFFunction signals the calling command to stop. In some cases you may want to signal a step or more further up the chain (notably from helper functions within a function). This parameter allows you to add additional steps up the callstack that it will notify. .EXAMPLE Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -ErrorRecord $_ return Depending on whether $EnableException is true or false it will: - Throw a bloody terminating error. Game over. - Write a nice warning about how Foo failed bar, then terminate the function. The return on the next line will then end the calling function. .EXAMPLE Stop-PSFFunction -Message "Foo failed bar!" -EnableException $EnableException -Category InvalidOperation -Target $foo -Continue Depending on whether $EnableException is true or false it will: - Throw a bloody terminating error. Game over. - Write a nice warning about how Foo failed bar, then call continue to process the next item in the loop. In both cases, the error record added to $error will have the content of $foo added, the better to figure out what went wrong. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Message', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFFunction')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Message')] [string] $Message, [Parameter(Mandatory = $true, ParameterSetName = 'String')] [string] $String, [Parameter(ParameterSetName = 'String')] [object[]] $StringValues, [bool] $EnableException, [System.Management.Automation.ErrorCategory] $Category = ([System.Management.Automation.ErrorCategory]::NotSpecified), [Alias('InnerErrorRecord')] [System.Management.Automation.ErrorRecord[]] $ErrorRecord, [string[]] $Tag, [string] $FunctionName, [string] $ModuleName, [string] $File, [int] $Line, [System.Exception] $Exception, [switch] $OverrideExceptionMessage, [object] $Target, [switch] $Continue, [switch] $SilentlyContinue, [string] $ContinueLabel, [System.Management.Automation.PSCmdlet] $Cmdlet, [int] $StepsUpward = 0 ) if ($Cmdlet) { $myCmdlet = $Cmdlet } else { $myCmdlet = $PSCmdlet } #region Initialize information on the calling command $callStack = (Get-PSCallStack)[1] if (-not $FunctionName) { $FunctionName = $callStack.Command } if (-not $FunctionName) { $FunctionName = "<Unknown>" } if (-not $ModuleName) { $ModuleName = $callstack.InvocationInfo.MyCommand.ModuleName } if (-not $ModuleName) { $ModuleName = "<Unknown>" } if (-not $File) { $File = $callStack.Position.File } if (-not $Line) { $Line = $callStack.Position.StartLineNumber } if ((Test-PSFParameterBinding -ParameterName EnableException -Not) -and (Test-PSFFeature -Name "PSFramework.InheritEnableException" -ModuleName $ModuleName)) { $EnableException = [bool]$PSCmdlet.GetVariableValue('EnableException') } #endregion Initialize information on the calling command #region Apply Transforms #region Target Transform if ($null -ne $Target) { $Target = Convert-PsfMessageTarget -Target $Target -FunctionName $FunctionName -ModuleName $ModuleName } #endregion Target Transform #region Exception Transforms if ($Exception) { $Exception = Convert-PsfMessageException -Exception $Exception -FunctionName $FunctionName -ModuleName $ModuleName } elseif ($ErrorRecord) { $int = 0 while ($int -lt $ErrorRecord.Length) { $tempException = Convert-PsfMessageException -Exception $ErrorRecord[$int].Exception -FunctionName $FunctionName -ModuleName $ModuleName if ($tempException -ne $ErrorRecord[$int].Exception) { $ErrorRecord[$int] = New-Object System.Management.Automation.ErrorRecord($tempException, $ErrorRecord[$int].FullyQualifiedErrorId, $ErrorRecord[$int].CategoryInfo.Category, $ErrorRecord[$int].TargetObject) } $int++ } } #endregion Exception Transforms #endregion Apply Transforms #region Message Handling $records = @() $paramWritePSFMessage = @{ Level = 'Warning' EnableException = $EnableException FunctionName = $FunctionName Target = $Target Tag = $Tag ModuleName = $ModuleName OverrideExceptionMessage = $true File = $File Line = $Line } if ($Message) { $paramWritePSFMessage["Message"] = $Message } else { $paramWritePSFMessage["String"] = $String $paramWritePSFMessage["StringValues"] = $StringValues } if ($ErrorRecord -or $Exception) { if ($ErrorRecord) { foreach ($record in $ErrorRecord) { if (-not $Exception) { $newException = New-Object System.Exception($record.Exception.Message, $record.Exception) } else { $newException = $Exception } if ($record.CategoryInfo.Category) { $Category = $record.CategoryInfo.Category } $records += New-Object System.Management.Automation.ErrorRecord($newException, "$($ModuleName)_$FunctionName", $Category, $Target) } } else { $records += New-Object System.Management.Automation.ErrorRecord($Exception, "$($ModuleName)_$FunctionName", $Category, $Target) } # Manage Debugging if ($EnableException) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null } else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage } } else { $exception = New-Object System.Exception($Message) $records += New-Object System.Management.Automation.ErrorRecord($Exception, "$($ModuleName)_$FunctionName", $Category, $Target) # Manage Debugging if ($EnableException) { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage 3>$null } else { Write-PSFMessage -ErrorRecord $records @paramWritePSFMessage } } #endregion Message Handling #region Silent Mode if ($EnableException) { if ($SilentlyContinue) { foreach ($record in $records) { $myCmdlet.WriteError($record) } if ($ContinueLabel) { continue $ContinueLabel } else { continue } } # Extra insurance that it'll stop $psframework_killqueue.Enqueue($callStack.InvocationInfo.GetHashCode()) # Need to use "throw" as otherwise calling function will not be interrupted without passing the cmdlet parameter if (-not $Cmdlet) { throw $records[0] } else { $Cmdlet.ThrowTerminatingError($records[0]) } } #endregion Silent Mode #region Non-Silent Mode else { # This ensures that the error is stored in the $error variable AND has its Stacktrace (simply adding the record would lack the stacktrace) foreach ($record in $records) { $null = Write-Error -Message $record -Category $Category -TargetObject $Target -Exception $record.Exception -ErrorId "$($ModuleName)_$FunctionName" -ErrorAction Continue 2>&1 } if ($Continue) { if ($ContinueLabel) { continue $ContinueLabel } else { continue } } else { # Make sure the function knows it should be stopping if ($StepsUpward -le 0) { $psframework_killqueue.Enqueue($callStack.InvocationInfo.GetHashCode()) } else { $psframework_killqueue.Enqueue((Get-PSCallStack)[($StepsUpward + 1)].InvocationInfo.GetHashCode()) } return } } #endregion Non-Silent Mode } function Test-PSFFunctionInterrupt { <# .SYNOPSIS Tests whether the calling function should be interrupted. .DESCRIPTION This helper function is designed to work in tandem with Stop-PSFFunction. When gracefully terminating a function, there is a major issue: "Return" will only stop the current one of the three blocks (Begin, Process, End). All other statements have side effects or produce lots of red text. So, Stop-PSFFunction writes a variable into the parent scope, that signals the function should cease. This function then checks for that very variable and returns true if it is set. This avoids having to handle odd variables in the parent function and causes the least impact on contributors. For a more detailed explanation - including commented full-scale implementation examples - see the associated help article: Get-Help about_psf_flowcontrol .EXAMPLE if (Test-PSFFunctionInterrupt) { return } The calling function will stop if this function returns true. #> [OutputType([System.Boolean])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFFunctionInterrupt')] Param ( ) $psframework_killqueue -contains (Get-PSCallStack)[1].InvocationInfo.GetHashCode() } function Test-PSFParameterBinding { <# .SYNOPSIS Helper function that tests, whether a parameter was bound. .DESCRIPTION Helper function that tests, whether a parameter was bound. .PARAMETER ParameterName The name(s) of the parameter that is tested for being bound. By default, the check is true when AT LEAST one was bound. .PARAMETER Not Reverses the result. Returns true if NOT bound and false if bound. .PARAMETER And All specified parameters must be present, rather than at least one of them. .PARAMETER BoundParameters The hashtable of bound parameters. Is automatically inherited from the calling function via default value. Needs not be bound explicitly. .EXAMPLE if (Test-PSFParameterBinding "Day") { } Snippet as part of a function. Will check whether the parameter "Day" was bound. If yes, whatever logic is in the conditional will be executed. .EXAMPLE Test-PSFParameterBinding -Not 'Login', 'Spid', 'ExcludeSpid', 'Host', 'Program', 'Database' Returns whether none of the parameters above were specified. .EXAMPLE Test-PSFParameterBinding -And 'Login', 'Spid', 'ExcludeSpid', 'Host', 'Program', 'Database' Returns whether any of the specified parameters was not bound #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFParameterBinding')] Param ( [Parameter(Mandatory = $true, Position = 0)] [string[]] $ParameterName, [Alias('Reverse')] [switch] $Not, [switch] $And, [object] $BoundParameters = (Get-PSCallStack)[0].InvocationInfo.BoundParameters ) if ($And) { $test = $true } else { $test = $false } foreach ($name in $ParameterName) { if ($And) { if (-not $BoundParameters.ContainsKey($name)) { $test = $false } } else { if ($BoundParameters.ContainsKey($name)) { $test = $true } } } return ((-not $Not) -eq $test) } if (-not (Test-Path Alias:Was-Bound)) { Set-Alias -Value Test-PSFParameterBinding -Name Was-Bound -Scope Global } function Test-PSFPowerShell { <# .SYNOPSIS Tests for conditions in the PowerShell environment. .DESCRIPTION This helper command can evaluate various runtime conditions, such as: - PowerShell Version - PowerShell Edition - Operating System - Elevation This makes it easier to do conditional code. It also makes it easier to simulate code-paths during pester tests, by mocking this command. .PARAMETER PSMinVersion PowerShell must be running under at least this version. .PARAMETER PSMaxVersion PowerShell most not be runnign on a version higher than this. .PARAMETER Edition PowerShell must be running in the specifioed edition (Core or Desktop) .PARAMETER OperatingSystem PowerShell must be running on the specified OS. .PARAMETER Elevated PowerShell must be running with elevation. Note: This test is only supported on Windows. On other OS it will automatically succede and assume root privileges. .EXAMPLE PS C:\> Test-PSFPowerShell -PSMinVersion 5.0 Will return $false, unless the executing powershell version is at least 5.0 .EXAMPLE PS C:\> Test-PSFPowerShell -Edition Core Will return $true, if the current powershell session is a PowerShell core session. .EXAMPLE PS C:\> Test-PSFPowerShell -Elevated Will return $false if on windows and not running as admin. Will return $true otherwise. .EXAMPLE PS C:\> Test-PSFPowerShell -PSMinVersion 6.1 -OperatingSystem Windows Will return $false unless executed on a PowerShell 6.1 console running on windows. #> [OutputType([System.Boolean])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFPowerShell')] param ( [Version] $PSMinVersion, [Version] $PSMaxVersion, [PSFramework.FlowControl.PSEdition] $Edition, [PSFramework.FlowControl.OperatingSystem] [Alias('OS')] $OperatingSystem, [switch] $Elevated ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug','start','param' } process { #region PS Version Test if ($PSMinVersion -and ($PSMinVersion -ge $PSVersionTable.PSVersion)) { return $false } if ($PSMaxVersion -and ($PSMaxVersion -le $PSVersionTable.PSVersion)) { return $false } #endregion PS Version Test #region PS Edition Test if ($Edition -like "Desktop") { if ($PSVersionTable.PSEdition -eq "Core") { return $false } } if ($Edition -like "Core") { if ($PSVersionTable.PSEdition -ne "Core") { return $false } } #endregion PS Edition Test #region OS Test if ($OperatingSystem) { switch ($OperatingSystem) { "MacOS" { if ($PSVersionTable.PSVersion.Major -lt 6) { return $false } if (-not $IsMacOS) { return $false } } "Linux" { if ($PSVersionTable.PSVersion.Major -lt 6) { return $false } if (-not $IsLinux) { return $false } } "Windows" { if (($PSVersionTable.PSVersion.Major -ge 6) -and (-not $IsWindows)) { return $false } } } } #endregion OS Test #region Elevation if ($Elevated) { if (($PSVersionTable.PSVersion.Major -lt 6) -or ($IsWindows)) { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal $identity if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { return $false } } } #endregion Elevation return $true } end { } } function Import-PSFCmdlet { <# .SYNOPSIS Loads a cmdlet into the current context. .DESCRIPTION Loads a cmdlet into the current context. This can be used to register a cmdlet during module import, making it easy to have hybrid modules publishing both cmdlets and functions. Can also be used to register cmdlets written in PowerShell classes. .PARAMETER Name The name of the cmdlet to register. .PARAMETER Type The type of the class implementing the cmdlet. .PARAMETER HelpFile Path to the help XML containing the help for the cmdlet. .PARAMETER Module Module to inject the cmdlet into. .EXAMPLE PS C:\> Import-PSFCmdlet -Name Get-Something -Type ([GetSomethingCommand]) Imports the Get-Something cmdlet into the current context. .EXAMPLE PS C:\> Import-PSFCmdlet -Name Get-Something -Type ([GetSomethingCommand]) -Module (Get-Module PSReadline) Imports the Get-Something cmdlet into the PSReadline module. .NOTES Original Author: Chris Dent Link: https://www.indented.co.uk/cmdlets-without-a-dll/ #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFCmdlet')] param ( [Parameter(Mandatory = $true)] [String] $Name, [Parameter(Mandatory = $true)] [Type] $Type, [string] $HelpFile, [System.Management.Automation.PSModuleInfo] $Module ) begin { $scriptBlock = { param ( [String] $Name, [Type] $Type, [string] $HelpFile ) $sessionStateCmdletEntry = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry( $Name, $Type, $HelpFile ) # System.Management.Automation.Runspaces.LocalPipeline will let us get at ExecutionContext. # Note: $ExecutionContext is *not* an instance of this object. $pipelineType = [PowerShell].Assembly.GetType('System.Management.Automation.Runspaces.LocalPipeline') $method = $pipelineType.GetMethod( 'GetExecutionContextFromTLS', [System.Reflection.BindingFlags]'Static,NonPublic' ) # Invoke the method to get an instance of ExecutionContext. $context = $method.Invoke( $null, [System.Reflection.BindingFlags]'Static,NonPublic', $null, $null, (Get-Culture) ) # Get the SessionStateInternal type $internalType = [PowerShell].Assembly.GetType('System.Management.Automation.SessionStateInternal') # Get a valid constructor which accepts a param of type ExecutionContext $constructor = $internalType.GetConstructor( [System.Reflection.BindingFlags]'Instance,NonPublic', $null, $context.GetType(), $null ) # Get the SessionStateInternal for this execution context $sessionStateInternal = $constructor.Invoke($context) # Get the method which allows Cmdlets to be added to the session $method = $internalType.GetMethod( 'AddSessionStateEntry', [System.Reflection.BindingFlags]'Instance,NonPublic', $null, $sessionStateCmdletEntry.GetType(), $null ) # Invoke the method. $method.Invoke($sessionStateInternal, $sessionStateCmdletEntry) } } process { if (-not $Module) { $scriptBlock.Invoke($Name, $Type, $HelpFile) } else { $Module.Invoke($scriptBlock, @($Name, $Type, $HelpFile)) } } } function Register-PSFParameterClassMapping { <# .SYNOPSIS Registers types to a parameter classes input interpretation. .DESCRIPTION The parameter classes shipped in PSFramework can be extended to support input of an unknown object type. In order to accomplish that, it is necessary to register the name of that type (and the properties to use) using this command. On input interpretation, it will check the TypeNames property on the PSObject for evaluation. This means you can also specify custom PSObjects by giving them a dedicated name. .PARAMETER ParameterClass The parameter class to extend. .PARAMETER TypeName The name of the type to register. .PARAMETER Properties The properties to check. When processing an object of this type, it will try to access the properties in this order, trying to make something fit the intended result. The first property that is a fit for the parameter class is chosen, other ones are ignored. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Register-PSFParameterClassMapping -ParameterClass 'Computer' -TypeName 'microsoft.activedirectory.management.adcomputer' -Properties 'DNSHostName', 'Name' This extends the computer parameter class by ... - having it accept the type 'microsoft.activedirectory.management.adcomputer' - having it use the 'DNSHostName' property if available, falling back to 'Name' if necessary #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFParameterClassMapping')] Param ( [Parameter(Mandatory = $true)] [PSFramework.Parameter.ParameterClasses] $ParameterClass, [Parameter(Mandatory = $true)] [string] $TypeName, [Parameter(Mandatory = $true)] [string[]] $Properties, [switch] $EnableException ) begin { } process { try { switch ($ParameterClass) { "Computer" { [PSFramework.Parameter.ComputerParameter]::SetTypePropertyMapping($TypeName.ToLower(), $Properties) } "DateTime" { [PSFramework.Parameter.DateTimeParameter]::SetTypePropertyMapping($TypeName.ToLower(), $Properties) } "TimeSpan" { [PSFramework.Parameter.TimeSpanParameter]::SetTypePropertyMapping($TypeName.ToLower(), $Properties) } "Encoding" { [PSFramework.Parameter.EncodingParameter]::SetTypePropertyMapping($TypeName.ToLower(), $Properties) } default { Stop-PSFFunction -Message "Support for the $ParameterClass parameter class has not yet been added!" -EnableException $EnableException -Tag 'fail', 'argument' -Category NotImplemented return } } } catch { Stop-PSFFunction -Message "Failed to update property mapping for $ParameterClass : $Typename. This is likely happening on some Linux distributions due to an underlying .NET issue and means the parameter class cannot be used." -EnableException $EnableException -Tag 'fail', '.NET' -ErrorRecord $_ return } } end { } } function Set-PSFTypeAlias { <# .SYNOPSIS Registers or updates an alias for a .NET type. .DESCRIPTION Registers or updates an alias for a .NET type. Use this function during module import to create shortcuts for typenames users can be expected to interact with directly. .PARAMETER AliasName The short and useful alias for the type. .PARAMETER TypeName The full name of the type. Example: 'System.IO.FileInfo' .PARAMETER Mapping A hashtable of alias to typename mappings. Useful to registering a full set of type aliases. .EXAMPLE PS C:\> Set-PSFTypeAlias -AliasName 'file' -TypeName 'System.IO.File' Creates an alias for the type 'System.IO.File' named 'file' .EXAMPLE PS C:\> Set-PSFTypeAlias -Mapping @{ file = 'System.IO.File' path = 'System.IO.Path' } Creates an alias for the type 'System.IO.File' named 'file' Creates an alias for the type 'System.IO.Path' named 'path' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Name', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTypeAlias')] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Name')] [string] $AliasName, [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'Name')] [string] $TypeName, [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Hashtable')] [hashtable] $Mapping ) begin { # Obtain a reference to the TypeAccelerators type $TypeAcceleratorType = [psobject].Assembly.GetType("System.Management.Automation.TypeAccelerators") } process { foreach ($key in $Mapping.Keys) { $TypeAcceleratorType::Add($key, $Mapping[$key]) } if ($AliasName) { $TypeAcceleratorType::Add($AliasName, $TypeName) } } end { } } function Get-PSFLicense { <# .SYNOPSIS Returns registered licenses .DESCRIPTION Returns all matching licenses from the PSFramework internal license cache. .PARAMETER Filter Default: "*" Filters for the name of the product. Uses the -like operator. .PARAMETER ProductType Only licenses of products for any of the specified types are considered. .PARAMETER LicenseType Only licenses of any matching type are returned. .PARAMETER Manufacturer Default: "*" Only licenses for products of a matching manufacturer are returned. Uses the -like operator for comparisons. .EXAMPLE PS C:\> Get-PSFLicense *Microsoft* Returns all registered licenses for products with the string "Microsoft" in their name .EXAMPLE PS C:\> Get-PSFLicense -LicenseType Commercial -ProductType Library Returns a list of all registered licenses for products that have commercial licenses and are libraries. #> [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFLicense')] [OutputType([PSFramework.License.License])] param ( [Parameter(Position = 0)] [Alias('Product')] [String] $Filter = "*", [PSFramework.License.ProductType[]] $ProductType, [PSFramework.License.LicenseType] $LicenseType, [String] $Manufacturer = "*" ) [PSFramework.License.LicenseHost]::Get() | Where-Object { if ($_.Product -notlike $Filter) { return $false } if ($_.Manufacturer -notlike $Manufacturer) { return $false } if ($ProductType -and ($_.ProductType -notin $ProductType)) { return $false } if ($licenseType -and -not ($_.LicenseType -band $LicenseType)) { return $false } return $true } } function New-PSFLicense { <# .SYNOPSIS Creates a new license object and registers it .DESCRIPTION This function creates a new license object used by the PSFramework licensing component. The license is automatically registered in the current process' license store. .PARAMETER Product The product that is being licensed .PARAMETER Manufacturer The entity that produced the licensed product .PARAMETER ProductVersion The version of the licensed product .PARAMETER ProductType What kind of product is te license for? Options: Module, Script, Library, Application, Other .PARAMETER Name Most licenses used have a name. Feel free to register that name as well. .PARAMETER Version What version is the license? .PARAMETER Date When was the product licensed with the registered license .PARAMETER Type Default: Free This shows the usual limitations that apply to this license. By Default, licenses are considered free and may be modified, but require attribution when used in your own product. .PARAMETER Text The full text of your license. .PARAMETER Description A description of the content. Useful when describing how some license is used within your own product. .PARAMETER Parent The license of the product within which the product of this license is used. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> New-PSFLicense -Product 'Awesome Test Product' -Manufacturer 'Awesome Inc.' -ProductVersion '1.0.1.0' -ProductType Application -Name FreeBSD -Version "3.0.0.0" -Date (Get-Date -Year 2016 -Month 11 -Day 28 -Hour 0 -Minute 0 -Second 0) -Text @" Copyright (c) 2016, Awesome Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. "@ This registers the Awesome Test Product as licensed under the common FreeBSD license. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFLicense')] [OutputType([PSFramework.License.License])] param ( [Parameter(Mandatory = $true)] [String] $Product, [String] $Manufacturer = "ACME ltd.", [Version] $ProductVersion = "1.0.0.0", [Parameter(Mandatory = $true)] [PSFramework.License.ProductType] $ProductType, [String] $Name = "Unknown", [Version] $Version = "1.0.0.0", [DateTime] $Date = (Get-Date -Year 1989 -Month 10 -Day 3 -Hour 0 -Minute 0 -Second 0), [PSFramework.License.LicenseType] $Type = "Free", [Parameter(Mandatory = $true)] [String] $Text, [string] $Description, [PSFramework.License.License] $Parent ) # Create and fill object $license = New-Object PSFramework.License.License -Property @{ Product = $Product Manufacturer = $Manufacturer ProductVersion = $ProductVersion ProductType = $ProductType LicenseName = $Name LicenseVersion = $Version LicenseDate = $Date LicenseType = $Type LicenseText = $Text Description = $Description Parent = $Parent } if ($PSCmdlet.ShouldProcess("$($license.Product) $($license.ProductVersion) ($($license.LicenseName))", "Create License")) { if (-not ([PSFramework.License.LicenseHost]::Get($license))) { [PSFramework.License.LicenseHost]::Add($license) } return $license } } function Remove-PSFLicense { <# .SYNOPSIS Removes a registered license from the license store .DESCRIPTION Removes a registered license from the license store .PARAMETER License The license to remove .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Get-PSFLicense "FooBar" | Remove-PSFLicense Removes the license for the product "FooBar" from the license store. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Remove-PSFLicense')] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSFramework.License.License[]] $License, [switch] $EnableException ) Begin { } Process { foreach ($l in $License) { if ($PSCmdlet.ShouldProcess("$($l.Product) $($l.ProductVersion) ($($l.LicenseName))", "Remove License")) { try { [PSFramework.License.LicenseHost]::Remove($l) } catch { Stop-PSFFunction -Message "Failed to remove license" -ErrorRecord $_ -EnableException $EnableException -Target $l -Continue } } } } End { } } function Get-PSFLocalizedString { <# .SYNOPSIS Returns the localized strings of a module. .DESCRIPTION Returns the localized strings of a module. By default, it creates a variable that has access to each localized string in the module (with string name as propertyname). Alternatively, by specifying a specific string, that string can instead be returned. .PARAMETER Module The name of the module to map. .PARAMETER Name The name of the string to return .EXAMPLE PS C:\> Get-PSFLocalizedString -Module 'MyModule' Returns an object that can be used to access any localized string. .EXAMPLE PS C:\> Get-PSFLocalizedString -Module 'MyModule' -Name 'ErrorValidation' Returns the string for the module 'MyModule' that is stored under the 'ErrorValidation' name. #> [OutputType([PSFramework.Localization.LocalStrings], ParameterSetName = 'Default')] [OutputType([System.String], ParameterSetName = 'Name')] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [string] $Module, [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name ) process { switch ($PSCmdlet.ParameterSetName) { 'Default' { New-Object PSFramework.Localization.LocalStrings($Module) } 'Name' { (New-Object PSFramework.Localization.LocalStrings($Module)).$Name } } } } function Import-PSFLocalizedString { <# .SYNOPSIS Imports a set of localized strings from a PowerShell data file. .DESCRIPTION Imports a set of localized strings from a PowerShell data file. This is used to feed the localized string feature set. Always import for all languages, do not select by current language - the system handles language selection. Strings are process wide, so loading additional languages can be offloaded into a background task. .PARAMETER Path The path to the psd1 file to import as strings file. .PARAMETER Module The module for which to import the strings. .PARAMETER Language The language of the specific strings file. Defaults to en-US. .EXAMPLE PS C:\> Import-PSFLocalizedString -Path '$moduleRoot\strings.psd1' -Module 'MyModule' Imports the strings stored in strings.psd1 for the module MyModule as 'en-US' language strings. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $Module, #[PsfValidateSet(TabCompletion = 'PSFramework-LanguageNames')] [string] $Language = 'en-US' ) begin { try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem } catch { Stop-PSFFunction -Message "Failed to resolve path: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ } } process { foreach ($pathItem in $resolvedPath) { $data = Import-PowerShellDataFile -Path $pathItem foreach ($key in $data.Keys) { [PSFramework.Localization.LocalizationHost]::Write($Module, $key, $Language, $data[$key]) } } } } function Get-PSFLoggingProvider { <# .SYNOPSIS Returns a list of the registered logging providers. .DESCRIPTION Returns a list of the registered logging providers. Those are used to log messages to whatever system they are designed to log to. PSFramework ships with a few default logging providers. Custom logging destinations can be created by implementing your own, custom provider and registering it using Register-PSFLoggingProvider. .PARAMETER Name Default: '*' The name to filter by .EXAMPLE PS C:\> Get-PSFLoggingProvider Returns all logging provider .EXAMPLE PS C:\> Get-PSFLoggingProvider -Name filesystem Returns the filesystem provider #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFLoggingProvider')] Param ( [Alias('Provider', 'ProviderName')] [string] $Name = "*" ) begin { } process { [PSFramework.Logging.ProviderHost]::Providers.Values | Where-Object Name -Like $Name } end { } } function Install-PSFLoggingProvider { <# .SYNOPSIS Installs a logging provider for the PSFramework. .DESCRIPTION This command installs a logging provider registered with the PSFramework. Some providers may require special installation steps, that cannot be handled by the common initialization / configuration. For example, a provider may require installation of binaries that require elevation. In order to cover those scenarios, a provider can include an installation script, which is called by this function. It can also provide additional parameters to this command, which are dynamically provided once the -Name parameter has been passed. When registering the logging provider (Using Register-PSFLoggingProvider), you can specify the logic executed by this command with these parameters: - IsInstalledScript : Must return $true when installation has already been performed. If this returns not $false, then this command will do nothing at all. - InstallationScript : The script performing the actual installation - InstallationParameters : A script that returns dynamic parameters. This can be used to generate additional parameters that can modify the installation process. NOTE: This module does not contain help/guidance on how to generate dynamic parameters! .PARAMETER Name The name of the provider to install .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Install-PSFLoggingProvider -Name Eventlog Installs a logging provider named 'eventlog' #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Install-PSFLoggingProvider')] Param ( [Alias('Provider', 'ProviderName')] [string] $Name, [switch] $EnableException ) dynamicparam { if ($Name -and ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name.ToLower()))) { [PSFramework.Logging.ProviderHost]::Providers[$Name.ToLower()].InstallationParameters.Invoke() } } begin { } process { if (Test-PSFFunctionInterrupt) { return } if (-not ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name.ToLower()))) { Stop-PSFFunction -Message "Provider $Name not found!" -EnableException $EnableException -Category InvalidArgument -Target $Name -Tag 'logging', 'provider', 'install' return } $provider = [PSFramework.Logging.ProviderHost]::Providers[$Name.ToLower()] if (-not ([System.Management.Automation.ScriptBlock]::Create($provider.IsInstalledScript).Invoke())) { try { [System.Management.Automation.ScriptBlock]::Create($provider.InstallationScript).Invoke() } catch { Stop-PSFFunction -Message "Failed to install provider '$Name'" -EnableException $EnableException -Target $Name -ErrorRecord $_ -Tag 'logging', 'provider', 'install' return } } } end { if (Test-PSFFunctionInterrupt) { return } } } function Register-PSFLoggingProvider { <# .SYNOPSIS Registers a new logging provider to the PSFramework logging system. .DESCRIPTION This function registers all components of the PSFramework logging provider systems. It allows you to define your own logging destination and configuration and tie them into the default logging system. In order to properly utilize its power, it becomes necessary to understand how the logging works beneath the covers: - On Start of the logging script, it runs a one-time scriptblock per enabled provider (this will also occur when later enabling a provider) - Thereafter the script will continue, logging in cycles of Start > Log all Messages > Log all Errors > End Each of those steps has its own event, allowing for fine control over what happens where. - Finally, on shutdown of a provider it again offers an option to execute some code (to dispose/free resources in use) All providers share the same scope for the execution of ALL of those actions/scriptblocks! This makes it important to give your variables/functions a unique name, in order to avoid conflicts. General Guideline: - All variables should start with the name of the provider and an underscore. Example: $filesystem_root - All functions should use the name of the provider as prefix. Example: Clean-FileSystemErrorXml A simple implementation example can be seen with the FileSystem provider, stored in the module folder: internal/loggingProvider/filesystem.provider.ps1 .PARAMETER Name A unique name for your provider. Registering a provider under a name already registered, NOTHING will happen. This function will instead silently terminate. .PARAMETER Enabled Setting this will enable the provider on registration. .PARAMETER RegistrationEvent Scriptblock that should be executed on registration. This allows you to perform installation actions synchronously, with direct user interaction. At the same time, by adding it as this parameter, it will only performed on the initial registration, rather than every time the provider is registered (runspaces, Remove-Module/Import-Module) .PARAMETER BeginEvent The actions that should be taken once when setting up the logging. Can well be used to register helper functions or loading other resources that should be loaded on start only. .PARAMETER StartEvent The actions taken at the beginning of each logging cycle. Typically used to establish connections or do some necessary pre-connections. .PARAMETER MessageEvent The actions taken to process individual messages. The very act of writing logs. This scriptblock receives a message object (As returned by Get-PSFMessage) as first and only argument. Under some circumstances, this message may be a $null object, your scriptblock must be able to handle this. .PARAMETER ErrorEvent The actions taken to process individual error messages. The very act of writing logs. This scriptblock receives a message object (As returned by 'Get-PSFMessage -Errors') as first and only argument. Under some circumstances, this message may be a $null object, your scriptblock must be able to handle this. This consists of complex, structured data and may not be suitable to all logging formats. However all errors are ALWAYS accompanied by a message, making integrating this optional. .PARAMETER EndEvent Actions taken when finishing up a logging cycle. Can be used to close connections. .PARAMETER FinalEvent Final action to take when the logging terminates. This should release all resources reserved. This event will fire when: - The console is being closed - The logging script is stopped / killed - The logging provider is disabled .PARAMETER ConfigurationParameters The function Set-PSFLoggingProvider can be used to configure this logging provider. Using this parameter it is possible to register dynamic parameters when configuring your provider. .PARAMETER ConfigurationScript When using Set-PSFLoggingProvider, this script can be used to input given by the dynamic parameters generated by the -ConfigurationParameters parameter. .PARAMETER IsInstalledScript A scriptblock verifying that all prerequisites are properly installed. .PARAMETER InstallationScript A scriptblock performing the installation of the provider's prerequisites. Used by Install-PSFProvider in conjunction with the script provided by -InstallationParameters .PARAMETER InstallationParameters A scriptblock returning dynamic parameters that are offered when running Install-PSFprovider. Those can then be used by the installation scriptblock specified in the aptly named '-InstallationScript' parameter. .PARAMETER ConfigurationSettings This is executed before actually registering the scriptblock. It allows you to include any logic you wish, but it is specifically designed for configuration settings using Set-PSFConfig with the '-Initialize' parameter. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE Register-PSFLoggingProvider -Name "filesystem" -Enabled $true -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings Registers the filesystem provider, providing events for every single occasion. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFLoggingProvider')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [switch] $Enabled, [System.Management.Automation.ScriptBlock] $RegistrationEvent, [System.Management.Automation.ScriptBlock] $BeginEvent = { }, [System.Management.Automation.ScriptBlock] $StartEvent = { }, [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $MessageEvent, [System.Management.Automation.ScriptBlock] $ErrorEvent = { }, [System.Management.Automation.ScriptBlock] $EndEvent = { }, [System.Management.Automation.ScriptBlock] $FinalEvent = { }, [System.Management.Automation.ScriptBlock] $ConfigurationParameters = { }, [System.Management.Automation.ScriptBlock] $ConfigurationScript = { }, [System.Management.Automation.ScriptBlock] $IsInstalledScript = { $true }, [System.Management.Automation.ScriptBlock] $InstallationScript = { }, [System.Management.Automation.ScriptBlock] $InstallationParameters = { }, [System.Management.Automation.ScriptBlock] $ConfigurationSettings, [switch] $EnableException ) if ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name.ToLower())) { return } if ($ConfigurationSettings) { . $ConfigurationSettings } if (Test-PSFParameterBinding -ParameterName Enabled) { Set-PSFConfig -FullName "LoggingProvider.$Name.Enabled" -Value $Enabled.ToBool() -DisableHandler } $provider = New-Object PSFramework.Logging.Provider $provider.Name = $Name $provider.BeginEvent = $BeginEvent $provider.StartEvent = $StartEvent $provider.MessageEvent = $MessageEvent $provider.ErrorEvent = $ErrorEvent $provider.EndEvent = $EndEvent $provider.FinalEvent = $FinalEvent $provider.ConfigurationParameters = $ConfigurationParameters $provider.ConfigurationScript = $ConfigurationScript $provider.IsInstalledScript = $IsInstalledScript $provider.InstallationScript = $InstallationScript $provider.InstallationParameters = $InstallationParameters $provider.IncludeModules = Get-PSFConfigValue -FullName "LoggingProvider.$Name.IncludeModules" -Fallback @() $provider.ExcludeModules = Get-PSFConfigValue -FullName "LoggingProvider.$Name.ExcludeModules" -Fallback @() $provider.IncludeTags = Get-PSFConfigValue -FullName "LoggingProvider.$Name.IncludeTags" -Fallback @() $provider.ExcludeTags = Get-PSFConfigValue -FullName "LoggingProvider.$Name.ExcludeTags" -Fallback @() $provider.InstallationOptional = Get-PSFConfigValue -FullName "LoggingProvider.$Name.InstallOptional" -Fallback $false [PSFramework.Logging.ProviderHost]::Providers[$Name.ToLower()] = $provider try { if ($RegistrationEvent) { . $RegistrationEvent } } catch { $dummy = $null $null = [PSFramework.Logging.ProviderHost]::Providers.TryRemove($Name.ToLower(), [ref] $dummy) Stop-PSFFunction -Message "Failed to register logging provider '$Name' - Registration event failed." -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'register' return } $shouldEnable = Get-PSFConfigValue -FullName "LoggingProvider.$Name.Enabled" -Fallback $false $isInstalled = [System.Management.Automation.ScriptBlock]::Create($provider.IsInstalledScript).Invoke() if (-not $isInstalled -and (Get-PSFConfigValue -FullName "LoggingProvider.$Name.AutoInstall" -Fallback $false)) { try { Install-PSFLoggingProvider -Name $Name -EnableException } catch { if ($provider.InstallationOptional) { Write-PSFMessage -Level Warning -Message "Failed to install logging provider '$Name'" -ErrorRecord $_ -Tag 'logging', 'provider', 'fail', 'install' -EnableException $EnableException } else { Stop-PSFFunction -Message "Failed to install logging provider '$Name'" -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'install' return } } } if ($shouldEnable) { if ($isInstalled) { $provider.Enabled = $true } else { Stop-PSFFunction -Message "Failed to enable logging provider $Name on registration! It was not recognized as installed. Consider running 'Install-PSFLoggingProvider' to properly install the prerequisites." -ErrorRecord $_ -EnableException $EnableException -Tag 'logging', 'provider', 'fail', 'install' return } } } function Set-PSFLoggingProvider { <# .SYNOPSIS Configures a logging provider. .DESCRIPTION This command allows configuring the way a logging provider works. This grants the ability to ... - Enable / Disable a provider - Set additional parameters defined by the provider (each provider may implement its own settings, exposed through dynamic parameters) - Configure filters about what messages get sent to a given provider. .PARAMETER Name The name of the provider to configure .PARAMETER Enabled Whether the provider should be enabled or disabled. .PARAMETER IncludeModules Only messages from modules listed here will be logged. Exact match only, an empty list results in all modules being logged. .PARAMETER ExcludeModules Messages from excluded modules will not be logged using this provider. Overrides -IncludeModules in case of overlap. .PARAMETER IncludeTags Only messages containing the listed tags will be logged. Exact match only, only a single match is required for a message to qualify. .PARAMETER ExcludeTags Messages containing any of the listed tags will not be logged. Overrides -IncludeTags in case of overlap. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Set-PSFLoggingProvider -Name filesystem -Enabled $false Disables the filesystem provider. .EXAMPLE PS C:\> Set-PSFLoggingProvider -Name filesystem -ExcludeModules "PSFramework" Prevents all messages from the PSFramework module to be logged to the file system #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFLoggingProvider')] Param ( [Alias('Provider', 'ProviderName')] [string] $Name, [bool] $Enabled, [string[]] $IncludeModules, [string[]] $ExcludeModules, [string[]] $IncludeTags, [string[]] $ExcludeTags, [switch] $EnableException ) dynamicparam { if ($Name -and ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name.ToLower()))) { [scriptblock]::Create(([PSFramework.Logging.ProviderHost]::Providers[$Name.ToLower()].ConfigurationParameters)).Invoke() } } begin { if (-not ([PSFramework.Logging.ProviderHost]::Providers.ContainsKey($Name.ToLower()))) { Stop-PSFFunction -Message "Provider $Name not found!" -EnableException $EnableException -Category InvalidArgument -Target $Name return } [PSFramework.Logging.Provider]$provider = [PSFramework.Logging.ProviderHost]::Providers[$Name.ToLower()] if ((-not $provider.Enabled) -and (-not ([scriptblock]::Create($provider.IsInstalledScript).Invoke())) -and $Enabled) { Stop-PSFFunction -Message "Provider $Name not installed! Run 'Install-PSFLoggingProvider' first" -EnableException $EnableException -Category InvalidOperation -Target $Name return } } process { if (Test-PSFFunctionInterrupt) { return } # Recreating the scriptblock this way ensures that it can properly inherit the variables in the current scope [System.Management.Automation.ScriptBlock]::Create($provider.ConfigurationScript).Invoke() #region Filter Configuration if (Test-PSFParameterBinding -ParameterName "IncludeModules") { $provider.IncludeModules = $IncludeModules Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).IncludeModules" -Value $IncludeModules } if (Test-PSFParameterBinding -ParameterName "ExcludeModules") { $provider.ExcludeModules = $ExcludeModules Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).ExcludeModules" -Value $ExcludeModules } if (Test-PSFParameterBinding -ParameterName "IncludeTags") { $provider.IncludeTags = $IncludeTags Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).IncludeTags" -Value $IncludeTags } if (Test-PSFParameterBinding -ParameterName "ExcludeTags") { $provider.ExcludeTags = $ExcludeTags Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).ExcludeTags" -Value $ExcludeTags } #endregion Filter Configuration if (Test-PSFParameterBinding -ParameterName "Enabled") { $provider.Enabled = $Enabled Set-PSFConfig -FullName "LoggingProvider.$($provider.Name).Enabled" -Value $Enabled } } end { if (Test-PSFFunctionInterrupt) { return } } } function Get-PSFMessage { <# .SYNOPSIS Returns log entries for the PSFramework .DESCRIPTION Returns log entries for the PSFramework. Handy when debugging or developing a script using it. .PARAMETER FunctionName Default: "*" Only messages written by similar functions will be returned. .PARAMETER ModuleName Default: "*" Only messages written by commands from similar modules will be returned. .PARAMETER Target Only messages handling the specified target will be returned. .PARAMETER Tag Only messages containing one of these tags will be returned. .PARAMETER Last Only messages written by the last X executions will be returned. Uses Get-History to determine execution. Ignores Get-PSFmessage commands. By default, this will also include messages from other runspaces. If your command executes in parallel, that's useful. If it doesn't and you were offloading executions to other runspaces, consider also filtering by runspace using '-Runspace' .PARAMETER Skip How many executions to skip when specifying '-Last'. Has no effect without the '-Last' parameter. .PARAMETER Runspace The guid of the runspace to return messages from. By default, messages from all runspaces are returned. Run the following line to see the list of guids: Get-Runspace | ft Id, Name, InstanceId -Autosize .PARAMETER Level Limit the message selection by level. Message levels have a numeric value, making it easier to select a range: -Level (1..6) Will select the first 6 levels (Critical - SomewhatVerbose). .PARAMETER Errors Instead of log entries, the error entries will be retrieved .EXAMPLE Get-PSFMessage Returns all log entries currently in memory. .EXAMPLE Get-PSFMessage -Target "a" -Last 1 -Skip 1 Returns all log entries that targeted the object "a" in the second last execution sent. .EXAMPLE Get-PSFMessage -Tag "fail" -Last 5 Returns all log entries within the last 5 executions that contained the tag "fail" #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFMessage')] param ( [string] $FunctionName = "*", [string] $ModuleName = "*", [AllowNull()] $Target, [string[]] $Tag, [int] $Last, [int] $Skip = 0, [guid] $Runspace, [PSFramework.Message.MessageLevel[]] $Level, [switch] $Errors ) begin { } process { if ($Errors) { $messages = [PSFramework.Message.LogHost]::GetErrors() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } } else { $messages = [PSFramework.Message.LogHost]::GetLog() | Where-Object { ($_.FunctionName -like $FunctionName) -and ($_.ModuleName -like $ModuleName) } } if (Test-PSFParameterBinding -ParameterName Target) { $messages = $messages | Where-Object TargetObject -EQ $Target } if (Test-PSFParameterBinding -ParameterName Tag) { $messages = $messages | Where-Object { $_.Tags | Where-Object { $_ -in $Tag } } } if (Test-PSFParameterBinding -ParameterName Runspace) { $messages = $messages | Where-Object Runspace -EQ $Runspace } if (Test-PSFParameterBinding -ParameterName Last) { $history = Get-History | Where-Object CommandLine -NotLike "Get-PSFMessage*" | Select-Object -Last $Last -Skip $Skip $start = $history[0].StartExecutionTime $end = $history[-1].EndExecutionTime $messages = $messages | Where-Object { ($_.Timestamp -gt $start) -and ($_.Timestamp -lt $end) -and ($_.Runspace -eq ([System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId))} } if (Test-PSFParameterBinding -ParameterName Level) { $messages = $messages | Where-Object Level -In $Level } return $messages } end { } } function Get-PSFMessageLevelModifier { <# .SYNOPSIS Returns all registered message level modifiers with similar name. .DESCRIPTION Returns all registered message level modifiers with similar name. Message level modifiers are created using New-PSFMessageLevelModifier and allow dynamically modifying the actual message level written by commands. .PARAMETER Name Default: "*" A name filter - only commands that are similar to the filter will be returned. .EXAMPLE PS C:\> Get-PSFMessageLevelModifier Returns all message level filters .EXAMPLE PS C:\> Get-PSFmessageLevelModifier -Name "mymodule.*" Returns all message level filters that start with "mymodule." #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFMessageLevelModifier')] Param ( [string] $Name = "*" ) ([PSFramework.Message.MessageHost]::MessageLevelModifiers.Values) | Where-Object Name -Like $Name } function New-PSFMessageLevelModifier { <# .SYNOPSIS Allows modifying message levels by powerful filters. .DESCRIPTION Allows modifying message levels by powerful filters. This is designed to allow a developer to have more control over what is written how during the development process. It also allows a debug user to fine tune what he is shown. This functionality is NOT designed for default implementation within a module. Instead, set healthy message levels for your own messages and leave others to tend to their own levels. Note: Adding too many level modifiers may impact performance, use with discretion. .PARAMETER Name The name of the level modifier. Can be arbitrary, but must be unique. Not case sensitive. .PARAMETER Modifier The level modifier to apply. - Use a negative value to make a message more relevant - Use a positive value to make a message less relevant While not limited to this range, the original levels range from 1 through 9: - 1-3 : Written to host and debug by default - 4-6 : Written to verbose and debug by default - 7-9 : Internas, written only to debug .PARAMETER IncludeFunctionName Only messages from functions with one of these exact names will be considered. .PARAMETER ExcludeFunctionName Messages from functions with one of these exact names will be ignored. .PARAMETER IncludeModuleName Only messages from modules with one of these exact names will be considered. .PARAMETER ExcludeModuleName Messages from module with one of these exact names will be ignored. .PARAMETER IncludeTags Only messages that contain one of these tags will be considered. .PARAMETER ExcludeTags Messages that contain one of these tags will be ignored. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> New-PSFMessageLevelModifier -Name 'MyModule-Include' -Modifier -9 -IncludeModuleName MyModule PS C:\> New-PSFMessageLevelModifier -Name 'MyModule-Exclude' -Modifier 9 -ExcludeModuleName MyModule These settings will cause all messages from the module 'MyModule' to be highly prioritized and almost certainly written to host. It will also make it highly unlikely, that messages from other modules will even be considered for anything but the lowest level. This is useful when prioritizing your own module during development. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFMessageLevelModifier')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [int] $Modifier, [string] $IncludeFunctionName, [string] $ExcludeFunctionName, [string] $IncludeModuleName, [string] $ExcludeModuleName, [string[]] $IncludeTags, [string[]] $ExcludeTags, [switch] $EnableException ) if (Test-PSFParameterBinding -ParameterName IncludeFunctionName, ExcludeFunctionName, IncludeModuleName, ExcludeModuleName, IncludeTags, ExcludeTags -Not) { Stop-PSFFunction -Message "Must specify at least one condition in order to apply message level modifier!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'argument', 'message', 'level' return } $levelModifier = New-Object PSFramework.Message.MessageLevelModifier $levelModifier.Name = $Name.ToLower() $levelModifier.Modifier = $Modifier if (Test-PSFParameterBinding -ParameterName IncludeFunctionName) { $levelModifier.IncludeFunctionName = $IncludeFunctionName } if (Test-PSFParameterBinding -ParameterName ExcludeFunctionName) { $levelModifier.ExcludeFunctionName = $ExcludeFunctionName } if (Test-PSFParameterBinding -ParameterName IncludeModuleName) { $levelModifier.IncludeModuleName = $IncludeModuleName } if (Test-PSFParameterBinding -ParameterName ExcludeModuleName) { $levelModifier.ExcludeModuleName = $ExcludeModuleName } if (Test-PSFParameterBinding -ParameterName IncludeTags) { $levelModifier.IncludeTags = $IncludeTags } if (Test-PSFParameterBinding -ParameterName ExcludeTags) { $levelModifier.ExcludeTags = $ExcludeTags } [PSFramework.Message.MessageHost]::MessageLevelModifiers[$levelModifier.Name] = $levelModifier $levelModifier } function Register-PSFMessageEvent { <# .SYNOPSIS Registers an event to when a message is written. .DESCRIPTION Registers an event to when a message is written. These events will fire whenever the written message fulfills the specified filter criteria. This allows integrating direct alerts and reactions to messages as they occur. Warnings: - Adding many subscriptions can impact overall performance, even without triggering. - Events are executed synchronously. executing complex operations may introduce a significant delay to the command execution. It is recommended to push processing that involves outside resources to a separate runspace, then use the event to pass the object as trigger. The TaskEngine component may prove to be just what is needed to accomplish this. .PARAMETER Name The name of the subscription. Each subscription must have a name, subscriptions of equal name will overwrite each other. This is in order to avoid having runspace uses explode the number of subscriptions on each invocation. .PARAMETER ScriptBlock The scriptblock to execute. It will receive the message entry (as returned by Get-PSFMessage) as its sole argument. .PARAMETER MessageFilter Filter by message content. Understands wildcards, but not regex. .PARAMETER ModuleNameFilter Filter by Name of the module, from which the message comes. Understands wildcards, but not regex. .PARAMETER FunctionNameFilter Filter by Name of the function, from which the message comes. Understands wildcards, but not regex. .PARAMETER TargetFilter Filter by target object. Performs equality comparison on an object level. .PARAMETER LevelFilter Include only messages of the specified levels. .PARAMETER TagFilter Only include messages with any of the specified tags. .PARAMETER RunspaceFilter Only include messages which were written by the specified runspace. You can find out the current runspace ID by running this: [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.InstanceId You can retrieve the primary runspace - the Guid used by the runspace the user sees - by running this: [PSFramework.Utility.UtilityHost]::PrimaryRunspace .EXAMPLE PS C:\> Register-PSFMessageEvent -Name 'Mymodule.OffloadTrigger' -ScriptBlock $ScriptBlock -Tag 'engine' -Module 'MyModule' -Level Warning Registers an event subscription ... - Under the name 'Mymodule.OffloadTrigger' ... - To execute $ScriptBlock ... - Whenever a message is written with the tag 'engine' by the module 'MyModule' at the level 'Warning' #> [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFMessageEvent')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock, [string] $MessageFilter, [string] $ModuleNameFilter, [string] $FunctionNameFilter, $TargetFilter, [PSFramework.Message.MessageLevel[]] $LevelFilter, [string[]] $TagFilter, [System.Guid] $RunspaceFilter ) $newName = $Name.ToLower() $eventSubscription = New-Object PSFramework.Message.MessageEventSubscription $eventSubscription.Name = $newName $eventSubscription.ScriptBlock = $ScriptBlock if (Test-PSFParameterBinding -ParameterName MessageFilter) { $eventSubscription.MessageFilter = $MessageFilter } if (Test-PSFParameterBinding -ParameterName ModuleNameFilter) { $eventSubscription.ModuleNameFilter = $ModuleNameFilter } if (Test-PSFParameterBinding -ParameterName FunctionNameFilter) { $eventSubscription.FunctionNameFilter = $FunctionNameFilter } if (Test-PSFParameterBinding -ParameterName TargetFilter) { $eventSubscription.TargetFilter = $TargetFilter } if (Test-PSFParameterBinding -ParameterName LevelFilter) { $eventSubscription.LevelFilter = $LevelFilter } if (Test-PSFParameterBinding -ParameterName TagFilter) { $eventSubscription.TagFilter = $TagFilter } if (Test-PSFParameterBinding -ParameterName RunspaceFilter) { $eventSubscription.RunspaceFilter = $RunspaceFilter } [PSFramework.Message.MessageHost]::Events[$newName] = $eventSubscription } function Register-PSFMessageTransform { <# .SYNOPSIS Registers a scriptblock that can transform message content. .DESCRIPTION Registers a scriptblock that can transform message content. This can be used to convert some kinds of input. Specifically: Target: When specifying a target, this target may require some conversion. For example, an object containing a live connection may need to have a static copy stored instead, as otherwise its export on a different runspace may cause access violations. Exceptions: Some exceptions may need transforming. For example some APIs might wrap the actual exception into a common wrapper. In this scenario you may want the actual exception in order to provide more specific information. In all instances, the scriptblock will be called, receiving only the relevant object as its sole input. Note: This transformation is performed synchronously on the active runspace. Complex scriptblocks may delay execution times when a matching object is passed. .PARAMETER TargetType The full typename of the target object to apply the scriptblock to. All objects of that typename will be processed through that scriptblock. .PARAMETER ExceptionType The full typename of the exception object to apply the scriptblock to. All objects of that typename will be processed through that scriptblock. Note: In case of error records, the type of the Exception Property is inspected. The error record as a whole will not be touched, except for having its exception exchanged. .PARAMETER ScriptBlock The scriptblock that performs the transformation. .PARAMETER TargetTypeFilter A filter for the typename of the target object to transform. Supports wildcards, but not regex. WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them! .PARAMETER ExceptionTypeFilter A filter for the typename of the exception object to transform. Supports wildcards, but not regex. WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them! .PARAMETER FunctionNameFilter Default: "*" Allows filtering by function name, in order to consider whether the function is affected. Supports wildcards, but not regex. WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them! .PARAMETER ModuleNameFilter Default: "*" Allows filtering by module name, in order to consider whether the function is affected. Supports wildcards, but not regex. WARNING: Adding too many filter-type transforms may impact overall performance, try to avoid using them! .EXAMPLE PS C:\> Register-PSFMessageTransform -TargetType 'mymodule.category.classname' -ScriptBlock $ScriptBlock Whenever a target object of type 'mymodule.category.classname' is specified, invoke $ScriptBlock (with the object as sole argument) and store the result as target instead. .EXAMPLE PS C:\> Register-PSFMessageTransform -ExceptionType 'mymodule.category.exceptionname' -ScriptBlock $ScriptBlock Whenever an exception or error record of type 'mymodule.category.classname' is specified, invoke $ScriptBlock (with the object as sole argument) and store the result as exception instead. If the full error record is specified, only the updated exception will be inserted .EXAMPLE PS C:\> Register-PSFMessageTransform -TargetTypeFilter 'mymodule.category.*' -ScriptBlock $ScriptBlock Adds a transform for all target objects that are of a type whose full name starts with 'mymodule.category.' All target objects matching that typename will be run through the specified scriptblock, which in return generates the new target object. #> [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFMessageTransform')] Param ( [Parameter(Mandatory = $true, ParameterSetName = "Target")] [string] $TargetType, [Parameter(Mandatory = $true, ParameterSetName = "Exception")] [string] $ExceptionType, [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $true, ParameterSetName = "TargetFilter")] [string] $TargetTypeFilter, [Parameter(Mandatory = $true, ParameterSetName = "ExceptionFilter")] [string] $ExceptionTypeFilter, [Parameter(ParameterSetName = "TargetFilter")] [Parameter(ParameterSetName = "ExceptionFilter")] $FunctionNameFilter = "*", [Parameter(ParameterSetName = "TargetFilter")] [Parameter(ParameterSetName = "ExceptionFilter")] $ModuleNameFilter = "*" ) process { if ($TargetType) { [PSFramework.Message.MessageHost]::TargetTransforms[$TargetType.ToLower()] = $ScriptBlock } if ($ExceptionType) { [PSFramework.Message.MessageHost]::ExceptionTransforms[$ExceptionType.ToLower()] = $ScriptBlock } if ($TargetTypeFilter) { $condition = New-Object PSFramework.Message.TransformCondition($TargetTypeFilter, $ModuleNameFilter, $FunctionNameFilter, $ScriptBlock, "Target") [PSFramework.Message.MessageHost]::TargetTransformList.Add($condition) } if ($ExceptionTypeFilter) { $condition = New-Object PSFramework.Message.TransformCondition($ExceptionTypeFilter, $ModuleNameFilter, $FunctionNameFilter, $ScriptBlock, "Exception") [PSFramework.Message.MessageHost]::ExceptionTransformList.Add($condition) } } } function Remove-PSFMessageLevelModifier { <# .SYNOPSIS Removes a message level modifier. .DESCRIPTION Removes a message level modifier. Message Level Modifiers can be created by using New-PSFMessageLevelModifier. They are used to emphasize or deemphasize messages, in order to help with debugging. .PARAMETER Name Name of the message level modifier to remove. .PARAMETER Modifier The actual modifier to remove, as returned by Get-PSFMessageLevelModifier. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Get-PSFMessageLevelModifier | Remove-PSFMessageLevelModifier Removes all message level modifiers, restoring everything to their default levels. .EXAMPLE PS C:\> Remove-PSFMessageLevelModifier -Name "mymodule.foo" Removes the message level modifier named "mymodule.foo" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Remove-PSFMessageLevelModifier')] Param ( [Parameter(ValueFromPipeline = $true)] [string[]] $Name, [Parameter(ValueFromPipeline = $true)] [PSFramework.Message.MessageLevelModifier[]] $Modifier, [switch] $EnableException ) process { foreach ($item in $Name) { if ($item -eq "PSFramework.Message.MessageLevelModifier") { continue } if ([PSFramework.Message.MessageHost]::MessageLevelModifiers.ContainsKey($item.ToLower())) { $dummy = $null $null = [PSFramework.Message.MessageHost]::MessageLevelModifiers.TryRemove($item.ToLower(), [ref] $dummy) } else { Stop-PSFFunction -Message "No message level modifier of name $item found!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail','input','level','message' -Continue } } foreach ($item in $Modifier) { if ([PSFramework.Message.MessageHost]::MessageLevelModifiers.ContainsKey($item.Name)) { $dummy = $null $null = [PSFramework.Message.MessageHost]::MessageLevelModifiers.TryRemove($item.Name, [ref]$dummy) } else { Stop-PSFFunction -Message "No message level modifier of name $($item.Name) found!" -EnableException $EnableException -Category InvalidArgument -Tag 'fail', 'input', 'level', 'message' -Continue } } } } function Wait-PSFMessage { <# .SYNOPSIS Waits until the PSFramework log queue has been flushed. .DESCRIPTION Waits until the PSFramework log queue has been flushed. Also supports ending the logging runspace. This is designed to explicitly handle script termination for tasks that run in custom hosts that do not properly fire runspace termination events, leading to infinitely hanging tasks. .PARAMETER Timeout Maximum duration for the command to wait until it terminates even if there are messages left. .PARAMETER Terminate If this parameter is specified it will terminate the running logging runspace. Use this if your script will run in a powershell host that does not properly execute termination events. Danger!!!! Should never be used in a script that might be called by other scripts, as this might prematurely end logging! .EXAMPLE PS C:\> Wait-PSFMessage Waits until all pending messages are logged. .EXAMPLE PS C:\> Wait-PSFMessage -Timeout 1m -Terminate Waits up to one minute for all messages to be flushed, then terminates the logging runspace #> [CmdletBinding()] param ( [PSFDateTime] $Timeout = "5m", [switch] $Terminate ) begin { #region Helper Functions function Test-LogFlushed { [OutputType([bool])] [CmdletBinding()] param ( ) # Catch pending messages if ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) { return $false } if ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0) { return $false } # Catch whether currently processing a message if ([PSFramework.Logging.ProviderHost]::LoggingState -like 'Writing') { return $false } if ([PSFramework.Logging.ProviderHost]::LoggingState -like 'Initializing') { return $false } return $true } #endregion Helper Functions } process { if (([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) -or ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0)) { if ((Get-PSFRunspace -Name 'psframework.logging').State -notlike 'Running') { Start-PSFRunspace -Name 'psframework.logging' } } while ($Timeout.Value -gt (Get-Date)) { if (Test-LogFlushed) { break } Start-Sleep -Milliseconds 50 } if ($Terminate) { Stop-PSFRunspace -Name 'psframework.logging' } } } function Write-PSFHostColor { <# .SYNOPSIS Function that recognizes html-style tags to insert color into printed text. .DESCRIPTION Function that recognizes html-style tags to insert color into printed text. Color tags should be designed to look like this: <c="<console color>">Text</c> For example this would be a valid string: "This message should <c="red">partially be painted in red</c>!" This allows specifying color within strings and avoids having to piece together colored text in multiple calls to Write-Host. Only colors that are part of the ConsoleColor enumeration can be used. Bad colors will be ignored in favor of the default color. .PARAMETER String The message to write to host. .PARAMETER DefaultColor Default: (Get-DbaConfigValue -Name "message.infocolor") The color to write stuff to host in when no (or bad) color-code was specified. .PARAMETER NoNewLine Specifies that the content displayed in the console does not end with a newline character. .PARAMETER Level By default, all messages to Write-PSFHostColor will be printed to host. By specifying a level, it will only print the text if that level is within the range visible to the user. Visibility is controlled by the following two configuration settings: psframework.message.info.maximum psframework.message.info.minimum .EXAMPLE Write-PSFHostColor -String 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color' Will print the specified line in multiple colors .EXAMPLE $string1 = 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color' $string2 = '<c="red">bloody red</c> text! And this is <c="green">green stuff</c> for extra color' $string3 = 'This is going to be <c="red">bloody red</c> text! And this is <c="green">green stuff</c>' $string1, $string2, $string3 | Write-HostColor -DefaultColor "Magenta" Will print all three lines, respecting the color-codes, but use the color "Magenta" as default color. .EXAMPLE $stringLong = @" Dear <c="red">Sirs</c><c="green"> and</c> <c="blue">Madams</c>, it has come to our attention that you are not sufficiently <c="darkblue">awesome!</c> Kindly improve your <c="yellow">AP</c> (<c="magenta">awesome-ness points</c>) by at least 50% to maintain you membership in Awesome Inc! You have <c="green">27 3/4</c> days time to meet this deadline. <c="darkyellow">After this we will unfortunately be forced to rend you assunder and sacrifice your remains to the devil</c>. Best regards, <c="red">Luzifer</c> "@ Write-PSFHostColor -String $stringLong Will print a long multiline text in its entirety while still respecting the colorcodes #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Write-PSFHostColor')] Param ( [Parameter(ValueFromPipeline = $true)] [string[]] $String, [ConsoleColor] $DefaultColor = (Get-PSFConfigValue -FullName "psframework.message.info.color"), [switch] $NoNewLine, [PSFramework.Message.MessageLevel] $Level ) begin { $em = [PSFramework.Message.MessageHost]::InfoColorEmphasis $sub = [PSFramework.Message.MessageHost]::InfoColorSubtle $max_info = [PSFramework.Message.MessageHost]::MaximumInformation $min_info = [PSFramework.Message.MessageHost]::MinimumInformation } process { if ($Level) { if (($max_info -lt $Level) -or ($min_info -gt $Level)) { return } } foreach ($line in $String) { foreach ($row in $line.Split("`n")) #.Split([environment]::NewLine)) { if ($row -notlike '*<c=["'']*["'']>*</c>*') { Microsoft.PowerShell.Utility\Write-Host -Object $row -ForegroundColor $DefaultColor -NoNewline:$NoNewLine } else { $row = $row -replace '<c=["'']em["'']>', "<c='$em'>" -replace '<c=["'']sub["'']>', "<c='$sub'>" $match = ($row | Select-String '<c=["''](.*?)["'']>(.*?)</c>' -AllMatches).Matches $index = 0 $count = 0 while ($count -le $match.Count) { if ($count -lt $Match.Count) { Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index, ($match[$count].Index - $Index)) -ForegroundColor $DefaultColor -NoNewline try { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $match[$count].Groups[1].Value -NoNewline -ErrorAction Stop } catch { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $DefaultColor -NoNewline -ErrorAction Stop } $index = $match[$count].Index + $match[$count].Length $count++ } else { Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index) -ForegroundColor $DefaultColor -NoNewline:$NoNewLine $count++ } } } } } } } function Write-PSFMessageProxy { <# .SYNOPSIS A proxy command that allows smoothly redirecting messages to Write-PSFMessage. .DESCRIPTION This function is designed to pick up the alias it was called by and to redirect the message that was sent to Write-PSFMessage. For example, by creating an alias for Write-Host pointing at 'Write-PSFMessageProxy' will cause it to redirect the message at 'Important' level (which is written to host by default, but also logged). By creating those aliases, it becomes easy to shift current scripts to use the logging, without having to actually update the code. .PARAMETER Message The message to write. .EXAMPLE PS C:\> Write-PSFMessageProxy "Example Message" Will write the message "Example Message" to verbose. .EXAMPLE PS C:\> Set-Alias Write-Host Write-PSFMessageProxy PS C:\> Write-Host "Example Message" This will create an alias named "Write-Host" pointing at "Write-PSFMessageProxy". Then it will write the message "Example Message", which is automatically written to Level "Important" (which by default will be written to host). #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Write-PSFMessageProxy')] Param ( [Parameter(Position = 0)] [string] $Message ) begin { $call = (Get-PSCallStack)[0].InvocationInfo $callStack = (Get-PSCallStack)[1] $FunctionName = $callStack.Command $ModuleName = $callstack.InvocationInfo.MyCommand.ModuleName if (-not $ModuleName) { $ModuleName = "<Unknown>" } $File = $callStack.Position.File $Line = $callStack.Position.StartLineNumber $splatParam = @{ Tag = 'proxied' FunctionName = $FunctionName ModuleName = $ModuleName File = $File Line = $Line } } process { switch ($call.InvocationName) { "Write-Host" { Write-PSFMessage -Level Important -Message $Message @splatParam } "Write-Verbose" { Write-PSFMessage -Level Verbose -Message $Message @splatParam } "Write-Warning" { Write-PSFMessage -Level Warning -Message $Message @splatParam } "Write-Debug" { Write-PSFMessage -Level System -Message $Message @splatParam } "Write-Information" { Write-PSFMessage -Level Important -Message $Message @splatParam } default { Write-PSFMessage -Level Verbose -Message $Message @splatParam } } } end { } } function Get-PSFPipeline { <# .SYNOPSIS Generates meta-information for the pipeline from the calling command. .DESCRIPTION Generates meta-information for the pipeline from the calling command. .EXAMPLE PS C:\> Get-Pipeline Generates meta-information for the pipeline from the calling command. #> [OutputType([PSFramework.Meta.Pipeline])] [CmdletBinding()] param ( ) begin { function Get-PrivateProperty { [CmdletBinding()] param ( $Object, [string] $Name, [ValidateSet('Any', 'Field', 'Property')] [string] $Type = 'Any' ) if ($null -eq $Object) { return } $typeObject = $Object.GetType() [System.Reflection.BindingFlags]$flags = "NonPublic, Instance" switch ($Type) { 'Field' { $field = $typeObject.GetField($Name, $flags) $field.GetValue($Object) } 'Property' { $property = $typeObject.GetProperty($Name, $flags) $property.GetValue($Object) } 'Any' { $field = $typeObject.GetField($Name, $flags) if ($field) { return $field.GetValue($Object) } $property = $typeObject.GetProperty($Name, $flags) $property.GetValue($Object) } } } } process { $callerCmdlet = (Get-PSCallStack)[1].GetFrameVariables()["PSCmdlet"].Value $commandRuntime = Get-PrivateProperty -Object $callerCmdlet -Name _commandRuntime -Type Field $pipelineProcessor = Get-PrivateProperty -Object $commandRuntime -Name PipelineProcessor -Type Property $localPipeline = Get-PrivateProperty -Object $pipelineProcessor -Name LocalPipeline -Type Property $pipeline = New-Object PSFramework.Meta.Pipeline -Property @{ InstanceId = $localPipeline.InstanceId StartTime = Get-PrivateProperty -Object $localPipeline -Name _pipelineStartTime -Type Field Text = Get-PrivateProperty -Object $localPipeline -Name HistoryString -Type Property PipelineItem = $localPipeline } if ($pipeline.Text) { $tokens = $null $errorItems = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($pipeline.Text, [ref]$tokens, [ref]$errorItems) $pipeline.Ast = $ast $baseItem = $ast.EndBlock.Statements[0] if ($baseItem -is [System.Management.Automation.Language.AssignmentStatementAst]) { $pipeline.OutputAssigned = $true $pipeline.OutputAssignedTo = $baseItem.Left $baseItem = $baseItem.Right.PipelineElements } else { $baseItem = $baseItem.PipelineElements } if ($baseItem[0] -is [System.Management.Automation.Language.CommandExpressionAst]) { if ($baseItem[0].Expression -is [System.Management.Automation.Language.VariableExpressionAst]) { $pipeline.InputFromVariable = $true $pipeline.InputVariable = $baseItem[0].Expression.VariablePath.UserPath } else { $pipeline.InputDirect = $true } if ($baseItem[0].Expression -is [System.Management.Automation.Language.ConstantExpressionAst]) { $pipeline.InputValue = $baseItem[0].Expression.Value } elseif ($baseItem[0].Expression -is [System.Management.Automation.Language.ArrayLiteralAst]) { $pipeline.InputValue = @() foreach ($element in $baseItem[0].Expression.Elements) { if ($element -is [System.Management.Automation.Language.ConstantExpressionAst]) { $pipeline.InputValue += $element.Value } else { $pipeline.InputValue += $element } } } else { $pipeline.InputValue = $baseItem[0].Expression } } } $commands = Get-PrivateProperty -Object $pipelineProcessor -Name Commands -Type Property $index = 0 foreach ($command in $commands) { $commandItem = Get-PrivateProperty -Object $command -Name Command $pipeline.Commands.Add((New-Object PSFramework.Meta.PipelineCommand($pipeline.InstanceId, $index, (Get-PrivateProperty -Object $command -Name CommandInfo), $commandItem.MyInvocation, $commandItem))) $index++ } $pipeline } } function Clear-PSFResultCache { <# .SYNOPSIS Clears the result cache .DESCRIPTION Clears the result cache, which can come in handy if you have a huge amount of data stored within and want to free the memory. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Clear-PSFResultCache Clears the result cache, freeing up any used memory. #> [CmdletBinding(ConfirmImpact = 'Low', SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Clear-PSFresultCache')] param ( ) if ($pscmdlet.ShouldProcess("Result cache", "Clearing the result cache")) { [PSFramework.ResultCache.ResultCache]::Clear() } } function Get-PSFResultCache { <# .SYNOPSIS Returns the last stored result .DESCRIPTION Functions that implement the result cache store their information in the cache. This can then be retrieved by the user running this command. This forgives the user for forgetting to store the output in a variable and is especially precious when running commands that take a while to execute. .PARAMETER Type Default: Value Options: All, Value By default, this function will return the output that was cached during the last execution. However, this mode can be switched: - All: Returns everything that has been cached. This includes the name of the command calling Set-PFSResultCache as well as the timestamp when it was called. - Value: Returns just the object(s) that were written to cache .EXAMPLE PS C:\> Get-PSFResultCache Returns the latest cached result. .EXAMPLE PS C:\> Get-PSFResultCache -Type 'All' Returns a meta-information object containing the last result, when it was written and which function did the writing. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFResultCache')] param ( [ValidateSet('Value','All')] [string] $Type = 'Value' ) switch ($Type) { 'All' { New-Object PSObject -Property @{ Result = ([PSFramework.ResultCache.ResultCache]::Result) Function = ([PSFramework.ResultCache.ResultCache]::Function) Timestamp = ([PSFramework.ResultCache.ResultCache]::Timestamp) } } 'Value' { [PSFramework.ResultCache.ResultCache]::Result } } } if (-not (Test-Path "alias:Get-LastResult")) { New-Alias -Name Get-LastResult -Value Get-PSFResultCache -Description "A more intuitive name for users to call Get-PSFResultCache" } if (-not (Test-Path "alias:glr")) { New-Alias -Name glr -Value Get-PSFResultCache -Description "A faster name for users to call Get-PSFResultCache" } function Set-PSFResultCache { <# .SYNOPSIS Stores a result in the result cache .DESCRIPTION Stores a result in the result cache. This function is designed for use in other functions, a user should never have cause to use it directly himself. .PARAMETER InputObject The value to store in the result cache. .PARAMETER DisableCache Allows you to control, whether the function actually writes to the cache. Useful when used in combination with -PassThru. Does not suppress output via -PassThru. However in combination, these two parameters make caching within a pipeline practical. .PARAMETER PassThru The objects that are being cached are passed through this function. By default, Set-PSFResultCache doesn't have any output. .PARAMETER CommandName Default: (Get-PSCallStack)[0].Command The name of the command that called Set-PSFResultCache. Is automatically detected and usually doesn't need to be changed. .EXAMPLE PS C:\> Set-PSFResultCache -InputObject $Results -DisableCache $NoRes Stores the contents of $Results in the result cache, but does nothing if $NoRes is $true (the default Switch-name for disabling the result cache) .EXAMPLE PS C:\> Get-ChildItem $path | Get-Acl | Set-PSFResultCache -DisableCache $NoRes -PassThru Gets all items in $Path, then retrieves each of their Acl, finally it stores those in the result cache (if it isn't disabled via $NoRes) and finally passes each Acl through for the user to see. This will return all objects, even if $NoRes is set to $True. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFResultCache')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowEmptyCollection()] [AllowEmptyString()] [AllowNull()] [Alias('Value')] [Object] $InputObject, [boolean] $DisableCache = $false, [Switch] $PassThru, [string] $CommandName = (Get-PSCallStack)[0].Command ) Begin { $IsPipeline = -not $PSBoundParameters.ContainsKey("InputObject") [PSFramework.ResultCache.ResultCache]::Function = $CommandName if ($IsPipeline -and (-not $DisableCache)) { [PSFramework.ResultCache.ResultCache]::Result = @() } } Process { if ($IsPipeline) { if (-not $DisableCache) { [PSFramework.ResultCache.ResultCache]::Result += $PSItem } if ($PassThru) { $PSItem } } else { if (-not $DisableCache) { [PSFramework.ResultCache.ResultCache]::Result = $InputObject } if ($PassThru) { $InputObject } } } End { } } function Get-PSFDynamicContentObject { <# .SYNOPSIS Retrieves a named value object that can be updated from another runspace. .DESCRIPTION Retrieves a named value object that can be updated from another runspace. This comes in handy to have a variable that is automatically updated. Use this function to receive an object under a given name. Use Set-PSFDynamicContentObject to update the value of the object. It matters not from what runspace you update the object. Note: When planning to use such an object, keep in mind that it can easily change its content at any given time. .PARAMETER Name The name of the object to retrieve. Will create an empty value object if the object doesn't already exist. .EXAMPLE PS C:\> Get-PSFDynamicContentObject -Name "Test" Returns the Dynamic Content Object named "test" #> [OutputType([PSFramework.Utility.DynamicContentObject])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFDynamicContentObject')] Param ( [Parameter(Mandatory = $true)] [string[]] $Name ) begin { } process { foreach ($item in $Name) { [PSFramework.Utility.DynamicContentObject]::Get($Name) } } end { } } function Get-PSFRunspace { <# .SYNOPSIS Returns registered runspaces. .DESCRIPTION Returns a list of runspaces that have been registered with the PSFramework .PARAMETER Name Default: "*" Only registered runspaces of similar names are returned. .EXAMPLE PS C:\> Get-PSFRunspace Returns all registered runspaces .EXAMPLE PS C:\> Get-PSFRunspace -Name 'mymodule.maintenance' Returns the runspace registered under the name 'mymodule.maintenance' #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFRunspace')] Param ( [string] $Name = "*" ) [PSFramework.Runspace.RunspaceHost]::Runspaces.Values | Where-Object Name -Like $Name } function Register-PSFRunspace { <# .SYNOPSIS Registers a scriptblock to run in the background. .DESCRIPTION This function registers a scriptblock to run in separate runspace. This is different from most runspace solutions, in that it is designed for permanent background tasks that need to be done. It guarantees a single copy of the task to run within the powershell process, even when running the same module in many runspaces in parallel. The scriptblock must be built with some rules in mind, for details on using this system run: Get-Help about_psf_runspace Updating: If this function is called multiple times, targeting the same name, it will update the scriptblock. - If that scriptblock is the same as the previous scriptblock, nothing changes - If that scriptblock is different from the previous ones, it will be registered, but will not be executed right away! Only after stopping and starting the runspace will it operate under the new scriptblock. .PARAMETER ScriptBlock The scriptblock to run in a dedicated runspace. .PARAMETER Name The name to register the scriptblock under. .PARAMETER NoMessage Setting this will prevent messages be written to the message / logging system. This is designed to make the PSFramework not flood the log on each import. .EXAMPLE PS C:\> Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'mymodule.maintenance' Registers the script defined in $scriptBlock under the name 'mymodule.maintenance' It does not start the runspace yet. If it already exists, it will overwrite the scriptblock without affecting the running script. .EXAMPLE PS C:\> Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'mymodule.maintenance' PS C:\> Start-PSFRunspace -Name 'mymodule.maintenance' Registers the script defined in $scriptBlock under the name 'mymodule.maintenance' Then it starts the runspace, running the registered $scriptBlock #> [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFRunspace')] param ( [Parameter(Mandatory = $true)] [Scriptblock] $ScriptBlock, [Parameter(Mandatory = $true)] [String] $Name, [switch] $NoMessage ) if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($Name.ToLower())) { if (-not $NoMessage) { Write-PSFMessage -Level Verbose -Message "Updating runspace: <c='em'>$($Name.ToLower())</c>" -Target $Name.ToLower() -Tag 'runspace','register' } [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name.ToLower()].SetScript($ScriptBlock) } else { if (-not $NoMessage) { Write-PSFMessage -Level Verbose -Message "Registering runspace: <c='em'>$($Name.ToLower())</c>" -Target $Name.ToLower() -Tag 'runspace', 'register' } [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name.ToLower()] = New-Object PSFramework.Runspace.RunspaceContainer($Name.ToLower(), $ScriptBlock) } } function Set-PSFDynamicContentObject { <# .SYNOPSIS Updates a value object that can easily be accessed on another runspace. .DESCRIPTION Updates a value object that can easily be accessed on another runspace. The Dynamic Content Object system allows the user to easily have the content of a variable updated in the background. The update is performed by this very function. .PARAMETER Name The name of the value to update. Not case sensitive. .PARAMETER Object The value object to update .PARAMETER Value The value to apply .PARAMETER Queue Set the object to be a threadsafe queue. Safe to use in multiple runspaces in parallel. Will not apply changes if the current value is already such an object. .PARAMETER Stack Set the object to be a threadsafe stack. Safe to use in multiple runspaces in parallel. Will not apply changes if the current value is already such an object. .PARAMETER List Set the object to be a threadsafe list. Safe to use in multiple runspaces in parallel. Will not apply changes if the current value is already such an object. .PARAMETER Dictionary Set the object to be a threadsafe dictionary. Safe to use in multiple runspaces in parallel. Will not apply changes if the current value is already such an object. .PARAMETER PassThru Has the command returning the object just set. .PARAMETER Reset Clears the dynamic content object's collection objects. Use this to ensure the collection is actually empty. Only used in combination of either -Queue, -Stack, -List or -Dictionary. .EXAMPLE PS C:\> Set-PSFDynamicContentObject -Name Test -Value $Value Sets the Dynamic Content Object named "test" to the value $Value. .EXAMPLE PS C:\> Set-PSFDynamicContentObject -Name MyModule.Value -Queue Sets the Dynamic Content Object named "MyModule.Value" to contain a threadsafe queue. This queue will be safe to enqueue and dequeue from, no matter the number of runspaces accessing it simultaneously. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [OutputType([PSFramework.Utility.DynamicContentObject])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFDynamicContentObject')] Param ( [string[]] $Name, [Parameter(ValueFromPipeline = $true)] [PSFramework.Utility.DynamicContentObject[]] $Object, [Parameter(Mandatory = $true, ParameterSetName = 'Value')] [AllowNull()] $Value = $null, [Parameter(Mandatory = $true, ParameterSetName = 'Queue')] [switch] $Queue, [Parameter(Mandatory = $true, ParameterSetName = 'Stack')] [switch] $Stack, [Parameter(Mandatory = $true, ParameterSetName = 'List')] [switch] $List, [Parameter(Mandatory = $true, ParameterSetName = 'Dictionary')] [switch] $Dictionary, [switch] $PassThru, [switch] $Reset ) process { foreach ($item in $Name) { if ($Queue) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Queue') } elseif ($Stack) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Stack') } elseif ($List) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'List') } elseif ($Dictionary) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Dictionary') } else { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Common') } if ($PassThru) { [PSFramework.Utility.DynamicContentObject]::Get($item) } } foreach ($item in $Object) { $item.Value = $Value if ($Queue) { $item.ConcurrentQueue($Reset) } if ($Stack) { $item.ConcurrentStack($Reset) } if ($List) { $item.ConcurrentList($Reset) } if ($Dictionary) { $item.ConcurrentDictionary($Reset) } if ($PassThru) { $item } } } } function Start-PSFRunspace { <# .SYNOPSIS Starts a runspace that was registered to the PSFramework .DESCRIPTION Starts a runspace that was registered to the PSFramework Simply registering does not automatically start a given runspace. Only by executing this function will it take effect. .PARAMETER Name The name of the registered runspace to launch .PARAMETER Runspace The runspace to launch. Returned by Get-PSFRunspace .PARAMETER NoMessage Setting this will prevent messages be written to the message / logging system. This is designed to make the PSFramework not flood the log on each import. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Start-PSFRunspace -Name 'mymodule.maintenance' Starts the runspace registered under the name 'mymodule.maintenance' #> [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Start-PSFRunspace')] Param ( [Parameter(ValueFromPipeline = $true)] [string[]] $Name, [Parameter(ValueFromPipeline = $true)] [PSFramework.Runspace.RunspaceContainer[]] $Runspace, [switch] $NoMessage, [switch] $EnableException ) process { foreach ($item in $Name) { # Ignore all output from Get-PSFRunspace - it'll be handled by the second loop if ($item -eq "psframework.runspace.runspacecontainer") { continue } if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($item.ToLower())) { if ($PSCmdlet.ShouldProcess($item, "Starting Runspace")) { try { if (-not $NoMessage) { Write-PSFMessage -Level Verbose -Message "Starting runspace: <c='em'>$($item.ToLower())</c>" -Target $item.ToLower() -Tag "runspace", "start" } [PSFramework.Runspace.RunspaceHost]::Runspaces[$item.ToLower()].Start() } catch { Stop-PSFFunction -Message "Failed to start runspace: <c='em'>$($item.ToLower())</c>" -ErrorRecord $_ -EnableException $EnableException -Tag "fail", "argument", "runspace", "start" -Target $item.ToLower() -Continue } } } else { Stop-PSFFunction -Message "Failed to start runspace: <c='em'>$($item.ToLower())</c> | No runspace registered under this name!" -EnableException $EnableException -Category InvalidArgument -Tag "fail", "argument", "runspace", "start" -Target $item.ToLower() -Continue } } foreach ($item in $Runspace) { if ($PSCmdlet.ShouldProcess($item.Name, "Starting Runspace")) { try { if (-not $NoMessage) { Write-PSFMessage -Level Verbose -Message "Starting runspace: <c='em'>$($item.Name.ToLower())</c>" -Target $item -Tag "runspace", "start" } $item.Start() } catch { Stop-PSFFunction -Message "Failed to start runspace: <c='em'>$($item.Name.ToLower())</c>" -EnableException $EnableException -Tag "fail", "argument", "runspace", "start" -Target $item -Continue } } } } } function Stop-PSFRunspace { <# .SYNOPSIS Stops a runspace that was registered to the PSFramework .DESCRIPTION Stops a runspace that was registered to the PSFramework Will not cause errors if the runspace is already halted. Runspaces may not automatically terminate immediately when calling this function. Depending on the implementation of the scriptblock, this may in fact take a little time. If the scriptblock hasn't finished and terminated the runspace in a seemingly time, it will be killed by the system. This timeout is by default 30 seconds, but can be altered by using the Configuration System. For example, this line will increase the timeout to 60 seconds: Set-PSFConfig -FullName PSFramework.Runspace.StopTimeout -Value 60 .PARAMETER Name The name of the registered runspace to stop .PARAMETER Runspace The runspace to stop. Returned by Get-PSFRunspace .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Stop-PSFRunspace -Name 'mymodule.maintenance' Stops the runspace registered under the name 'mymodule.maintenance' .NOTES Additional information about the function. #> [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Stop-PSFRunspace')] Param ( [Parameter(ValueFromPipeline = $true)] [string[]] $Name, [Parameter(ValueFromPipeline = $true)] [PSFramework.Runspace.RunspaceContainer[]] $Runspace, [switch] $EnableException ) process { foreach ($item in $Name) { # Ignore all output from Get-PSFRunspace - it'll be handled by the second loop if ($item -eq "psframework.runspace.runspacecontainer") { continue } if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($item.ToLower())) { if ($PSCmdlet.ShouldProcess($item, "Stopping Runspace")) { try { Write-PSFMessage -Level Verbose -Message "Stopping runspace: <c='em'>$($item.ToLower())</c>" -Target $item.ToLower() -Tag "runspace", "stop" [PSFramework.Runspace.RunspaceHost]::Runspaces[$item.ToLower()].Stop() } catch { Stop-PSFFunction -Message "Failed to stop runspace: <c='em'>$($item.ToLower())</c>" -EnableException $EnableException -Tag "fail", "argument", "runspace", "stop" -Target $item.ToLower() -Continue -ErrorRecord $_ } } } else { Stop-PSFFunction -Message "Failed to stop runspace: <c='em'>$($item.ToLower())</c> | No runspace registered under this name!" -EnableException $EnableException -Category InvalidArgument -Tag "fail", "argument", "runspace", "stop" -Target $item.ToLower() -Continue } } foreach ($item in $Runspace) { if ($PSCmdlet.ShouldProcess($item.Name, "Stopping Runspace")) { try { Write-PSFMessage -Level Verbose -Message "Stopping runspace: <c='em'>$($item.Name.ToLower())</c>" -Target $item -Tag "runspace", "stop" $item.Stop() } catch { Stop-PSFFunction -Message "Failed to stop runspace: <c='em'>$($item.Name.ToLower())</c>" -EnableException $EnableException -Tag "fail", "argument", "runspace", "stop" -Target $item -Continue -ErrorRecord $_ } } } } } function ConvertFrom-PSFClixml { <# .SYNOPSIS Converts data that was serialized from an object back into that object. .DESCRIPTION Converts data that was serialized from an object back into that object. Use Import-PSFclixml to restore objects serialized and written to file. This command is designed for converting serialized data in memory, for example to expand objects returned by a network api. .PARAMETER InputObject The serialized data to restore to objects. .EXAMPLE PS C:\> $data | ConvertFrom-PSFClixml Converts the data stored in $data back into objects #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] $InputObject ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $byteList = New-Object System.Collections.ArrayList function Convert-Item { [CmdletBinding()] param ( $Data ) if ($Data -is [System.String]) { try { [PSFramework.Serialization.ClixmlSerializer]::FromStringCompressed($Data) } catch { [PSFramework.Serialization.ClixmlSerializer]::FromString($Data) } } else { try { [PSFramework.Serialization.ClixmlSerializer]::FromByteCompressed($Data) } catch { [PSFramework.Serialization.ClixmlSerializer]::FromByte($Data) } } } } process { if ($InputObject -is [string]) { Convert-Item -Data $InputObject } elseif ($InputObject -is [System.Byte[]]) { Convert-Item -Data $InputObject } elseif ($InputObject -is [System.Byte]) { $null = $byteList.Add($InputObject) } else { Stop-PSFFunction -Message "Unsupported input! Provide either a string or byte-array that previously were serialized from objects in powershell" -EnableException $true } } end { if ($byteList.Count -gt 0) { Convert-Item -Data ([System.Byte[]]$byteList.ToArray()) } } } function ConvertTo-PSFClixml { <# .SYNOPSIS Converts an input object into a serialized string or byte array. .DESCRIPTION Converts an input object into a serialized string or byte array. Works analogous to Export-PSFClixml, only it does not require being written to file. .PARAMETER Depth Specifies how many levels of contained objects are included in the XML representation. The default value is 2. .PARAMETER InputObject The object(s) to serialize. .PARAMETER Style Whether to export as byte (better compression) or as string (often easier to transmit using other utilities/apis). .PARAMETER NoCompression By default, exported data is compressed, saving a lot of storage at the cost of some CPU cycles. This switch disables this compression, making string-style exports compatible with Import-Clixml. .EXAMPLE PS C:\> Get-ChildItem | ConvertTo-PSFClixml Scans all items in the current folder and then converts that into a compressed clixml string. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [int] $Depth, [Parameter(ValueFromPipeline = $true)] $InputObject, [PSFramework.Serialization.ClixmlDataStyle] $Style = 'String', [switch] $NoCompression ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' $data = @() } process { $data += $InputObject } end { try { Write-PSFMessage -Level Verbose -Message "Writing data to $style" if ($Style -like 'Byte') { if ($NoCompression) { if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToByte($data, $Depth) } else { [PSFramework.Serialization.ClixmlSerializer]::ToByte($data) } } else { if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data, $Depth) } else { [PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data) } } } else { if ($NoCompression) { if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToString($data, $Depth) } else { [PSFramework.Serialization.ClixmlSerializer]::ToString($data) } } else { if ($Depth) { [PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data, $Depth) } else { [PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data) } } } } catch { Stop-PSFFunction -Message "Failed to export object" -ErrorRecord $_ -EnableException $true -Target $resolvedPath -Cmdlet $PSCmdlet } } } function Export-PSFClixml { <# .SYNOPSIS Writes objects to the filesystem. .DESCRIPTION Writes objects to the filesystem. In opposite to the default Export-Clixml cmdlet, this function offers data compression as the default option. Exporting to regular clixml is still supported though. .PARAMETER Path The path to write to. .PARAMETER Depth Specifies how many levels of contained objects are included in the XML representation. The default value is 2. .PARAMETER InputObject The object(s) to serialize. .PARAMETER Style Whether to export as byte (better compression) or as string (often easier to transmit using other utilities/apis). .PARAMETER NoCompression By default, exported data is compressed, saving a lot of storage at the cost of some CPU cycles. This switch disables this compression, making string-style exports compatible with Import-Clixml. .PARAMETER Encoding The encoding to use when using string-style export. By default, it exports as UTF8 encoding. .EXAMPLE PS C:\> Get-ChildItem | Export-PSFClixml -Path 'C:\temp\data.byte' Exports a list of all items in the current path as compressed binary file to C:\temp\data.byte .EXAMPLE PS C:\> Get-ChildItem | Export-PSFClixml -Path C:\temp\data.xml -Style 'String' -NoCompression Exports a list of all items in the current path as a default clixml readable by Import-Clixml #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Export-PSFClixml')] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Path, [int] $Depth, [Parameter(ValueFromPipeline = $true)] $InputObject, [PSFramework.Serialization.ClixmlDataStyle] $Style = 'Byte', [switch] $NoCompression, [PSFEncoding] $Encoding = (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.DefaultWrite') ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem -NewChild } catch { Stop-PSFFunction -Message "Could not resolve outputpath: $Path" -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ } $data = @() } process { $data += $InputObject } end { try { Write-PSFMessage -Level Verbose -Message "Writing data to '$resolvedPath'" if ($Style -like 'Byte') { if ($NoCompression) { if ($Depth) { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByte($data, $Depth))) } else { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByte($data))) } } else { if ($Depth) { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data, $Depth))) } else { [System.IO.File]::WriteAllBytes($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToByteCompressed($data))) } } } else { if ($NoCompression) { if ($Depth) { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToString($data, $Depth)), $Encoding) } else { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToString($data)), $Encoding) } } else { if ($Depth) { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data, $Depth)), $Encoding) } else { [System.IO.File]::WriteAllText($resolvedPath, ([PSFramework.Serialization.ClixmlSerializer]::ToStringCompressed($data)), $Encoding) } } } } catch { Stop-PSFFunction -Message "Failed to export object" -ErrorRecord $_ -EnableException $true -Target $resolvedPath -Cmdlet $PSCmdlet } } } function Get-PSFTypeSerializationData { <# .SYNOPSIS Creates a type extension XML for serializing an object .DESCRIPTION Creates a type extension XML for serializing an object Use this to register a type with a type serializer, so it will retain its integrity across process borders. This is relevant in order to have an object retain its type when ... - sending it over PowerShell Remoting - writing it to file via Export-Clixml and reading it later via Import-Clixml Note: In the default serializer, all types registered must: - Have all public properties be read & writable (the write needs not do anything, but it must not throw an exception). - All non-public properties will be ignored. - Come from an Assembly with a static name (like an existing dll file, not compiled at runtime). .PARAMETER InputObject The type to serialize. - Accepts a type object - The string name of the type - An object, whose type will then be determined .PARAMETER Mode Whether all types listed should be generated as a single definition ('Grouped'; default) or as one definition per type. Since multiple files have worse performance, it is generally recommended to group them all in a single file. .PARAMETER Fragment By setting this, the type XML is emitted without the outer XML shell, containing only the <Type> node(s). Use this if you want to add the output to existing type extension xml. .PARAMETER Serializer The serializer to use for the conversion. By default, the PSFramework serializer is used, which should work well enough, but requires the PSFramework to be present. .PARAMETER Method The serialization method to use. By default, the PSFramework serialization method is used, which should work well enough, but requires the PSFramework to be present. .EXAMPLE PS C:\> Get-PSFTypeSerializationData -InputObject 'My.Custom.Type' Generates an XML text that can be used to register via Update-TypeData. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTypeSerializationData')] Param ( [Parameter(ValueFromPipeline = $true)] [object[]] $InputObject, [ValidateSet('Grouped','SingleItem')] [string] $Mode = "Grouped", [switch] $Fragment, [string] $Serializer = "PSFramework.Serialization.SerializationTypeConverter", [string] $Method = "GetSerializationData" ) begin { #region XML builder functions function Get-XmlHeader { <# .SYNOPSIS Returns the header for a types XML file #> Param ( ) @" <?xml version="1.0" encoding="utf-8"?> <Types> "@ } function Get-XmlBody { <# .SYNOPSIS Processes a type into proper XML #> Param ( [string] $Type, [string] $Serializer, [string] $Method ) @" <!-- $Type --> <Type> <Name>Deserialized.$Type</Name> <Members> <MemberSet> <Name>PSStandardMembers</Name> <Members> <NoteProperty> <Name> TargetTypeForDeserialization </Name> <Value> $Type </Value> </NoteProperty> </Members> </MemberSet> </Members> </Type> <Type> <Name>$Type</Name> <Members> <CodeProperty IsHidden="true"> <Name>SerializationData</Name> <GetCodeReference> <TypeName>$Serializer</TypeName> <MethodName>$Method</MethodName> </GetCodeReference> </CodeProperty> </Members> <TypeConverter> <TypeName>$Serializer</TypeName> </TypeConverter> </Type> "@ } function Get-XmlFooter { <# .SYNOPSIS Returns the footer for a types XML file #> Param ( ) @" </Types> "@ } #endregion XML builder functions $types = @() if ($Mode -eq 'Grouped') { if (-not $Fragment) { $xml = Get-XmlHeader } else { $xml = "" } } } process { foreach ($item in $InputObject) { if ($null -eq $item) { continue } $type = $null if ($res = $item -as [System.Type]) { $type = $res } else { $type = $item.GetType() } if ($type -in $types) { continue } switch ($Mode) { 'Grouped' { $xml += Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName } 'SingleItem' { if (-not $Fragment) { $xml = Get-XmlHeader $xml += Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName $xml += Get-XmlFooter $xml } else { Get-XmlBody -Method $Method -Serializer $Serializer -Type $type.FullName } } } $types += $type } } end { if ($Mode -eq 'Grouped') { if (-not $Fragment) { $xml += Get-XmlFooter } $xml } } } function Import-PSFClixml { <# .SYNOPSIS Imports objects serialized using Export-Clixml or Export-PSFClixml. .DESCRIPTION Imports objects serialized using Export-Clixml or Export-PSFClixml. It can handle compressed and non-compressed exports. .PARAMETER Path Path to the files to import. .PARAMETER Encoding Text-based files might be stored with any arbitrary encoding chosen. By default, this function assumes UTF8 encoding (the default export encoding for Export-PSFClixml). .EXAMPLE PS C:\> Import-PSFClixml -Path '.\object.xml' Imports the objects serialized to object.xml in the current folder. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Import-PSFClixml')] Param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string[]] $Path, [PSFEncoding] $Encoding = (Get-PSFConfigValue -FullName 'psframework.text.encoding.defaultread' -Fallback 'utf-8') ) begin { Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' } process { try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem } catch { Stop-PSFFunction -Message "Failed to resolve path." -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $Path } foreach ($pathItem in $resolvedPath) { if ((Get-Item $pathItem).PSIsContainer) { Stop-PSFFunction -Message "$pathItem is not a file" -EnableException $true -Target $pathItem } Write-PSFMessage -Level Verbose -Message "Processing $($pathItem)" -Target $pathItem [byte[]]$bytes = [System.IO.File]::ReadAllBytes($pathItem) try { [PSFramework.Serialization.ClixmlSerializer]::FromByteCompressed($bytes) } catch { [string]$string = [System.IO.File]::ReadAllText($pathItem, $Encoding) try { [PSFramework.Serialization.ClixmlSerializer]::FromString($string) } catch { try { [PSFramework.Serialization.ClixmlSerializer]::FromStringCompressed($string) } catch { try { [PSFramework.Serialization.ClixmlSerializer]::FromByte($bytes) } catch { Stop-PSFFunction -Message "Failed to convert input object" -EnableException $true -Target $pathItem -Cmdlet $PSCmdlet } } } } } } } function Register-PSFTypeSerializationData { <# .SYNOPSIS Registers serialization xml Typedata. .DESCRIPTION Registers serialization xml Typedata. Use Get-PSFTypeSerializationData to generate such a string. When building a module, consider shipping that xml type extension in a dedicated file as part of the module and import it as part of the manifest's 'TypesToProcess' node. .PARAMETER TypeData The data to register. Generate with Get-PSFTypeSerializationData. .PARAMETER Path Where the file should be stored before appending. While type extensions can be added at runtime directly from memory, from file is more reliable. By default, a PSFramework path is chosen. The default path can be configured under the 'PSFramework.Serialization.WorkingDirectory' confguration setting. .EXAMPLE PS C:\> Get-PSFTypeSerializationData -InputObject 'My.Custom.Type' | Register-PSFTypeSerializationData Generates a custom type serialization xml and registers it. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTypeSerializationData')] Param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [string[]] $TypeData, [string] $Path = (Get-PSFConfigValue -FullName 'PSFramework.Serialization.WorkingDirectory' -Fallback $script:path_typedata) ) begin { if (-not (Test-Path $Path -PathType Container)) { $null = New-Item -Path $Path -ItemType Directory -Force } } process { foreach ($item in $TypeData) { $name = $item -split "`n" | Select-String "<Name>(.*?)</Name>" | Where-Object { $_ -notmatch "<Name>Deserialized.|<Name>PSStandardMembers</Name>|<Name>SerializationData</Name>" } | Select-Object -First 1 | ForEach-Object { $_.Matches[0].Groups[1].Value } $fullName = Join-Path $Path.Trim "$($name).Types.ps1xml" $item | Set-Content -Path $fullName -Force -Encoding UTF8 Update-TypeData -AppendPath $fullName } } end { } } function Register-PSFTeppArgumentCompleter { <# .SYNOPSIS Registers a parameter for a prestored Tepp. .DESCRIPTION Registers a parameter for a prestored Tepp. This function allows easily registering a function's parameter for Tepp in the function-file, rather than in a centralized location. .PARAMETER Command Name of the command whose parameter should receive Tepp. .PARAMETER Parameter Name of the parameter that should be Tepp'ed. .PARAMETER Name Name of the Tepp Completioner to use. Use the same name as was assigned in Register-PSFTeppScriptblock (which needs to be called first). .EXAMPLE Register-PSFTeppArgumentCompleter -Command Get-Test -Parameter Example -Name MyModule.Example Registers the parameter 'Example' of the command 'Get-Test' to receive the tab completion registered to 'MyModule.Example' #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTeppArgumentCompleter')] Param ( [Parameter(Mandatory = $true)] [string[]] $Command, [Parameter(Mandatory = $true)] [string[]] $Parameter, [Parameter(Mandatory = $true)] [string] $Name ) if (($PSVersionTable["PSVersion"].Major -lt 5) -and (-not (Get-Item function:Register-ArgumentCompleter -ErrorAction Ignore))) { return } foreach ($Param in $Parameter) { $scriptBlock = [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$Name.ToLower()].ScriptBlock Register-ArgumentCompleter -CommandName $Command -ParameterName $Param -ScriptBlock $scriptBlock } } function Register-PSFTeppScriptblock { <# .SYNOPSIS Registers a scriptblock under name, to later be available for TabExpansion. .DESCRIPTION Registers a scriptblock under name, to later be available for TabExpansion. This system supports two separate types of input: Full or Simple. Simple: The scriptblock simply must return string values. PSFramework will then do the rest of the processing when the user asks for tab completion. This is the simple-most way to implement tab completion, for a full example, look at the first example in this help. Full: A full scriptblock implements all that is needed to provide Tab Expansion. For more details and guidance, see the following concept help: Get-Help about_psf_tabexpansion .PARAMETER ScriptBlock The scriptblock to register. .PARAMETER Name The name under which the scriptblock should be registered. It is recommended to prefix the name with the module (e.g.: mymodule.<name>), as names are shared across all implementing modules. .PARAMETER Mode Whether the script provided is a full or simple scriptblock. By default, this function automatically detects this, but just in case, you can override this detection. .PARAMETER CacheDuration How long a tab completion result is valid. By default, PSFramework tab completion will run the scriptblock on each call. This can be used together with a background refresh mechanism to offload the cost of expensive queries into the background. See Set-PSFTeppResult for details on how to refresh the cache. .EXAMPLE Register-PSFTeppScriptblock -Name "psalcohol-liquids" -ScriptBlock { "beer", "mead", "wine", "vodka", "whiskey", "rum" } Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name "psalcohol-liquids" In step one we set a list of questionable liquids as the list of available beverages for parameter 'Type' on the command 'Get-Alcohol' .EXAMPLE Register-PSFTeppScriptblock -ScriptBlock $scriptBlock -Name MyFirstTeppScriptBlock Stores the scriptblock stored in $scriptBlock under the name "MyFirstTeppScriptBlock" .EXAMPLE $scriptBlock = { (Get-ChildItem (Get-PSFConfigValue -FullName mymodule.path.scripts -Fallback "$env:USERPROFILE\Documents\WindowsPowerShell\Scripts")).FullName } Register-PSFTeppScriptblock -Name mymodule-scripts -ScriptBlock $scriptBlock -Mode Simple Stores a simple scriptblock that will return a list of strings under the name "mymodule-scripts". The system will wrap all the stuff around this that is necessary to provide Tab Expansion and filter out output that doesn't fit the user input so far. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTeppScriptblock')] Param ( [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $true)] [string] $Name, [PSFramework.TabExpansion.TeppScriptMode] $Mode = "Auto", [PSFramework.Parameter.TimeSpanParameter] $CacheDuration = 0 ) $scp = New-Object PSFramework.TabExpansion.ScriptContainer $scp.Name = $Name.ToLower() $scp.LastDuration = New-TimeSpan -Seconds -1 $scp.LastResultsValidity = $CacheDuration if ($Mode -like "Auto") { $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock, [ref]$null, [ref]$null) $simple = $null -eq $ast.ParamBlock } elseif ($Mode -like "Simple") { $simple = $true } else { $simple = $false } if ($simple) { $scr = [scriptblock]::Create(@' param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter ) $start = Get-Date $scriptContainer = [PSFramework.TabExpansion.TabExpansionHost]::Scripts["<name>"] if ($scriptContainer.ShouldExecute) { $scriptContainer.LastExecution = $start $innerScript = [ScriptBlock]::Create(($scriptContainer.InnerScriptBlock)) # Use Write-Output to enumerate arrays properly, avoids trouble with persisting cached results try { $items = $innerScript.Invoke() | Write-Output } catch { $null = $scriptContainer.ErrorRecords.Enqueue($_) } foreach ($item in ($items | Where-Object { "$_" -like "$wordToComplete*"} | Sort-Object)) { New-PSFTeppCompletionResult -CompletionText $item -ToolTip $item } $scriptContainer.LastDuration = (Get-Date) - $start if ($items) { $scriptContainer.LastResult = $items } } else { foreach ($item in ($scriptContainer.LastResult | Where-Object { "$_" -like "$wordToComplete*"} | Sort-Object)) { New-PSFTeppCompletionResult -CompletionText $item -ToolTip $item } } '@.Replace("<name>", $Name.ToLower())) $scp.ScriptBlock = $scr $scp.InnerScriptBlock = $ScriptBlock } else { $scp.ScriptBlock = $ScriptBlock } [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$Name.ToLower()] = $scp } function Set-PSFTeppResult { <# .SYNOPSIS Refreshes the tab completion value cache. .DESCRIPTION Refreshes the tab completion value cache for the specified completion scriptblock. Tab Completion scriptblocks can be configured to retrieve values from a dedicated cache. This allows seamless background refreshes of completion data and eliminates all waits for the user. .PARAMETER TabCompletion The name of the completion script to set the last results for. .PARAMETER Value The values to set. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Set-PSFTeppResult -TabCompletion 'MyModule.Computer' -Value (Get-ADComputer -Filter *).Name Stores the names of all computers in AD into the tab completion cache of the completion scriptblock 'MyModule.Computer' #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTeppResult')] param ( [Parameter(Mandatory = $true)] [PSFramework.Validation.PsfValidateSetAttribute(ScriptBlock = { [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys } )] [string] $TabCompletion, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]] $Value ) begin { } process { if (Test-PSFShouldProcess -PSCmdlet $PSCmdlet -Target $TabCompletion -Action "Setting the cache") { [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$TabCompletion.ToLower()].LastResult = $Value [PSFramework.TabExpansion.TabExpansionHost]::Scripts[$TabCompletion.ToLower()].LastExecution = ([System.DateTime]::Now) } } end { } } function Disable-PSFTaskEngineTask { <# .SYNOPSIS Disables a task registered to the PSFramework task engine. .DESCRIPTION Disables a task registered to the PSFramework task engine. .PARAMETER Task The task registered. Must be a task object returned by Get-PSFTaskEngineTask. .EXAMPLE PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.maintenance' | Disable-PSFTaskEngineTask Disables the task named 'mymodule.maintenance' #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Disable-PSFTaskEngineTask')] Param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [PSFramework.TaskEngine.PsfTask[]] $Task ) process { foreach ($item in $Task) { if ($item.Enabled) { Write-PSFMessage -Level Verbose -Message "Disabling task engine task: $($item.Name)" -Tag 'disable', 'taskengine', 'task' $item.Enabled = $false } } } } function Enable-PSFTaskEngineTask { <# .SYNOPSIS Enables a task registered to the PSFramework task engine. .DESCRIPTION Enables a task registered to the PSFramework task engine. Note: Tasks are enabled by default. Use this function to re-enable a task disabled by Disable-PSFTaskEngineTask. .PARAMETER Task The task registered. Must be a task object returned by Get-PSFTaskEngineTask. .EXAMPLE PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.maintenance' | Enable-PSFTaskEngineTask Enables the task named 'mymodule.maintenance' #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Enable-PSFTaskEngineTask')] Param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [PSFramework.TaskEngine.PsfTask[]] $Task ) begin { $didSomething = $false } process { foreach ($item in $Task) { if (-not $item.Enabled) { Write-PSFMessage -Level Verbose -Message "Enabling task engine task: $($item.Name)" -Tag 'enable','taskengine','task' $item.Enabled = $true $didSomething = $true } } } end { # If we enabled any task, start the runspace again, in case it isn't already running (no effect if it is) if ($didSomething) { Start-PSFRunspace -Name 'psframework.taskengine' } } } function Get-PSFTaskEngineCache { <# .SYNOPSIS Retrieve values from the cache for a task engine task. .DESCRIPTION Retrieve values from the cache for a task engine task. Tasks scheduled under the PSFramework task engine do not have a way to directly pass information to the primary runspace. Using Set-PSFTaskEngineCache, they can store the information somewhere where the main runspace can retrieve it using this function. .PARAMETER Module The name of the module that generated the task. Use scriptname in case of using this within a script. Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache' .PARAMETER Name The name of the task for which the cache is. Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache' .EXAMPLE PS C:\> Get-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask' Retrieves the information stored under 'mymodule.maintenancetask' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTaskEngineCache')] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Module, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Name ) $tempModule = $Module.ToLower() $tempName = $Name.ToLower() try { [PSFramework.TaskEngine.TaskHost]::Cache[$tempModule][$tempName] } catch { } } function Get-PSFTaskEngineTask { <# .SYNOPSIS Returns tasks registered for the task engine .DESCRIPTION Returns tasks registered for the task engine .PARAMETER Name Default: "*" Only tasks with similar names are returned. .EXAMPLE PS C:\> Get-PSFTaskEngineTask Returns all tasks registered to the task engine .EXAMPLE PS C:\> Get-PSFTaskEngineTask -Name 'mymodule.*' Returns all tasks registered to the task engine whose name starts with 'mymodule.' (It stands to reason that only tasks belonging to the module 'mymodule' would be returned that way) #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Get-PSFTaskEngineTask')] Param ( [string] $Name = "*" ) [PSFramework.TaskEngine.TaskHost]::Tasks.Values | Where-Object Name -Like $Name } function Register-PSFTaskEngineTask { <# .SYNOPSIS Allows scheduling PowerShell tasks, that are perfomed in the background. .DESCRIPTION Allows scheduling PowerShell tasks, that are perfomed in the background. All scriptblocks scheduled like this will be performed on a separate runspace. None of the scriptblocks will affect the main session (so you cannot manipulate variables, etc.) This system is designed for two use-cases: - Reducing module import time by off-loading expensive one-time actions (such as building a cache) in the background - Scheduling periodic script executions that should occur while the process is running (e.g.: continuous maintenance, cache updates, ...) It also avoids overloading the client computer by executing too many tasks at the same time, as multiple modules running code in the background might. Instead tasks that are due simultaneously are processed by priority. .PARAMETER Name The name of the task. Must be unique, otherwise it will update the existing task. .PARAMETER Description Description of the task. Helps documenting the task and what it is supposed to be doing. .PARAMETER ScriptBlock The task/scriptblock that should be performed as a background task. .PARAMETER Once Whether the task should be performed only once. .PARAMETER Interval The interval at which the task should be repeated. .PARAMETER Delay How far after the initial registration should the task script wait before processing this. This can be used to delay background stuff that should not content with items that would be good to have as part of the module import. .PARAMETER Priority How important is this task? If multiple tasks are due at the same maintenance cycle, the more critical one will be processed first. .PARAMETER ResetTask If the task already exists, it will be reset by setting this parameter (this switch is ignored when creating new tasks). This allows explicitly registering tasks for re-execution, even though they were set to execute once only. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Register-PSFTaskEngineTask -Name 'mymodule.buildcache' -ScriptBlock $ScriptBlock -Once -Description 'Builds the object cache used by the mymodule module' Registers the task contained in $ScriptBlock under the name 'mymodule.buildcache' to execute once at the system's earliest convenience in a medium (default) priority. .EXAMPLE PS C:\> Register-PSFTaskEngineTask -Name 'mymodule.maintenance' -ScriptBlock $ScriptBlock -Interval "00:05:00" -Delay "00:01:00" -Priority Critical -Description 'Performs critical system maintenance in order for the mymodule module to function' Registers the task contained in $ScriptBlock under the name 'mymodule.maintenance' - Sets it to execute every 5 minutes - Sets it to wait for 1 minute after registration before starting the first execution - Sets it to priority "Critical", ensuring it takes precedence over most other tasks. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFTaskEngineTask')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [string] $Description, [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $true, ParameterSetName = "Once")] [switch] $Once, [Parameter(Mandatory = $true, ParameterSetName = "Repeating")] [PSFTimeSpan] $Interval, [PSFTimeSpan] $Delay, [PSFramework.TaskEngine.Priority] $Priority = "Medium", [switch] $ResetTask, [switch] $EnableException ) #region Case: Task already registered if ([PSFramework.TaskEngine.TaskHost]::Tasks.ContainsKey($Name.ToLower())) { $task = [PSFramework.TaskEngine.TaskHost]::Tasks[$Name.ToLower()] if (Test-PSFParameterBinding -ParameterName Description) { $task.Description = $Description} if ($task.ScriptBlock -ne $ScriptBlock) { $task.ScriptBlock = $ScriptBlock } if (Test-PSFParameterBinding -ParameterName Once) { $task.Once = $Once } if (Test-PSFParameterBinding -ParameterName Interval) { $task.Once = $false $task.Interval = $Interval } if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay } if (Test-PSFParameterBinding -ParameterName Priority) { $task.Priority = $Priority } if ($ResetTask) { $task.Registered = Get-Date $task.LastExecution = New-Object System.DateTime(0) } } #endregion Case: Task already registered #region New Task else { $task = New-Object PSFramework.TaskEngine.PsfTask $task.Name = $Name.ToLower() if (Test-PSFParameterBinding -ParameterName Description) { $task.Description = $Description } $task.ScriptBlock = $ScriptBlock if (Test-PSFParameterBinding -ParameterName Once) { $task.Once = $true } if (Test-PSFParameterBinding -ParameterName Interval) { if ($Interval.Value.Ticks -le 0) { Stop-PSFFunction -Message "Failed to register task: $Name - Interval cannot be 0 or less" -Category InvalidArgument -EnableException $EnableException return } else { $task.Interval = $Interval } } if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay } $task.Priority = $Priority $task.Registered = Get-Date [PSFramework.TaskEngine.TaskHost]::Tasks[$Name.ToLower()] = $task } #endregion New Task Start-PSFRunspace -Name "psframework.taskengine" } function Set-PSFTaskEngineCache { <# .SYNOPSIS Sets the cache for a task engine task. .DESCRIPTION Sets the cache for a task engine task. Tasks executed by the task engine have no way to directly transfer output to the main runspace. This function is designed to work around this by providing a central storage. This function should only be called tasks scheduled to execute within the task engine. .PARAMETER Module The name of the module that generated the task. Use scriptname in case of using this within a script. .PARAMETER Name The name of the task for which the cache is. .PARAMETER Value The value to set this cache to. .EXAMPLE PS C:\> Set-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask' -Value $results Stores the content of $results in the cache 'mymodule / maintenancetask' These values can now be retrieved using Get-PSFTaskEngineCache. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Set-PSFTaskEngineCache')] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Module, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Mandatory = $true)] [AllowNull()] $Value ) $tempModule = $Module.ToLower() $tempName = $Name.ToLower() if (-not ([PSFramework.TaskEngine.TaskHost]::Cache.ContainsKey($tempModule))) { [PSFramework.TaskEngine.TaskHost]::Cache[$tempModule] = @{ } } [PSFramework.TaskEngine.TaskHost]::Cache[$tempModule][$tempName] = $Value } function Test-PSFTaskEngineCache { <# .SYNOPSIS Tests, whether the specified task engine cache-entry has been written. .DESCRIPTION Tests, whether the specified task engine cache-entry has been written. .PARAMETER Module The name of the module that generated the task. Use scriptname in case of using this within a script. Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache' .PARAMETER Name The name of the task for which the cache is. Note: Must be the same as the name used within the task when calling 'Set-PSFTaskEngineCache' .EXAMPLE PS C:\> Test-PSFTaskEngineCache -Module 'mymodule' -Name 'maintenancetask' Returns, whether the cache has been set for the module 'mymodule' and the task 'maintenancetask' Does not require the cache to actually contain a value, but must exist. #> [OutputType([System.Boolean])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFTaskEngineCache')] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Module, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Name ) $tempModule = $Module.ToLower() $tempName = $Name.ToLower() if (-not ([PSFramework.TaskEngine.TaskHost]::Cache.ContainsKey($tempModule))) { return $false } if (-not ([PSFramework.TaskEngine.TaskHost]::Cache[$tempModule].ContainsKey($tempName))) { return $false } return $true } function Test-PSFTaskEngineTask { <# .SYNOPSIS Tests, whether the specified task has already been executed. .DESCRIPTION Tests, whether the specified task has already been executed. Returns false, if the task doesn't exist. .PARAMETER Name Name of the task to test .EXAMPLE PS C:\> Test-PSFTaskEngineTask -Name 'mymodule.maintenance' Returns, whether the task named 'mymodule.maintenance' has already been executed at least once. #> [OutputType([System.Boolean])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Test-PSFTaskEngineTask')] Param ( [Parameter(Mandatory = $true)] [string] $Name ) if (-not ([PSFramework.TaskEngine.TaskHost]::Tasks.ContainsKey($Name.ToLower()))) { return $false } $task = [PSFramework.TaskEngine.TaskHost]::Tasks[$Name.ToLower()] $task.LastExecution -gt $task.Registered } function ConvertFrom-PSFArray { <# .SYNOPSIS Flattens properties that have array values. .DESCRIPTION Flattens properties that have array values. With this you can prepare objects for export to systems that cannot handle collection in propertyvalues. This flattening happens using a string join operation, so the output on modified properties is guaranteed to be a string. .PARAMETER JoinBy The string sequence to join values by. Defaults to ", " .PARAMETER PropertyName The properties to affect. Interprets wildcards, defaults to '*'. .PARAMETER InputObject The objects the properties of which to flatten. .EXAMPLE PS C:\> Get-Something | ConvertFrom-PSFArray | Export-Csv -Path .\output.csv Processes the output of Get-Something in order to produce a flat table to export data to csv without trimming collections. #> [CmdletBinding()] param ( [Parameter(Position = 0)] [string] $JoinBy = ', ', [Parameter(Position = 1)] [string[]] $PropertyName = '*', [Parameter(ValueFromPipeline = $true)] $InputObject ) process { $props = [ordered]@{ } foreach ($property in $InputObject.PSObject.Properties) { #region Skip non-collection properties if ($property.Value -isnot [System.Collections.ICollection]) { $props[$property.Name] = $property.Value continue } #endregion Skip non-collection properties #region Handle whether the property should be processed at all $found = $false foreach ($name in $PropertyName) { if ($property.Name -like $name) { $found = $true break } } if (-not $found) { $props[$property.Name] = $property.Value continue } #endregion Handle whether the property should be processed at all $props[$property.Name] = $property.Value -join $JoinBy } [PSCustomObject]$props } } function ConvertTo-PSFHashtable { <# .SYNOPSIS Converts an object into a hashtable. .DESCRIPTION Converts an object into a hashtable. Can exclude individual properties from being included. .PARAMETER Exclude The propertynames to exclude. Must be full property-names, no wildcard/regex matching. .PARAMETER InputObject The object(s) to convert .EXAMPLE PS C:\> Get-ChildItem | ConvertTo-PSFHashtable Scans all items in the current path and converts those objects into hashtables. #> [OutputType([System.Collections.Hashtable])] [CmdletBinding()] Param ( [string[]] $Exclude, [Parameter(ValueFromPipeline = $true)] $InputObject ) process { foreach ($item in $InputObject) { if ($null -eq $item) { continue } if ($item -is [System.Collections.Hashtable]) { $hashTable = $item.Clone() foreach ($name in $Exclude) { $hashTable.Remove($name) } $hashTable } elseif ($item -is [System.Collections.IDictionary]) { $hashTable = @{ } foreach ($name in $item.Keys) { if ($name -in $Exclude) { continue } $hashTable[$name] = $item[$name] } $hashTable } else { $hashTable = @{ } foreach ($property in $item.PSObject.Properties) { if ($property.Name -in $Exclude) { continue } $hashTable[$property.Name] = $property.Value } $hashTable } } } } function Get-PSFScriptblock { <# .SYNOPSIS Access the scriptblocks stored with Set-PSFScriptblock. .DESCRIPTION Access the scriptblocks stored with Set-PSFScriptblock. Use this command to access scriptblocks designed for easy, processwide access. .PARAMETER Name The name of the scriptblock to request. It's mandatory for explicitly requesting a scriptblock, but optional to use with -List as a filter. .PARAMETER List Instead of requesting a specific scriptblock, list the available ones. This can be further filtered by using a wildcard supporting string as -Name. .EXAMPLE PS C:\> Get-PSFScriptblock -Name 'MyModule.TestServer' Returns the scriptblock stored as 'MyModule.TestServer' .EXAMPLE PS C:\> Get-PSFScriptblock -List Returns a list of all scriptblocks .EXAMPLE PS C:\> Get-PSFScriptblock -List -Name 'MyModule.TestServer' Returns scriptblock and meta information for the MyModule.TestServer scriptblock. #> [OutputType([PSFramework.Utility.ScriptBlockItem], ParameterSetName = 'List')] [OutputType([System.Management.Automation.ScriptBlock], ParameterSetName = 'Name')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueForMandatoryParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Name')] param ( [Parameter(ParameterSetName = 'List')] [Parameter(Mandatory = $true, ParameterSetName = 'Name', ValueFromPipeline = $true)] [string[]] $Name = '*', [Parameter(Mandatory = $true, ParameterSetName = 'List')] [switch] $List ) begin { [System.Collections.ArrayList]$sent = @() $allItems = [PSFramework.Utility.UtilityHost]::ScriptBlocks.Values } process { :main foreach ($nameText in $Name) { switch ($PSCmdlet.ParameterSetName) { 'Name' { if ($sent -contains $nameText) { continue main } $null = $sent.Add($nameText) [PSFramework.Utility.UtilityHost]::ScriptBlocks[$nameText].ScriptBlock } 'List' { foreach ($item in $allItems) { if ($item.Name -notlike $nameText) { continue } if ($sent -contains $item.Name) { continue } $null = $sent.Add($item.Name) $item } } } } } } function Join-PSFPath { <# .SYNOPSIS Performs multisegment path joins. .DESCRIPTION Performs multisegment path joins. .PARAMETER Path The basepath to join on. .PARAMETER Child Any number of child paths to add. .PARAMETER Normalize Normalizes path separators for the path segments offered. This ensures the correct path separators for the current OS are chosen. .EXAMPLE PS C:\> Join-PSFPath -Path 'C:\temp' 'Foo' 'Bar' Returns 'C:\temp\Foo\Bar' .EXAMPLE PS C:\> Join-PSFPath -Path 'C:\temp' 'Foo' 'Bar' -Normalize Returns 'C:\temp\Foo\Bar' on a Windows OS. Returns 'C:/temp/Foo/Bar' on most non-Windows OSes. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Path, [Parameter(ValueFromRemainingArguments = $true)] [string[]] $Child, [switch] $Normalize ) $resultingPath = $Path foreach ($childItem in $Child) { $resultingPath = Join-Path -Path $resultingPath -ChildPath $childItem } if ($Normalize) { $defaultSeparator = [System.IO.Path]::DirectorySeparatorChar $altSeparator = "/" if ($defaultSeparator -eq "/") { $altSeparator = "\" } $resultingPath = $resultingPath.Replace($altSeparator, $defaultSeparator) } $resultingPath } function New-PSFSupportPackage { <# .SYNOPSIS Creates a package of troubleshooting information that can be used by developers to help debug issues. .DESCRIPTION This function creates an extensive debugging package that can help with reproducing and fixing issues. The file will be created on the desktop by default and will contain quite a bit of information: - OS Information - Hardware Information (CPU, Ram, things like that) - .NET Information - PowerShell Information - Your input history - The In-Memory message log - The In-Memory error log - Screenshot of the console buffer (Basically, everything written in your current console, even if you have to scroll upwards to see it). .PARAMETER Path The folder where to place the output xml in. Defaults to your desktop. .PARAMETER Include What to include in the export. By default, all is included. .PARAMETER Exclude Anything not to include in the export. Use this to explicitly exclude content you do not wish to be part of the dump (for example for data protection reasons). .PARAMETER Variables Name of additional variables to attach. This allows you to add the content of variables to the support package, if you believe them to be relevant to the case. .PARAMETER ExcludeError By default, the content of $Error is included, as it often can be helpful in debugging, even with error handling using the message system. However, there can be rare instances where this will explode the total export size to gigabytes, in which case it becomes necessary to skip this. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE New-PSFSupportPackage Creates a large support pack in order to help us troubleshoot stuff. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/New-PSFSupportPackage')] param ( [string] $Path = "$($env:USERPROFILE)\Desktop", [PSFramework.Utility.SupportData] $Include = 'All', [PSFramework.Utility.SupportData] $Exclude = 'None', [string[]] $Variables, [switch] $ExcludeError, [switch] [Alias('Silent')] $EnableException ) begin { Write-PSFMessage -Level InternalComment -Message "Starting" Write-PSFMessage -Level Verbose -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" #region Helper functions function Get-ShellBuffer { [CmdletBinding()] param () try { # Define limits $rec = New-Object System.Management.Automation.Host.Rectangle $rec.Left = 0 $rec.Right = $host.ui.rawui.BufferSize.Width - 1 $rec.Top = 0 $rec.Bottom = $host.ui.rawui.BufferSize.Height - 1 # Load buffer $buffer = $host.ui.rawui.GetBufferContents($rec) # Convert Buffer to list of strings $int = 0 $lines = @() while ($int -le $rec.Bottom) { $n = 0 $line = "" while ($n -le $rec.Right) { $line += $buffer[$int, $n].Character $n++ } $line = $line.TrimEnd() $lines += $line $int++ } # Measure empty lines at the beginning $int = 0 $temp = $lines[$int] while ($temp -eq "") { $int++; $temp = $lines[$int] } # Measure empty lines at the end $z = $rec.Bottom $temp = $lines[$z] while ($temp -eq "") { $z--; $temp = $lines[$z] } # Skip the line launching this very function $z-- # Measure empty lines at the end (continued) $temp = $lines[$z] while ($temp -eq "") { $z--; $temp = $lines[$z] } # Cut results to the limit and return them return $lines[$int .. $z] } catch { } } #endregion Helper functions } process { $filePathXml = Join-Path $Path "powershell_support_pack_$(Get-Date -Format "yyyy_MM_dd-HH_mm_ss").cliDat" $filePathZip = $filePathXml -replace "\.cliDat$", ".zip" Write-PSFMessage -Level Critical -Message @" Gathering information... Will write the final output to: $filePathZip $(Get-PSFConfigValue -FullName 'psframework.supportpackage.contactmessage' -Fallback '') Be aware that this package contains a lot of information including your input history in the console. Please make sure no sensitive data (such as passwords) can be caught this way. Ideally start a new console, perform the minimal steps required to reproduce the issue, then run this command. This will make it easier for us to troubleshoot and you won't be sending us the keys to your castle. "@ $hash = @{ } if (($Include -band 1) -and -not ($Exclude -band 1)) { Write-PSFMessage -Level Important -Message "Collecting PSFramework logged messages (Get-PSFMessage)" $hash["Messages"] = Get-PSFMessage } if (($Include -band 2) -and -not ($Exclude -band 2)) { Write-PSFMessage -Level Important -Message "Collecting PSFramework logged errors (Get-PSFMessage -Errors)" $hash["Errors"] = Get-PSFMessage -Errors } if (($Include -band 4) -and -not ($Exclude -band 4)) { Write-PSFMessage -Level Important -Message "Trying to collect copy of console buffer (what you can see on your console)" $hash["ConsoleBuffer"] = Get-ShellBuffer } if (($Include -band 8) -and -not ($Exclude -band 8)) { Write-PSFMessage -Level Important -Message "Collecting Operating System information (Win32_OperatingSystem)" $hash["OperatingSystem"] = Get-CimInstance -ClassName Win32_OperatingSystem } if (($Include -band 16) -and -not ($Exclude -band 16)) { Write-PSFMessage -Level Important -Message "Collecting CPU information (Win32_Processor)" $hash["CPU"] = Get-CimInstance -ClassName Win32_Processor } if (($Include -band 32) -and -not ($Exclude -band 32)) { Write-PSFMessage -Level Important -Message "Collecting Ram information (Win32_PhysicalMemory)" $hash["Ram"] = Get-CimInstance -ClassName Win32_PhysicalMemory } if (($Include -band 64) -and -not ($Exclude -band 64)) { Write-PSFMessage -Level Important -Message "Collecting PowerShell & .NET Version (`$PSVersionTable)" $hash["PSVersion"] = $PSVersionTable } if (($Include -band 128) -and -not ($Exclude -band 128)) { Write-PSFMessage -Level Important -Message "Collecting Input history (Get-History)" $hash["History"] = Get-History } if (($Include -band 256) -and -not ($Exclude -band 256)) { Write-PSFMessage -Level Important -Message "Collecting list of loaded modules (Get-Module)" $hash["Modules"] = Get-Module } if (($Include -band 512) -and -not ($Exclude -band 512)) { Write-PSFMessage -Level Important -Message "Collecting list of loaded snapins (Get-PSSnapin)" $hash["SnapIns"] = Get-PSSnapin } if (($Include -band 1024) -and -not ($Exclude -band 1024)) { Write-PSFMessage -Level Important -Message "Collecting list of loaded assemblies (Name, Version, and Location)" $hash["Assemblies"] = [appdomain]::CurrentDomain.GetAssemblies() | Select-Object CodeBase, FullName, Location, ImageRuntimeVersion, GlobalAssemblyCache, IsDynamic } if (Test-PSFParameterBinding -ParameterName "Variables") { Write-PSFMessage -Level Important -Message "Adding variables specified for export: $($Variables -join ", ")" $hash["Variables"] = $Variables | Get-Variable -ErrorAction Ignore } if (($Include -band 2048) -and -not ($Exclude -band 2048) -and (-not $ExcludeError)) { Write-PSFMessage -Level Important -Message "Adding content of `$Error" $hash["PSErrors"] = @() foreach ($errorItem in $global:Error) { $hash["PSErrors"] += New-Object PSFramework.Message.PsfException($errorItem) } } if (($Include -band 4096) -and -not ($Exclude -band 4096)) { if (Test-Path function:Get-DbatoolsLog) { Write-PSFMessage -Level Important -Message "Collecting dbatools logged messages (Get-DbatoolsLog)" $hash["DbatoolsMessages"] = Get-DbatoolsLog Write-PSFMessage -Level Important -Message "Collecting dbatools logged errors (Get-DbatoolsLog -Errors)" $hash["DbatoolsErrors"] = Get-DbatoolsLog -Errors } } $data = [pscustomobject]$hash try { $data | Export-PsfClixml -Path $filePathXml -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to export dump to file!" -ErrorRecord $_ -Target $filePathXml return } try { Compress-Archive -Path $filePathXml -DestinationPath $filePathZip -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to pack dump-file into a zip archive. Please do so manually before submitting the results as the unpacked xml file will be rather large." -ErrorRecord $_ -Target $filePathZip return } Remove-Item -Path $filePathXml -ErrorAction Ignore } end { Write-PSFMessage -Level InternalComment -Message "Ending" } } function Remove-PSFAlias { <# .SYNOPSIS Removes an alias from the global scope. .DESCRIPTION Removes an alias from the global* scope. Please note that this always affects the global scope and should not be used lightly. This has the potential to break code that does not comply with PowerShell best practices and relies on the use of aliases. Refuses to delete constant aliases. Requires the '-Force' parameter to delete ReadOnly aliases. *This includes aliases exported by modules. .PARAMETER Name The name of the alias to remove. .PARAMETER Force Enforce removal of aliases. Required to remove ReadOnly aliases (including default aliases such as "select" or "group"). .EXAMPLE PS C:\> Remove-PSFAlias -Name 'grep' Removes the global alias 'grep' .EXAMPLE PS C:\> Remove-PSFAlias -Name 'select' -Force Removes the default alias 'select' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [string[]] $Name, [switch] $Force ) process { foreach ($alias in $Name) { try { [PSFramework.Utility.UtilityHost]::RemovePowerShellAlias($alias, $Force.ToBool()) } catch { Stop-PSFFunction -Message $_ -EnableException $true -Cmdlet $PSCmdlet -ErrorRecord $_ -OverrideExceptionMessage } } } } function Resolve-PSFDefaultParameterValue { <# .SYNOPSIS Used to filter and process default parameter values. .DESCRIPTION This command picks all the default parameter values from a reference hashtable. It then filters all that match a specified command and binds them to that specific command, narrowing its focus. These get merged into either a new or a specified hashtable and returned. .PARAMETER Reference The hashtable to pick default parameter values from. .PARAMETER CommandName The commands to pick default parameter values for. .PARAMETER Target The target hashtable to merge results into. By default an empty hashtable is used. .PARAMETER ParameterName Only resolve for specific parameter names. .EXAMPLE PS C:\> Resolve-PSFDefaultParameterValue -Reference $global:PSDefaultParameterValues -CommandName 'Invoke-WebRequest' Returns a hashtable containing all default parameter values in the global scope affecting the command 'Invoke-WebRequest'. #> [OutputType([System.Collections.Hashtable])] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Resolve-PSFDefaultParameterValue')] param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable] $Reference, [Parameter(Mandatory = $true)] [string[]] $CommandName, [System.Collections.Hashtable] $Target = @{ }, [string[]] $ParameterName = "*" ) begin { $defaultItems = @() foreach ($key in $Reference.Keys) { $defaultItems += [PSCustomObject]@{ Key = $key Value = $Reference[$key] Command = $key.Split(":")[0] Parameter = $key.Split(":")[1] } } } process { foreach ($command in $CommandName) { foreach ($item in $defaultItems) { if ($command -notlike $item.Command) { continue } foreach ($parameter in $ParameterName) { if ($item.Parameter -like $parameter) { if ($parameter -ne "*") { $Target["$($command):$($parameter)"] = $item.Value } else { $Target["$($command):$($item.Parameter)"] = $item.Value } } } } } } end { $Target } } function Resolve-PSFPath { <# .SYNOPSIS Resolves a path. .DESCRIPTION Resolves a path. Will try to resolve to paths including some basic path validation and resolution. Will fail if the path cannot be resolved (so an existing path must be reached at). .PARAMETER Path The path to validate. .PARAMETER Provider Ensure the path is of the expected provider. Allows ensuring one does not operate in the wrong provider. Common providers include the filesystem, the registry or the active directory. .PARAMETER SingleItem Ensure the path should resolve to a single path only. This may - intentionally or not - trip up wildcard paths. .PARAMETER NewChild Assumes one wishes to create a new child item. The parent path will be resolved and must validate true. The final leaf will be treated as a leaf item that does not exist yet. .EXAMPLE PS C:\> Resolve-PSFPath -Path report.log -Provider FileSystem -NewChild -SingleItem Ensures the resolved path is a FileSystem path. This will resolve to the current folder and the file report.log. Will not ensure the file exists or doesn't exist. If the current path is in a different provider, it will throw an exception. .EXAMPLE PS C:\> Resolve-PSFPath -Path ..\* This will resolve all items in the parent folder, whatever the current path or drive might be. #> [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Resolve-PSFPath')] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $Path, [string] $Provider, [switch] $SingleItem, [switch] $NewChild ) process { foreach ($inputPath in $Path) { if ($inputPath -eq ".") { $inputPath = (Get-Location).Path } if ($NewChild) { $parent = Split-Path -Path $inputPath $child = Split-Path -Path $inputPath -Leaf try { if (-not $parent) { $parentPath = Get-Location -ErrorAction Stop } else { $parentPath = Resolve-Path $parent -ErrorAction Stop } } catch { Stop-PSFFunction -Message "Failed to resolve path" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet } if ($SingleItem -and (($parentPath | Measure-Object).Count -gt 1)) { Stop-PSFFunction -Message "Could not resolve to a single parent path!" -EnableException $true -Cmdlet $PSCmdlet } if ($Provider -and ($parentPath.Provider.Name -ne $Provider)) { Stop-PSFFunction -Message "Resolved provider is $($parentPath.Provider.Name) when it should be $($Provider)" -EnableException $true -Cmdlet $PSCmdlet } foreach ($parentItem in $parentPath) { Join-Path $parentItem.ProviderPath $child } } else { try { $resolvedPaths = Resolve-Path $inputPath -ErrorAction Stop } catch { Stop-PSFFunction -Message "Failed to resolve path" -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet } if ($SingleItem -and (($resolvedPaths | Measure-Object).Count -gt 1)) { Stop-PSFFunction -Message "Could not resolve to a single parent path!" -EnableException $true -Cmdlet $PSCmdlet } if ($Provider -and ($resolvedPaths.Provider.Name -ne $Provider)) { Stop-PSFFunction -Message "Resolved provider is $($resolvedPaths.Provider.Name) when it should be $($Provider)" -EnableException $true -Cmdlet $PSCmdlet } $resolvedPaths.ProviderPath } } } } function Select-PSFPropertyValue { <# .SYNOPSIS Expand specific property values based on selection logic. .DESCRIPTION This command allows picking a set of properties and then returning ... - All their values - The value that meets specific rules - A composite value .PARAMETER Property The properties to work with, in the order they should be considered. .PARAMETER Fallback Whether to fall back on other properties if the first one doesn't contain values. This picks the value of the first property that actually has a value. .PARAMETER Select Select either the largest or lowest propertyvalue in the Propertynames specified. .PARAMETER JoinBy Joins the selected properties by the string specified. .PARAMETER FormatWith Formats the selected properties into the specified format string. .PARAMETER InputObject The object(s) whose properties to inspect. .EXAMPLE PS C:\> Get-ADComputer -Filter * | Select-PSFPropertyValue -Property 'DNSHostName', 'Name' -Fallback For each computer in the domain, it will pick the DNSHostName if available, otherwise the Name. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, Position = 0)] [string[]] $Property, [Parameter(ParameterSetName = 'Fallback')] [switch] $Fallback, [Parameter(ParameterSetName = 'Select')] [ValidateSet('Lowest', 'Largest')] [string] $Select, [Parameter(ParameterSetName = 'Join')] [string] $JoinBy, [Parameter(ParameterSetName = 'Format')] [string] $FormatWith, [Parameter(ValueFromPipeline = $true)] $InputObject ) process { foreach ($object in $InputObject) { switch ($PSCmdlet.ParameterSetName) { 'Default' { foreach ($prop in $Property) { $object.$Prop } } 'Fallback' { foreach ($prop in $Property) { if ($null -ne ($object.$Prop | Remove-PSFNull -Enumerate)) { $object.$prop break } } } 'Select' { $values = @() foreach ($prop in $Property) { $values += $object.$Prop } if ($Select -eq 'Largest') { $values | Sort-Object -Descending | Select-Object -First 1 } else { $values | Sort-Object | Select-Object -First 1 } } 'Join' { $values = @() foreach ($prop in $Property) { $values += $object.$Prop } $values -join $JoinBy } 'Format' { $values = @() foreach ($prop in $Property) { $values += $object.$Prop } $FormatWith -f $values } } } } } function Set-PSFScriptblock { <# .SYNOPSIS Stores a scriptblock in the central scriptblock store. .DESCRIPTION Stores a scriptblock in the central scriptblock store. This store can be accessed using Get-PSFScriptblock. It is used to share scriptblocks outside of scope and runspace boundaries. Scriptblocks thus registered can be accessed by C#-based services, such as the PsfValidateScript attribute. .PARAMETER Name The name of the scriptblock. Must be unique, it is recommended to prefix the module name: <Module>.<Scriptblock> .PARAMETER Scriptblock The scriptcode to register .EXAMPLE PS C:\> Set-PSFScriptblock -Name 'MyModule.TestServer' -Scriptblock $Scriptblock Stores the scriptblock contained in $Scriptblock under the 'MyModule.TestServer' name. .NOTES Repeatedly registering the same scriptblock (e.g. in multi-runspace scenarios) is completely safe: - Access is threadsafe & Runspacesafe - Overwriting the scriptblock does not affect the statistics #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [string] $Name, [Parameter(Position = 1, Mandatory = $true)] [System.Management.Automation.ScriptBlock] $Scriptblock ) process { if ([PSFramework.Utility.UtilityHost]::ScriptBlocks.ContainsKey($Name)) { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name].Scriptblock = $Scriptblock } else { [PSFramework.Utility.UtilityHost]::ScriptBlocks[$Name] = New-Object PSFramework.Utility.ScriptBlockItem($Name, $Scriptblock) } } } <# Registers the cmdlets published by this module. Necessary for full hybrid module support. #> $commonParam = @{ HelpFile = (Resolve-Path "$($script:ModuleRoot)\en-us\PSFramework.dll-Help.xml") Module = $ExecutionContext.SessionState.Module } Import-PSFCmdlet @commonParam -Name Invoke-PSFProtectedCommand -Type ([PSFramework.Commands.InvokePSFProtectedCommand]) Import-PSFCmdlet @commonParam -Name Remove-PSFNull -Type ([PSFramework.Commands.RemovePSFNullCommand]) Import-PSFCmdlet @commonParam -Name Select-PSFObject -Type ([PSFramework.Commands.SelectPSFObjectCommand]) Import-PSFCmdlet @commonParam -Name Set-PSFConfig -Type ([PSFramework.Commands.SetPSFConfigCommand]) Import-PSFCmdlet @commonParam -Name Test-PSFShouldProcess -Type ([PSFramework.Commands.TestPSFShouldProcessCommand]) Import-PSFCmdlet @commonParam -Name Write-PSFMessage -Type ([PSFramework.Commands.WritePSFMessageCommand]) Import-LocalizedString -Path "$script:ModuleRoot\en-us\*.psd1" -Module PSFramework -Language 'en-US' $script:strings = Get-PSFLocalizedString -Module PSFramework Register-PSFConfigSchema -Name Default -Schema { param ( [string] $Resource, [System.Collections.Hashtable] $Settings ) #region Converting parameters $Peek = $Settings["Peek"] $ExcludeFilter = $Settings["ExcludeFilter"] $IncludeFilter = $Settings["IncludeFilter"] $AllowDelete = $Settings["AllowDelete"] $EnableException = $Settings["EnableException"] Set-Location -Path $Settings["Path"] #endregion Converting parameters #region Utility Function function Read-PsfConfigFile { <# .SYNOPSIS Reads a configuration file and parses it. .DESCRIPTION Reads a configuration file and parses it. .PARAMETER Path The path to the file to parse. .PARAMETER WebLink The link to a website to download straight as raw json. .PARAMETER RawJson Raw json data to interpret. .EXAMPLE PS C:\> Read-PsfConfigFile -Path config.json Reads the config.json file and returns interpreted configuration objects. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'Weblink')] [string] $Weblink, [Parameter(Mandatory = $true, ParameterSetName = 'RawJson')] [string] $RawJson ) #region Utility Function function New-ConfigItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $FullName, $Value, $Type, [switch] $KeepPersisted, [switch] $Enforced, [switch] $Policy ) [pscustomobject]@{ FullName = $FullName Value = $Value Type = $Type KeepPersisted = $KeepPersisted Enforced = $Enforced Policy = $Policy } } function Get-WebContent { [CmdletBinding()] param ( [string] $WebLink ) $webClient = New-Object System.Net.WebClient $webClient.Encoding = [System.Text.Encoding]::UTF8 $webClient.DownloadString($WebLink) } #endregion Utility Function if ($Path) { if (-not (Test-Path $Path)) { return } $data = Get-Content -Path $Path -Encoding UTF8 -Raw | ConvertFrom-Json -ErrorAction Stop } if ($Weblink) { $data = Get-WebContent -WebLink $Weblink | ConvertFrom-Json -ErrorAction Stop } if ($RawJson) { $data = $RawJson | ConvertFrom-Json -ErrorAction Stop } foreach ($item in $data) { #region No Version if (-not $item.Version) { New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type)) } #endregion No Version #region Version One if ($item.Version -eq 1) { if ((-not $item.Style) -or ($item.Style -eq "Simple")) { New-ConfigItem -FullName $item.FullName -Value $item.Data } else { if (($item.Type -eq "Object") -or ($item.Type -eq 12)) { New-ConfigItem -FullName $item.FullName -Value $item.Value -Type "Object" -KeepPersisted } else { New-ConfigItem -FullName $item.FullName -Value ([PSFramework.Configuration.ConfigurationHost]::ConvertFromPersistedValue($item.Value, $item.Type)) } } } #endregion Version One } } #endregion Utility Function try { if ($Resource -like "http*") { $data = Read-PsfConfigFile -Weblink $Resource -ErrorAction Stop } else { $pathItem = $null try { $pathItem = Resolve-PSFPath -Path $Resource -SingleItem -Provider FileSystem } catch { } if ($pathItem) { $data = Read-PsfConfigFile -Path $pathItem -ErrorAction Stop } else { $data = Read-PsfConfigFile -RawJson $Resource -ErrorAction Stop } } } catch { Stop-PSFFunction -Message "Failed to import $Resource" -EnableException $EnableException -Tag 'fail', 'import' -ErrorRecord $_ -Continue -Target $Resource -Cmdlet $Settings["Cmdlet"] } :element foreach ($element in $data) { #region Exclude Filter foreach ($exclusion in $ExcludeFilter) { if ($element.FullName -like $exclusion) { continue element } } #endregion Exclude Filter #region Include Filter if ($IncludeFilter) { $isIncluded = $false foreach ($inclusion in $IncludeFilter) { if ($element.FullName -like $inclusion) { $isIncluded = $true break } } if (-not $isIncluded) { continue } } #endregion Include Filter if ($Peek) { $element } else { try { if (-not $element.KeepPersisted) { Set-PSFConfig -FullName $element.FullName -Value $element.Value -EnableException -AllowDelete:$AllowDelete } else { Set-PSFConfig -FullName $element.FullName -PersistedValue $element.Value -PersistedType $element.Type -AllowDelete:$AllowDelete } } catch { Stop-PSFFunction -Message "Failed to set '$($element.FullName)'" -ErrorRecord $_ -EnableException $EnableException -Tag 'fail', 'import' -Continue -Target $Resource -Cmdlet $Settings["Cmdlet"] } } } } Register-PSFConfigSchema -Name MetaJson -Schema { param ( [string] $Resource, [System.Collections.Hashtable] $Settings ) Write-PSFMessage -String 'Configuration.Schema.MetaJson.ProcessResource' -StringValues $Resource -ModuleName PSFramework #region Converting parameters $Peek = $Settings["Peek"] $ExcludeFilter = $Settings["ExcludeFilter"] $IncludeFilter = $Settings["IncludeFilter"] $AllowDelete = $Settings["AllowDelete"] $script:EnableException = $Settings["EnableException"] $script:cmdlet = $Settings["Cmdlet"] Set-Location -Path $Settings["Path"] #endregion Converting parameters #region Utility Function function Read-V1Node { [CmdletBinding()] param ( $NodeData, [string] $Path, [Hashtable] $Result ) Write-PSFMessage -String 'Configuration.Schema.MetaJson.ProcessFile' -StringValues $Path -ModuleName PSFramework $basePath = Split-Path -Path $Path if ($NodeData.ModuleName) { $moduleName = "{0}." -f $NodeData.ModuleName } else { $moduleName = "" } #region Import Resources foreach ($property in $NodeData.Static.PSObject.Properties) { $Result["$($moduleName)$($property.Name)"] = $property.Value } foreach ($property in $NodeData.Object.PSObject.Properties) { $Result["$($moduleName)$($property.Name)"] = $property.Value | ConvertFrom-PSFClixml } foreach ($property in $NodeData.Dynamic.PSObject.Properties) { $Result["$($moduleName)$(Resolve-V1String -String $property.Name)"] = Resolve-V1String -String $property.Value } #endregion Import Resources #region Import included / linked configuration files foreach ($include in $NodeData.Include) { $resolvedInclude = Resolve-V1String -String $include $uri = [uri]$resolvedInclude if ($uri.IsAbsoluteUri) { try { $newData = Get-Content $resolvedInclude -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidJson' -StringValues $resolvedInclude -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -Cmdlet $script:cmdlet } try { $null = Read-V1Node -NodeData $newData -Result $Result -Path $resolvedInclude continue } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.NestedError' -StringValues $resolvedInclude -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -Cmdlet $script:cmdlet } } $joinedPath = Join-Path -Path $basePath -ChildPath ($resolvedInclude -replace '^\.\\', '\') try { $resolvedIncludeNew = Resolve-PSFPath -Path $joinedPath -Provider FileSystem -SingleItem } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.ResolveFile' -StringValues $joinedPath -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -Cmdlet $script:cmdlet } try { $newData = Get-Content $resolvedIncludeNew -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidJson' -StringValues $resolvedIncludeNew -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -Cmdlet $script:cmdlet } try { $null = Read-V1Node -NodeData $newData -Result $Result -Path $resolvedIncludeNew continue } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.NestedError' -StringValues $resolvedIncludeNew -EnableException $script:EnableException -ModuleName PSFramework -ErrorRecord $_ -Continue -Cmdlet $script:cmdlet } } #endregion Import included / linked configuration files $Result } function Resolve-V1String { <# .SYNOPSIS Resolves a string by inserting placeholders for environment variables. .DESCRIPTION Resolves a string by inserting placeholders for environment variables. .PARAMETER String The string to resolve. .EXAMPLE PS C:\> Resolve-V1String -String '.\%COMPUTERNAME%\config.json' Resolves the specified string, inserting the local computername for %COMPUTERNAME%. #> [CmdletBinding()] param ( $String ) if ($String -isnot [string]) { return $String } $scriptblock = { param ( $Match ) $script:envData[$Match.Value] } [regex]::Replace($String, $script:envDataNamesRGX, $scriptblock) } #endregion Utility Function #region Utility Computation $script:envData = @{ } foreach ($envItem in (Get-ChildItem env:\)) { $script:envData["%$($envItem.Name)%"] = $envItem.Value } $script:envDataNamesRGX = $script:envData.Keys -join '|' #endregion Utility Computation #region Accessing Content try { $resolvedPath = Resolve-PSFPath -Path $Resource -Provider FileSystem -SingleItem } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.ResolveFile' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet return } try { $importData = Get-Content -Path $resolvedPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.InvalidJson' -StringValues $Resource -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -ErrorRecord $_ -Cmdlet $script:cmdlet return } #endregion Accessing Content switch ($importData.Version) { 1 { $configurationHash = Read-V1Node -NodeData $importData -Path $resolvedPath -Result @{ } $configurationItems = $configurationHash.Keys | ForEach-Object { [pscustomobject]@{ FullName = $_ Value = $configurationHash[$_] } } foreach ($configItem in $configurationItems) { if ($ExcludeFilter | Where-Object { $configItem.FullName -like $_ }) { continue } if ($IncludeFilter -and -not ($IncludeFilter | Where-Object { $configItem.FullName -like $_ })) { continue } if ($Peek) { $configItem continue } Set-PSFConfig -FullName $configItem.FullName -Value $configItem.Value -AllowDelete:$AllowDelete } } default { Stop-PSFFunction -String 'Configuration.Schema.MetaJson.UnknownVersion' -StringValues $Resource, $importData.Version -ModuleName PSFramework -FunctionName 'Schema: MetaJson' -EnableException $EnableException -Cmdlet $script:cmdlet return } } } Register-PSFConfigValidation -Name "bool" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { if ($Value.GetType().FullName -ne "System.Boolean") { $Result.Message = "Not a boolean: $Value" $Result.Success = $False return $Result } } catch { $Result.Message = "Not a boolean: $Value" $Result.Success = $False return $Result } $Result.Value = $Value return $Result } Register-PSFConfigValidation -Name "consolecolor" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { [System.ConsoleColor]$color = $Value } catch { $Result.Message = "Not a console color: $Value" $Result.Success = $False return $Result } $Result.Value = $color return $Result } Register-PSFConfigValidation -Name "credential" -ScriptBlock { param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { if ($Value.GetType().FullName -ne "System.Management.Automation.PSCredential") { $Result.Message = "Not a credential: $Value" $Result.Success = $False return $Result } } catch { $Result.Message = "Not a credential: $Value" $Result.Success = $False return $Result } $Result.Value = $Value return $Result } Register-PSFConfigValidation -Name "datetime" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { [DateTime]$DateTime = $Value } catch { $Result.Message = "Not a DateTime: $Value" $Result.Success = $False return $Result } $Result.Value = $DateTime return $Result } Register-PSFConfigValidation -Name "double" -ScriptBlock { Param ( $Value ) $Result = New-Object PSOBject -Property @{ Success = $True Value = $null Message = "" } try { [double]$number = $Value } catch { $Result.Message = "Not a double: $Value" $Result.Success = $False return $Result } $Result.Value = $number return $Result } Register-PSFConfigValidation -Name "integer" -ScriptBlock { Param ( $Value ) $Result = New-Object PSOBject -Property @{ Success = $True Value = $null Message = "" } try { [int]$number = $Value } catch { $Result.Message = "Not an integer: $Value" $Result.Success = $False return $Result } $Result.Value = $number return $Result } Register-PSFConfigValidation -Name "integer0to9" -ScriptBlock { Param ( $Value ) $Result = New-Object PSOBject -Property @{ Success = $True Value = $null Message = "" } try { [int]$number = $Value } catch { $Result.Message = "Not an integer: $Value" $Result.Success = $False return $Result } if (($number -lt 0) -or ($number -gt 9)) { $Result.Message = "Out of range. Specify a number ranging from 0 to 9" $Result.Success = $False return $Result } $Result.Value = $Number return $Result } Register-PSFConfigValidation -Name "integerarray" -ScriptBlock { param ( $var ) $test = $true try { [int[]]$res = $var } catch { $test = $false } [pscustomobject]@{ Success = $test Value = $res Message = "Casting $var as [int[]] failure. Input is being identified as $($var.GetType())" } } Register-PSFConfigValidation -Name "integerpositive" -ScriptBlock { Param ( $Value ) $Result = New-Object PSOBject -Property @{ Success = $True Value = $null Message = "" } try { [int]$number = $Value } catch { $Result.Message = "Not an integer: $Value" $Result.Success = $False return $Result } if ($number -lt 0) { $Result.Message = "Negative value: $Value" $Result.Success = $False return $Result } $Result.Value = $number return $Result } Register-PSFConfigValidation -Name "languagecode" -ScriptBlock { param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } $legal = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | Where-Object { $_ -and ($_.Trim()) } if ($Value -in $legal) { $Result.Value = [string]$Value } else { $Result.Success = $false $Result.Message = [PSFramework.Localization.LocalizationHost]::Read('PSFramework.Configuration_ValidateLanguage') } return $Result } Register-PSFConfigValidation -Name "psframework.logfilefiletype" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { [PSFramework.Logging.LogFileFileType]$type = $Value } catch { $Result.Message = "Not a logfile file type: $Value . Specify one of these values: $(([enum]::GetNames([PSFramework.Logging.LogFileFileType])) -join ", ")" $Result.Success = $False return $Result } $Result.Value = $type return $Result } Register-PSFConfigValidation -Name "long" -ScriptBlock { Param ( $Value ) $Result = New-Object PSOBject -Property @{ Success = $True Value = $null Message = "" } try { [long]$number = $Value } catch { $Result.Message = "Not a long: $Value" $Result.Success = $False return $Result } $Result.Value = $number return $Result } Register-PSFConfigValidation -Name "string" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { # Seriously, this should work for almost anybody and anything [string]$data = $Value } catch { $Result.Message = "Not a string: $Value" $Result.Success = $False return $Result } if ([string]::IsNullOrEmpty($data)) { $Result.Message = "Is an empty string: $Value" $Result.Success = $False return $Result } if ($data -eq $Value.GetType().FullName) { $Result.Message = "Is an object with no proper string representation: $Value" $Result.Success = $False return $Result } $Result.Value = $data return $Result } Register-PSFConfigValidation -Name "stringarray" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { $data = @() # Seriously, this should work for almost anybody and anything foreach ($item in $Value) { $data += [string]$item } } catch { $Result.Message = "Not a string array: $Value" $Result.Success = $False return $Result } $Result.Value = $data return $Result } Register-PSFConfigValidation -Name "timespan" -ScriptBlock { Param ( $Value ) $Result = New-Object PSObject -Property @{ Success = $True Value = $null Message = "" } try { [timespan]$timespan = $Value } catch { $Result.Message = "Not a Timespan: $Value" $Result.Success = $False return $Result } $Result.Value = $timespan return $Result } Set-PSFConfig -Module 'PSFramework' -Name 'ComputerManagement.PSSession.IdleTimeout' -Value (New-TimeSpan -Minutes 15) -Initialize -Validation 'timespan' -Handler { [PSFramework.ComputerManagement.ComputerManagementHost]::PSSessionIdleTimeout = $args[0] } -Description "The idle timeout for cached pssessions. When using Invoke-PSFCommand, it will remember sessions for up to this time after last using them, before cleaning them up." # Unattended mode, so there is a central flag scripts & modules can detect Set-PSFConfig -Module PSFramework -Name 'System.Unattended' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.PSFCore.PSFCoreHost]::Unattended = $args[0] } -Description "Central setting, showing whether the current execution is unattended or not. This allows scripts/modules to react to whether there is a user at the controls or not." Set-PSFConfig -Module PSFramework -Name 'SupportPackage.ContactMessage' -Value ' ' -Initialize -Validation 'string' -Description 'Message shown when using New-PSFSUpportPackage. This allows an organization to tie information on how to submit a support package into the command that generates it' # Encoding Settings Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.FullTabCompletion' -Value $false -Initialize -Validation 'bool' -Description 'Whether all encodings should be part of the tab completion for encodings. By default, only a manageable subset is shown.' Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultWrite' -Value 'utf-8' -Initialize -Validation 'string' -Description 'The default encoding to use when writing to file. Only applied by implementing commands.' Set-PSFConfig -Module PSFramework -Name 'Text.Encoding.DefaultRead' -Value 'utf-8' -Initialize -Validation 'string' -Description 'The default encoding to use when reading from file. Only applied by implementing commands.' # Localization Stuff Set-PSFConfig -Module PSFramework -Name 'Localization.Language' -Value ([System.Globalization.CultureInfo]::CurrentUICulture.Name) -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::Language = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under' Set-PSFConfig -Module PSFramework -Name 'Localization.LoggingLanguage' -Value 'en-US' -Initialize -Handler { [PSFramework.Localization.LocalizationHost]::LoggingLanguage = $args[0] } -Validation 'languagecode' -Description 'The language the current PowerShell session is operating under' Set-PSFConfig -Module PSFramework -Name 'Logging.MaxErrorCount' -Value 128 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxErrorCount = $args[0] } -Description "The maximum number of error records maintained in-memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.MaxMessageCount' -Value 1024 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxMessageCount = $args[0] } -Description "The maximum number of messages that can be maintained in the in-memory message queue. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.MessageLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogEnabled = $args[0] } -Description "Governs, whether a log of recent messages is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.ErrorLogEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogEnabled = $args[0] } -Description "Governs, whether a log of recent errors is kept in memory. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.DisableLogFlush' -Value $false -Initialize -Validation "bool" -Handler { } -Description "When shutting down the process, PSFramework will by default flush the log. This ensures that all events are properly logged. If this is not desired, it can be turned off with this setting." Set-PSFConfig -Module PSFramework -Name 'message.info.minimum' -Value 1 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumInformation = $args[0] } -Description "The minimum required message level for messages that will be shown to the user." Set-PSFConfig -Module PSFramework -Name 'message.info.maximum' -Value 3 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumInformation = $args[0] } -Description "The maximum message level to still display to the user directly." Set-PSFConfig -Module PSFramework -Name 'message.verbose.minimum' -Value 4 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumVerbose = $args[0] } -Description "The minimum required message level where verbose information is written." Set-PSFConfig -Module PSFramework -Name 'message.verbose.maximum' -Value 6 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumVerbose = $args[0] } -Description "The maximum message level where verbose information is still written." Set-PSFConfig -Module PSFramework -Name 'message.debug.minimum' -Value 1 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumDebug = $args[0] } -Description "The minimum required message level where debug information is written." Set-PSFConfig -Module PSFramework -Name 'message.debug.maximum' -Value 9 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumDebug = $args[0] } -Description "The maximum message level where debug information is still written." Set-PSFConfig -Module PSFramework -Name 'message.info.color' -Value 'Cyan' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColor = $args[0] } -Description "The color to use when writing text to the screen on PowerShell." Set-PSFConfig -Module PSFramework -Name 'message.info.color.emphasis' -Value 'green' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorEmphasis = $args[0] } -Description "The color to use when emphasizing written text to the screen on PowerShell." Set-PSFConfig -Module PSFramework -Name 'message.info.color.subtle' -Value 'gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::InfoColorSubtle = $args[0] } -Description "The color to use when making writing text to the screen on PowerShell appear subtle." Set-PSFConfig -Module PSFramework -Name 'message.developercolor' -Value 'Gray' -Initialize -Validation "consolecolor" -Handler { [PSFramework.Message.MessageHost]::DeveloperColor = $args[0] } -Description "The color to use when writing text with developer specific additional information to the screen on PowerShell." Set-PSFConfig -Module PSFramework -Name 'message.consoleoutput.disable' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::DisableVerbosity = $args[0] } -Description "Global toggle that allows disabling all regular messages to screen. Messages from '-Verbose' and '-Debug' are unaffected" Set-PSFConfig -Module PSFramework -Name 'message.transform.errorqueuesize' -Value 512 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.MessageHost]::TransformErrorQueueSize = $args[0] } -Description "The size of the queue for transformation errors. May be useful for advanced development, but can be ignored usually." Set-PSFConfig -Module PSFramework -Name 'message.nestedlevel.decrement' -Value 0 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::NestedLevelDecrement = $args[0] } -Description "How many levels should be reduced per callstack depth. This makes commands less verbose, the more nested they are called" Set-PSFConfig -Module PSFramework -Name 'developer.mode.enable' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::DeveloperMode = $args[0] } -Description "Developermode enables advanced logging and verbosity features. There is little benefit for enabling this as a regular user. but developers can use it to more easily troubleshoot issues." Set-PSFConfig -Module PSFramework -Name 'message.style.breadcrumbs' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageBreadcrumbs = $args[0] } -Description "Controls how messages are displayed. Enables Breadcrumb display, showing the entire callstack. Takes precedence over command name display." Set-PSFConfig -Module PSFramework -Name 'message.style.functionname' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageDisplayCommand = $args[0] } -Description "Controls how messages are displayed. Enables command name, showing the name of the writing command. Is overwritten by enabling breadcrumbs." Set-PSFConfig -Module PSFramework -Name 'message.style.timestamp' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageTimestamp = $args[0] } -Description "Controls how messages are displayed. Enables timestamp display, including a timestamp in each message." #region Setting the configuration Set-PSFConfig -Module PSFramework -Name 'Runspace.StopTimeoutSeconds' -Value 30 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Runspace.RunspaceHost]::StopTimeoutSeconds = $args[0] } -Description "Time in seconds that Stop-PSFRunspace will wait for a scriptspace to selfterminate before killing it." #endregion Setting the configuration # The path where type-files are stored when registered Set-PSFConfig -Module PSFramework -Name 'Serialization.WorkingDirectory' -Value $script:path_typedata -Initialize -Validation "string" -Description "The folder in which registered type extension files are placed before import. Relevant for Register-PSFTypeSerializationData." if (-not [PSFramework.Configuration.ConfigurationHost]::ImportFromRegistryDone) { # Read config from all settings $config_hash = Read-PsfConfigPersisted -Scope 127 foreach ($value in $config_hash.Values) { try { if (-not $value.KeepPersisted) { Set-PSFConfig -FullName $value.FullName -Value $value.Value -EnableException } else { Set-PSFConfig -FullName $value.FullName -PersistedValue $value.Value -PersistedType $value.Type -EnableException } [PSFramework.Configuration.ConfigurationHost]::Configurations[$value.FullName.ToLower()].PolicySet = $value.Policy [PSFramework.Configuration.ConfigurationHost]::Configurations[$value.FullName.ToLower()].PolicyEnforced = $value.Enforced } catch { } } [PSFramework.Configuration.ConfigurationHost]::ImportFromRegistryDone = $true } # Action that is performed on registration of the provider using Register-PSFLoggingProvider $registrationEvent = { } #region Logging Execution # Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script) $begin_event = { #region Helper Functions function Clean-FileSystemErrorXml { [CmdletBinding()] Param ( $Path ) $totalLength = $Null $files = Get-ChildItem -Path $Path.FullName -Filter "$($env:ComputerName)_$($pid)_error_*.xml" | Sort-Object LastWriteTime $totalLength = $files | Measure-Object Length -Sum | Select-Object -ExpandProperty Sum if (([PSFramework.Message.LogHost]::MaxErrorFileBytes) -gt $totalLength) { return } $removed = 0 foreach ($file in $files) { $removed += $file.Length Remove-Item -Path $file.FullName -Force -Confirm:$false if (($totalLength - $removed) -lt ([PSFramework.Message.LogHost]::MaxErrorFileBytes)) { break } } } function Clean-FileSystemMessageLog { [CmdletBinding()] Param ( $Path ) if ([PSFramework.Message.LogHost]::MaxMessagefileCount -eq 0) { return } $files = Get-ChildItem -Path $Path.FullName -Filter "$($env:ComputerName)_$($pid)_message_*.log" | Sort-Object LastWriteTime if (([PSFramework.Message.LogHost]::MaxMessagefileCount) -ge $files.Count) { return } $removed = 0 foreach ($file in $files) { $removed++ Remove-Item -Path $file.FullName -Force -Confirm:$false if (($files.Count - $removed) -le ([PSFramework.Message.LogHost]::MaxMessagefileCount)) { break } } } function Clean-FileSystemGlobalLog { [CmdletBinding()] Param ( $Path ) # Kill too old files Get-ChildItem -Path $Path.FullName | Where-Object Name -Match "^$([regex]::Escape($env:ComputerName))_.+" | Where-Object LastWriteTime -LT ((Get-Date) - ([PSFramework.Message.LogHost]::MaxLogFileAge)) | Remove-Item -Force -Confirm:$false # Handle the global overcrowding $files = Get-ChildItem -Path $Path.FullName | Where-Object Name -Match "^$([regex]::Escape($env:ComputerName))_.+" | Sort-Object LastWriteTime if (-not ($files)) { return } $totalLength = $files | Measure-Object Length -Sum | Select-Object -ExpandProperty Sum if (([PSFramework.Message.LogHost]::MaxTotalFolderSize) -gt $totalLength) { return } $removed = 0 foreach ($file in $files) { $removed += $file.Length Remove-Item -Path $file.FullName -Force -Confirm:$false if (($totalLength - $removed) -lt ([PSFramework.Message.LogHost]::MaxTotalFolderSize)) { break } } } #endregion Helper Functions } # Action that is performed at the beginning of each logging cycle $start_event = { $filesystem_path = [PSFramework.Message.LogHost]::LoggingPath if (-not (Test-Path $filesystem_path)) { $filesystem_root = New-Item $filesystem_path -ItemType Directory -Force -ErrorAction Stop } else { $filesystem_root = Get-Item -Path $filesystem_path } try { [int]$filesystem_num_Error = (Get-ChildItem -Path $filesystem_path.FullName -Filter "$($env:ComputerName)_$($pid)_error_*.xml" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty Name | Select-String -Pattern "(\d+)" -AllMatches).Matches[1].Value } catch { } try { [int]$filesystem_num_Message = (Get-ChildItem -Path $filesystem_path.FullName -Filter "$($env:ComputerName)_$($pid)_message_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty Name | Select-String -Pattern "(\d+)" -AllMatches).Matches[1].Value } catch { } if (-not ($filesystem_num_Error)) { $filesystem_num_Error = 0 } if (-not ($filesystem_num_Message)) { $filesystem_num_Message = 0 } } # Action that is performed for each message item that is being logged $message_Event = { Param ( $Message ) $filesystem_CurrentFile = Join-Path $filesystem_root.FullName "$($env:ComputerName)_$($pid)_message_$($filesystem_num_Message).log" if (Test-Path $filesystem_CurrentFile) { $filesystem_item = Get-Item $filesystem_CurrentFile if ($filesystem_item.Length -gt ([PSFramework.Message.LogHost]::MaxMessagefileBytes)) { $filesystem_num_Message++ $filesystem_CurrentFile = Join-Path $($filesystem_root.FullName) "$($env:ComputerName)_$($pid)_message_$($filesystem_num_Message).log" } } if ($Message) { if ([PSFramework.Message.LogHost]::FileSystemModernLog) { if (-not (Test-Path $filesystem_CurrentFile)) { $Message | Select-PSFObject ComputerName, Username, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ "} } | Export-Csv -Path $filesystem_CurrentFile -NoTypeInformation } else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Username, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace, @{ n = "Callstack"; e = { $_.CallStack.ToString().Split("`n") -join " þ " } }) -NoTypeInformation)[1] } } else { Add-Content -Path $filesystem_CurrentFile -Value (ConvertTo-Csv ($Message | Select-PSFObject ComputerName, Timestamp, Level, 'LogMessage as Message', Type, FunctionName, ModuleName, File, Line, @{ n = "Tags"; e = { $_.Tags -join "," } }, TargetObject, Runspace) -NoTypeInformation)[1] } } } # Action that is performed for each error item that is being logged $error_Event = { Param ( $ErrorItem ) if ($ErrorItem) { $ErrorItem | Export-Clixml -Path (Join-Path $filesystem_root.FullName "$($env:ComputerName)_$($pid)_error_$($filesystem_num_Error).xml") -Depth 3 $filesystem_num_Error++ } Clean-FileSystemErrorXml -Path $filesystem_root } # Action that is performed at the end of each logging cycle $end_event = { Clean-FileSystemMessageLog -Path $filesystem_root Clean-FileSystemGlobalLog -Path $filesystem_root } # Action that is performed when stopping the logging script $final_event = { } #endregion Logging Execution #region Function Extension / Integration # Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider $configurationParameters = { $configroot = "psframework.logging.filesystem" $configurations = Get-PSFConfig -FullName "$configroot.*" $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($config in $configurations) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.ParameterSetName = '__AllParameterSets' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl) $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam) } return $RuntimeParamDic } # Script that is executes when configuring the provider using Set-PSFLoggingProvider $configurationScript = { $configroot = "psframework.logging.filesystem" $configurations = Get-PSFConfig -FullName "$configroot.*" foreach ($config in $configurations) { if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim(".")))) { Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))] } } } # Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required $isInstalledScript = { return $true } # Script that provides dynamic parameter for Install-PSFLoggingProvider $installationParameters = { # None needed } # Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script $installationScript = { # Nothing to be done - if you need to install your filesystem, you probably have other issues you need to deal with first ;) } #endregion Function Extension / Integration # Configuration settings to initialize $configuration_Settings = { Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxMessagefileBytes' -Value 5MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxMessagefileBytes = $args[0] } -Description "The maximum size of a given logfile. When reaching this limit, the file will be abandoned and a new log created. Set to 0 to not limit the size. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxMessagefileCount' -Value 5 -Initialize -Validation "integerpositive" -Handler { [PSFramework.Message.LogHost]::MaxMessagefileCount = $args[0] } -Description "The maximum number of logfiles maintained at a time. Exceeding this number will cause the oldest to be culled. Set to 0 to disable the limit. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxErrorFileBytes' -Value 20MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxErrorFileBytes = $args[0] } -Description "The maximum size all error files combined may have. When this number is exceeded, the oldest entry is culled. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxTotalFolderSize' -Value 100MB -Initialize -Validation "long" -Handler { [PSFramework.Message.LogHost]::MaxTotalFolderSize = $args[0] } -Description "This is the upper limit of length all items in the log folder may have combined across all processes." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MaxLogFileAge' -Value (New-TimeSpan -Days 7) -Initialize -Validation "timespan" -Handler { [PSFramework.Message.LogHost]::MaxLogFileAge = $args[0] } -Description "Any logfile older than this will automatically be cleansed. This setting is global." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.MessageLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::MessageLogFileEnabled = $args[0] } -Description "Governs, whether a log file for the system messages is written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ErrorLogFileEnabled' -Value $true -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::ErrorLogFileEnabled = $args[0] } -Description "Governs, whether log files for errors are written. This setting is on a per-Process basis. Runspaces share, jobs or other consoles counted separately." Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.ModernLog' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.LogHost]::FileSystemModernLog = $args[0] } -Description "Enables the modern, more powereful version of the filesystem log, including headers and extra columns" Set-PSFConfig -Module PSFramework -Name 'Logging.FileSystem.LogPath' -Value $script:path_Logging -Initialize -Validation "string" -Handler { [PSFramework.Message.LogHost]::LoggingPath = $args[0] } -Description "The path where the PSFramework writes all its logs and debugging information." Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.Enabled' -Value $true -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.InstallOptional' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.IncludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].IncludeModules = ($args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].ExcludeModules = ($args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].IncludeTags = ($args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged" Set-PSFConfig -Module LoggingProvider -Name 'FileSystem.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['filesystem']) { [PSFramework.Logging.ProviderHost]::Providers['filesystem'].ExcludeTags = ($args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged" } Register-PSFLoggingProvider -Name "filesystem" -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings # Action that is performed on registration of the provider using Register-PSFLoggingProvider $registrationEvent = { } #region Logging Execution # Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script) $begin_event = { if (-not (Get-Module -Name 'PSGELF')) { Import-Module -Name 'PSGELF' } } # Action that is performed at the beginning of each logging cycle $start_event = { $gelf_gelfserver = Get-PSFConfigValue -FullName 'PSFramework.Logging.GELF.GelfServer' $gelf_port = Get-PSFConfigValue -FullName 'PSFramework.Logging.GELF.Port' $gelf_encrypt = Get-PSFConfigValue -FullName 'PSFramework.Logging.GELF.Encrypt' $gelf_paramSendPsgelfTcp = @{ 'GelfServer' = $gelf_gelfserver 'Port' = $gelf_port 'Encrypt' = $gelf_encrypt } } # Action that is performed for each message item that is being logged $message_Event = { Param ( $Message ) $gelf_params = $gelf_paramSendPsgelfTcp.Clone() $gelf_params['ShortMessage'] = $Message.LogMessage $gelf_params['HostName'] = $Message.ComputerName $gelf_params['DateTime'] = $Message.Timestamp $gelf_params['Level'] = switch ($Message.Level) { 'Critical' { 1 } 'Important' { 1 } 'Output' { 3 } 'Host' { 4 } 'Significant' { 5 } 'VeryVerbose' { 6 } 'Verbose' { 6 } 'SomewhatVerbose' { 6 } 'System' { 6 } default { 7 } } if ($Message.ErrorRecord) { $gelf_params['FullMessage'] = $Message.ErrorRecord | ConvertTo-Json } # build the additional fields $gelf_properties = $Message.PSObject.Properties | Where-Object { $_.Name -notin @('Message', 'LogMessage', 'ComputerName', 'Timestamp', 'Level', 'ErrorRecord') } $gelf_params['AdditionalField'] = @{} foreach ($gelf_property in $gelf_properties) { $gelf_params['AdditionalField'][$gelf_property.Name] = $gelf_property.Value } PSGELF\Send-PSGelfTCP @gelf_params } $error_Event = { Param ( $ErrorItem ) } # Action that is performed at the end of each logging cycle $end_event = { } # Action that is performed when stopping the logging script $final_event = { } #endregion Logging Execution #region Function Extension / Integration # Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider $configurationParameters = { $configroot = "psframework.logging.gelf" $configurations = Get-PSFConfig -FullName "$configroot.*" $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($config in $configurations) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.ParameterSetName = '__AllParameterSets' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl) $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam) } return $RuntimeParamDic } # Script that is executes when configuring the provider using Set-PSFLoggingProvider $configurationScript = { $configroot = "psframework.logging.gelf" $configurations = Get-PSFConfig -FullName "$configroot.*" foreach ($config in $configurations) { if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim(".")))) { Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))] } } } # Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required $isInstalledScript = { if (Get-Module -Name PSGELF -ListAvailable) { return $true } else { return $false } } # Script that provides dynamic parameter for Install-PSFLoggingProvider $installationParameters = { # None needed } # Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script $installationScript = { # install PSGELF module if (-not (Get-Module -Name PSGELF -ListAvailable)) { Install-Module -Name PSGELF } } #endregion Function Extension / Integration # Configuration settings to initialize $configuration_Settings = { Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.GelfServer' -Value "" -Initialize -Validation string -Handler { } -Description "The GELF server to send logs to" Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.Port' -Value "" -Initialize -Validation string -Handler { } -Description "The port number the GELF server listens on" Set-PSFConfig -Module PSFramework -Name 'Logging.GELF.Encrypt' -Value $true -Initialize -Validation bool -Handler { } -Description "Whether to use TLS encryption when communicating with the GELF server" Set-PSFConfig -Module LoggingProvider -Name 'GELF.Enabled' -Value $false -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['gelf']) { [PSFramework.Logging.ProviderHost]::Providers['gelf'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration" Set-PSFConfig -Module LoggingProvider -Name 'GELF.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration" Set-PSFConfig -Module LoggingProvider -Name 'GELF.InstallOptional' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled" Set-PSFConfig -Module LoggingProvider -Name 'GELF.IncludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['gelf']) { ([PSFramework.Logging.ProviderHost]::Providers['gelf'].IncludeModules = $args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged" Set-PSFConfig -Module LoggingProvider -Name 'GELF.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['gelf']) { ([PSFramework.Logging.ProviderHost]::Providers['gelf'].ExcludeModules = $args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged" Set-PSFConfig -Module LoggingProvider -Name 'GELF.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['gelf']) { ([PSFramework.Logging.ProviderHost]::Providers['gelf'].IncludeTags = $args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged" Set-PSFConfig -Module LoggingProvider -Name 'GELF.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['gelf']) { ([PSFramework.Logging.ProviderHost]::Providers['gelf'].ExcludeTags = $args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged" } Register-PSFLoggingProvider -Name "gelf" -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings # Action that is performed on registration of the provider using Register-PSFLoggingProvider $registrationEvent = { } #region Logging Execution # Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script) $begin_event = { function Get-LogFilePath { [CmdletBinding()] param ( ) $path = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.FilePath' $logname = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.LogName' $scriptBlock = { param ( [string] $Match ) $hash = @{ '%date%' = (Get-Date -Format 'yyyy-MM-dd') '%dayofweek%' = (Get-Date).DayOfWeek '%day%' = (Get-Date).Day '%hour%' = (Get-Date).Hour '%minute%' = (Get-Date).Minute '%username%' = $env:USERNAME '%userdomain%' = $env:USERDOMAIN '%computername%' = $env:COMPUTERNAME '%processid%' = $PID '%logname%' = $logname } $hash.$Match } [regex]::Replace($path, '%day%|%computername%|%hour%|%processid%|%date%|%username%|%dayofweek%|%minute%|%userdomain%|%logname%', $scriptBlock) } function Write-LogFileMessage { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Message, [bool] $IncludeHeader, [string] $FileType, [string] $Path, [string] $CsvDelimiter, [string[]] $Headers ) $parent = Split-Path $Path if (-not (Test-Path $parent)) { $null = New-Item $parent -ItemType Directory -Force } $fileExists = Test-Path $Path #region Type-Based Output switch ($FileType) { #region Csv "Csv" { if ((-not $fileExists) -and $IncludeHeader) { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Set-Content -Path $Path -Encoding UTF8 } else { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Select-Object -Skip 1 | Add-Content -Path $Path -Encoding UTF8 } } #endregion Csv #region Json "Json" { if ($fileExists) { Add-Content -Path $Path -Value "," -Encoding UTF8 } $Message | ConvertTo-Json | Add-Content -Path $Path -NoNewline -Encoding UTF8 } #endregion Json #region XML "XML" { [xml]$xml = $message | ConvertTo-Xml -NoTypeInformation $xml.Objects.InnerXml | Add-Content -Path $Path -Encoding UTF8 } #endregion XML #region Html "Html" { [xml]$xml = $message | ConvertTo-Html -Fragment if ((-not $fileExists) -and $IncludeHeader) { $xml.table.tr[0].OuterXml | Add-Content -Path $Path -Encoding UTF8 } $xml.table.tr[1].OuterXml | Add-Content -Path $Path -Encoding UTF8 } #endregion Html } #endregion Type-Based Output } $logfile_includeheader = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.IncludeHeader' $logfile_headers = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.Headers' $logfile_filetype = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.FileType' $logfile_CsvDelimiter = Get-PSFConfigValue -FullName 'PSFramework.Logging.LogFile.CsvDelimiter' if ($logfile_headers -contains 'Tags') { $logfile_headers = $logfile_headers | ForEach-Object { switch ($_) { 'Tags' { @{ Name = 'Tags' Expression = { $_.Tags -join "," } } } 'Message' { @{ Name = 'Message' Expression = { $_.LogMessage } } } default { $_ } } } } $logfile_paramWriteLogFileMessage = @{ IncludeHeader = $logfile_includeheader FileType = $logfile_filetype CsvDelimiter = $logfile_CsvDelimiter Headers = $logfile_headers } } # Action that is performed at the beginning of each logging cycle $start_event = { $logfile_paramWriteLogFileMessage["Path"] = Get-LogFilePath } # Action that is performed for each message item that is being logged $message_Event = { Param ( $Message ) $Message | Select-Object $logfile_headers | Write-LogFileMessage @logfile_paramWriteLogFileMessage } # Action that is performed for each error item that is being logged $error_Event = { Param ( $ErrorItem ) } # Action that is performed at the end of each logging cycle $end_event = { } # Action that is performed when stopping the logging script $final_event = { } #endregion Logging Execution #region Function Extension / Integration # Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider $configurationParameters = { $configroot = "psframework.logging.logfile" $configurations = Get-PSFConfig -FullName "$configroot.*" $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($config in $configurations) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.ParameterSetName = '__AllParameterSets' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl) $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam) } return $RuntimeParamDic } # Script that is executes when configuring the provider using Set-PSFLoggingProvider $configurationScript = { $configroot = "psframework.logging.logfile" $configurations = Get-PSFConfig -FullName "$configroot.*" foreach ($config in $configurations) { if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim(".")))) { Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))] } } } # Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required $isInstalledScript = { return $true } # Script that provides dynamic parameter for Install-PSFLoggingProvider $installationParameters = { # None needed } # Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script $installationScript = { # Nothing to be done - if you need to install your filesystem, you probably have other issues you need to deal with first ;) } #endregion Function Extension / Integration # Configuration settings to initialize $configuration_Settings = { Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.FilePath' -Value "" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.Logname' -Value "" -Initialize -Validation string -Handler { } -Description "A special string you can use as a placeholder in the logfile path (by using '%logname%' as placeholder)" Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.IncludeHeader' -Value $true -Initialize -Validation bool -Handler { } -Description "Whether a written csv file will include headers" Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.Headers' -Value @('ComputerName', 'File', 'FunctionName', 'Level', 'Line', 'Message', 'ModuleName', 'Runspace', 'Tags', 'TargetObject', 'Timestamp', 'Type', 'Username') -Initialize -Validation stringarray -Handler { } -Description "The properties to export, in the order to select them." Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.FileType' -Value "CSV" -Initialize -Validation psframework.logfilefiletype -Handler { } -Description "In what format to write the logfile. Supported styles: CSV, XML, Html or Json. Html, XML and Json will be written as fragments." Set-PSFConfig -Module PSFramework -Name 'Logging.LogFile.CsvDelimiter' -Value "," -Initialize -Validation string -Handler { } -Description "The delimiter to use when writing to csv." Set-PSFConfig -Module LoggingProvider -Name 'LogFile.Enabled' -Value $false -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['logfile']) { [PSFramework.Logging.ProviderHost]::Providers['logfile'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.InstallOptional' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.IncludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['logfile']) { [PSFramework.Logging.ProviderHost]::Providers['logfile'].IncludeModules = ($args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['logfile']) { [PSFramework.Logging.ProviderHost]::Providers['logfile'].ExcludeModules = ($args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['logfile']) { [PSFramework.Logging.ProviderHost]::Providers['logfile'].IncludeTags = ($args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged" Set-PSFConfig -Module LoggingProvider -Name 'LogFile.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['logfile']) { [PSFramework.Logging.ProviderHost]::Providers['logfile'].ExcludeTags = ($args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged" } Register-PSFLoggingProvider -Name "logfile" -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings $scriptBlock = { try { $script:___ScriptName = 'PSFramework.Logging' Import-Module (Join-Path ([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot) 'PSFramework.psd1') while ($true) { # This portion is critical to gracefully closing the script if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName.ToLower()].State -notlike "Running") { break } #region Manage Begin Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetEnabled()) { if (-not $___provider.Initialized) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Initializing' try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.BeginEvent)), $null, $null) $___provider.Initialized = $true } catch { $___provider.Errors.Push($_) } } } [PSFramework.Logging.ProviderHost]::LoggingState = 'Ready' #endregion Manage Begin Event #region Start Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.StartEvent)), $null, $null) } catch { $___provider.Errors.Push($_) } } #endregion Start Event #region Message Event while ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) { $Entry = $null [PSFramework.Message.LogHost]::OutQueueLog.TryDequeue([ref]$Entry) if ($Entry) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing' foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { if ($___provider.MessageApplies($Entry)) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.MessageEvent)), $null, $Entry) } catch { $___provider.Errors.Push($_) } } } } } #endregion Message Event #region Error Event while ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0) { $Record = $null [PSFramework.Message.LogHost]::OutQueueError.TryDequeue([ref]$Record) if ($Record) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing' foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { if ($___provider.MessageApplies($Record)) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.ErrorEvent)), $null, $Record) } catch { $___provider.Errors.Push($_) } } } } } #endregion Error Event #region End Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.EndEvent)), $null, $null) } catch { $___provider.Errors.Push($_) } } #endregion End Event [PSFramework.Logging.ProviderHost]::LoggingState = 'Ready' Start-Sleep -Milliseconds 100 } } catch { $wasBroken = $true } finally { #region Flush log on exit if (([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName.ToLower()].State -like "Running") -and (-not [PSFramework.Configuration.ConfigurationHost]::Configurations["psframework.logging.disablelogflush"].Value)) { #region Start Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.StartEvent)), $null, $null) } catch { $___provider.Errors.Push($_) } } #endregion Start Event #region Message Event while ([PSFramework.Message.LogHost]::OutQueueLog.Count -gt 0) { $Entry = $null [PSFramework.Message.LogHost]::OutQueueLog.TryDequeue([ref]$Entry) if ($Entry) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing' foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { if ($___provider.MessageApplies($Entry)) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.MessageEvent)), $null, $Entry) } catch { $___provider.Errors.Push($_) } } } } } #endregion Message Event #region Error Event while ([PSFramework.Message.LogHost]::OutQueueError.Count -gt 0) { $Record = $null [PSFramework.Message.LogHost]::OutQueueError.TryDequeue([ref]$Record) if ($Record) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Writing' foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { if ($___provider.MessageApplies($Record)) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.MessageEvent)), $null, $Record) } catch { $___provider.Errors.Push($_) } } } } } #endregion Error Event #region End Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.EndEvent)), $null, $null) } catch { $___provider.Errors.Push($_) } } #endregion End Event } #endregion Flush log on exit #region Final Event foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { try { $ExecutionContext.InvokeCommand.InvokeScript($false, ([System.Management.Automation.ScriptBlock]::Create($___provider.FinalEvent)), $null, $null) } catch { $___provider.Errors.Push($_) } } foreach ($___provider in [PSFramework.Logging.ProviderHost]::GetInitialized()) { $___provider.Initialized = $false } #endregion Final Event if ($wasBroken) { [PSFramework.Logging.ProviderHost]::LoggingState = 'Broken' } else { [PSFramework.Logging.ProviderHost]::LoggingState = 'Stopped' } [PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName.ToLower()].SignalStopped() } } Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'PSFramework.Logging' -NoMessage Start-PSFRunspace -Name 'PSFramework.Logging' -NoMessage Register-PSFTeppScriptblock -Name "PSFramework-config-fullname" -ScriptBlock { [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { -not $_.Hidden } | Select-Object -ExpandProperty FullName } Register-PSFTeppScriptblock -Name "PSFramework-config-module" -ScriptBlock { [PSFramework.Configuration.ConfigurationHost]::Configurations.Values.Module | Select-Object -Unique } Register-PSFTeppScriptblock -Name "PSFramework-config-name" -ScriptBlock { $moduleName = "*" if ($fakeBoundParameter.Module) { $moduleName = $fakeBoundParameter.Module } [PSFramework.Configuration.ConfigurationHost]::Configurations.Values | Where-Object { -not $_.Hidden -and ($_.Module -like $moduleName) } | Select-Object -ExpandProperty Name } Register-PSFTeppScriptblock -Name 'PSFramework-Config-Schema' -ScriptBlock { [PSFramework.Configuration.ConfigurationHost]::Schemata.Keys } Register-PSFTeppScriptblock -Name 'PSFramework-config-validation' -ScriptBlock { [PSFramework.Configuration.ConfigurationHost]::Validation.Keys } Register-PSFTeppScriptblock -Name 'PSFramework-dynamiccontentobject-name' -ScriptBlock { [PSFramework.Utility.DynamicContentObject]::List } Register-PSFTeppScriptblock -Name "PSFramework-Encoding" -ScriptBlock { 'Unicode' 'BigEndianUnicode' 'UTF8' 'UTF8Bom' 'UTF8NoBom' 'UTF7' 'UTF32' 'Ascii' 'Default' 'BigEndianUTF32' if (Get-PSFConfigValue -FullName 'PSFramework.Text.Encoding.FullTabCompletion') { [System.Text.Encoding]::GetEncodings().BodyName } } Register-PSFTeppScriptblock -Name "PSFramework.Feature.Name" -ScriptBlock { (Get-PSFFeature).Name } Register-PSFTeppScriptblock -Name PSFramework-Input-ObjectProperty -ScriptBlock { #region Utility Functions function Get-Property { [CmdletBinding()] param ( $InputObject ) if (-not $InputObject) { return @{ } } $properties = @{ } switch ($InputObject.GetType().FullName) { #region Variables or static input 'System.Management.Automation.Language.CommandExpressionAst' { switch ($InputObject.Expression.GetType().Name) { 'BinaryExpressionAst' { # Return an empty array. A binary expression ast means pure numbers as input, no properties return @{ } } 'VariableExpressionAst' { $members = Get-Variable -Name $InputObject.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Select-Object -First 1 | Get-Member -MemberType Properties foreach ($member in $members) { try { $typeString = $member.Definition.Split(" ")[0] $memberType = [type]$typeString $typeKnown = $true } catch { $memberType = $null $typeKnown = $false } $properties[$member.Name] = [pscustomobject]@{ Name = $member.Name Type = $memberType TypeKnown = $typeKnown } } return $properties } 'MemberExpressionAst' { try { $members = Get-Variable -Name $InputObject.Expression.Expression.VariablePath.UserPath -ValueOnly -ErrorAction Ignore | Where-Object $InputObject.Expression.Member.Value -ne $null | Select-Object -First 1 -ExpandProperty $InputObject.Expression.Member.Value -ErrorAction Ignore | Get-Member -MemberType Properties } catch { return $properties } foreach ($member in $members) { try { $typeString = $member.Definition.Split(" ")[0] $memberType = [type]$typeString $typeKnown = $true } catch { $memberType = $null $typeKnown = $false } $properties[$member.Name] = [pscustomobject]@{ Name = $member.Name Type = $memberType TypeKnown = $typeKnown } } return $properties } 'ArrayLiteralAst' { # Not yet supported return @{ } } } #region Input from Variable if ($pipelineAst.PipelineElements[$inputIndex].Expression -and $pipelineAst.PipelineElements[0].Expression[0].VariablePath) { $properties += ((Get-Variable -Name $pipelineAst.PipelineElements[0].Expression[0].VariablePath.UserPath -ValueOnly) | Select-Object -First 1 | Get-Member -MemberType Properties).Name } #endregion Input from Variable } #endregion Variables or static input #region Input from Command 'System.Management.Automation.Language.CommandAst' { $command = Get-Command $InputObject.CommandElements[0].Value -ErrorAction Ignore if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand } if (-not $command) { return $properties } foreach ($type in $command.OutputType.Type) { foreach ($member in $type.GetMembers("Instance, Public")) { # Skip all members except Fields (4) or Properties (16) if (-not ($member.MemberType -band 20)) { continue } $properties[$member.Name] = [pscustomobject]@{ Name = $member.Name Type = $null TypeKnown = $true } if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType } else { $properties[$member.Name].Type = $member.FieldType } } foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$type.FullName])) { $properties[$propertyExtensionItem.Name] = $propertyExtensionItem } } #region Command Specific Inserts foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionCommandData[$command.Name])) { $properties[$propertyExtensionItem.Name] = $propertyExtensionItem } #endregion Command Specific Inserts return $properties } #endregion Input from Command # Unknown / Unexpected input default { return @{ } } } } function Update-Property { [CmdletBinding()] param ( [Hashtable] $Property, $Step ) $properties = @{ } #region Expand Property if ($Step.ExpandProperty) { if (-not ($Property[$Step.ExpandProperty])) { return $properties } $expanded = $Property[$Step.ExpandProperty] if (-not $expanded.TypeKnown) { return $properties } foreach ($member in $expanded.Type.GetMembers("Instance, Public")) { # Skip all members except Fields (4) or Properties (16) if (-not ($member.MemberType -band 20)) { continue } $properties[$member.Name] = [pscustomobject]@{ Name = $member.Name Type = $null TypeKnown = $true } if ($member.PropertyType) { $properties[$member.Name].Type = $member.PropertyType } else { $properties[$member.Name].Type = $member.FieldType } } foreach ($propertyExtensionItem in ([PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData[$expanded.Type.FullName])) { $properties[$propertyExtensionItem.Name] = $propertyExtensionItem } return $properties } #endregion Expand Property # In keep input mode, the original properties will not be affected in any way if ($Step.KeepInputObject) { $properties = $Property.Clone() } $filterProperties = $Step.Properties | Where-Object Kind -eq "Property" #region Select What to keep if (-not $Step.KeepInputObject) { :main foreach ($propertyItem in $Property.Values) { #region Excluded Properties foreach ($exclusion in $Step.Excluded) { if ($propertyItem.Name -like $exclusion) { continue main } } #endregion Excluded Properties foreach ($stepProperty in $filterProperties) { if ($propertyItem.Name -like $stepProperty.Name) { $properties[$propertyItem.Name] = $propertyItem continue main } } } } #endregion Select What to keep #region Adding Content :main foreach ($stepProperty in $Step.Properties) { switch ($stepProperty.Kind) { 'Property' { if ($stepProperty.Filter) { continue main } if ($properties[$stepProperty.Name]) { continue main } foreach ($exclusion in $Step.Excluded) { if ($stepProperty.Name -like $exclusion) { continue main } } $properties[$stepProperty.Name] = [PSCustomObject]@{ Name = $stepProperty.Name Type = $null TypeKnown = $false } continue main } 'CalculatedProperty' { if ($properties[$stepProperty.Name]) { continue main } $properties[$stepProperty.Name] = [PSCustomObject]@{ Name = $stepProperty.Name Type = $null TypeKnown = $false } continue main } 'ScriptProperty' { if ($properties[$stepProperty.Name]) { continue main } $properties[$stepProperty.Name] = [PSCustomObject]@{ Name = $stepProperty.Name Type = $null TypeKnown = $false } continue main } 'AliasProperty' { if ($properties[$stepProperty.Name]) { continue main } $properties[$stepProperty.Name] = [PSCustomObject]@{ Name = $stepProperty.Name Type = $null TypeKnown = $false } if ($properties[$stepProperty.Target].TypeKnown) { $properties[$stepProperty.Name].Type = $properties[$stepProperty.Target].Type $properties[$stepProperty.Name].TypeKnown = $properties[$stepProperty.Target].TypeKnown } continue main } } } #endregion Adding Content $properties } function Read-SelectObject { [CmdletBinding()] param ( [System.Management.Automation.Language.CommandAst] $Ast, [string] $CommandName = 'Select-Object' ) $results = [pscustomobject]@{ Ast = $Ast BoundParameters = @() Property = @() ExcludeProperty = @() ExpandProperty = '' ScriptProperty = @() AliasProperty = @() KeepInputObject = $false } #region Process Ast if ($Ast.CommandElements.Count -gt 1) { $index = 1 $parameterName = '' $position = 0 while ($index -lt $Ast.CommandElements.Count) { $element = $Ast.CommandElements[$index] switch ($element.GetType().FullName) { 'System.Management.Automation.Language.CommandParameterAst' { $parameterName = $element.ParameterName if ($parameterName -like "k*") { $results.KeepInputObject = $true } $results.BoundParameters += $element.ParameterName break } 'System.Management.Automation.Language.StringConstantExpressionAst' { if (-not $parameterName) { switch ($position) { 0 { $results.Property = $element } 1 { $results.AliasProperty = $element } 2 { $results.ScriptProperty = $element } } $position = $position + 1 } if ($parameterName -like "pr*") { $results.Property = $element } if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value } if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value } if ($parameterName -like "a*") { $results.AliasProperty = $element } if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element } $parameterName = '' break } 'System.Management.Automation.Language.ArrayLiteralAst' { if (-not $parameterName) { switch ($position) { 0 { $results.Property = $element.Elements } 1 { $results.AliasProperty = $element.Elements } 2 { $results.ScriptProperty = $element.Elements } } $position = $position + 1 } if ($parameterName -like "pr*") { $results.Property = $element.Elements } if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Elements.Value } if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Elements.Value } if ($parameterName -like "a*") { $results.AliasProperty = $element.Elements } if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element.Elements } $parameterName = '' break } 'System.Management.Automation.Language.ConstantExpressionAst' { if (-not $parameterName) { switch ($position) { 0 { $results.Property = $element } 1 { $results.AliasProperty = $element } 2 { $results.ScriptProperty = $element } } $position = $position + 1 } if ($parameterName -like "pr*") { $results.Property = $element } if ($parameterName -like "exp*") { $results.ExpandProperty = $element.Value.ToString() } if ($parameterName -like "exc*") { $results.ExcludeProperty = $element.Value.ToString() } if ($parameterName -like "a*") { $results.AliasProperty = $element } if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element } $parameterName = '' break } 'System.Management.Automation.Language.HashtableAst' { if (-not $parameterName) { switch ($position) { 0 { $results.Property = $element } 1 { $results.AliasProperty = $element } 2 { $results.ScriptProperty = $element } } $position = $position + 1 } if ($parameterName -like "pr*") { $results.Property = $element } if ($parameterName -like "a*") { $results.AliasProperty = $element } if ($parameterName -like "scriptp*") { $results.ScriptProperty = $element } $parameterName = '' break } default { $parameterName = '' } } $index = $index + 1 } } #endregion Process Ast #region Convert Results $resultsProcessed = [pscustomobject]@{ HasIncludeFilter = $false RawResult = $results Properties = @() Excluded = $results.ExcludeProperty ExpandProperty = $results.ExpandProperty KeepInputObject = $results.KeepInputObject } switch ($CommandName) { #region Select-Object 'Select-Object' { #region Properties foreach ($element in $results.Property) { switch ($element.GetType().FullName) { 'System.Management.Automation.Language.HashtableAst' { try { $resultsProcessed.Properties += [pscustomobject]@{ Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value Kind = "CalculatedProperty" Type = "Unknown" Filter = $false } } catch { } } default { if ($element.Value -match "\*") { $resultsProcessed.HasIncludeFilter = $true } $resultsProcessed.Properties += [pscustomobject]@{ Name = $element.Value.ToString() Kind = "Property" Type = "Inherited" Filter = $element.Value -match "\*" } } } } #endregion Properties } #endregion Select-Object #region Select-PSFObject 'Select-PSFObject' { #region Properties foreach ($element in $results.Property) { switch ($element.GetType().FullName) { 'System.Management.Automation.Language.HashtableAst' { try { $resultsProcessed.Properties += [pscustomobject]@{ Name = ($element.KeyValuePairs | Where-Object Item1 -Match '^N$|^Name$|^L$|^Label$' | Select-Object -First 1).Item2.PipelineElements[0].Expression.Value Kind = "CalculatedProperty" Type = "Unknown" Filter = $false } } catch { } } default { try { $parameterItem = ([PSFramework.Parameter.SelectParameter]$element.Value).Value } catch { continue } if ($parameterItem -is [System.String]) { if ($parameterItem -match "\*") { $resultsProcessed.HasIncludeFilter = $true } $resultsProcessed.Properties += [pscustomobject]@{ Name = $parameterItem Kind = "Property" Type = "Inherited" Filter = $parameterItem -match "\*" } } else { $resultsProcessed.Properties += [pscustomobject]@{ Name = $parameterItem Kind = "CalculatedProperty" Type = "Unknown" Filter = $false } } } } } #endregion Properties #region Script Properties foreach ($scriptProperty in $results.ScriptProperty) { switch ($scriptProperty.GetType().FullName) { 'System.Management.Automation.Language.HashtableAst' { foreach ($name in $scriptProperty.KeyValuePairs.Item1.Value) { $resultsProcessed.Properties += [pscustomobject]@{ Name = $name Kind = "ScriptProperty" Type = "Unknown" Filter = $false } } } default { try { $propertyValue = [PSFramework.Parameter.SelectScriptPropertyParameter]$scriptProperty.Value } catch { continue } $resultsProcessed.Properties += [pscustomobject]@{ Name = $propertyValue.Value.Name Kind = "ScriptProperty" Type = "Unknown" Filter = $false } } } } #endregion Script Properties #region Alias Properties foreach ($scriptProperty in $results.AliasProperty) { switch ($scriptProperty.GetType().FullName) { 'System.Management.Automation.Language.HashtableAst' { foreach ($aliasPair in $scriptProperty.KeyValuePairs) { $resultsProcessed.Properties += [pscustomobject]@{ Name = $aliasPair.Item1.Value Kind = "AliasProperty" Type = "Alias" Filter = $false Target = $aliasPair.Item2.PipelineElements.Expression.Value } } } default { try { $propertyValue = [PSFramework.Parameter.SelectAliasParameter]$scriptProperty.Value } catch { continue } $resultsProcessed.Properties += [pscustomobject]@{ Name = $propertyValue.Aliases[0].Name Kind = "AliasProperty" Type = "Alias" Filter = $false Target = $propertyValue.Aliases[0].ReferencedMemberName } } } } #endregion Alias Properties } #endregion Select-PSFObject } #endregion Convert Results $resultsProcessed } #endregion Utility Functions # Grab Pipeline and find starting index [System.Management.Automation.Language.PipelineAst]$pipelineAst = $commandAst.parent $index = $pipelineAst.PipelineElements.IndexOf($commandAst) # If it's the first item: Skip, no input to parse if ($index -lt 1) { return } $inputIndex = $index - 1 $steps = @{ } #region Step backwards through the pipeline until the definitive object giver is found :outmain while ($true) { if ($pipelineAst.PipelineElements[$inputIndex].CommandElements) { # Resolve command and fail if it breaks $command = $null # Work around the ? alias for Where-Object being a wildcard if ($pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -eq "?") { $command = Get-Alias -Name "?" | Where-Object Name -eq "?" } else { $command = Get-Command $pipelineAst.PipelineElements[$inputIndex].CommandElements[0].Value -ErrorAction Ignore } if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand } if (-not $command) { return } switch ($command.Name) { 'Where-Object' { $steps[$inputIndex] = [pscustomobject]@{ Index = $inputIndex Skip = $true Type = 'Where' } $inputIndex = $inputIndex - 1 continue outmain } 'Tee-Object' { $steps[$inputIndex] = [pscustomobject]@{ Index = $inputIndex Skip = $true Type = 'Tee' } $inputIndex = $inputIndex - 1 continue outmain } 'Sort-Object' { $steps[$inputIndex] = [pscustomobject]@{ Index = $inputIndex Skip = $true Type = 'Sort' } $inputIndex = $inputIndex - 1 continue outmain } #region Select-Object 'Select-Object' { $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-Object' $steps[$inputIndex] = [pscustomobject]@{ Index = $inputIndex Skip = $false Type = 'Select' Data = $selectObject } if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty) { $inputIndex = $inputIndex - 1 continue outmain } break outmain } #endregion Select-Object #region Select-PSFObject 'Select-PSFObject' { $selectObject = Read-SelectObject -Ast $pipelineAst.PipelineElements[$inputIndex] -CommandName 'Select-PSFObject' $steps[$inputIndex] = [pscustomobject]@{ Index = $inputIndex Skip = $false Type = 'PSFSelect' Data = $selectObject } if ($selectObject.HasIncludeFilter -or ($selectObject.Properties.Type -eq "Inherited") -or $selectObject.ExpandProperty) { $inputIndex = $inputIndex - 1 continue outmain } break outmain } #endregion Select-PSFObject default { break outmain } } } else { break } } #endregion Step backwards through the pipeline until the definitive object giver is found # Catch moving through _all_ options in the pipeline if ($inputIndex -lt 0) { return } #region Process resulting / reaching properties $properties = Get-Property -InputObject $pipelineAst.PipelineElements[$inputIndex] $inputIndex = $inputIndex + 1 while ($inputIndex -lt $index) { # Eliminate preliminary follies if (-not $steps[$inputIndex]) { $inputIndex = $inputIndex + 1; continue } if ($steps[$inputIndex].Skip) { $inputIndex = $inputIndex + 1; continue } # Process the current step, then move on unless done $properties = Update-Property -Property $properties -Step $steps[$inputIndex].Data $inputIndex = $inputIndex + 1 } #endregion Process resulting / reaching properties $properties.Keys | Sort-Object } Register-PSFTeppScriptblock -Name 'PSFramework-license-name' -ScriptBlock { (Get-PSFLicense).Product } Register-PSFTeppScriptblock -Name 'PSFramework-LanguageNames' -ScriptBlock { [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | Where-Object { $_ -and ($_.Trim()) } } Register-PSFTeppScriptblock -Name 'PSFramework-LocalizedStrings-Names' -ScriptBlock { ([PSFRamework.Localization.LocalizationHost]::Strings.Values | Where-Object Module -EQ $fakeBoundParameter.Module).Name } Register-PSFTeppScriptblock -Name 'PSFramework-LocalizedStrings-Modules' -ScriptBlock { [PSFRamework.Localization.LocalizationHost]::Strings.Values.Module | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework-logging-provider' -ScriptBlock { (Get-PSFLoggingProvider).Name } Register-PSFTeppScriptblock -Name 'PSFramework.Message.Module' -ScriptBlock { Get-PSFMessage | Select-Object -ExpandProperty ModuleName | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework.Message.Function' -ScriptBlock { Get-PSFMessage | Select-Object -ExpandProperty FunctionName | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework.Message.Tags' -ScriptBlock { Get-PSFMessage | Select-Object -ExpandProperty Tags | Remove-PSFNull -Enumerate | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework.Message.Runspace' -ScriptBlock { Get-PSFMessage | Select-Object -ExpandProperty Runspace | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework.Message.Level' -ScriptBlock { Get-PSFMessage | Select-Object -ExpandProperty Level | Select-Object -Unique } Register-PSFTeppScriptblock -Name 'PSFramework-utility-psprovider' -ScriptBlock { (Get-PSProvider).Name } Register-PSFTeppScriptblock -Name 'PSFramework-runspace-name' -ScriptBlock { (Get-PSFRunspace).Name } Register-PSFTeppScriptblock -Name 'PSFramework-tepp-scriptblockname' -ScriptBlock { [PSFramework.TabExpansion.TabExpansionHost]::Scripts.Keys } Register-PSFTeppScriptblock -Name 'PSFramework-tepp-parametername' -ScriptBlock { if ($fakeBoundParameter.Command) { $common = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm' try { $command = Get-Command $fakeBoundParameter.Command if ($command -is [System.Management.Automation.AliasInfo]) { $command = $command.ResolvedCommand } $command.Parameters.Keys | Where-Object { $_ -notin $common } } catch { } } } Register-PSFTeppScriptblock -Name "PSFramework-Unregister-PSFConfig-FullName" -ScriptBlock { switch ("$($fakeBoundParameter.Scope)") { "UserDefault" { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" } "UserMandatory" { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" } "SystemDefault" { $path = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" } "SystemMandatory" { $path = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced" } default { $path = "HKCU:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default" } } if (Test-Path $path) { $properties = Get-ItemProperty -Path $path $common = 'PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider' $properties.PSObject.Properties.Name | Where-Object { $_ -notin $common } } } Register-PSFTeppScriptblock -Name "PSFramework-Unregister-PSFConfig-Module" -ScriptBlock { [PSFramework.Configuration.ConfigurationHost]::Configurations.Values.Module | Select-Object -Unique } #region Configuration Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname' Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter Module -Name 'PSFramework-config-module' Register-PSFTeppArgumentCompleter -Command Export-PSFConfig -Parameter Name -Name 'PSFramework-config-name' Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname' Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter Module -Name 'PSFramework-config-module' Register-PSFTeppArgumentCompleter -Command Get-PSFConfig -Parameter Name -Name 'PSFramework-config-name' Register-PSFTeppArgumentCompleter -Command Import-PSFConfig -Parameter Schema -Name 'PSFramework-Config-Schema' Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname' Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Module -Name 'PSFramework-config-module' Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Name -Name 'PSFramework-config-name' Register-PSFTeppArgumentCompleter -Command Set-PSFConfig -Parameter Validation -Name 'PSFramework-config-validation' Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter FullName -Name 'PSFramework-config-fullname' Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter Module -Name 'PSFramework-config-module' Register-PSFTeppArgumentCompleter -Command Register-PSFConfig -Parameter Name -Name 'PSFramework-config-name' Register-PSFTeppArgumentCompleter -Command Get-PSFConfigValue -Parameter FullName -Name 'PSFramework-config-fullname' Register-PSFTeppArgumentCompleter -Command Unregister-PSFConfig -Parameter FullName -Name 'PSFramework-Unregister-PSFConfig-FullName' Register-PSFTeppArgumentCompleter -Command Unregister-PSFConfig -Parameter Module -Name 'PSFramework-Unregister-PSFConfig-Module' #endregion Configuration #region Features Register-PSFTeppArgumentCompleter -Command Get-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name' Register-PSFTeppArgumentCompleter -Command Set-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name' Register-PSFTeppArgumentCompleter -Command Test-PSFFeature -Parameter Name -Name 'PSFramework.Feature.Name' #endregion Features #region License Register-PSFTeppArgumentCompleter -Command Get-PSFLicense -Parameter Filter -Name 'PSFramework-license-name' #endregion License #region Localization Register-PSFTeppArgumentCompleter -Command Import-PSFLocalizedString -Parameter Language -Name 'PSFramework-LanguageNames' Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Module -Name 'PSFramework-LocalizedStrings-Modules' Register-PSFTeppArgumentCompleter -Command Get-PSFLocalizedString -Parameter Name -Name 'PSFramework-LocalizedStrings-Names' #endregion Localization #region Logging Register-PSFTeppArgumentCompleter -Command Get-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider' Register-PSFTeppArgumentCompleter -Command Install-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider' Register-PSFTeppArgumentCompleter -Command Set-PSFLoggingProvider -Parameter Name -Name 'PSFramework-logging-provider' #endregion Logging #region Message Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter ModuleName -Name 'PSFramework.Message.Module' Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter FunctionName -Name 'PSFramework.Message.Function' Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Tag -Name 'PSFramework.Message.Tags' Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Runspace -Name 'PSFramework.Message.Runspace' Register-PSFTeppArgumentCompleter -Command Get-PSFMessage -Parameter Level -Name 'PSFramework.Message.Level' #endregion Message #region Runspace Register-PSFTeppArgumentCompleter -Command Get-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name' Register-PSFTeppArgumentCompleter -Command Register-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name' Register-PSFTeppArgumentCompleter -Command Stop-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name' Register-PSFTeppArgumentCompleter -Command Start-PSFRunspace -Parameter Name -Name 'PSFramework-runspace-name' Register-PSFTeppArgumentCompleter -Command Get-PSFDynamicContentObject -Parameter Name -Name 'PSFramework-dynamiccontentobject-name' Register-PSFTeppArgumentCompleter -Command Set-PSFDynamicContentObject -Parameter Name -Name 'PSFramework-dynamiccontentobject-name' #endregion Runspace #region Serialization Register-PSFTeppArgumentCompleter -Command Export-PSFClixml -Parameter Encoding -Name 'PSFramework-Encoding' Register-PSFTeppArgumentCompleter -Command Import-PSFClixml -Parameter Encoding -Name 'PSFramework-Encoding' #endregion Serialization #region Tab Completion Register-PSFTeppArgumentCompleter -Command Set-PSFTeppResult -Parameter TabCompletion -Name 'PSFramework-tepp-scriptblockname' Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Name -Name 'PSFramework-tepp-scriptblockname' Register-PSFTeppArgumentCompleter -Command Register-PSFTeppArgumentCompleter -Parameter Parameter -Name 'PSFramework-tepp-parametername' #endregion Tab Completion #region Utility Register-PSFTeppArgumentCompleter -Command ConvertFrom-PSFArray -Parameter PropertyName -Name PSFramework-Input-ObjectProperty Register-PSFTeppArgumentCompleter -Command Resolve-PSFPath -Parameter Provider -Name PSFramework-utility-psprovider Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter Property -Name PSFramework-Input-ObjectProperty Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExpandProperty -Name PSFramework-Input-ObjectProperty Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ExcludeProperty -Name PSFramework-Input-ObjectProperty Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowProperty -Name PSFramework-Input-ObjectProperty Register-PSFTeppArgumentCompleter -Command Select-PSFObject -Parameter ShowExcludeProperty -Name PSFramework-Input-ObjectProperty #endregion Utility $mappings = @{ "microsoft.sqlserver.management.smo.server" = @("NetName", "DomainInstanceName") "deserialized.microsoft.sqlserver.management.smo.server" = @("NetName", "DomainInstanceName") "microsoft.sqlserver.management.smo.linkedserver" = @("Name") "deserialized.microsoft.sqlserver.management.smo.linkedserver" = @("Name") "microsoft.activedirectory.management.adcomputer" = @("DNSHostName", "Name") "deserialized.microsoft.activedirectory.management.adcomputer" = @("DNSHostName", "Name") "Microsoft.DnsClient.Commands.DnsRecord_A" = @("Name", "IPAddress") "Deserialized.Microsoft.DnsClient.Commands.DnsRecord_A" = @("Name", "IPAddress") "Microsoft.DnsClient.Commands.DnsRecord_AAAA" = @("Name", "IPAddress") "Deserialized.Microsoft.DnsClient.Commands.DnsRecord_AAAA" = @("Name", "IPAddress") } foreach ($key in $mappings.Keys) { Register-PSFParameterClassMapping -ParameterClass 'Computer' -TypeName $key -Properties $mappings[$key] } # Define our type aliases $TypeAliasTable = @{ PSFComputer = "PSFramework.Parameter.ComputerParameter" PSFComputerParameter = "PSFramework.Parameter.ComputerParameter" PSFDateTime = "PSFramework.Parameter.DateTimeParameter" PSFDateTimeParameter = "PSFramework.Parameter.DateTimeParameter" PSFEncoding = "PSFramework.Parameter.EncodingParameter" PSFEncodingParameter = "PSFramework.Parameter.EncodingParameter" psfrgx = "PSFramework.Utility.RegexHelper" PSFTimeSpan = "PSFramework.Parameter.TimeSpanParameter" PSFTimeSpanParameter = "PSFramework.Parameter.TimeSpanParameter" PSFValidatePattern = "PSFramework.Validation.PsfValidatePatternAttribute" PSFValidateScript = "PSFramework.Validation.PsfValidateScriptAttribute" PSFValidateSet = "PSFramework.Validation.PsfValidateSetAttribute" } Set-PSFTypeAlias -Mapping $TypeAliasTable #region Configuration Static Remove() Compatibility Update-TypeData -TypeName "System.Collections.Concurrent.ConcurrentDictionary``2[[$([System.String].AssemblyQualifiedName)],[$([PSFramework.Configuration.Config].AssemblyQualifiedName)]]" -MemberType ScriptMethod -MemberName Remove -Value ([scriptblock]::Create(@' param ( $Item ) $dummyItem = $null $null = $this.TryRemove($Item, [ref] $dummyItem) '@)) -Force #endregion Configuration Static Remove() Compatibility $scriptBlock = { $script:___ScriptName = 'psframework.taskengine' try { #region Main Execution while ($true) { # This portion is critical to gracefully closing the script if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName.ToLower()].State -notlike "Running") { break } $task = $null $tasksDone = @() while ($task = [PSFramework.TaskEngine.TaskHost]::GetNextTask($tasksDone)) { try { ([ScriptBlock]::Create($task.ScriptBlock.ToString())).Invoke() } catch { Write-PSFMessage -EnableException $false -Level Warning -Message "[Maintenance] Task '$($task.Name)' failed to execute: $_" -ErrorRecord $_ -FunctionName "task:TaskEngine" -Target $task } $task.LastExecution = Get-Date $tasksDone += $task.Name } # If there will no more tasks need executing in the future, might as well kill the runspace if (-not ([PSFramework.TaskEngine.TaskHost]::HasPendingTasks)) { break } Start-Sleep -Seconds 5 } #endregion Main Execution } catch { } finally { [PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName.ToLower()].SignalStopped() } } Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'psframework.taskengine' -NoMessage #region Handle Module Removal $PSF_OnRemoveScript = { # Stop all managed runspaces ONLY on the main runspace's termination if ([runspace]::DefaultRunspace.Id -eq 1) { Wait-PSFMessage -Timeout 30s -Terminate Get-PSFRunspace | Stop-PSFRunspace [PSFramework.PSFCore.PSFCoreHost]::Uninitialize() } # Properly disconnect all remote sessions still held open $psframework_pssessions.Values | Remove-PSSession } $ExecutionContext.SessionState.Module.OnRemove += $PSF_OnRemoveScript Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action $PSF_OnRemoveScript #endregion Handle Module Removal #region Declare runtime variable for the flow control component $paramNewVariable = @{ Name = "psframework_killqueue" Value = (New-Object PSFramework.Utility.LimitedConcurrentQueue[int](25)) Option = 'ReadOnly' Scope = 'Script' Description = 'Variable that is used to maintain the list of commands to kill. This is used by Test-PSFFunctionInterrupt. Note: The value tested is the hashcade from the callstack item.' } New-Variable @paramNewVariable #endregion Declare runtime variable for the flow control component #region Declare PSSession Cache $paramNewVariable2 = @{ Name = "psframework_pssessions" Value = (New-Object PSFramework.ComputerManagement.PSSessionContainer) Option = 'ReadOnly' Scope = 'Script' Description = 'Variable containing the list of established powershell remoting sessions. This is used by Invoke-PSFCommand to track connections, disconnect expired sessions and reconnect sessions by name.' } New-Variable @paramNewVariable2 #endregion Declare PSSession Cache #region Register Features Register-PSFFeature -Name 'PSFramework.InheritEnableException' -NotGlobal -Owner PSFramework -Description 'Causes all PSFramework commands with the -EnableException parameter to check, whether the caller has that variable set (e.g. by having a parameter with the same name) and respect that as well.' #endregion Register Features # Load Session Registrations for the Session Container feature # See: New-PSSessionContainer Register-PSFSessionObjectType -DisplayName CimSession -TypeName Microsoft.Management.Infrastructure.CimSession Register-PSFSessionObjectType -DisplayName PSSession -TypeName System.Management.Automation.Runspaces.PSSession Register-PSFSessionObjectType -DisplayName SmoServer -TypeName Microsoft.SqlServer.Management.Smo.Server [PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.FileInfo'] = @( [PSCustomObject]@{ Name = 'PSChildName' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSDrive' Type = ([type]'System.Management.Automation.PSDriveInfo') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSIsContainer' Type = ([type]'System.Boolean') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSParentPath' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSPath' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSProvider' Type = ([type]'System.Management.Automation.ProviderInfo') TypeKnown = $true }, [PSCustomObject]@{ Name = 'BaseName' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'VersionInfo' Type = ([type]'System.Diagnostics.FileVersionInfo') TypeKnown = $true } ) [PSFramework.TabExpansion.TabExpansionHost]::InputCompletionTypeData['System.IO.DirectoryInfo'] = @( [PSCustomObject]@{ Name = 'PSChildName' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSDrive' Type = ([type]'System.Management.Automation.PSDriveInfo') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSIsContainer' Type = ([type]'System.Boolean') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSParentPath' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSPath' Type = ([type]'System.String') TypeKnown = $true }, [PSCustomObject]@{ Name = 'PSProvider' Type = ([type]'System.Management.Automation.ProviderInfo') TypeKnown = $true }, [PSCustomObject]@{ Name = 'BaseName' Type = ([type]'System.String') TypeKnown = $true } ) $license = New-PSFLicense -Product 'PSFramework' -Manufacturer 'Friedrich Weinmann' -ProductVersion $ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date -Year 2017 -Month 04 -Day 27 -Hour 0 -Minute 0 -Second 0) -Text @" Copyright (c) Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #region Chris Dent $null = New-PSFLicense -Product 'Import-PSCmdlet' -Manufacturer 'Chris Dent' -ProductVersion '1.0.0.0' -ProductType Script -Name MIT -Version '1.0.0.0' -Date (Get-Date -Year 2018 -Month 05 -Day 16).Date -Text @" Copyright (c) Chris Dent Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ -Description @" The PSFramework is happy to publish the Import-PSFCmdlet command, based on the original work of Chris Dent's, 'Import-PSCmdlet' Thank you for allowing its use :) - Original Source: https://www.indented.co.uk/cmdlets-without-a-dll/ - Author blog: https://www.indented.co.uk/ "@ -Parent $license #endregion Chris Dent #region Joel Bennet $null = New-PSFLicense -Product 'Configuration-ExportPaths' -Manufacturer 'Joel Bennet' -ProductVersion '1.3.0' -ProductType Script -Name MIT -Version '1.0.0.0' -Date (Get-Date -Year 2018 -Month 05 -Day 16).Date -Text @" Copyright (c) 2015 Joel Bennett Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ -Description @" The PSFramework is happy to base its internal path selection for configuration exports on the original work of Joel Bennet's, 'Configuration' module. Its implementation can be found in the internal script file: internal/scripts/loadConfigurationPersisted.ps1 Thank you for allowing its use :) - Original Source: https://github.com/PoshCode/Configuration - Author blog: http://huddledmasses.org/blog/ - Author Twitter: https://twitter.com/jaykul?lang=en "@ -Parent $license #endregion Joel Bennet #endregion Load compiled code |