DSCResources/DSC_xPackageResource/DSC_xPackageResource.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param ()

$errorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules'

# Import the shared modules
Import-Module -Name (Join-Path -Path $modulePath `
    -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' `
        -ChildPath 'xPSDesiredStateConfiguration.Common.psm1'))

Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common')

# Import Localization Strings
$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

$script:packageCacheLocation = "$env:programData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\DSC_xPackageResource"
$script:msiTools = $null

<#
    .SYNOPSIS
        Retrieves the current installation state of the package.
 
    .PARAMETER Name
        The name of the package to be added or removed.
 
    .PARAMETER ProductId
        The identifying number used to uniquely identify this package.
 
    .PARAMETER Path
        The path, URL or UNC path to the package.
 
    .PARAMETER CreateCheckRegValue
        Specifies if a registry value should be created when the packages is installed.
 
    .PARAMETER InstalledCheckRegHive
        The hive in which to create the registry key. Defaults to 'LocalMachine'.
 
    .PARAMETER InstalledCheckRegKey
        The registry key to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueName
        The registry value name to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueData
        The registry value to validate the package is installed.
#>


function Get-TargetResource
{
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $Name,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $ProductId,

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

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

        [Parameter()]
        [ValidateSet('LocalMachine', 'CurrentUser')]
        [System.String]
        $InstalledCheckRegHive = 'LocalMachine',

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

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

        [Parameter()]
        [System.String]
        $InstalledCheckRegValueData
    )

    Write-Verbose -Message $script:localizedData.EnteringGetTargetResource

    Assert-PathExtensionValid -Path $Path

    $identifyingNumber = [System.String]::Empty

    if (-not [System.String]::IsNullOrEmpty($ProductId))
    {
        $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
    }

    $packageResourceResult = @{}

    $getProductEntryParameters = @{
        Name              = $Name
        IdentifyingNumber = $identifyingNumber
    }

    $checkRegistryValueParameters = @{
        CreateCheckRegValue        = $CreateCheckRegValue
        InstalledCheckRegHive      = $InstalledCheckRegHive
        InstalledCheckRegKey       = $InstalledCheckRegKey
        InstalledCheckRegValueName = $InstalledCheckRegValueName
        InstalledCheckRegValueData = $InstalledCheckRegValueData
    }

    if ($CreateCheckRegValue)
    {
        Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData

        $getProductEntryParameters += $checkRegistryValueParameters
        $packageResourceResult += $checkRegistryValueParameters
    }

    $productEntry = Get-ProductEntry @getProductEntryParameters

    if ($null -eq $productEntry)
    {
        $packageResourceResult += @{
            Ensure    = 'Absent'
            Name      = $Name
            ProductId = $identifyingNumber
            Path      = $Path
            Installed = $false
        }

        return $packageResourceResult
    }
    elseif ($CreateCheckRegValue)
    {
        $packageResourceResult += @{
            Ensure    = 'Present'
            Name      = $Name
            ProductId = $identifyingNumber
            Path      = $Path
            Installed = $true
        }

        return $packageResourceResult
    }

    <#
        Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE).
        If the user gave a product ID just pass it through, otherwise get it from the product.
    #>

    if ($null -eq $identifyingNumber -and $null -ne $productEntry.Name)
    {
        $identifyingNumber = Split-Path -Path $productEntry.Name -Leaf
    }

    $installDate = $productEntry.GetValue('InstallDate')

    if ($null -ne $installDate)
    {
        try
        {
            $installDate = '{0:d}' -f [System.DateTime]::ParseExact($installDate, 'yyyyMMdd', [System.Globalization.CultureInfo]::CurrentCulture).Date
        }
        catch
        {
            $installDate = $null
        }
    }

    $publisher = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'Publisher'

    $estimatedSize = $productEntry.GetValue('EstimatedSize')

    if ($null -ne $estimatedSize)
    {
        $estimatedSize = $estimatedSize / 1024
    }

    $displayVersion = $productEntry.GetValue('DisplayVersion')

    $comments = $productEntry.GetValue('Comments')

    $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName'

    $packageResourceResult += @{
        Ensure             = 'Present'
        Name               = $displayName
        Path               = $Path
        InstalledOn        = $installDate
        ProductId          = $identifyingNumber
        Size               = $estimatedSize
        Installed          = $true
        Version            = $displayVersion
        PackageDescription = $comments
        Publisher          = $publisher
    }

    return $packageResourceResult
}

<#
    .SYNOPSIS
        Installs or uninstalls a package.
 
    .PARAMETER Ensure
        Indicates whether to Ensure that the package is 'Present' or 'Absent'.
        Defaults to 'Present'.
 
    .PARAMETER Name
        The name of the package to be added or removed.
 
    .PARAMETER ProductId
        The identifying number used to uniquely identify this package.
 
    .PARAMETER Path
        The path, URL or UNC path to the package.
 
    .PARAMETER Arguments
        The arguments to be passed to the package during addition or removal.
 
        When installing MSI packages, the `/quiet` and `/norestart` arguments are
        automatically applied.
 
    .PARAMETER IgnoreReboot
        Ignore a pending reboot if requested by package installation. The default
        value is $false and DSC will try to reboot the system.
 
    .PARAMETER Credential
        The credentials to be used for mounting the UNC path (if applicable).
 
    .PARAMETER ReturnCode
        The list of possible valid return codes for this install or removal.
 
    .PARAMETER LogPath
        The path to log the output of the MSI or EXE.
 
    .PARAMETER FileHash
        The expected hash value of the file found in the Path location.
 
    .PARAMETER HashAlgorithm
        The algorithm used to generate the FileHash value. Defaults to 'SHA256'.
 
    .PARAMETER SignerSubject
        The subject that must match the signer certificate of the digital signature.
        Wildcards are allowed.
 
    .PARAMETER SignerThumbprint
        The certificate thumbprint which must match the signer certificate of the
        digital signature.
 
    .PARAMETER ServerCertificateValidationCallback
        PowerShell code used to validate SSL certificates of HTTPS url assigned to Path.
 
    .PARAMETER RunAsCredential
        The credentials under which to run the installation.
 
    .PARAMETER CreateCheckRegValue
        Specifies if a registry value should be created when the packages is installed.
 
    .PARAMETER InstalledCheckRegHive
        The hive in which to create the registry key. Defaults to 'LocalMachine'.
 
    .PARAMETER InstalledCheckRegKey
        The registry key to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueName
        The registry value name to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueData
        The registry value to validate the package is installed.
