PoshToolbox.psm1
#region: #region: ./src/Exceptions/New_ActiveDirectoryObjectNotFoundException.ps1 function New_ActiveDirectoryObjectNotFoundException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] $Message, 'System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_ActiveDirectoryOperationException.ps1 function New_ActiveDirectoryOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException] $Message, 'System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_ActiveDirectoryServerDownException.ps1 function New_ActiveDirectoryServerDownException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.DirectoryServices.ActiveDirectory.ActiveDirectoryServerDownException] $Message, 'System.DirectoryServices.ActiveDirectory.ActiveDirectoryServerDownException', [System.Management.Automation.ErrorCategory]::ResourceUnavailable, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_ArgumentException.ps1 function New_ArgumentException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.ArgumentException] $Message, 'System.ArgumentException', [System.Management.Automation.ErrorCategory]::InvalidArgument, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_InvalidOperationException.ps1 function New_InvalidOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException] $Message, 'System.InvalidOperationException', [System.Management.Automation.ErrorCategory]::ConnectionError, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_LimitException.ps1 function New_LimitException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [PoshToolbox.LimitException] $Message, 'PoshToolbox.LimitException', [System.Management.Automation.ErrorCategory]::LimitsExceeded, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_MethodInvocationException.ps1 function New_MethodInvocationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( $Exception, 'System.Management.Automation.MethodInvocationException', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_PSInvalidOperationException.ps1 function New_PSInvalidOperationException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Management.Automation.PSInvalidOperationException] $Message, 'System.Management.Automation.PSInvalidOperationException', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Exceptions/New_UnauthorizedAccessException.ps1 function New_UnauthorizedAccessException { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw = $false ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.UnauthorizedAccessException] $Message, 'System.UnauthorizedAccessException', [System.Management.Automation.ErrorCategory]::PermissionDenied, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #endregion #region: #region: ./src/Public/ConvertFrom-Base64String.ps1 function ConvertFrom-Base64String { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [ValidateScript({ if ($_ -notmatch '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$') { throw 'The argument specified must be a valid base 64 string.' } return $true })] [string[]] $InputObject ) ## LOGIC ################################################################### process { foreach ($Object in $InputObject) { try { [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} $Disposable.MemoryStream = [System.IO.MemoryStream]::new() $Disposable.MemoryStream.Write(([byte[]] $Buffer = [System.Console]::InputEncoding.GetBytes($Object)), 0, $Buffer.Length) $Disposable.MemoryStream.Flush() $Disposable.MemoryStream.Position = 0 $Disposable.CryptoStream = [System.Security.Cryptography.CryptoStream]::new( $Disposable.MemoryStream, [System.Security.Cryptography.FromBase64Transform]::new(), [System.Security.Cryptography.CryptoStreamMode]::Read ) $Disposable.StreamReader = [System.IO.StreamReader]::new( $Disposable.CryptoStream, [System.Text.Encoding]::Unicode ) [string] $String = $Disposable.StreamReader.ReadToEnd() try { $PSCmdlet.WriteObject([System.Management.Automation.PSSerializer]::Deserialize($String)) } catch { $PSCmdlet.WriteObject([System.Text.Encoding]::Unicode.GetBytes($String)) } } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() } } } } #endregion #region: ./src/Public/ConvertTo-Base64String.ps1 function ConvertTo-Base64String { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([string])] param ( [Parameter( Mandatory, ValueFromPipeline )] [ValidateNotNullOrEmpty()] [object] $InputObject, [Parameter()] [int32] $Depth = 2 ) ## LOGIC ################################################################### process { try { [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} $Disposable.MemoryStream = [System.IO.MemoryStream]::new() try { $Disposable.MemoryStream.Write(([byte[]] $Buffer = $InputObject), 0, $Buffer.Length) } catch { [xml] $Serialize = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth) $Disposable.MemoryStream.Write(([byte[]] $Buffer = [System.Text.Encoding]::Unicode.GetBytes($Serialize.OuterXml)), 0, $Buffer.Length) } $Disposable.MemoryStream.Flush() $Disposable.MemoryStream.Position = 0 $Disposable.CryptoStream = [System.Security.Cryptography.CryptoStream]::new( $Disposable.MemoryStream, [System.Security.Cryptography.ToBase64Transform]::new(), [System.Security.Cryptography.CryptoStreamMode]::Read ) $Disposable.StreamReader = [System.IO.StreamReader]::new( $Disposable.CryptoStream, [System.Console]::OutputEncoding ) $PSCmdlet.WriteObject($Disposable.StreamReader.ReadToEnd()) } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() } } } #endregion #region: ./src/Public/Find-NlMtu.ps1 function Find-NlMtu { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '', Justification = 'Assignment Operator is intended.')] [CmdletBinding()] [OutputType([object])] param ( [Alias('Hostname', 'IPAddress', 'Address')] [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]] $ComputerName, [Parameter()] [ValidateRange(1, [int32]::MaxValue)] [int32] $Timeout = 10000, [Alias('Ttl', 'TimeToLive', 'Hops')] [Parameter()] [ValidateRange(1, [int32]::MaxValue)] [int32] $MaxHops = 128 ) ## LOGIC ################################################################### begin { [System.Net.NetworkInformation.PingOptions] $PingOptions = @{ 'Ttl' = $MaxHops 'DontFragment' = $true } } process { foreach ($Computer in $ComputerName) { try { [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} $Disposable.Ping = [System.Net.NetworkInformation.Ping]::new() [uint16] $UpperBound = 65535 [uint16] $LowerBound = 1 [uint16] $Size = 9000 [byte[]] $Buffer = [byte[]]::new($Size) [System.Collections.Generic.List[System.Net.NetworkInformation.PingReply]] $Result = @() while ($Size -ne $LowerBound) { try { $PSCmdlet.WriteVerbose("PING ${Computer} with ${Size}-byte payload") [System.Net.NetworkInformation.PingReply] $Reply = $Disposable.Ping.Send($Computer, $Timeout, $Buffer, $PingOptions) } catch { New_InvalidOperationException -Message "Connection to '${Computer}' failed." -Throw } switch ($Reply.Status) { 'PacketTooBig' { $UpperBound = $Size } 'Success' { $LowerBound = $Size } 'TimedOut' { $UpperBound = $Size } default { New_InvalidOperationException -Message "Connection to '${Computer}' failed with status '$( $Reply.Status ).'" -Throw } } $Result.Add($Reply) if (($Size = [System.Math]::Floor(($LowerBound + $UpperBound) / 2)) -eq 1) { New_InvalidOperationException -Message "Connection to '${Computer}' failed with status 'NoReply.'" -Throw } [array]::Resize([ref] $Buffer, $Size) } if (([int32] $Hops = $MaxHops - $Result.Where{ $_.Status -eq 'Success' }[-1].Options.Ttl) -lt 0) { $Hops = 0 } $PSCmdlet.WriteObject( [pscustomobject] @{ ComputerName = $Computer ReplyFrom = $Result.Where{ $_.Status -eq 'Success' }[-1].Address 'Time(ms)' = [int32] ($Result.Where{ $_.Status -eq 'Success' }.RoundtripTime | Measure-Object -Average).Average Hops = $Hops # IP Header (20 bytes) + ICMP Header (8 bytes) = 28 bytes MTU = $Size + 28 } ) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() } } } } #endregion #region: ./src/Public/Get-ADServiceAccountCredential.ps1 function Get-ADServiceAccountCredential { # Copyright (c) 2021 Ryan Ephgrave, https://github.com/Ryan2065/gMSACredentialModule # Modified "Get-GMSACredential.ps1" by Anthony J. Raymond [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Retrieved password is plaintext.')] [CmdletBinding()] [OutputType([pscredential])] param ( [Alias('distinguishedName', 'objectGuid', 'objectSid', 'sAMAccountName')] [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]] $Identity, [Parameter()] [ValidateNotNullOrEmpty()] [string] $Server ) ## LOGIC ################################################################### begin { [hashtable[]] $Properties = @( @{ n = 'sAMAccountName'; e = { $_.Properties.'samaccountname' } } @{ n = 'Length'; e = { $_.Properties.'msds-managedpassword'.Length } } @{ n = 'ManagedPassword'; e = { [int32] $Length = $_.Properties.'msds-managedpassword'.Length [int32] $IntPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($Length) [System.Runtime.InteropServices.Marshal]::Copy([byte[]] $_.Properties.'msds-managedpassword'.ForEach{ $_ }, 0, $IntPtr, $Length) return $IntPtr } } ) } process { foreach ($Object in $Identity) { try { [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} try { # https://ldapwiki.com/wiki/ObjectGuid [string] $ObjectGuid = ([guid] $Object).ToByteArray().ForEach{ $_.ToString('X2') } -join '\' [string] $Filter = "(&(objectGuid=\${ObjectGuid})(ObjectCategory=msDS-GroupManagedServiceAccount))" } catch { [string] $Filter = '(&(|(distinguishedName={0})(objectSid={0})(sAMAccountName={1}))(ObjectCategory=msDS-GroupManagedServiceAccount))' -f $Object, ($Object -ireplace '[^$]$', '$&$') } $Disposable.DirectorySearcher = [System.DirectoryServices.DirectorySearcher]::new($Filter) if ($Server) { $Disposable.DirectorySearcher.SearchRoot = "LDAP://${Server}" } $Disposable.DirectorySearcher.SearchRoot.AuthenticationType = 'Sealing' $Disposable.DirectorySearcher.PropertiesToLoad.AddRange(@('sAMAccountName', 'msDS-ManagedPassword')) [object] $ADServiceAccount = $Disposable.DirectorySearcher.FindOne() | Select-Object -Property $Properties if (-not $ADServiceAccount) { New_ActiveDirectoryObjectNotFoundException -Message "Cannot find an object with identity: '${Object}' under: '$( $Disposable.DirectorySearcher.SearchRoot.distinguishedName )'." -Throw } elseif ($ADServiceAccount.Length -eq 0) { New_ActiveDirectoryOperationException -Message 'Cannot retrieve service account password. A process has requested access to an object, but has not been granted those access rights.' -Throw } # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a9019740-3d73-46ef-a9ae-3ea8eb86ac2e [securestring] $SecureString = ConvertTo-SecureString -String ([System.Runtime.InteropServices.Marshal]::PtrToStringUni([int64] $ADServiceAccount.ManagedPassword + 16)) -AsPlainText -Force $PSCmdlet.WriteObject([pscredential]::new($ADServiceAccount.sAMAccountName, $SecureString)) } catch [System.Management.Automation.SetValueInvocationException] { $PSCmdlet.WriteError(( New_ActiveDirectoryServerDownException -Message 'Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Services running.' )) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() if ($ADServiceAccount) { [System.Runtime.InteropServices.Marshal]::Copy([byte[]]::new($ADServiceAccount.Length), 0, $ADServiceAccount.ManagedPassword, $ADServiceAccount.Length) [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ADServiceAccount.ManagedPassword) $ADServiceAccount = $SecureString = $null $null = [System.GC]::GetTotalMemory($true) } } } } } #endregion #region: ./src/Public/Get-FolderProperties.ps1 function Get-FolderProperties { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Named to match Windows context menu.')] [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path' )] [ValidateScript({ if (Test-Path -Path $_ -PathType 'Container') { return $true } throw 'The argument specified must resolve to a valid folder path.' })] [string[]] $Path, [Alias('PSPath')] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath' )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType 'Container') { return $true } throw 'The argument specified must resolve to a valid folder path.' })] [string[]] $LiteralPath, [Parameter()] [ValidateSet( 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', # Decimal Metric (Base 10) 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' # Binary IEC (Base 2) )] [string] $Unit = 'MiB' ) ## LOGIC ################################################################### begin { [hashtable] $Prefix = @{ [char] 'K' = 1 # kilo [char] 'M' = 2 # mega [char] 'G' = 3 # giga [char] 'T' = 4 # tera [char] 'P' = 5 # peta [char] 'E' = 6 # exa [char] 'Z' = 7 # zetta [char] 'Y' = 8 # yotta } [int32] $Base = $Unit.Contains('i') | Use-Ternary 1024 1000 [double] $Divisor = [System.Math]::Pow($Base, $Prefix[$Unit[0]]) } process { [hashtable] $Splat = @{ $PSCmdlet.ParameterSetName = $PSBoundParameters[$PSCmdlet.ParameterSetName] } [object] $Process = Resolve-PoshPath @Splat foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne 'FileSystem') { New_ArgumentException 'The argument specified must resolve to a valid path on the FileSystem provider.' -Throw } [System.IO.DirectoryInfo] $Folder = $Object.ProviderPath $PSCmdlet.WriteVerbose("GET ${Folder}") [int32] $Dirs = [int32] $Files = [int32] $Bytes = 0 # https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy [string[]] $Result = robocopy $Folder.FullName.TrimEnd('\') \\null /l /e /np /xj /r:0 /w:0 /bytes /nfl /ndl if (($LASTEXITCODE -eq 16) -and ($Result[-2] -eq 'Access is denied.')) { New_UnauthorizedAccessException -Message "Access to the path '${Folder}' is denied." -Throw } elseif ($LASTEXITCODE -eq 16) { New_ArgumentException -Message "The specified path '${Folder}' is invalid." -Throw } switch -Regex ($Result) { 'Dirs :\s+(?<match>\d+)' { $Dirs = [int32] $Matches.match - 1 } 'Files :\s+(?<match>\d+)' { $Files = [int32] $Matches.match } 'Bytes :\s+(?<match>\d+)' { $Bytes = [int64] $Matches.match } } $PSCmdlet.WriteObject( [pscustomobject] @{ FullName = $Folder.FullName Length = $Bytes Size = "{0:n2} ${Unit}" -f ($Bytes / $Divisor) Contains = "${Files} Files, ${Dirs} Folders" Created = '{0:F}' -f $Folder.CreationTime } ) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/Invoke-ExponentialBackoff.ps1 function Invoke-ExponentialBackoff { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0 )] [scriptblock] $ScriptBlock, [Parameter()] [int32] $RetryCount = 3, [Parameter()] [int32] $Base = 2, [Parameter()] [int32] $Scalar = 1 ) ## LOGIC ################################################################### end { for ([int32] $i = 0; $i -lt $RetryCount; $i++) { try { . $ScriptBlock return } catch { $PSCmdlet.WriteError($_) if (($i + 1) -ge $RetryCount) { $PSCmdlet.ThrowTerminatingError(( New_LimitException -Message "The operation has reached the limit of ${RetryCount} retries." )) } Start-Sleep -Milliseconds ((Get-Random -Maximum 1000) * $Scalar * [bigint]::Pow($Base, $i)) } } } } #endregion #region: ./src/Public/Join-File.ps1 function Join-File { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding(SupportsShouldProcess)] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path' )] [ValidateScript({ if (Test-Path -Path $_ -PathType 'Leaf' -Filter '*.*split') { return $true } throw 'The argument specified must resolve to a valid split type file.' })] [string[]] $Path, [Alias('PSPath')] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath' )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType 'Leaf' -Filter '*.*split') { return $true } throw 'The argument specified must resolve to a valid split type file.' })] [string[]] $LiteralPath, [Parameter()] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw 'The argument specified must resolve to a valid file or folder path.' })] [string] $Destination = (Get-Location -PSProvider 'FileSystem').ProviderPath ) ## LOGIC ################################################################### begin { [System.IO.FileInfo] $DestinationInfo = (Resolve-PoshPath -LiteralPath $Destination).ProviderPath } process { [hashtable] $Splat = @{ $PSCmdlet.ParameterSetName = $PSBoundParameters[$PSCmdlet.ParameterSetName] } [object] $Process = Resolve-PoshPath @Splat foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne 'FileSystem') { New_ArgumentException 'The argument specified must resolve to a valid path on the FileSystem provider.' -Throw } [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} [System.IO.FileInfo] $File = $Object.ProviderPath [string] $CalculatedDestination = if ($DestinationInfo.Extension) { $DestinationInfo } else { "$( $DestinationInfo.FullName.TrimEnd('\/') )/$( $File.BaseName )" } if ($PSCmdlet.ShouldProcess($CalculatedDestination, 'Write Content')) { [string] $Directory = if ($DestinationInfo.Extension) { $DestinationInfo.Directory } else { $DestinationInfo } if (-not $Directory.Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } $PSCmdlet.WriteVerbose("WRITE ${CalculatedDestination}") $Disposable.Writer = [System.IO.File]::OpenWrite($CalculatedDestination) # sort to fix ChildItem number sorting foreach ($SplitFile in (Get-ChildItem -Path "$( $File.Directory )/$( $File.BaseName ).*split").FullName | Sort-Object -Property { [int32] [regex]::Match($_, '\.(?<match>\d+)split$').Groups['match'].Value }) { $PSCmdlet.WriteVerbose("READ ${SplitFile}") [byte[]] $Bytes = [System.IO.File]::ReadAllBytes($SplitFile) $Disposable.Writer.Write($Bytes, 0, $Bytes.Length) } } $PSCmdlet.WriteObject(( Get-ChildItem -Path $CalculatedDestination )) } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() } } } } #endregion #region: ./src/Public/New-Exception.ps1 function New-Exception { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter( Mandatory, Position = 0 )] [ValidateNotNullOrEmpty()] [string] $Message, [Parameter()] [switch] $Throw ) ## LOGIC ################################################################### end { [System.Management.Automation.ErrorRecord] $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception] $Message, 'System.Exception', [System.Management.Automation.ErrorCategory]::NotSpecified, $null ) if ($Throw) { throw $ErrorRecord } $PSCmdlet.WriteObject($ErrorRecord) } } #endregion #region: ./src/Public/New-IPAddress.ps1 function New-IPAddress { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([object])] param ( [Alias('Address')] [Parameter( Mandatory, Position = 0, ValueFromPipeline )] [string[]] $IPAddress ) ## LOGIC ################################################################### process { foreach ($Address in $IPAddress) { try { $PSCmdlet.WriteObject([System.Net.IPAddress]::Parse($Address)) } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/New-IPSubnet.ps1 function New-IPSubnet { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change system state.')] [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'InputObject' )] [string[]] $InputObject, [Alias('Address')] [Parameter( Mandatory, ParameterSetName = 'IPAddress' )] [string] $IPAddress, [Alias('Prefix')] [Parameter( Mandatory, ParameterSetName = 'IPAddress' )] [int32] $IPPrefix ) ## LOGIC ################################################################### process { foreach ($Object in $PSBoundParameters[$PSCmdlet.ParameterSetName]) { try { if ($PSCmdlet.ParameterSetName -eq 'InputObject') { $IPAddress = ($Object -isplit '\\|\/')[0] $IPPrefix = ($Object -isplit '\\|\/')[-1] } $PSCmdlet.WriteObject([System.Net.IPSubnet]::Parse($IPAddress, $IPPrefix)) } catch [System.Management.Automation.MethodException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/Resolve-PoshPath.ps1 function Resolve-PoshPath { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path' )] [ValidateNotNullOrEmpty()] [string[]] $Path, [Alias('PSPath')] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath' )] [ValidateNotNullOrEmpty()] [string[]] $LiteralPath ) ## LOGIC ################################################################### process { foreach ($PSPath in $PSBoundParameters[$PSCmdlet.ParameterSetName]) { try { if ($PSCmdlet.ParameterSetName -eq 'Path') { try { [string[]] $PSPath = $PSCmdlet.SessionState.Path.GetResolvedPSPathFromPSPath($PSPath) } catch [System.Management.Automation.MethodInvocationException] { [string[]] $PSPath = $_.Exception.InnerException.ItemName } } foreach ($String in $PSPath) { [System.Management.Automation.ProviderInfo] $Provider = [System.Management.Automation.PSDriveInfo] $Drive = $null [string] $ProviderPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($String, [ref] $Provider, [ref] $Drive) $PSCmdlet.WriteObject( [pscustomobject] @{ ProviderPath = $ProviderPath Provider = $Provider Drive = $Drive } ) } } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/Split-File.ps1 function Split-File { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding(SupportsShouldProcess)] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path' )] [ValidateScript({ if (Test-Path -Path $_ -PathType 'Leaf') { return $true } throw 'The argument specified must resolve to a valid file path.' })] [string[]] $Path, [Alias('PSPath')] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath' )] [ValidateScript({ if (Test-Path -LiteralPath $_ -PathType 'Leaf') { return $true } throw 'The argument specified must resolve to a valid file path.' })] [string[]] $LiteralPath, [Parameter()] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw 'The argument specified must resolve to a valid file or folder path.' })] [string] $Destination = (Get-Location -PSProvider 'FileSystem').ProviderPath, [Parameter ( Mandatory )] [ValidateRange(0, [int32]::MaxValue)] [int32] $Size ) ## LOGIC ################################################################### begin { [System.IO.FileInfo] $DestinationInfo = (Resolve-PoshPath -LiteralPath $Destination).ProviderPath } process { [hashtable] $Splat = @{ $PSCmdlet.ParameterSetName = $PSBoundParameters[$PSCmdlet.ParameterSetName] } [object] $Process = Resolve-PoshPath @Splat foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne 'FileSystem') { New_ArgumentException 'The argument specified must resolve to a valid path on the FileSystem provider.' -Throw } [System.Collections.Generic.Dictionary[string, System.IDisposable]] $Disposable = @{} [System.IO.FileInfo] $File = $Object.ProviderPath $PSCmdlet.WriteVerbose("READ ${File}") $Disposable.Reader = [System.IO.File]::OpenRead($File) [byte[]] $Buffer = [byte[]]::new($Size) [int32] $Count = 1 [string] $CalculatedDestination = if ($DestinationInfo.Extension) { "$( $DestinationInfo.Directory.FullName )/$( $File.Name )" } else { "$( $DestinationInfo.FullName.TrimEnd('\/') )/$( $File.Name )" } while ([int32] $Read = $Disposable.Reader.Read($Buffer, 0, $Buffer.Length)) { if ($Read -ne $Buffer.Length) { [array]::Resize([ref] $Buffer, $Read) } [string] $SplitFile = "${CalculatedDestination}.${Count}split" if ($PSCmdlet.ShouldProcess($SplitFile, 'Write Content')) { [string] $Directory = if ($DestinationInfo.Extension) { $DestinationInfo.Directory } else { $DestinationInfo } if (-not $Directory.Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } $PSCmdlet.WriteVerbose("WRITE ${SplitFile}") [System.IO.File]::WriteAllBytes($SplitFile, $Buffer) } $Count++ } # sort to fix ChildItem number sorting $PSCmdlet.WriteObject(( Get-ChildItem -Path "${CalculatedDestination}.*split" | Sort-Object -Property { [int32] [regex]::Match($_.FullName, '\.(?<match>\d+)split$').Groups['match'].Value } )) } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } finally { $Disposable.Values.Dispose() } } } } #endregion #region: ./src/Public/Start-PoshLog.ps1 function Start-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'As designed to receive log events.')] [CmdletBinding(DefaultParameterSetName = 'Path')] [OutputType([void])] param ( [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path' )] [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'PathAppend' )] [Parameter( Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'PathNoClobber' )] [ValidateScript({ if (Test-Path -Path $_ -IsValid) { return $true } throw 'The argument specified must resolve to a valid file or folder path.' })] [string[]] $Path = [Environment]::GetFolderPath('MyDocuments'), [Alias('PSPath')] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath' )] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPathAppend' )] [Parameter( Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPathNoClobber' )] [ValidateScript({ if (Test-Path -LiteralPath $_ -IsValid) { return $true } throw 'The argument specified must resolve to a valid file or folder path.' })] [string[]] $LiteralPath, [Parameter(ParameterSetName = 'PathAppend')] [Parameter(ParameterSetName = 'LiteralPathAppend')] [switch] $Append, [Parameter(ParameterSetName = 'PathNoClobber')] [Parameter(ParameterSetName = 'LiteralPathNoClobber')] [switch] $NoClobber, [Parameter()] [switch] $AsUtc, [Parameter()] [switch] $PassThru ) ## LOGIC ################################################################### begin { $DateTime = [datetime]::Now $Format = $AsUtc | Use-Ternary ('yyyy\-MM\-dd HH:mm:ss\Z', 'ToUniversalTime', 'yyyyMMdd\-HHmmss\Z') ('yyyy\-MM\-dd HH:mm:ss', 'ToLocalTime', 'yyyyMMdd\-HHmmss') $FileMode = $Append | Use-Ternary { [System.IO.FileMode]::Append } { $NoClobber | Use-Ternary { [System.IO.FileMode]::CreateNew } { [System.IO.FileMode]::Create } } $Template = { '**********************' 'Windows PowerShell log start' 'Version: {0} ({1})' -f $PSVersionTable.PSVersion, $PSVersionTable.PSEdition "Start time: {0:$( $Format[0] )}" -f $DateTime.($Format[1]).Invoke() '**********************' } } process { $Process = ($PSCmdlet.ParameterSetName -cmatch '^LiteralPath') | Use-Ternary { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path } foreach ($Object in $Process) { try { if ($Object.Provider.Name -ne 'FileSystem') { New_ArgumentException 'The argument specified must resolve to a valid path on the FileSystem provider.' -Throw } $FileInfo = [System.IO.FileInfo] $Object.ProviderPath if (-not ($Directory = $FileInfo.Extension | Use-Ternary { $FileInfo.Directory } { $FileInfo }).Exists) { $null = [System.IO.Directory]::CreateDirectory($Directory) } if (-not $FileInfo.Extension) { $FileInfo = [System.IO.FileInfo] ("PowerShell_log.{0}.{1:$( $Format[2] )}.txt" -f ([guid]::NewGuid() -isplit '-')[0], $DateTime.($Format[1]).Invoke()) } Use-Object ($File = [System.IO.File]::Open($Directory.FullName + '\' + $FileInfo.Name, $FileMode)) { if ($Append) { $NewLine = [System.Text.Encoding]::UTF8.GetBytes([System.Environment]::NewLine) $File.Write($NewLine, 0, $NewLine.Length) } foreach ($Line in $Template.Invoke()) { $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Line + [System.Environment]::NewLine) $File.Write($Bytes, 0, $Bytes.Length) } } $Global:PSLogDetails += @(@{ Path = $File.Name; Utc = $AsUtc }) Write-Information -InformationAction Continue -MessageData ("Log started, output file is '{0}'" -f $File.Name) -InformationVariable null ## EXCEPTIONS ################################################## } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } end { if (-not (Get-EventSubscriber | Where-Object SourceIdentifier -CMatch '^PSLog') -and $PSLogDetails) { $Global:PSLogInformation = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.InformationRecord]]::new() $Global:PSLogWarning = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.WarningRecord]]::new() $Global:PSLogError = [System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.ErrorRecord]]::new() $Action = { Write-PoshLog -PSEventArgs $Event } $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogInformation -SourceIdentifier PSLogInformation -Action $Action $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogWarning -SourceIdentifier PSLogWarning -Action $Action $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogError -SourceIdentifier PSLogError -Action $Action $Global:PSDefaultParameterValues['Write-Information:InformationVariable'] = '+PSLogInformation' $Global:PSDefaultParameterValues['Write-Warning:WarningVariable'] = '+PSLogWarning' $Global:PSDefaultParameterValues['Write-Error:ErrorVariable'] = '+PSLogError' } if ($PassThru) { Write-Output (, $PSLogDetails.Path) } } } #endregion #region: ./src/Public/Stop-PoshLog.ps1 function Stop-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([void])] param () ## LOGIC ################################################################### end { if (-not $PSLogDetails) { $PSCmdlet.ThrowTerminatingError(( New_PSInvalidOperationException -Message 'An error occurred stopping the log: The host is not currently logging.' )) } if ($Events = Get-EventSubscriber | Where-Object SourceIdentifier -CMatch '^PSLog') { $Events | Unregister-Event $Global:PSDefaultParameterValues.Remove('Write-Information:InformationVariable') $Global:PSDefaultParameterValues.Remove('Write-Warning:WarningVariable') $Global:PSDefaultParameterValues.Remove('Write-Error:ErrorVariable') } $DateTime = [datetime]::Now $Template = { '**********************' 'Windows PowerShell log end' "End time: {0:$( $Format[0] )}" -f $DateTime.($Format[1]).Invoke() '**********************' } foreach ($PSLog in $PSLogDetails) { try { $Format = $PSLog.Utc | Use-Ternary ('yyyy\-MM\-dd HH:mm:ss\Z', 'ToUniversalTime') ('yyyy\-MM\-dd HH:mm:ss', 'ToLocalTime') Use-Object ($File = [System.IO.File]::AppendText($PSLog.Path)) { foreach ($Line in $Template.Invoke()) { $File.WriteLine($Line) } } Write-Information -InformationAction Continue -MessageData ("Log stopped, output file is '{0}'" -f $PSLog.Path) -InformationVariable null ## EXCEPTIONS ################################################## } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } Remove-Variable -Name PSLog* -Scope Global } } #endregion #region: ./src/Public/Use-ErrorCoalescing.ps1 function Use-ErrorCoalescing { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias('?!')] [OutputType([object])] param ( [Parameter( Mandatory, ValueFromPipeline )] [scriptblock] $InputObject, [Parameter( Position = 0 )] [AllowNull()] [object] $IfError ) ## LOGIC ################################################################### process { foreach ($Object in , $InputObject) { try { . $Object } catch { [System.Management.Automation.ErrorRecord] $ErrorRecord = $_ [object] $Output = if ($IfError -is [System.Collections.IDictionary]) { $IfError.GetEnumerator().Where{ $ErrorRecord.Exception -is $_.Key }[0].Value } else { $IfError } if ($Output -is [scriptblock]) { $Output.InvokeWithContext( $null, [psvariable]::new('_', $ErrorRecord), $null ) } else { $PSCmdlet.WriteObject($Output) } } } } } #endregion #region: ./src/Public/Use-NullCoalescing.ps1 function Use-NullCoalescing { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias('??')] [OutputType([object])] param ( [Parameter( Mandatory, ValueFromPipeline )] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [object] $InputObject, [Parameter( Mandatory, Position = 0 )] [object] $IfNull ) ## LOGIC ################################################################### end { if (-not ($InputObject = $input)) { $InputObject = , $null } foreach ($Object in $InputObject) { try { if ($null -eq $Object) { if ($IfNull -is [scriptblock]) { . $IfNull } else { $PSCmdlet.WriteObject($IfNull) } } else { $PSCmdlet.WriteObject($Object) } } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/Use-Object.ps1 function Use-Object { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [OutputType([object])] param ( [Parameter( Mandatory, Position = 0 )] [AllowEmptyString()] [AllowEmptyCollection()] [AllowNull()] [object] $InputObject, [Parameter( Mandatory, Position = 1 )] [scriptblock] $ScriptBlock ) ## LOGIC ################################################################### end { try { $ScriptBlock.InvokeWithContext( $null, [psvariable]::new('_', $InputObject), $null ) } catch { throw $_ } finally { foreach ($Object in $InputObject) { if ($Object -is [System.IDisposable]) { $Object.Dispose() } elseif ([System.Runtime.InteropServices.Marshal]::IsComObject($Object)) { $null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Object) } } } } } #endregion #region: ./src/Public/Use-Ternary.ps1 function Use-Ternary { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding()] [Alias('?:')] [OutputType([object])] param ( [Parameter( Mandatory, ValueFromPipeline )] [AllowNull()] [AllowEmptyString()] [AllowEmptyCollection()] [object] $InputObject, [Parameter( Mandatory, Position = 0 )] [object] $IfTrue, [Parameter( Mandatory, Position = 1 )] [object] $IfFalse ) ## LOGIC ################################################################### process { foreach ($Object in , $InputObject) { try { [object] $Output = if ($Object) { $IfTrue } else { $IfFalse } if ($Output -is [scriptblock]) { . $Output } else { $PSCmdlet.WriteObject($Output) } } catch { $PSCmdlet.WriteError($_) } } } } #endregion #region: ./src/Public/Write-PoshLog.ps1 function Write-PoshLog { # Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details) [CmdletBinding( DefaultParameterSetName = 'Type' )] [OutputType([void])] param ( [Parameter( Mandatory, DontShow, ParameterSetName = 'PSEventArgs' )] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSEventArgs] $PSEventArgs, [Parameter( ParameterSetName = 'Type' )] [ValidateSet('Log', 'Information', 'Warning', 'Error')] [string] $Type = 'Log', [Parameter( Mandatory, Position = 0, ParameterSetName = 'Type' )] [string] $Message ) ## LOGIC ################################################################### end { if (-not $PSLogDetails) { $PSCmdlet.ThrowTerminatingError(( New_PSInvalidOperationException -Message 'An error occurred writing the log: The host is not currently logging.' )) } $TypeMap = @{ 'Log' = 'LOG' 'Information' = 'INFO' 'Warning' = 'WARN' 'Error' = 'ERROR' } $Template = { "{0:$( $Format[0] )}`t[{1}] `t{2}" -f $DateTime.($Format[1]).Invoke(), $TypeMap.$Type, $Message } if ($PSEventArgs) { $DateTime = $PSEventArgs.TimeGenerated switch ($PSEventArgs.SourceIdentifier) { 'PSLogInformation' { $Type = 'Information' $Message = $PSEventArgs.SourceEventArgs.NewItems.MessageData } 'PSLogWarning' { $Type = 'Warning' $Message = $PSEventArgs.SourceEventArgs.NewItems.Message } 'PSLogError' { $Type = 'Error' $Message = $PSEventArgs.SourceEventArgs.NewItems.Exception.Message } } } else { $DateTime = [datetime]::Now } foreach ($PSLog in $PSLogDetails) { try { $Format = $PSLog.Utc | Use-Ternary ('yyyy\-MM\-dd HH:mm:ss\Z', 'ToUniversalTime') ('yyyy\-MM\-dd HH:mm:ss', 'ToLocalTime') Use-Object ($File = [System.IO.File]::AppendText($PSLog.Path)) { $File.WriteLine($Template.Invoke()[0]) } ## EXCEPTIONS ################################################## } catch [System.Management.Automation.MethodInvocationException] { $PSCmdlet.WriteError(( New_MethodInvocationException -Exception $_.Exception.InnerException )) } catch { $PSCmdlet.WriteError($_) } } } } #endregion #endregion |