DSCResources/DSC_SoftwareInstallResource/DSC_SoftwareInstallResource.psm1

using namespace System.Diagnostics
using namespace System.IO
using namespace System.Text
using namespace System.Net
using namespace System.Net.Security
using namespace System.Security.Cryptography
using namespace System.Management.Automation

$Script:PackageCacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\DSC_SoftwareInstallResource"


function Get-StringMD5 ( [string]$String ) {
    <#
    .SYNOPSIS
    Generates and MD5 hash for a given string.
    #>

    
    [MD5CryptoServiceProvider]::new().ComputeHash( [Encoding]::UTF8.GetBytes( $Name ) ).ForEach({ '{0:x2}' -f $_ }) -join ''

}

function Get-CacheFolder {
    <#
    .SYNOPSIS
    Create and return the path for a cache folder on the disk.
    .DESCRIPTION
    Create and return the path for a cache folder on the disk. Uses the Name property MD5 hash as the folder name.
    #>

    [CmdletBinding()]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string]
        $Name,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams

    )

    $FolderName = Get-StringMD5 $Name
    $FolderPath = Join-Path $Script:PackageCacheLocation $FolderName

    Write-Verbose ( 'Cache folder: {0}' -f $FolderPath )

    if ( Test-Path -Path $FolderPath -PathType Container ) {

        Get-Item -Path $FolderPath -ErrorAction Stop | Convert-Path

    } else {

        New-Item -Path $FolderPath -ItemType Directory -Force -ErrorAction Stop | Convert-Path

    }

}


function Remove-CacheFolder {
    <#
    .SYNOPSIS
    Remove a cache folder on the disk.
    .DESCRIPTION
    Remove a cache folder on the disk. Uses the Name property MD5 hash as the folder name.
    #>

    [CmdletBinding()]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string]
        $Name,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams

    )

    $FolderName = Get-StringMD5 $Name
    $FolderPath = Join-Path $Script:PackageCacheLocation $FolderName

    if ( Test-Path -Path $FolderPath -PathType Container ) {

        Write-Verbose ( 'Removing cache folder: {0}' -f $FolderPath )

        Remove-Item -Path $FolderPath -Recurse -Force -ErrorAction Stop

    }

}


function ConvertFrom-CommandLine {
    <#
    .SYNOPSIS
    Parse a command line using native command parsing and return a splat compatible with Start-Process.
    .DESCRIPTION
    Parse a command line using native command parsing and return a splat compatible with Start-Process.
    #>

    param( [string]$CommandLine )
    function __args { $args }
    $Splat = @{}
    $CommandLine = [environment]::ExpandEnvironmentVariables( $CommandLine ) -replace '([{}$])', '`$1'
    $Splat.FilePath, $Arguments = Invoke-Expression "__args $CommandLine"
    if ( $Arguments ) {
        $Arguments = $Arguments.Trim().ForEach({ if ( $_.IndexOf(' ') -gt 0 ) { '"{0}"' -f $_ } else { $_ } }) -join ' '
        $Splat.ArgumentList = $Arguments
    }
    return $Splat
}


function Get-UninstallEntry {
    <#
    .SYNOPSIS
    Return uninstall entries matching given parameters.
    .DESCRIPTION
    Return uninstall entries matching given parameters.
    .PARAMETER Name
    Matches the DisplayName of the uninstall entry.
    .PARAMETER Publisher
    Matches the Publisher of the uninstall entry.
    .PARAMETER Version
    Matches the DisplayVersion of the uninstall entry.
    .PARAMETER LatestVersion
    How many versions to return, newest to oldest.
    #>

    [CmdletBinding()]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string]
        $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string]
        $Publisher,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Version,

        [Parameter()]
        [ValidateRange( 1, [uint32]::MaxValue)]
        [uint32]
        $LatestVersions = 1,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams

    )

    Write-Verbose ( 'Searching for products matching ''{0}'' with versions matching {1}' -f $Name, (Get-VersionStringDisplayString -VersionString $Version) )

    $RegistryLocations = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )

    [object[]]$MatchingProducts = Get-ItemProperty -Path $RegistryLocations |
        Where-Object { -not ( [string]::IsNullOrEmpty( $_.DisplayName ) -or [string]::IsNullOrEmpty( $_.DisplayVersion ) -or [string]::IsNullOrEmpty( $_.UninstallString ) ) } |
        Where-Object { $_.DisplayName -like $Name -and ( -not $Publisher -or $_.Publisher -like $Publisher ) } |
        Where-Object { $_.DisplayVersion | Test-IsVersionApplicable -VersionString $Version } |
        Sort-Object { [version]$_.DisplayVersion } -Descending |
        ForEach-Object {

            Write-Verbose ( 'Found matching product ''{0}'' {1} from publisher {2}.' -f $_.DisplayName, $_.DisplayVersion, $_.Publisher )
            
            [pscustomobject]@{
                Name                    = $_.DisplayName
                Publisher               = $_.Publisher
                Version                 = [version]$_.DisplayVersion
                ProductId               = $( try { [guid]$_.PSChildName } catch {} )
                UninstallString         = $_.UninstallString
                QuietUninstallString    = $_.QuietUninstallString
            }
        
        } |
        Group-Object Name

    if ( $MatchingProducts.Count -eq 0 ) {

        return

    }

    if ( $MatchingProducts.Count -gt 1 ) {

        Write-Error 'More than one product returned from search.' -ErrorAction Stop

    }

    return $MatchingProducts[0].Group | Select-Object -First $LatestVersions

}