#>

function Set-TargetResource
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $ProductId,

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

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

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

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

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

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

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

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

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

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

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

        [Parameter()]
        [ValidateSet('LocalMachine', 'CurrentUser')]
        [System.String]
        $InstalledCheckRegHive = 'LocalMachine',

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

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

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

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

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

    $ErrorActionPreference = 'Stop'

    if (Test-TargetResource @PSBoundParameters)
    {
        return
    }

    Assert-PathExtensionValid -Path $Path
    $uri = Convert-PathToUri -Path $Path

    if (-not [System.String]::IsNullOrEmpty($ProductId))
    {
        $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
    }
    else
    {
        $identifyingNumber = $ProductId
    }

    $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber

    <#
        Path gets overwritten in the download code path. Retain the user's original Path in case
        the install succeeded but the named package wasn't present on the system afterward so we
        can give a better error message.
    #>

    $originalPath = $Path

    Write-Verbose -Message $script:localizedData.PackageConfigurationStarting

    $logStream = $null
    $psDrive = $null
    $downloadedFileName = $null

    try
    {
        $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower()
        if (-not [System.String]::IsNullOrEmpty($LogPath))
        {
            try
            {
                if ($fileExtension -eq '.msi')
                {
                    <#
                        We want to pre-verify the log path exists and is writable ahead of time
                        even in the MSI case, as detecting WHY the MSI log path doesn't exist would
                        be rather problematic for the user.
                    #>

                    if ((Test-Path -Path $LogPath) -and $PSCmdlet.ShouldProcess($script:localizedData.RemoveExistingLogFile, $null, $null))
                    {
                        Remove-Item -Path $LogPath
                    }

                    if ($PSCmdlet.ShouldProcess($script:localizedData.CreateLogFile, $null, $null))
                    {
                        New-Item -Path $LogPath -Type 'File' | Out-Null
                    }
                }
                elseif ($PSCmdlet.ShouldProcess($script:localizedData.CreateLogFile, $null, $null))
                {
                    $logStream = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $LogPath, $false )
                }
            }
            catch
            {
                New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_
            }
        }

        # Download or mount file as necessary
        if (-not ($fileExtension -eq '.msi' -and $Ensure -eq 'Absent'))
        {
            if ($uri.IsUnc -and $PSCmdlet.ShouldProcess($script:localizedData.MountSharePath, $null, $null))
            {
                $psDriveArgs = @{
                    Name       = [System.Guid]::NewGuid()
                    PSProvider = 'FileSystem'
                    Root       = Split-Path -Path $uri.LocalPath
                }

                # If we pass a null for Credential, a dialog will pop up.
                if ($null -ne $Credential)
                {
                    $psDriveArgs['Credential'] = $Credential
                }

                $psDrive = New-PSDrive @psDriveArgs
                $Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $uri.LocalPath -Leaf)
            }
            elseif (@( 'http', 'https' ) -contains $uri.Scheme -and $Ensure -eq 'Present' -and $PSCmdlet.ShouldProcess($script:localizedData.DownloadHTTPFile, $null, $null))
            {
                $uriScheme = $uri.Scheme
                $outStream = $null
                $responseStream = $null

                try
                {
                    Write-Verbose -Message ($script:localizedData.CreatingCacheLocation)

                    if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container'))
                    {
                        New-Item -Path $script:packageCacheLocation -ItemType 'Directory' | Out-Null
                    }

                    $destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $uri.LocalPath -Leaf)

                    Write-Verbose -Message ($script:localizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $uriScheme, $destinationPath)

                    try
                    {
                        Write-Verbose -Message ($script:localizedData.CreatingTheDestinationCacheFile)
                        $outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' )
                    }
                    catch
                    {
                        # Should never happen since we own the cache directory
                        New-InvalidOperationException -Message ($script:localizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_
                    }

                    try
                    {
                        Write-Verbose -Message ($script:localizedData.CreatingTheSchemeStream -f $uriScheme)
                        $webRequest = [System.Net.WebRequest]::Create($uri)

                        Write-Verbose -Message ($script:localizedData.SettingDefaultCredential)
                        $webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials

                        if ($uriScheme -eq 'http')
                        {
                            # Default value is MutualAuthRequested, which applies to the https scheme
                            Write-Verbose -Message ($script:localizedData.SettingAuthenticationLevel)
                            $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None
                        }
                        elseif ($uriScheme -eq 'https' -and -not [System.String]::IsNullOrEmpty($ServerCertificateValidationCallback))
                        {
                            Write-Verbose -Message 'Assigning user-specified certificate verification callback'
                            $serverCertificateValidationScriptBlock = [System.Management.Automation.ScriptBlock]::Create($ServerCertificateValidationCallback)
                            $webRequest.ServerCertificateValidationCallBack = $serverCertificateValidationScriptBlock
                        }

                        Write-Verbose -Message ($script:localizedData.Gettingtheschemeresponsestream -f $uriScheme)
                        $responseStream = (([System.Net.HttpWebRequest] $webRequest).GetResponse()).GetResponseStream()
                    }
                    catch
                    {
                        Write-Verbose -Message ($script:localizedData.ErrorOutString -f ($_ | Out-String))
                        New-InvalidOperationException -Message ($script:localizedData.CouldNotGetHttpStream -f $uriScheme, $Path) -ErrorRecord $_
                    }

                    try
                    {
                        Write-Verbose -Message ($script:localizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $uriScheme)
                        $responseStream.CopyTo($outStream)
                        $responseStream.Flush()
                        $outStream.Flush()
                    }
                    catch
                    {
                        New-InvalidOperationException -Message ($script:localizedData.ErrorCopyingDataToFile -f $Path, $destinationPath) -ErrorRecord $_
                    }
                }
                finally
                {
                    if ($null -ne $outStream)
                    {
                        $outStream.Close()
                    }

                    if ($null -ne $responseStream)
                    {
                        $responseStream.Close()
                    }
                }

                Write-Verbose -Message ($script:localizedData.RedirectingPackagePathToCacheFileLocation)
                $Path = $destinationPath
                $downloadedFileName = $destinationPath
            }

            # At this point the Path ought to be valid unless it's a MSI uninstall case
            if (-not (Test-Path -Path $Path -PathType 'Leaf'))
            {
                New-InvalidOperationException -Message ($script:localizedData.PathDoesNotExist -f $Path)
            }

            Assert-FileValid -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint
        }

        $startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo'

        # Necessary for I/O redirection and just generally a good idea
        $startInfo.UseShellExecute = $false

        $process = New-Object -TypeName 'System.Diagnostics.Process'
        $process.StartInfo = $startInfo

        # Concept only, will never touch disk
        $errorLogPath = $LogPath + '.err'

        if ($fileExtension -eq '.msi')
        {
            $startInfo.FileName = "$env:winDir\system32\msiexec.exe"

            if ($Ensure -eq 'Present')
            {
                # Check if the MSI package specifies the ProductName and Code
                $productName = Get-MsiProductName -Path $Path
                $productCode = Get-MsiProductCode -Path $Path

                if ((-not [System.String]::IsNullOrEmpty($Name)) -and ($productName -ne $Name))
                {
                    New-InvalidArgumentException -ArgumentName 'Name' -Message ($script:localizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode)
                }

                if ((-not [System.String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode))
                {
                    New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode)
                }

                $startInfo.Arguments = '/i "{0}"' -f $Path
            }
            else
            {
                $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber

                # We may have used the Name earlier, now we need the actual ID
                $id = Split-Path -Path $productEntry.Name -Leaf
                $startInfo.Arguments = '/x{0}' -f $id
            }

            if ($LogPath)
            {
                $startInfo.Arguments += ' /log "{0}"' -f $LogPath
            }

            $startInfo.Arguments += ' /quiet /norestart'

            if ($Arguments)
            {
                # Append any specified arguments with a space (#195)
                $startInfo.Arguments += ' {0}' -f $Arguments
            }
        }
        else
        {
            # EXE
            Write-Verbose -Message $script:localizedData.TheBinaryIsAnExe

            if ($Ensure -eq 'Present')
            {
                $startInfo.FileName = $Path
                $startInfo.Arguments = $Arguments

                if ($LogPath)
                {
                    Write-Verbose -Message ($script:localizedData.UserHasRequestedLoggingNeedToAttachEventHandlersToTheProcess)
                    $startInfo.RedirectStandardError = $true
                    $startInfo.RedirectStandardOutput = $true

                    Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -SourceIdentifier $LogPath
                    Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -SourceIdentifier $errorLogPath
                }
            }
            else
            {
                # Absent case
                $startInfo.FileName = "$env:winDir\system32\msiexec.exe"

                # We may have used the Name earlier, now we need the actual ID
                if ($null -eq $productEntry -or $null -eq $productEntry.Name)
                {
                    $id = $Path
                }
                else
                {
                    $id = Split-Path -Path $productEntry.Name -Leaf
                }

                $startInfo.Arguments = "/x `"$id`" /quiet /norestart"

                if ($LogPath)
                {
                    $startInfo.Arguments += ' /log "{0}"' -f $LogPath
                }

                if ($Arguments)
                {
                    # Append the specified arguments with a space (#195)
                    $startInfo.Arguments += ' {0}' -f $Arguments
                }
            }
        }

        Write-Verbose -Message ($script:localizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments)

        if ($PSCmdlet.ShouldProcess(($script:localizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null))
        {
            try
            {
                [System.Int32] $exitCode = 0
                if ($PSBoundParameters.ContainsKey('RunAsCredential'))
                {
                    $commandLine = '"{0}" {1}' -f $startInfo.FileName, $startInfo.Arguments
                    $exitCode = Invoke-PInvoke -CommandLine $commandLine -Credential $RunAsCredential
                }
                else
                {
                    $process = Invoke-Process -Process $process -LogStream ($null -ne $logStream)
                    $exitCode = $process.ExitCode
                }
            }
            catch
            {
                New-InvalidOperationException -Message ($script:localizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_
            }

            if ($logStream)
            {
                <#
                    We have to re-mux these since they appear to us as different streams
                    the underlying Win32 APIs prevent this problem, as would constructing a script
                    on the fly and executing it, but the former is highly problematic from PowerShell
                    and the latter doesn't let us get the return code for UI-based EXEs
                #>

                $outputEvents = Get-Event -SourceIdentifier $LogPath
                $errorEvents = Get-Event -SourceIdentifier $errorLogPath
                $masterEvents = @() + $outputEvents + $errorEvents
                $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated

                foreach ($event in $masterEvents)
                {
                    $logStream.Write($event.SourceEventArgs.Data);
                }

                Remove-Event -SourceIdentifier $LogPath
                Remove-Event -SourceIdentifier $errorLogPath
            }

            if (-not ($ReturnCode -contains $exitCode))
            {
                # Some .exe files do not support uninstall
                if ($Ensure -eq 'Absent' -and $fileExtension -eq '.exe' -and $exitCode -eq '1620')
                {
                    Write-Warning -Message ($script:localizedData.ExeCouldNotBeUninstalled -f $Path)
                }
                else
                {
                    New-InvalidOperationException ($script:localizedData.UnexpectedReturnCode -f $exitCode.ToString())
                }
            }
        }
    }
    finally
    {
        if ($psDrive)
        {
            Remove-PSDrive -Name $psDrive -Force
        }

        if ($logStream)
        {
            $logStream.Dispose()
        }
    }

    if ($downloadedFileName -and $PSCmdlet.ShouldProcess($script:localizedData.RemoveDownloadedFile, $null, $null))
    {
        <#
            This is deliberately not in the finally block because we want to leave the downloaded
            file on disk if an error occurred as a debugging aid for the user.
        #>

        Remove-Item -Path $downloadedFileName
    }

    $operationMessageString = $script:localizedData.PackageUninstalled
    if ($Ensure -eq 'Present')
    {
        $operationMessageString = $script:localizedData.PackageInstalled
    }

    if ($CreateCheckRegValue)
    {
        $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName
        if ($Ensure -eq 'Present')
        {
            Write-Verbose -Message ($script:localizedData.CreatingRegistryValue -f $registryValueString)
            Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData
        }
        else
        {
            Write-Verbose ($script:localizedData.RemovingRegistryValue -f $registryValueString)
            Remove-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName
        }
    }

    <#
        Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is
        missing on some client SKUs (worked on both Server and Client Skus in Windows 10).
    #>


    $serverFeatureData = Invoke-CimMethod `
        -Name 'GetServerFeature' `
        -Namespace 'root\microsoft\windows\servermanager' `
        -Class 'MSFT_ServerManagerTasks' `
        -Arguments @{
            BatchSize = 256
        } `
        -ErrorAction 'Ignore'
    $registryData = Get-ItemProperty `
        -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' `
        -Name 'PendingFileRenameOperations' `
        -ErrorAction 'Ignore'

    if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641)
    {
        Write-Verbose $script:localizedData.MachineRequiresReboot
        if ($IgnoreReboot)
        {
            Write-Verbose $script:localizedData.IgnoreReboot
        }
        else
        {
            Set-DscMachineRebootRequired
        }
    }
    elseif ($Ensure -eq 'Present')
    {
        $getProductEntryParameters = @{
            Name              = $Name
            IdentifyingNumber = $identifyingNumber
        }

        $checkRegistryValueParameters = @{
            CreateCheckRegValue        = $CreateCheckRegValue
            InstalledCheckRegHive      = $InstalledCheckRegHive
            InstalledCheckRegKey       = $InstalledCheckRegKey
            InstalledCheckRegValueName = $InstalledCheckRegValueName
            InstalledCheckRegValueData = $InstalledCheckRegValueData
        }

        if ($CreateCheckRegValue)
        {
            $getProductEntryParameters += $checkRegistryValueParameters
        }

        $productEntry = Get-ProductEntry @getProductEntryParameters

        if ($null -eq $productEntry)
        {
            New-InvalidOperationException -Message ($script:localizedData.PostValidationError -f $originalPath)
        }
    }

    Write-Verbose -Message $operationMessageString
    Write-Verbose -Message $script:localizedData.PackageConfigurationComplete
}

<#
    .SYNOPSIS
        Tests the current state of the installed package.
 
    .PARAMETER Ensure
        Indicates whether to Ensure that the package is 'Present' or 'Absent'.
        Defaults to 'Present'.
 
    .PARAMETER Name
        The name of the package to be added or removed.
 
    .PARAMETER ProductId
        The identifying number used to uniquely identify this package.
 
    .PARAMETER Path
        The path, URL or UNC path to the package.
 
    .PARAMETER Arguments
        The arguments to be passed to the package during addition or removal.
 
        When installing MSI packages, the '/quiet' and '/norestart' arguments are
        automatically applied.
 
    .PARAMETER IgnoreReboot
        Ignore a pending reboot if requested by package installation. The default
        value is $false and DSC will try to reboot the system.
 
    .PARAMETER Credential
        The credentials to be used for mounting the UNC path (if applicable).
 
    .PARAMETER ReturnCode
        The list of possible valid return codes for this install or removal.
 
    .PARAMETER LogPath
        The path to log the output of the MSI or EXE.
 
    .PARAMETER FileHash
        The expected hash value of the file found in the Path location.
 
    .PARAMETER HashAlgorithm
        The algorithm used to generate the FileHash value. Defaults to 'SHA256'.
 
    .PARAMETER SignerSubject
        The subject that must match the signer certificate of the digital signature.
        Wildcards are allowed.
 
    .PARAMETER SignerThumbprint
        The certificate thumbprint which must match the signer certificate of the
        digital signature.
 
    .PARAMETER ServerCertificateValidationCallback
        PowerShell code used to validate SSL certificates of HTTPS url assigned to Path.
 
    .PARAMETER RunAsCredential
        The credentials under which to run the installation.
 
    .PARAMETER CreateCheckRegValue
        Specifies if a registry value should be created when the packages is installed.
 
    .PARAMETER InstalledCheckRegHive
        The hive in which to create the registry key. Defaults to 'LocalMachine'.
 
    .PARAMETER InstalledCheckRegKey
        The registry key to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueName
        The registry value name to validate the package is installed.
 
    .PARAMETER InstalledCheckRegValueData
        The registry value to validate the package is installed.
#>


function Test-TargetResource
{
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [System.String]
        $ProductId,

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

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

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

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

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

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

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

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

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

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

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

        [Parameter()]
        [ValidateSet('LocalMachine', 'CurrentUser')]
        [System.String]
        $InstalledCheckRegHive = 'LocalMachine',

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

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

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

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

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

    Assert-PathExtensionValid -Path $Path
    $identifyingNumber = $null

    if (-not [System.String]::IsNullOrEmpty($ProductId))
    {
        $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId
    }

    $getProductEntryParameters = @{
        Name              = $Name
        IdentifyingNumber = $identifyingNumber
    }

    $checkRegistryValueParameters = @{
        CreateCheckRegValue        = $CreateCheckRegValue
        InstalledCheckRegHive      = $InstalledCheckRegHive
        InstalledCheckRegKey       = $InstalledCheckRegKey
        InstalledCheckRegValueName = $InstalledCheckRegValueName
        InstalledCheckRegValueData = $InstalledCheckRegValueData
    }

    if ($CreateCheckRegValue)
    {
        Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData
        $getProductEntryParameters += $checkRegistryValueParameters
    }

    $productEntry = Get-ProductEntry @getProductEntryParameters

    Write-Verbose -Message ($script:localizedData.EnsureIsEnsure -f $Ensure)

    if ($null -ne $productEntry)
    {
        Write-Verbose -Message ($script:localizedData.ProductIsProduct -f $productEntry)
    }
    else
    {
        Write-Verbose -Message 'Product installation cannot be determined'
    }

    Write-Verbose -Message ($script:localizedData.ProductAsBooleanIs -f [System.Boolean] $productEntry)

    if ($null -ne $productEntry)
    {
        if ($CreateCheckRegValue)
        {
            Write-Verbose -Message ($script:localizedData.PackageAppearsInstalled -f $Name)
        }
        else
        {
            $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName'
            Write-Verbose -Message ($script:localizedData.PackageAppearsInstalled -f $displayName)
        }
    }
    else
    {
        $displayName = $null

        if (-not [System.String]::IsNullOrEmpty($Name))
        {
            $displayName = $Name
        }
        else
        {
            $displayName = $ProductId
        }

        Write-Verbose -Message ($script:localizedData.PackageDoesNotAppearInstalled -f $displayName)
    }

    return ($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent')
}

<#
    .SYNOPSIS
        Asserts that the path extension is valid - either .msi or .exe.
 
    .PARAMETER Path
        The path to validate the extension of.
#>

function Assert-PathExtensionValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path
    )

    $pathExtension = [System.IO.Path]::GetExtension($Path)
    Write-Verbose -Message ($script:localizedData.ThePathExtensionWasPathExt -f $pathExtension)

    $validPathExtensions = @( '.msi', '.exe' )

    if ($validPathExtensions -notcontains $pathExtension.ToLower())
    {
        New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidBinaryType -f $Path)
    }
}

<#
    .SYNOPSIS
        Converts the given path to a URI.
        Throws an exception if the path's scheme as a URI is not valid.
 
    .PARAMETER Path
        The path to retrieve as a URI.
#>

function Convert-PathToUri
{
    [OutputType([System.Uri])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path
    )

    try
    {
        $uri = [System.Uri] $Path
    }
    catch
    {
        New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path)
    }

    $validUriSchemes = @( 'file', 'http', 'https' )

    if ($validUriSchemes -notcontains $uri.Scheme)
    {
        Write-Verbose -Message ($script:localizedData.TheUriSchemeWasUriScheme -f $uri.Scheme)
        New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.InvalidPath -f $Path)
    }

    return $uri
}

<#
    .SYNOPSIS
        Retrieves the product ID as an identifying number.
 
    .PARAMETER ProductId
        The product id to retrieve as an identifying number.
#>

function Convert-ProductIdToIdentifyingNumber
{
    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ProductId
    )

    try
    {
        Write-Verbose -Message ($script:localizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId)
        $identifyingNumber = '{{{0}}}' -f [System.Guid]::Parse($ProductId).ToString().ToUpper()

        Write-Verbose -Message ($script:localizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber)
        return $identifyingNumber
    }
    catch
    {
        New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($script:localizedData.InvalidIdentifyingNumber -f $ProductId)
    }
}

<#
    .SYNOPSIS
        Asserts that the InstalledCheckRegKey, InstalledCheckRegValueName, and
        InstalledCheckRegValueData parameter required for retrieving package installation status
        from a registry are not null or empty.
 
    .PARAMETER InstalledCheckRegKey
        The InstalledCheckRegKey parameter to check.
 
    .PARAMETER InstalledCheckRegValueName
        The InstalledCheckRegValueName parameter to check.
 
    .PARAMETER InstalledCheckRegValueData
        The InstalledCheckRegValueData parameter to check.
 
    .NOTES
        This could be done with parameter validation.
        It is implemented this way to provide a clearer error message.
#>

function Assert-RegistryParametersValid
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $InstalledCheckRegKey,

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

        [Parameter()]
        [System.String]
        $InstalledCheckRegValueData
    )

    foreach ($parameter in $PSBoundParameters.Keys)
    {
        if ([System.String]::IsNullOrEmpty($PSBoundParameters[$parameter]))
        {
            New-InvalidArgumentException -ArgumentName $parameter -Message ($script:localizedData.ProvideParameterForRegistryCheck -f $parameter)
        }
    }
}

<#
    .SYNOPSIS
        Retrieves the product entry for the package with the given name and/or identifying number.
 
    .PARAMETER Name
        The name of the product entry to retrieve.
 
    .PARAMETER CreateCheckRegValue
        Indicates whether or not to retrieve the package installation status from a registry.
 
    .PARAMETER IdentifyingNumber
        The identifying number of the product entry to retrieve.
 
    .PARAMETER InstalledCheckRegHive
        The registry hive to check for package installation status.
 
    .PARAMETER InstalledCheckRegKey
        The registry key to open to check for package installation status.
 
    .PARAMETER InstalledCheckRegValueName
        The registry value name to check for package installation status.
 
    .PARAMETER InstalledCheckRegValueData
        The value to compare against the retrieved registry value to check for package installation.
#>

function Get-ProductEntry
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $Name,

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

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $CreateCheckRegValue,

        [Parameter()]
        [ValidateSet('LocalMachine', 'CurrentUser')]
        [System.String]
        $InstalledCheckRegHive = 'LocalMachine',

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

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

        [Parameter()]
        [System.String]
        $InstalledCheckRegValueData
    )

    $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
    $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'

    $productEntry = $null

    if (-not [System.String]::IsNullOrEmpty($IdentifyingNumber))
    {
        $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber
        $productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue'

        if ($null -eq $productEntry)
        {
            $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber
            $productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue'
        }
    }
    else
    {
        foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' ))
        {
            if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName'))
            {
                $productEntry = $registryKeyEntry
                break
            }
        }
    }

    if ($null -eq $productEntry)
    {
        if ($CreateCheckRegValue)
        {
            $installValue = $null

            $win32OperatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue'

            # If 64-bit OS, check 64-bit registry view first
            if ($win32OperatingSystem.OSArchitecture -ieq '64-bit')
            {
                $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry64'
            }

            if ($null -eq $installValue)
            {
                $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry32'
            }

            if ($null -ne $installValue)
            {
                if ($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData)
                {
                    $productEntry = @{
                        Installed = $true
                    }
                }
            }
        }
    }

    return $productEntry
}

<#
    .SYNOPSIS
        Retrieves a value from a registry without throwing errors.
 
    .PARAMETER Key
        The key of the registry to get the value from.
 
    .PARAMETER Value
        The name of the value to retrieve.
 
    .PARAMETER RegistryHive
        The registry hive to retrieve the value from.
 
    .PARAMETER RegistyView
        The registry view to retrieve the value from.
#>

function Get-RegistryValueWithErrorsIgnored
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Key,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Value,

        [Parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive,

        [Parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryView]
        $RegistryView
    )

    $registryValue = $null

    try
    {
        $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView)
        $subRegistryKey = $baseRegistryKey.OpenSubKey($Key)

        if ($null -ne $subRegistryKey)
        {
            $registryValue = $subRegistryKey.GetValue($Value)
        }
    }
    catch
    {
        $exceptionText = ($_ | Out-String).Trim()
        Write-Verbose -Message "An exception occured while attempting to retrieve a registry value: $exceptionText"
    }

    return $registryValue
}

<#
    .SYNOPSIS
        Retrieves a localized registry key value.
 
    .PARAMETER RegistryKey
        The registry key to retrieve the value from.
 
    .PARAMETER ValueName
        The name of the value to retrieve.
#>

function Get-LocalizedRegistryKeyValue
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.Object]
        $RegistryKey,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ValueName
    )

    $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName)

    if ($null -eq $localizedRegistryKeyValue)
    {
        $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName)
    }

    return $localizedRegistryKeyValue
}

<#
    .SYNOPSIS
        Asserts that the file at the given path is valid.
 
    .PARAMETER Path
        The path to the file to check.
 
    .PARAMETER FileHash
        The hash that should match the hash of the file.
 
    .PARAMETER HashAlgorithm
        The algorithm to use to retrieve the file hash.
 
    .PARAMETER SignerThumbprint
        The certificate thumbprint that should match the file's signer certificate.
 
    .PARAMETER SignerSubject
        The certificate subject that should match the file's signer certificate.
#>

function Assert-FileValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

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

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

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

        [Parameter()]
        [System.String]
        $SignerSubject
    )

    if (-not [System.String]::IsNullOrEmpty($FileHash))
    {
        Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm
    }

    if (-not [System.String]::IsNullOrEmpty($SignerThumbprint) -or -not [System.String]::IsNullOrEmpty($SignerSubject))
    {
        Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject
    }
}

<#
    .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.
#>

function Assert-FileHashValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Hash,

        [Parameter()]
        [System.String]
        $Algorithm = 'SHA256'
    )

    if ([System.String]::IsNullOrEmpty($Algorithm))
    {
        $Algorithm = 'SHA256'
    }

    Write-Verbose -Message ($script:localizedData.CheckingFileHash -f $Path, $Hash, $Algorithm)

    $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction 'Stop'

    if ($fileHash.Hash -ne $Hash)
    {
        throw ($script:localizedData.InvalidFileHash -f $Path, $Hash, $Algorithm)
    }
}

<#
    .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.
#>

function Assert-FileSignatureValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

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

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

    Write-Verbose -Message ($script:localizedData.CheckingFileSignature -f $Path)

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

    if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid)
    {
        throw ($script:localizedData.InvalidFileSignature -f $Path, $signature.Status)
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject)
    }

    if ($null -ne $Subject -and ($signature.SignerCertificate.Subject -notlike $Subject))
    {
        throw ($script:localizedData.WrongSignerSubject -f $Path, $Subject)
    }

    if ($null -ne $Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint))
    {
        throw ($script:localizedData.WrongSignerThumbprint -f $Path, $Thumbprint)
    }
}

<#
    .SYNOPSIS
        Retrieves the name of a product from an msi.
 
    .PARAMETER Path
        The path to the msi to retrieve the name from.
#>

function Get-MsiProductName
{
    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path
    )

    $msiTools = Get-MsiTool

    $productName = $msiTools::GetProductName($Path)

    return $productName
}

<#
    .SYNOPSIS
        Retrieves the code of a product from an msi.
 
    .PARAMETER Path
        The path to the msi to retrieve the code from.
#>

function Get-MsiProductCode
{
    [OutputType([System.String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Path
    )

    $msiTools = Get-MsiTool

    $productCode = $msiTools::GetProductCode($Path)

    return $productCode
}

<#
    .SYNOPSIS
        Retrieves the MSI tools type.
#>

function Get-MsiTool
{
    [OutputType([System.Type])]
    [CmdletBinding()]
    param ()

    if ($null -ne $script:msiTools)
    {
        return $script:msiTools
    }

    $msiToolsCodeDefinition = @'
    [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
    private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct);
 
    [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
    private static extern uint MsiCloseHandle(IntPtr hAny);
 
    [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
    private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength);
 
    private static string GetPackageProperty(string msi, string property)
    {
        IntPtr MsiHandle = IntPtr.Zero;
        try
        {
            var res = MsiOpenPackageExW(msi, 1, out MsiHandle);
            if (res != 0)
            {
                return null;
            }
 
            int length = 256;
            var buffer = new StringBuilder(length);
            res = MsiGetPropertyW(MsiHandle, property, buffer, ref length);
            return buffer.ToString();
        }
        finally
        {
            if (MsiHandle != IntPtr.Zero)
            {
                MsiCloseHandle(MsiHandle);
            }
        }
    }
    public static string GetProductCode(string msi)
    {
        return GetPackageProperty(msi, "ProductCode");
    }
 
    public static string GetProductName(string msi)
    {
        return GetPackageProperty(msi, "ProductName");
    }
'@


    if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type)
    {
        $script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type
    }
    else
    {
        $script:msiTools = Add-Type `
            -Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' `
            -Name 'MsiTools' `
            -Using 'System.Text' `
            -MemberDefinition $msiToolsCodeDefinition `
            -PassThru
    }

    return $script:msiTools
}

<#
    .SYNOPSIS
        Runs a process as the specified user via PInvoke.
 
    .PARAMETER CommandLine
        The command line (including arguments) of the process to start.
 
    .PARAMETER Credential
        The user credential to start the process as.
#>

function Invoke-PInvoke
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $CommandLine,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )

    Register-PInvoke
    [System.Int32] $exitCode = 0

    [Source.NativeMethods]::CreateProcessAsUser($CommandLine, `
            $Credential.GetNetworkCredential().Domain, `
            $Credential.GetNetworkCredential().UserName, `
            $Credential.GetNetworkCredential().Password, `
            [ref] $exitCode
    )

    return $exitCode;
}

<#
    .SYNOPSIS
        Starts and waits for a process.
 
    .DESCRIPTION
        Allows mocking and testing of process arguments.
 
    .PARAMETER Process
        The System.Diagnositics.Process object to start.
 
    .PARAMETER LogStream
        Redirect STDOUT and STDERR output.
#>

function Invoke-Process
{
    [CmdletBinding()]
    [OutputType([System.Diagnostics.Process])]
    param (
        [Parameter(Mandatory = $true)]
        [System.Diagnostics.Process]
        $Process,

        [Parameter()]
        [System.Boolean]
        $LogStream
    )

    $Process.Start() | Out-Null

    if ($LogStream)
    {
        $Process.BeginOutputReadLine()
        $Process.BeginErrorReadLine()
    }

    $Process.WaitForExit()
    return $Process
}

<#
    .SYNOPSIS
        Sets the value of a registry key to the specified data.
 
    .PARAMETER Key
        The registry key that contains the value to set.
 
    .PARAMETER Value
        The value name of the registry key value to set.
 
    .PARAMETER RegistryHive
        The registry hive that contains the registry key to set.
 
    .PARAMETER Data
        The data to set the registry key value to.
#>

function Set-RegistryValue
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Key,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Value,

        [Parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Data
    )

    try
    {
        $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default)

        # Opens the subkey with write access
        $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true)

        if ($null -eq $subRegistryKey)
        {
            Write-Verbose "Key: '$Key'"
            $subRegistryKey = $baseRegistryKey.CreateSubKey($Key)
        }

        $subRegistryKey.SetValue($Value, $Data)
        $subRegistryKey.Close()
    }
    catch
    {
        New-InvalidOperationException -Message ($script:localizedData.ErrorSettingRegistryValue -f $Key, $Value, $Data) -ErrorRecord $_
    }
}

<#
    .SYNOPSIS
        Removes the specified value of a registry key.
 
    .PARAMETER Key
        The registry key that contains the value to remove.
 
    .PARAMETER Value
        The value name of the registry key value to remove.
 
    .PARAMETER RegistryHive
        The registry hive that contains the registry key to remove.
#>

function Remove-RegistryValue
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Key,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Value,

        [Parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive
    )

    try
    {
        $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default)

        $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true)
        $subRegistryKey.DeleteValue($Value)
        $subRegistryKey.Close()
    }
    catch
    {
        New-InvalidOperationException -Message ($script:localizedData.ErrorRemovingRegistryValue -f $Key, $Value) -ErrorRecord $_
    }
}

<#
    .SYNOPSIS
        Registers PInvoke to run a process as a user.
#>

function Register-PInvoke
{
    $programSource = @'
        using System;
        using System.Collections.Generic;
        using System.Text;
        using System.Security;
        using System.Runtime.InteropServices;
        using System.Diagnostics;
        using System.Security.Principal;
        using System.ComponentModel;
        using System.IO;
 
        namespace Source
        {
            [SuppressUnmanagedCodeSecurity]
            public static class NativeMethods
            {
                //The following structs and enums are used by the various Win32 API's that are used in the code below
 
                [StructLayout(LayoutKind.Sequential)]
                public struct STARTUPINFO
                {
                    public Int32 cb;
                    public string lpReserved;
                    public string lpDesktop;
                    public string lpTitle;
                    public Int32 dwX;
                    public Int32 dwY;
                    public Int32 dwXSize;
                    public Int32 dwXCountChars;
                    public Int32 dwYCountChars;
                    public Int32 dwFillAttribute;
                    public Int32 dwFlags;
                    public Int16 wShowWindow;
                    public Int16 cbReserved2;
                    public IntPtr lpReserved2;
                    public IntPtr hStdInput;
                    public IntPtr hStdOutput;
                    public IntPtr hStdError;
                }
 
                [StructLayout(LayoutKind.Sequential)]
                public struct PROCESS_INFORMATION
                {
                    public IntPtr hProcess;
                    public IntPtr hThread;
                    public Int32 dwProcessID;
                    public Int32 dwThreadID;
                }
 
                [Flags]
                public enum LogonType
                {
                    LOGON32_LOGON_INTERACTIVE = 2,
                    LOGON32_LOGON_NETWORK = 3,
                    LOGON32_LOGON_BATCH = 4,
                    LOGON32_LOGON_SERVICE = 5,
                    LOGON32_LOGON_UNLOCK = 7,
                    LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
                    LOGON32_LOGON_NEW_CREDENTIALS = 9
                }
 
                [Flags]
                public enum LogonProvider
                {
                    LOGON32_PROVIDER_DEFAULT = 0,
                    LOGON32_PROVIDER_WINNT35,
                    LOGON32_PROVIDER_WINNT40,
                    LOGON32_PROVIDER_WINNT50
                }
                [StructLayout(LayoutKind.Sequential)]
                public struct SECURITY_ATTRIBUTES
                {
                    public Int32 Length;
                    public IntPtr lpSecurityDescriptor;
                    public bool bInheritHandle;
                }
 
                public enum SECURITY_IMPERSONATION_LEVEL
                {
                    SecurityAnonymous,
                    SecurityIdentification,
                    SecurityImpersonation,
                    SecurityDelegation
                }
 
                public enum TOKEN_TYPE
                {
                    TokenPrimary = 1,
                    TokenImpersonation
                }
 
                [StructLayout(LayoutKind.Sequential, Pack = 1)]
                internal struct TokPriv1Luid
                {
                    public int Count;
                    public long Luid;
                    public int Attr;
                }
 
                public const int GENERIC_ALL_ACCESS = 0x10000000;
                public const int CREATE_NO_WINDOW = 0x08000000;
                internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
                internal const int TOKEN_QUERY = 0x00000008;
                internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
                internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege";
 
                [DllImport("kernel32.dll",
                    EntryPoint = "CloseHandle", SetLastError = true,
                    CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
                public static extern bool CloseHandle(IntPtr handle);
 
                [DllImport("advapi32.dll",
                    EntryPoint = "CreateProcessAsUser", SetLastError = true,
                    CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
                public static extern bool CreateProcessAsUser(
                    IntPtr hToken,
                    string lpApplicationName,
                    string lpCommandLine,
                    ref SECURITY_ATTRIBUTES lpProcessAttributes,
                    ref SECURITY_ATTRIBUTES lpThreadAttributes,
                    bool bInheritHandle,
                    Int32 dwCreationFlags,
                    IntPtr lpEnvrionment,
                    string lpCurrentDirectory,
                    ref STARTUPINFO lpStartupInfo,
                    ref PROCESS_INFORMATION lpProcessInformation
                    );
 
                [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
                public static extern bool DuplicateTokenEx(
                    IntPtr hExistingToken,
                    Int32 dwDesiredAccess,
                    ref SECURITY_ATTRIBUTES lpThreadAttributes,
                    Int32 ImpersonationLevel,
                    Int32 dwTokenType,
                    ref IntPtr phNewToken
                    );
 
                [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
                public static extern Boolean LogonUser(
                    String lpszUserName,
                    String lpszDomain,
                    String lpszPassword,
                    LogonType dwLogonType,
                    LogonProvider dwLogonProvider,
                    out IntPtr phToken
                    );
 
                [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
                internal static extern bool AdjustTokenPrivileges(
                    IntPtr htok,
                    bool disall,
                    ref TokPriv1Luid newst,
                    int len,
                    IntPtr prev,
                    IntPtr relen
                    );
 
                [DllImport("kernel32.dll", ExactSpelling = true)]
                internal static extern IntPtr GetCurrentProcess();
 
                [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
                internal static extern bool OpenProcessToken(
                    IntPtr h,
                    int acc,
                    ref IntPtr phtok
                    );
 
                [DllImport("kernel32.dll", ExactSpelling = true)]
                internal static extern int WaitForSingleObject(
                    IntPtr h,
                    int milliseconds
                    );
 
                [DllImport("kernel32.dll", ExactSpelling = true)]
                internal static extern bool GetExitCodeProcess(
                    IntPtr h,
                    out int exitcode
                    );
 
                [DllImport("advapi32.dll", SetLastError = true)]
                internal static extern bool LookupPrivilegeValue(
                    string host,
                    string name,
                    ref long pluid
                    );
 
                public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode )
                {
                    var hToken = IntPtr.Zero;
                    var hDupedToken = IntPtr.Zero;
                    TokPriv1Luid tp;
                    var pi = new PROCESS_INFORMATION();
                    var sa = new SECURITY_ATTRIBUTES();
                    sa.Length = Marshal.SizeOf(sa);
                    Boolean bResult = false;
                    try
                    {
                        bResult = LogonUser(
                            strName,
                            strDomain,
                            strPassword,
                            LogonType.LOGON32_LOGON_BATCH,
                            LogonProvider.LOGON32_PROVIDER_DEFAULT,
                            out hToken
                            );
                        if (!bResult)
                        {
                            throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString());
                        }
                        IntPtr hproc = GetCurrentProcess();
                        IntPtr htok = IntPtr.Zero;
                        bResult = OpenProcessToken(
                                hproc,
                                TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
                                ref htok
                            );
                        if(!bResult)
                        {
                            throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString());
                        }
                        tp.Count = 1;
                        tp.Luid = 0;
                        tp.Attr = SE_PRIVILEGE_ENABLED;
                        bResult = LookupPrivilegeValue(
                            null,
                            SE_INCRASE_QUOTA,
                            ref tp.Luid
                            );
                        if(!bResult)
                        {
                            throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString());
                        }
                        bResult = AdjustTokenPrivileges(
                            htok,
                            false,
                            ref tp,
                            0,
                            IntPtr.Zero,
                            IntPtr.Zero
                            );
                        if(!bResult)
                        {
                            throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString());
                        }
 
                        bResult = DuplicateTokenEx(
                            hToken,
                            GENERIC_ALL_ACCESS,
                            ref sa,
                            (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                            (int)TOKEN_TYPE.TokenPrimary,
                            ref hDupedToken
                            );
                        if(!bResult)
                        {
                            throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString());
                        }
                        var si = new STARTUPINFO();
                        si.cb = Marshal.SizeOf(si);
                        si.lpDesktop = "";
                        bResult = CreateProcessAsUser(
                            hDupedToken,
                            null,
                            strCommand,
                            ref sa,
                            ref sa,
                            false,
                            0,
                            IntPtr.Zero,
                            null,
                            ref si,
                            ref pi
                            );
                        if(!bResult)
                        {
                            throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString());
                        }
 
                        int status = WaitForSingleObject(pi.hProcess, -1);
                        if(status == -1)
                        {
                            throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString());
                        }
 
                        bResult = GetExitCodeProcess(pi.hProcess, out ExitCode);
                        if(!bResult)
                        {
                            throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString());
                        }
                    }
                    finally
                    {
                        if (pi.hThread != IntPtr.Zero)
                        {
                            CloseHandle(pi.hThread);
                        }
                        if (pi.hProcess != IntPtr.Zero)
                        {
                            CloseHandle(pi.hProcess);
                        }
                        if (hDupedToken != IntPtr.Zero)
                        {
                            CloseHandle(hDupedToken);
                        }
                    }
                }
            }
        }
'@

    Add-Type -TypeDefinition $programSource -ReferencedAssemblies 'System.ServiceProcess'
}

Export-ModuleMember -Function *-TargetResource