function Assert-FileHashValid {
    <#
    .SYNOPSIS
    Asserts that the hash of the file at the given path matches the given hash.
    .PARAMETER Path
    The path to the file to check the hash of.
    .PARAMETER Hash
    The hash to check against.
    .PARAMETER Algorithm
    The algorithm to use to retrieve the file's hash.
    .NOTES
    Inspired by a similar function in xPSDesiredStateConfiguration
    .LINK
    https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/9940d6bd5b839773ffc0427047598ef9cb5693f0/source/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1#L1507
    #>

    [CmdletBinding()]
    param(

        [Parameter( Mandatory )]
        [string]
        $Path,

        [Parameter( Mandatory )]
        [string]
        $Hash,

        [Parameter()]
        [ValidateSet( 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160' )]
        [string]
        $Algorithm = 'SHA256',

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams

    )

    Write-Verbose ( 'Validating {1} hash for file: {0}' -f $Path, $Algorithm )
    
    $FileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction 'Stop' |
        Select-Object -ExpandProperty Hash

    if ( $FileHash -ne $Hash ) {

        Write-Verbose ( 'Target Hash: {0}' -f $Hash )
        Write-Verbose ( 'Actual Hash: {0}' -f $FileHash )

        throw 'File hash does not match!'
    
    } else {

        Write-Verbose 'File hash matches.'

    }

}


function Assert-FileSignatureValid {
    <#
    .SYNOPSIS
    Asserts that the signature of the file at the given path is valid.
    .PARAMETER Path
    The path to the file to check the signature of
    .PARAMETER Thumbprint
    The certificate thumbprint that should match the file's signer certificate.
    .PARAMETER Subject
    The certificate subject that should match the file's signer certificate.
    .NOTES
    Inspired by a similar function in xPSDesiredStateConfiguration
    .LINK
    https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/9940d6bd5b839773ffc0427047598ef9cb5693f0/source/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1#L1553
    #>

    [CmdletBinding()]
    param(

        [Parameter( Mandatory )]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Thumbprint,

        [Parameter()]
        [string]
        $Subject,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams
    
    )

    Write-Verbose -Message ( 'Checking file signing status: {0}' -f $Path )

    $Signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction 'Stop'

    if ( $Signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid ) {

        throw ( 'Signature Status: {0}' -f $Signature.Status )
    
    } else {

        Write-Verbose 'File has valid signature.'

    }

    if ( -not [string]::IsNullOrEmpty( $Subject ) ) {
        
        Write-Verbose ( 'Target Subject: {0}' -f $Subject )
        Write-Verbose ( 'Signer Subject: {0}' -f $Signature.SignerCertificate.Subject )
        
        if ( $Signature.SignerCertificate.Subject -notlike $Subject ) {

            throw 'Signer subject does not match.'

        } else {

            Write-Verbose 'Signer subject matches.'

        }
    
    } else {

        Write-Verbose ( 'Signer subject was not checked. Actual value: {0}' -f $Signature.SignerCertificate.Subject )

    }

    if ( -not [string]::IsNullOrEmpty( $Thumbprint ) ) {

        Write-Verbose ( 'Target Thumbprint: {0}' -f $Thumbprint )
        Write-Verbose ( 'Signer Thumbprint: {0}' -f $Signature.SignerCertificate.Thumbprint )

        if ( $Signature.SignerCertificate.Thumbprint -ne $Thumbprint ) {

            throw 'Signer thumbprint does not match.'
    
        } else {

            Write-Verbose 'Signer thumbprint matches.'

        }

    } else {

        Write-Verbose ( 'Signer thumbprint was not checked. Actual value: {0}' -f $Signature.SignerCertificate.Thumbprint )
        
    }

}


function Invoke-WebFileDownload {
    <#
    .SYNOPSIS
    Download a file from the web.
    .PARAMETER Uri
    The URI of the file on the web.
    .PARAMETER OutputFolder
    Where the file will be output.
    .PARAMETER FileName
    Specify a specific output filename.
    .PARAMETER ServerCertificateValidationCallback
    Callback function to validate server certificate is valid.
    .PARAMETER Credential
    Credential to authenticate for download. Ignored for HTTP or FTP file transfers.
    .PARAMETER UseDefaultCredential
    Use default credential to authenticate for download. Ignored for HTTP or FTP file transfers.
    .PARAMETER Proxy
    Use a proxy server to download files.
    .PARAMETER ProxyCredential
    Credential to authenticate to the proxy server.
    .PARAMETER UseDefaultCredential
    Use default credential to authenticate to the proxy server.
    .PARAMETER Force
    Force a download.
    .NOTES
    Inspired by similar functions in xPSDesiredStateConfiguration and chocolatey
    .LINK
    https://github.com/dsccommunity/xPSDesiredStateConfiguration/blob/9940d6bd5b839773ffc0427047598ef9cb5693f0/source/DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1#L498
    .LINK
    https://github.com/chocolatey/choco/blob/develop/src/chocolatey.resources/helpers/functions/Get-ChocolateyWebFile.ps1
    #>

    [CmdletBinding( DefaultParameterSetName = 'NoCredential_ProxyNoCredential' )]
    param(
    
        [Parameter( Mandatory, Position = 0 )]
        [uri]
        $Uri,

        [Parameter()]
        [string]
        $OutputFolder = $env:TEMP,

        [Parameter()]
        [string]
        $FileName,

        [Parameter()]
        [string]
        $ServerCertificateValidationCallback,

        [Parameter( ParameterSetName = 'Credential_ProxyNoCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyDefaultCredential', Mandatory )]
        [pscredential]
        $Credential,

        [Parameter( ParameterSetName = 'DefaultCredential_ProxyNoCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyDefaultCredential', Mandatory )]
        [switch]
        $UseDefaultCredential,

        [Parameter( ParameterSetName = 'NoCredential_ProxyNoCredential' )]
        [Parameter( ParameterSetName = 'Credential_ProxyNoCredential' )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyNoCredential' )]
        [Parameter( ParameterSetName = 'NoCredential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'NoCredential_ProxyDefaultCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyDefaultCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyDefaultCredential', Mandatory )]
        [uri]
        $Proxy,

        [Parameter( ParameterSetName = 'NoCredential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyCredential', Mandatory )]
        [pscredential]
        $ProxyCredential,

        [Parameter( ParameterSetName = 'NoCredential_ProxyDefaultCredential', Mandatory )]
        [Parameter( ParameterSetName = 'Credential_ProxyDefaultCredential', Mandatory )]
        [Parameter( ParameterSetName = 'DefaultCredential_ProxyDefaultCredential', Mandatory )]
        [switch]
        $ProxyUseDefaultCredential,

        [switch]
        $Force,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams
    
    )

    $WebFileTypes = '.php*', '.asp*', '.jsp*'

    $CredentialType, $ProxyCredentialType = $PSCmdlet.ParameterSetName.Split('_')

    Write-Verbose 'Beginning file download.'
    Write-Verbose ( 'Source: {0}' -f $Uri )

    try {

        Write-Verbose 'Resolving output folder.'

        $OutputFolder = $OutputFolder | Resolve-Path | Convert-Path

        if ( -not( Test-Path -Path $OutputFolder -PathType Container ) ) {

            throw ( 'Output folder does not exist: {0}' -f $OutputFolder )

        }

        Write-Verbose 'Creating web request.'

        switch -Wildcard ( $Uri.Scheme ) {
            
            'http*' {

                [HttpWebRequest]$WebRequest = [WebRequest]::Create($Uri)

            }

            'ftp*' {

                [FtpWebRequest]$WebRequest = [WebRequest]::Create($Uri)

            }

            default {

                throw ( 'Protocol {0} not supported.' -f $Uri.Scheme )

            }

        }

        if ( $_ -match '(ht|f)tp' ) {
            
            Write-Verbose -Message 'Setting authentication level.'
            $WebRequest.AuthenticationLevel = [AuthenticationLevel]::None

        }

        if ( $_ -match '(ht|f)tps' -and $ServerCertificateValidationCallback ) {

            Write-Verbose -Message 'Assigning user-specified certificate verification callback'
            $WebRequest.ServerCertificateValidationCallBack = [scriptblock]::Create( $ServerCertificateValidationCallback )

        }

        if ( $Proxy ) {

            Write-Verbose ( 'Request will use proxy: {0}' -f $Proxy )
        
            $WebRequest.Proxy = [WebProxy]::new( $Proxy )

            switch ( $ProxyCredentialType ) {
            
                'ProxyCredential' {
                    Write-Verbose 'Proxy will use supplied credentials'
                    $WebRequest.Proxy.Credentials = $Credential
                }
            
                'ProxyDefaultCredential' {
                    Write-Verbose 'Proxy will use default credentials.'
                    $WebRequest.Proxy.Credentials = [CredentialCache]::DefaultCredentials
                }

            }
        
        
        }

        switch ( $CredentialType ) {
            
            'Credential' {
                Write-Verbose 'Will use supplied credentials'
                $WebRequest.Credentials = $Credential
            }
            
            'DefaultCredential' {
                Write-Verbose 'Will use default credentials.'
                $WebRequest.Credentials = [CredentialCache]::DefaultCredentials
            }

        }

        Write-Verbose ( 'Getting {0} response.' -f $Uri.Scheme )

        switch -Wildcard ( $Uri.Scheme ) {
            
            'http*' {

                [HttpWebResponse]$Response = $WebRequest.GetResponse()

            }

            'ftp*' {

                [FtpWebResponse]$Response = $WebRequest.GetResponse()

            }

        }

        Write-Verbose 'Response received.'

        if ( -not $PSBoundParameters.ContainsKey( 'FileName' ) ) {

            Write-Verbose 'No file name supplied, attempting to determine filename from URI.'

            $FileName = Split-Path $Uri.AbsolutePath -Leaf

            $Extension = [path]::GetExtension( $FileName )

            if ( -not $Extension -or $WebFileTypes.Where({ $Extension -like $_ }) ) {
    
                Write-Verbose 'Detected invalid file name extension.'

                if ( $Uri.Scheme -like 'ftp*' ) {

                    throw 'Invalid file name.'

                }
                
                Write-Verbose 'Checking Content-Disposition header.'

                $FileName = [string]$Response.Headers['Content-Disposition'] -replace '.*filename=' |
                    ForEach-Object { $_.Trim('"''') } |
                    Select-Object -First 1

                if ( -not $FileName ) {

                    throw 'Invalid file name, and no Content-Disposition header from server.'

                }

            }
    
        }

        Write-Verbose ( 'Will use file name: {0}' -f $FileName )

        $OutputPath = Join-Path $OutputFolder $FileName

        if ( -not $Force -and ( Test-Path -Path $OutputPath -PathType Leaf ) ) {

            Write-Verbose ( 'Found existing file: {0}' -f $OutputPath )
        
        } else {

            Write-Verbose ( 'Creating output file: {0}' -f $OutputPath )
            $OutStream = [FileStream]::new( $OutputPath, 'Create' )
        
            Write-Verbose -Message ( 'Getting {0} response stream.' -f $Uri.Scheme )
            $ResponseStream = $Response.GetResponseStream()

            Write-Verbose -Message ( 'Downloading file: {0}' -f $FileName )
            $ResponseStream.CopyTo( $OutStream )
            $ResponseStream.Flush()
            $OutStream.Flush()

        }

    } finally {

        if ( $null -ne $Response ) {
            $Response.Close()
            $Response.Dispose()
        }
        
        if ( $null -ne $ResponseStream ) {
            $ResponseStream.Close()
            $ResponseStream.Close()
        }

        if ( $null -ne $OutStream ) {
            $OutStream.Close()
            $OutStream.Dispose()
        }

        'WebRequest', 'OutStream', 'Response' | ForEach-Object {

            Remove-Variable -Name $_ -ErrorAction SilentlyContinue

        }

    }

    Write-Verbose 'Download complete.'

    Get-Item -Path $OutputPath

}


function Invoke-FileCopy {
    <#
    .SYNOPSIS
    Copy a file from a local or network share.
    .PARAMETER Uri
    The URI of the file.
    .PARAMETER OutputFolder
    Where the file will be output.
    .PARAMETER FileName
    Specify a specific output filename.
    .PARAMETER Credential
    Credential to authenticate for download. Ignored for HTTP or FTP file transfers.
    .PARAMETER UseDefaultCredential
    Use default credential to authenticate for download. Ignored for HTTP or FTP file transfers.
    .PARAMETER Force
    Force a download.
    #>

    [CmdletBinding()]
    param(
    
        [Parameter( Mandatory, Position = 0 )]
        [uri]
        $Uri,

        [Parameter()]
        [string]
        $OutputFolder = $env:TEMP,

        [Parameter()]
        [string]
        $FileName,

        [Parameter()]
        [pscredential]
        $Credential,

        [Parameter()]
        [switch]
        $UseDefaultCredential,

        [switch]
        $Force,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams
    
    )

    $CredentialSplat = @{}
    if ( $Credential ) { $CredentialSplat.Credential = $Credential }
    if ( $UseDefaultCredential ) { $CredentialSplat.Credential = [CredentialCache]::DefaultCredentials }

    Write-Verbose 'Beginning file copy.'
    Write-Verbose ( 'Source: {0}' -f $Uri.AbsolutePath )

    if ( -not $FileName ) {

        $FileName = Split-Path $Uri.AbsolutePath -Leaf

    }

    Write-Verbose ( 'Will use file name: {0}' -f $FileName )

    $OutputPath = Join-Path $OutputFolder $FileName

    if ( -not $Force -and ( Test-Path -Path $OutputPath -PathType Leaf ) ) {

        Write-Verbose ( 'Found existing file: {0}' -f $OutputPath )
    
    } else {

        if ( $Uri.IsUnc ) {

            Write-Verbose 'Copying file from network path.'

            $SourcePath = Split-Path $Uri.AbsolutePath -Parent
            $SourceFile = Split-Path $Uri.AbsolutePath -Leaf
            
            New-PSDrive -PSProvider FileSystem -Name 'xSoftwareInstallSource' -Root $SourcePath @CredentialSplat -ErrorAction Stop > $null

            try {

                Copy-Item -Path "xSoftwareInstallSource:\$SourceFile" -Destination $OutputPath -ErrorAction Stop

            } finally {

                Remove-PSDrive -Name 'xSoftwareInstallSource'

            }

        } else {

            Write-Verbose 'Copying file from local path.'

            Copy-Item -Path $Uri.AbsolutePath -Destination $OutputPath -ErrorAction Stop

        }

    }

    Write-Verbose 'Copy complete.'

    Get-Item -Path $OutputPath

}


function Resolve-PathEx {
    <#
    .SYNOPSIS
    Resolve exiting and missing file paths.
    #>

    [CmdletBinding( DefaultParameterSetName = 'Path' )]
    param(

        [Parameter(
            ParameterSetName = 'LiteralPath',
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias( 'PSPath' )]
        [string[]]
        $LiteralPath,

        [Parameter(
            ParameterSetName = 'Path',
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string[]]
        $Path,

        [switch]
        $Relative,

        [switch]
        $UseTransaction

    )

    process {

        $PSBoundParameters.ErrorAction = 'Continue'
        $PSBoundParameters.Remove( 'ErrorVariable' ) > $null

        Resolve-Path @PSBoundParameters 2>&1 | ForEach-Object {

            if ( $_ -is [System.Management.Automation.ErrorRecord] ) {

                [pscustomobject]@{
                    Path = $_.TargetObject
                }
            
            } else {
            
                $_
                
            }

        }
    
    }

}


function Convert-PathEx {
    <#
    .SYNOPSIS
    Convert paths for existing and non-existant files.
    #>

    [CmdletBinding( DefaultParameterSetName = 'Path' )]
    param(

        [Parameter(
            ParameterSetName = 'LiteralPath',
            Mandatory = $true,
            Position = 0,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias( 'PSPath' )]
        [string[]]
        $LiteralPath,

        [Parameter(
            ParameterSetName = 'Path',
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string[]]
        $Path,

        [switch]
        $UseTransaction

    )

    process {

        $ErrorParams = @{}

        if ( $PSBoundParameters.ContainsKey( 'ErrorAction' ) ) {

            $ErrorParams.ErrorAction = $PSBoundParameters.ErrorAction
            $PSBoundParameters.ErrorAction = 'Continue'

        }

        if ( $PSBoundParameters.ContainsKey( 'ErrorVariable' ) ) {
    
            $ErrorParams.ErrorVariable = $PSBoundParameters.ErrorVariable
            $PSBoundParameters.Remove( 'ErrorVariable' ) > $null
    
        }

        Convert-Path @PSBoundParameters 2>&1 | ForEach-Object {
    
            if ( $_ -is [System.Management.Automation.ErrorRecord] ) {

                [System.Collections.Generic.List[string]]$Parts = @()
                $TargetPath = $_.TargetObject

                do {
            
                    $Leaf = Split-Path $TargetPath -Leaf
                    $TargetPath = Split-Path $TargetPath -Parent
                    $Parts.Add( $Leaf )

                    if ( -not $TargetPath ) {
                
                        Write-Error @_ @ErrorParams
                        return
                    
                    }

                } until ( $ConvertedPath = Convert-Path $TargetPath -UseTransaction:$UseTransaction -ErrorAction SilentlyContinue )

                $Parts.Insert( 0, $ConvertedPath )

                $Parts -join [IO.Path]::DirectorySeparatorChar
            
            } else {
            
                $_
                
            }

        }

    }

}


function Get-VersionStringDisplayString {
    <#
    .SYNOPSIS
    Get a friendly display version string
    .DESCRIPTION
    Get a friendly display version string
    .EXAMPLE
    Convert-VersionStringToFilter -VersionString '[1.0,]'
    #>

    [CmdletBinding()]
    [OutputType( [string] )]
    param(
        [Parameter( Position=1 )]
        [string]
        $VersionString
    )

    switch -Regex ( $VersionString ) {
        '^\[(?<Version>\d+\.\d+[^,]*),[\)\]]$'                            { '{0} ≤ ?.?' -f $Matches.Version                               } # Example: [1.0,] or [1.0,)
        '^\((?<Version>\d+\.\d+[^,]*),[\)\]]$'                            { '{0} < ?.?' -f $Matches.Version                               } # Example: (1.0,) or (1.0,]
        '^\[(?<Version>\d+\.\d+[^,]*)\]$'                                 { '?.? = {0}' -f $Matches.Version                               } # Example: [1.0]
        '^(?<Version>\d+\.\d+[^,]*)$'                                     { '?.? = {0}' -f $Matches.Version                               } # Example: 1.0
        '^[\(\[],(?<Version>\d+\.\d+[^,]*)\)$'                            { '?.? < {0}' -f $Matches.Version                               } # Example: (,1.0) or [,1.0)
        '^[\(\[],(?<Version>\d+\.\d+[^,]*)\]$'                            { '?.? ≤ {0}' -f $Matches.Version                               } # Example: (,1.0] or [,1.0]
        '^\[(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\]$' { '{0} ≤ ?.? ≤ {1}' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: [1.0,2.0]
        '^(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)$'     { '{0} ≤ ?.? ≤ {1}' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: 1.0,2.0
        '^\((?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\)$' { '{0} < ?.? < {1}' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: (1.0,2.0)
        '^\[(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\)$' { '{0} ≤ ?.? < {1}' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: [1.0,2.0)
        '^\((?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\]$' { '{0} < ?.? ≤ {1}' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: (1.0,2.0]
        default { 'ANY' }
    }

}


function Convert-VersionStringToFilter {
    <#
    .SYNOPSIS
    Convert a version string to a filter
    .DESCRIPTION
    Convert a version string to a filter
    .EXAMPLE
    Convert-VersionStringToFilter -VersionString '[1.0,]'
    #>

    [CmdletBinding()]
    [OutputType( [scriptblock] )]
    param(
        [Parameter( Mandatory=$true, Position=1 )]
        [string]
        $VersionString
    )

    $FilterDefinition = switch -Regex ( $VersionString ) {
        '^\[(?<Version>\d+\.\d+[^,]*),[\)\]]$'                            { '[version]$_ -ge ''{0}''' -f $Matches.Version                                                      } # Example: [1.0,] or [1.0,)
        '^\((?<Version>\d+\.\d+[^,]*),[\)\]]$'                            { '[version]$_ -gt ''{0}''' -f $Matches.Version                                                      } # Example: (1.0,) or (1.0,]
        '^\[(?<Version>\d+\.\d+[^,]*)\]$'                                 { '[version]$_ -eq ''{0}''' -f $Matches.Version                                                      } # Example: [1.0]
        '^(?<Version>\d+\.\d+[^,]*)$'                                     { '[version]$_ -eq ''{0}''' -f $Matches.Version                                                      } # Example: 1.0
        '^[\(\[],(?<Version>\d+\.\d+[^,]*)\)$'                            { '[version]$_ -lt ''{0}''' -f $Matches.Version                                                      } # Example: (,1.0) or [,1.0)
        '^[\(\[],(?<Version>\d+\.\d+[^,]*)\]$'                            { '[version]$_ -le ''{0}''' -f $Matches.Version                                                      } # Example: (,1.0] or [,1.0]
        '^\[(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\]$' { '[version]''{0}'' -le $_ -and [version]$_ -le ''{1}''' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: [1.0,2.0]
        '^(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)$'     { '[version]''{0}'' -le $_ -and [version]$_ -le ''{1}''' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: 1.0,2.0
        '^\((?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\)$' { '[version]''{0}'' -lt $_ -and [version]$_ -lt ''{1}''' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: (1.0,2.0)
        '^\[(?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\)$' { '[version]''{0}'' -le $_ -and [version]$_ -lt ''{1}''' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: [1.0,2.0)
        '^\((?<MinVersion>\d+\.\d+[^,]*),(?<MaxVersion>\d+\.\d+[^,]*)\]$' { '[version]''{0}'' -lt $_ -and [version]$_ -le ''{1}''' -f $Matches.MinVersion, $Matches.MaxVersion } # Example: (1.0,2.0]
        default { throw 'Unrecognized Version Pattern - see https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#version-ranges' }
    }

    return [scriptblock]::Create($FilterDefinition)

}


function Test-IsVersionApplicable {
    <#
    .SYNOPSIS
    Check if a version is applicable given a particular version string
    .DESCRIPTION
    Check if a version is applicable given a particular version string
    .EXAMPLE
    Test-IsVersionApplicable -VersionString '[1.0,]' -ComparisonVersion '1.5.1'
    #>

    [CmdletBinding( PositionalBinding=$false )]
    [OutputType( [scriptblock] )]
    param(
        
        [string]
        $VersionString,

        [Parameter( Mandatory=$true, ValueFromPipeline=$true )]
        [string]
        $ComparisonVersion
    )

    process {

        if ( [string]::IsNullOrEmpty($VersionString) ) { return $true }

        return ( $ComparisonVersion | ForEach-Object (Convert-VersionStringToFilter -VersionString $VersionString) | Select-Object -First 1 )

    }

}


function Test-LogFileIsWritable {
    <#
    .SYNOPSIS
    Validate that log file is writable.
    #>

    [CmdletBinding()]
    param(
    
        [Parameter( Mandatory )]
        [string]
        $LogPath,

        [Parameter( ValueFromRemainingArguments = $true, DontShow = $true )]
        $IgnoredParams
    
    )

    $LogPath = Resolve-PathEx -LiteralPath $LogPath | Convert-PathEx

    $LogExists = Test-Path -Path $LogPath -PathType Leaf

    if ( $LogExists ) {

        try {

            [System.IO.File]::OpenWrite($LogPath).Close()
            $Writeable = $true

        } catch {

            $Writeable = $false

        }

    } else {

        try {

            New-Item -Path $LogPath -ItemType File -ErrorAction Stop | Remove-Item
            $Writeable = $true

        } catch {
        
            $Writeable = $false
        
        }

    }

    Write-Verbose ( 'Log file path: {0}' -f $LogPath )
    Write-Verbose ( 'Log file is writable: {0}' -f $Writeable )

    return $Writeable

}


function Get-TargetResource {

    [CmdletBinding( SupportsShouldProcess = $true )]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'Present', 'Absent' )]
        [System.String]
        $Ensure,

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'MSI', 'EXE' )]
        [System.String]
        $Type,

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ProductId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Publisher,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Version,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Uri,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $InstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $ReturnCode = @( 0, 1641, 3010 ),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $UninstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $UninstallReturnCode = @( 0, 3010 ),

        [Parameter()]
        [System.Boolean]
        $UninstallRequiresInstaller = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter()]
        [System.Boolean]
        $UseDefaultCredential = $false,

        [Parameter()]
        [System.String]
        $Proxy,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $ProxyCredential,

        [Parameter()]
        [System.Boolean]
        $ProxyUseDefaultCredential,

        [Parameter()]
        [System.String]
        $LogPath,

        [Parameter()]
        [System.String]
        $Hash,

        [Parameter()]
        [ValidateSet( 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160' )]
        [System.String]
        $Algorithm,

        [Parameter()]
        [System.Boolean]
        $RequireValidSignature,

        [Parameter()]
        [System.String]
        $Subject,

        [Parameter()]
        [System.String]
        $Thumbprint,

        [Parameter()]
        [System.String]
        $ServerCertificateValidationCallback,

        [Parameter()]
        [System.Boolean]
        $IgnoreReboot = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $RunAsCredential,

        [Parameter()]
        [System.UInt32]
        $TimeoutSeconds

    )

    Write-Verbose 'Entering Get-TargetResource in file DSC_xSoftwareInstallResource.psm1.'

    $CommonParameters = & { [CmdletBinding( SupportsShouldProcess )]param() $MyInvocation.MyCommand.Parameters.Keys }
    $PackageParameters = $MyInvocation.MyCommand.Parameters.Keys.Where({ $_ -notin $CommonParameters })
    
    $Package = [ordered]@{}
    $PackageParameters | ForEach-Object {
        
        $Package[$_] = (Get-Variable -Name $_ -ErrorAction SilentlyContinue).Value
        if ( $_ -eq 'Name' ) { $Package['ProductId'] = $null }

    }

    $Package.Ensure = 'Absent'

    $UninstallEntry = Get-UninstallEntry @PSBoundParameters -LatestVersions 1

    if ( $UninstallEntry ) {
        $Package.Ensure            = 'Present'
        $Package.Name              = $UninstallEntry.Name
        $Package.Publisher         = $UninstallEntry.Publisher
        $Package.Version           = $UninstallEntry.Version.ToString()
        # Product ID is not populated with EXE installers (generally) so only set it if it is a guid
        if ( $UninstallEntry.ProductId -is [guid] ) {
            $Package.ProductId         = $UninstallEntry.ProductId.ToString('B')
        }
    }

    # if no install string provided, we're going to assume a bare command for .EXE and default .MSI command
    if ( [string]::IsNullOrEmpty( $InstallCommand ) ) {

        if ( $Type -eq 'MSI' ) { 

            $Package.InstallCommand = 'msiexec.exe /I "{0}" /QN /norestart'

            if ( $LogPath ) {

                $Package.InstallCommand += ' /log "{1}"'

            }

        } elseif ( $Type -eq 'EXE' ) {
            
            $Package.InstallCommand = '"{0}"'

        }

    }

    # if no uninstall string is provided we'll use the value from the uninstall entry
    if ( [string]::IsNullOrEmpty( $UninstallCommand ) ) {

        if ( $Type -eq 'MSI' ) { 

            $Package.UninstallCommand = 'msiexec.exe /X "{2}" /QN /norestart'

            if ( $LogPath ) {

                $Package.UninstallCommand += ' /log "{1}"'

            }

        } elseif ( $Type -eq 'EXE' -and $UninstallEntry ) {
            
            if ( $UninstallEntry.QuietUninstallString ) {

                $Package.UninstallCommand = $UninstallEntry.QuietUninstallString

            } elseif ( $UninstallEntry.UninstallString ) {
                
                $Package.UninstallCommand = $UninstallEntry.UninstallString
                
            }

        }

    }

    return $Package

}


function Test-TargetResource {

    [CmdletBinding( SupportsShouldProcess = $true )]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'Present', 'Absent' )]
        [System.String]
        $Ensure,

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'MSI', 'EXE' )]
        [System.String]
        $Type,

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ProductId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Publisher,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Version,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Uri,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $InstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $ReturnCode = @( 0, 1641, 3010 ),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $UninstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $UninstallReturnCode = @( 0, 3010 ),

        [Parameter()]
        [System.Boolean]
        $UninstallRequiresInstaller = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter()]
        [System.Boolean]
        $UseDefaultCredential = $false,

        [Parameter()]
        [System.String]
        $Proxy,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $ProxyCredential,

        [Parameter()]
        [System.Boolean]
        $ProxyUseDefaultCredential,

        [Parameter()]
        [System.String]
        $LogPath,

        [Parameter()]
        [System.String]
        $Hash,

        [Parameter()]
        [ValidateSet( 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160' )]
        [System.String]
        $Algorithm,

        [Parameter()]
        [System.Boolean]
        $RequireValidSignature,

        [Parameter()]
        [System.String]
        $Subject,

        [Parameter()]
        [System.String]
        $Thumbprint,

        [Parameter()]
        [System.String]
        $ServerCertificateValidationCallback,

        [Parameter()]
        [System.Boolean]
        $IgnoreReboot = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $RunAsCredential,

        [Parameter()]
        [System.UInt32]
        $TimeoutSeconds

    )

    Write-Verbose 'Entering Test-TargetResource in file DSC_xSoftwareInstallResource.psm1.'

    $Package = Get-TargetResource @PSBoundParameters

    Write-Verbose ( 'Software status: {0}' -f $Package.Ensure )

    return $Ensure -eq $Package.Ensure

}


function Set-TargetResource {

    [CmdletBinding( SupportsShouldProcess = $true )]
    param(

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'Present', 'Absent' )]
        [System.String]
        $Ensure,

        [Parameter( Mandatory = $true )]
        [ValidateSet( 'MSI', 'EXE' )]
        [System.String]
        $Type,

        [Parameter( Mandatory = $true )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ProductId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [System.String]
        $Publisher,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Version,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Uri,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $InstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $ReturnCode = @( 0, 1641, 3010 ),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $UninstallCommand,

        # Return codes 1641 and 3010 indicate success when a restart is requested per installation
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.UInt32[]]
        $UninstallReturnCode = @( 0, 3010 ),

        [Parameter()]
        [System.Boolean]
        $UninstallRequiresInstaller = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter()]
        [System.Boolean]
        $UseDefaultCredential = $false,

        [Parameter()]
        [System.String]
        $Proxy,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $ProxyCredential,

        [Parameter()]
        [System.Boolean]
        $ProxyUseDefaultCredential,

        [Parameter()]
        [System.String]
        $LogPath,

        [Parameter()]
        [System.String]
        $Hash,

        [Parameter()]
        [ValidateSet( 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160' )]
        [System.String]
        $Algorithm,

        [Parameter()]
        [System.Boolean]
        $RequireValidSignature,

        [Parameter()]
        [System.String]
        $Subject,

        [Parameter()]
        [System.String]
        $Thumbprint,

        [Parameter()]
        [System.String]
        $ServerCertificateValidationCallback,

        [Parameter()]
        [System.Boolean]
        $IgnoreReboot = $false,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $RunAsCredential,

        [Parameter()]
        [System.UInt32]
        $TimeoutSeconds

    )

    Write-Verbose 'Entering Set-TargetResource in file DSC_xSoftwareInstallResource.psm1.'

    $ErrorActionPreference = 'Stop'

    $Package = Get-TargetResource @PSBoundParameters

    if ( $Ensure -eq $Package.Ensure ) {
        
        Write-Verbose 'Package in desired state.'
        return
    
    }

    Write-Verbose 'Package configuration starting.'

    $InstallerUri = $Uri -as [uri]
    $Installer = $null

    # cache the file if the InstallerURI is specifed
    if ( $InstallerUri -and ( $Ensure -eq 'Present' -or $UninstallRequiresInstaller ) ) {
            
        if ( $InstallerUri.IsFile -and -not $InstallerUri.IsUnc ) {
            
            $Installer = Get-Item -LiteralPath $InstallerUri.AbsolutePath -ErrorAction Stop | Convert-Path
        
        } else {
        
            $CacheFolder = Get-CacheFolder $Name
            $Installer = Invoke-WebFileDownload @PSBoundParameters -OutputFolder $CacheFolder | Convert-Path
        
        }

        if ( -not $Installer ) {
            Write-Error 'Installer was not found.' -ErrorAction Stop
        }

        if ( $Hash ) {
            Assert-FileHashValid -Path $Installer @PSBoundParameters
        } else {
            Write-Warning 'File hash was not verified!'
        }

        if ( $RequireValidSignature -or $Subject -or $Thumbprint ) {
            Assert-FileSignatureValid -Path $Installer @PSBoundParameters
        } else {
            Write-Warning 'File signature was not verified!'
        }

    }

    # are we installing or uninstalling?
    $CommandLine = $Package.InstallCommand, $Package.UninstallCommand | Select-Object -Index ( $Ensure -eq 'Absent' )
    $ValidReturnCodes = $ReturnCode, $UninstallReturnCode | Select-Object -Index ( $Ensure -eq 'Absent' )
    $CommandTypeVerb = 'install', 'uninstall' | Select-Object -Index ( $Ensure -eq 'Absent' )

    # if the command line is empty throw an error, user must supply a command line
    if ( [string]::IsNullOrEmpty( $CommandLine ) ) {

        Write-Error ( 'No {0} command supplied, and command was not able to be automatically generated.' -f $CommandTypeVerb ) -ErrorAction Stop

    }

    # escape any curly brace that is not explicitly '{0}', '{1}', etc..
    $CommandLine = $CommandLine -replace '{(?!\d})', '{{' -replace '(?<!{\d)}', '}}'

    # interpolate the Installer, Log Path, and Product ID in the command
    $CommandLine = $CommandLine -f $Installer, $LogPath, $Package.ProductId

    if ( $PSCmdlet.ShouldProcess( ( 'Run command line: {0}' -f $CommandLine ), $null, $null ) ) {
            
        #$ExitCode = Invoke-CommandLine -CommandLine $CommandLine

        $StartProcessSplat = ConvertFrom-CommandLine $CommandLine
        if ( $RunAsCredential ) {
            $StartProcessSplat.Credential = $RunAsCredential
        }

        $Process = Start-Process @StartProcessSplat -PassThru

        $InstallTimeout = $null
        
        $WaitProcessSplat = @{
            ErrorAction = 'SilentlyContinue'
            ErrorVariable = 'InstallTimeout'
        }

        if ( $TimeoutSeconds ) {
            $WaitProcessSplat = @{ Timeout = $TimeoutSeconds }
        }

        $Process | Wait-Process @WaitProcessSplat

        if ( $InstallTimeout ) {

            $Process | Stop-Process -Force -ErrorAction Stop
            $ExitCode = 1

        } else {

            $ExitCode = $Process.ExitCode

        }

        if ( -not $IgnoreReboot -and $ExitCode -eq 3010 ) {

            $global:DSCMachineStatus = 1

        }

        if ( $ExitCode -notin $ValidReturnCodes ) {

            Write-Error ( 'Could not {0} the package.' -f $CommandTypeVerb ) -ErrorAction Stop

        } else {

            Write-Verbose ( 'Package {0} was completed.' -f $CommandTypeVerb )

        }
    
    }

    if ( -not $UninstallRequiresInstaller ) {

        Remove-CacheFolder $Name
        
    }

}