DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1

data LocalizedData
{
    # culture="en-US"
    # TODO: Support WhatIf
    ConvertFrom-StringData @'
InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid
InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP
InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file
NeedsMoreInfo=Either Name or ProductId is required
InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported
CouldNotOpenLog=The specified LogPath ({0}) could not be opened
CouldNotStartProcess=The process {0} could not be started
UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct
PathDoesNotExist=The given Path ({0}) could not be found
CouldNotOpenDestFile=Could not open the file {0} for writing
CouldNotGetHttpStream=Could not get the {0} stream for file {1}
ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1}
PackageConfigurationComplete=Package configuration finished
PackageConfigurationStarting=Package configuration starting
InstalledPackage=Installed package
UninstalledPackage=Uninstalled package
NoChangeRequired=Package found in desired state, no action required
RemoveExistingLogFile=Remove existing log file
CreateLogFile=Create log file
MountSharePath=Mount share to get media
DownloadHTTPFile=Download the media over HTTP or HTTPS
StartingProcessMessage=Starting process {0} with arguments {1}
RemoveDownloadedFile=Remove the downloaded file
PackageInstalled=Package has been installed
PackageUninstalled=Package has been uninstalled
MachineRequiresReboot=The machine requires a reboot
PackageDoesNotAppearInstalled=The package {0} is not installed
PackageAppearsInstalled=The package {0} is already installed
PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details
CheckingFileHash=Checking file '{0}' for expected {2} hash value of {1}
InvalidFileHash=File '{0}' does not match expected {2} hash value of {1}.
CheckingFileSignature=Checking file '{0}' for valid digital signature.
FileHasValidSignature=File '{0}' contains a valid digital signature. Signer Thumbprint: {1}, Subject: {2}
InvalidFileSignature=File '{0}' does not have a valid Authenticode signature. Status: {1}
WrongSignerSubject=File '{0}' was not signed by expected signer subject '{1}'
WrongSignerThumbprint=File '{0}' was not signed by expected signer certificate thumbprint '{1}'
CreatingRegistryValue=Creating package registry value of {0}.
'@

}

$Debug = $true
Function Trace-Message
{
    param([string] $Message)
    if($Debug)
    {
        Write-Verbose $Message
    }
}

$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource"

Function Throw-InvalidArgumentException
{
    param(
        [string] $Message,
        [string] $ParamName
    )

    $exception = new-object System.ArgumentException $Message,$ParamName
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null
    throw $errorRecord
}

Function Throw-InvalidNameOrIdException
{
    param(
        [string] $Message
    )

    $exception = new-object System.ArgumentException $Message
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null
    throw $errorRecord
}

Function Throw-TerminatingError
{
    param(
        [string] $Message,
        [System.Management.Automation.ErrorRecord] $ErrorRecord
    )

    $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
    $errorRecord = New-Object System.Management.Automation.ErrorRecord ($exception.ToString()),"MachineStateIncorrect","InvalidOperation",$null
    throw $errorRecord
}

Function Set-RegistryValue
{
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Key,

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

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

    try
    {
        $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default)
        $subKey =  $baseKey.OpenSubKey($Key, $true) ## Opens the subkey with write access
        if($subKey -eq $null)
        {
            $subKey = $baseKey.CreateSubKey($Key)
        }
        $subKey.SetValue($Value, $Data)
    }
    catch
    {
        $exceptionText = ($_ | Out-String).Trim()
        Write-Verbose "Exception occured in Set-RegistryValue: $exceptionText"
    }
}

Function Get-RegistryValueIgnoreError
{
    param
    (
        [parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive,

        [parameter(Mandatory = $true)]
        [System.String]
        $Key,

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

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

    try
    {
        $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView)
        $subKey =  $baseKey.OpenSubKey($Key)
        if($subKey -ne $null)
        {
            return $subKey.GetValue($Value)
        }
    }
    catch
    {
        $exceptionText = ($_ | Out-String).Trim()
        Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText"
    }
    return $null
}

Function Validate-StandardArguments
{
    param(
        $Path,
        $ProductId,
        $Name
    )

    Trace-Message "Validate-StandardArguments, Path was $Path"
    $uri = $null
    try
    {
        $uri = [uri] $Path
    }
    catch
    {
        Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path"
    }

    if(-not @("file", "http", "https") -contains $uri.Scheme)
    {
        Trace-Message "The uri scheme was $uri.Scheme"
        Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path"
    }

    $pathExt = [System.IO.Path]::GetExtension($Path)
    Trace-Message "The path extension was $pathExt"
    if(-not @(".msi",".exe") -contains $pathExt.ToLower())
    {
        Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path"
    }

    $identifyingNumber = $null
    if(-not $Name -and -not $ProductId)
    {
        #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version
        Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId"
    }
    elseif($ProductId)
    {
        try
        {
            Trace-Message "Parsing $ProductId as an identifyingNumber"
            $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper()
            Trace-Message "Parsed $ProductId as $identifyingNumber"
        }
        catch
        {
            Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId
        }
    }

    return $uri, $identifyingNumber
}

Function Get-ProductEntry
{
    param
    (
        [string] $Name,
        [string] $IdentifyingNumber,
        [string] $InstalledCheckRegHive = 'LocalMachine',
        [string] $InstalledCheckRegKey,
        [string] $InstalledCheckRegValueName,
        [string] $InstalledCheckRegValueData
    )

    $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
    $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"

    if($IdentifyingNumber)
    {
        $keyLocation = "$uninstallKey\$identifyingNumber"
        $item = Get-Item $keyLocation -EA SilentlyContinue
        if(-not $item)
        {
            $keyLocation = "$uninstallKeyWow64\$identifyingNumber"
            $item = Get-Item $keyLocation -EA SilentlyContinue
        }

        return $item
    }

    foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64))
    {
        if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName"))
        {
            return $item
        }
    }

    if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData)
    {
        $installValue = $null

        #if 64bit OS, check 64bit registry view first
        if ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName "localhost" -ea 0).OSArchitecture -eq '64-bit')
        {
            $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64
        }

        if($installValue -eq $null)
        {
            $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32
        }

        if($installValue)
        {
            if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData)
            {
                return @{
                    Installed = $true
                }
            }
        }
    }

    return $null
}

function Test-TargetResource
{
    [OutputType([System.Boolean])]
    param
    (
        [ValidateSet("Present", "Absent")]
        [string] $Ensure = "Present",

        [parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string] $Name,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string] $ProductId,

        [string] $Arguments,

        [pscredential] $Credential,

        [System.UInt32[]] $ReturnCode,

        [string] $LogPath,

        [pscredential] $RunAsCredential,

        [ValidateSet('LocalMachine','CurrentUser')]
        [string] $InstalledCheckRegHive = 'LocalMachine',

        [string] $InstalledCheckRegKey,

        [string] $InstalledCheckRegValueName,

        [string] $InstalledCheckRegValueData,

        [boolean] $CreateCheckRegValue,

        [string] $FileHash,

        [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')]
        [string] $HashAlgorithm,

        [string] $SignerSubject,
        [string] $SignerThumbprint,

        [string] $ServerCertificateValidationCallback
    )

    $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name
    $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData
    Trace-Message "Ensure is $Ensure"
    if($product)
    {
        Trace-Message "product found"
    }
    else
    {
        Trace-Message "product installation cannot be determined"
    }
    Trace-Message ("product as boolean is {0}" -f [boolean]$product)
    $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent")

    # install registry test overrides the product id test and there is no true product information
    # when doing a lookup via registry key
    if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData)
    {
        Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name)
    }
    else
    {
        if ($product -ne $null)
        {
            $name = Get-LocalizableRegKeyValue $product "DisplayName"
            Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name)
        }
        else
        {
            $displayName = $null
            if($Name)
            {
                $displayName = $Name
            }
            else
            {
                $displayName = $ProductId
            }

            Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName)
        }

    }

    return $res
}

function Get-LocalizableRegKeyValue
{
    param(
        [object] $RegKey,
        [string] $ValueName
    )

    $res = $RegKey.GetValue("{0}_Localized" -f $ValueName)
    if(-not $res)
    {
        $res = $RegKey.GetValue($ValueName)
    }

    return $res
}

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

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string] $ProductId,

        [ValidateSet('LocalMachine','CurrentUser')]
        [string] $InstalledCheckRegHive = 'LocalMachine',

        [string] $InstalledCheckRegKey,

        [string] $InstalledCheckRegValueName,

        [string] $InstalledCheckRegValueData,

        [boolean] $CreateCheckRegValue        
    )

    #If the user gave the ProductId then we derive $identifyingNumber
    $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name

    $localMsi = $uri.IsFile -and -not $uri.IsUnc

    $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData

    if(-not $product)
    {
        return @{
            Ensure = "Absent"
            Name = $Name
            ProductId = $identifyingNumber
            Installed = $false
            InstalledCheckRegHive = $InstalledCheckRegHive
            InstalledCheckRegKey = $InstalledCheckRegKey
            InstalledCheckRegValueName = $InstalledCheckRegValueName
            InstalledCheckRegValueData = $InstalledCheckRegValueData
        }
    }

    if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData)
    {
        return @{
            Ensure = "Present"
            Name = $Name
            ProductId = $identifyingNumber
            Installed = $true
            InstalledCheckRegHive = $InstalledCheckRegHive
            InstalledCheckRegKey = $InstalledCheckRegKey
            InstalledCheckRegValueName = $InstalledCheckRegValueName
            InstalledCheckRegValueData = $InstalledCheckRegValueData
        }
    }

    #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE)
    #If the user gave a ProductId just pass it through, otherwise fill it from the product
    if(-not $identifyingNumber)
    {
        $identifyingNumber = Split-Path -Leaf $product.Name
    }

    $date = $product.GetValue("InstallDate")
    if($date)
    {
        try
        {
            $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date
        }
        catch
        {
            $date = $null
        }
    }

    $publisher = Get-LocalizableRegKeyValue $product "Publisher"
    $size = $product.GetValue("EstimatedSize")
    if($size)
    {
        $size = $size/1024
    }

    $version = $product.GetValue("DisplayVersion")
    $description = $product.GetValue("Comments")
    $name = Get-LocalizableRegKeyValue $product "DisplayName"
    return @{
        Ensure = "Present"
        Name = $name
        Path = $Path
        InstalledOn = $date
        ProductId = $identifyingNumber
        Size = $size
        Installed = $true
        Version = $version
        PackageDescription = $description
        Publisher = $publisher
    }
}

Function Get-MsiTools
{
    if($script:MsiTools)
    {
        return $script:MsiTools
    }

    $sig = @'
    [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
    private static extern UInt32 MsiOpenPackageW(string szPackagePath, 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 = MsiOpenPackageW(msi, 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");
    }
'@

    $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.xPackageResource `
        -Name MsiTools -Using System.Text -MemberDefinition $sig
    return $script:MsiTools
}


Function Get-MsiProductEntry
{
    param
    (
        [string] $Path
    )

    if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi"))
    {
        Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path)
    }

    $tools = Get-MsiTools

    $pn = $tools::GetProductName($Path)

    $pc = $tools::GetProductCode($Path)

    return $pn,$pc
}


function Set-TargetResource
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [ValidateSet("Present", "Absent")]
        [string] $Ensure = "Present",

        [parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string] $Name,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string] $ProductId,

        [string] $Arguments,

        [pscredential] $Credential,

        [System.UInt32[]] $ReturnCode,

        [string] $LogPath,

        [pscredential] $RunAsCredential,

        [ValidateSet('LocalMachine','CurrentUser')]
        [string] $InstalledCheckRegHive = 'LocalMachine',

        [string] $InstalledCheckRegKey,

        [string] $InstalledCheckRegValueName,

        [string] $InstalledCheckRegValueData,
        
        [boolean] $CreateCheckRegValue,

        [string] $FileHash,

        [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')]
        [string] $HashAlgorithm,

        [string] $SignerSubject,
        [string] $SignerThumbprint,

        [string] $ServerCertificateValidationCallback
    )

    $ErrorActionPreference = "Stop"

    if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId `
        -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName `
        -InstalledCheckRegValueData $InstalledCheckRegValueData))
    {
        return
    }

    $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name

    #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 message
    $OrigPath = $Path

    Write-Verbose $LocalizedData.PackageConfigurationStarting
    if(-not $ReturnCode)
    {
        $ReturnCode = @(0)
    }

    $logStream = $null
    $psdrive = $null
    $downloadedFileName = $null
    try
    {
        $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower()
        if($LogPath)
        {
            try
            {
                if($fileExtension -eq ".msi")
                {
                    #We want to pre-verify the path exists and is writable ahead of time
                    #even in the MSI case, as detecting WHY the MSI log doesn't exist would
                    #be rather problematic for the user
                    if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null))
                    {
                        rm $LogPath
                    }

                    if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
                    {
                        New-Item -Type File $LogPath | Out-Null
                    }
                }
                elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
                {
                    $logStream = new-object "System.IO.StreamWriter" $LogPath,$false
                }
            }
            catch
            {
                Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_
            }
        }

        #Download or mount file as necessary
        if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent"))
        {
            if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null))
            {
                $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)}
                if($Credential)
                {
                    #We need to optionally include these and then splat the hash otherwise
                    #we pass a null for Credential which causes the cmdlet to pop a dialog up
                    $psdriveArgs["Credential"] = $Credential
                }

                $psdrive = New-PSDrive @psdriveArgs
                $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary?
            }
            elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null))
            {
                $scheme = $uri.Scheme
                $outStream = $null
                $responseStream = $null

                try
                {
                    Trace-Message "Creating cache location"

                    if(-not (Test-Path -PathType Container $CacheLocation))
                    {
                        mkdir $CacheLocation | Out-Null
                    }

                    $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath)

                    Trace-Message "Need to download file from $scheme, destination will be $destName"

                    try
                    {
                        Trace-Message "Creating the destination cache file"
                        $outStream = New-Object System.IO.FileStream $destName, "Create"
                    }
                    catch
                    {
                        #Should never happen since we own the cache directory
                        Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_
                    }

                    try
                    {
                        Trace-Message "Creating the $scheme stream"
                        $request = [System.Net.WebRequest]::Create($uri)
                        Trace-Message "Setting default credential"
                        $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials
                        if ($scheme -eq "http")
                        {
                            Trace-Message "Setting authentication level"
                            # default value is MutualAuthRequested, which applies to https scheme
                            $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None
                        }
                        if ($scheme -eq "https" -and -not [string]::IsNullOrEmpty($ServerCertificateValidationCallback))
                        {
                            Trace-Message "Assigning user-specified certificate verification callback"
                            $scriptBlock = [scriptblock]::Create($ServerCertificateValidationCallback)
                            $request.ServerCertificateValidationCallBack = $scriptBlock
                        }
                        Trace-Message "Getting the $scheme response stream"
                        $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream()
                    }
                    catch
                    {
                         Trace-Message ("Error: " + ($_ | Out-String))
                         Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_
                    }

                    try
                    {
                        Trace-Message "Copying the $scheme stream bytes to the disk cache"
                        $responseStream.CopyTo($outStream)
                        $responseStream.Flush()
                        $outStream.Flush()
                    }
                    catch
                    {
                        Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_
                    }
                }
                finally
                {
                    if($outStream)
                    {
                        $outStream.Close()
                    }

                    if($responseStream)
                    {
                        $responseStream.Close()
                    }
                }
                Trace-Message "Redirecting package path to cache file location"
                $Path = $downloadedFileName = $destName
            }
        }

        if (-not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi"))
        {
            #At this point the Path ought to be valid unless it's an MSI uninstall case
            if(-not (Test-Path -PathType Leaf $Path))
            {
                Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path)
            }

            ValidateFile -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint
        }

        $startInfo = New-Object System.Diagnostics.ProcessStartInfo
        $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $startInfo
        $errLogPath = $LogPath + ".err" #Concept only, will never touch disk
        if($fileExtension -eq ".msi")
        {
            $startInfo.FileName = "$env:windir\system32\msiexec.exe"
            if($Ensure -eq "Present")
            {
                # check if Msi package contains the ProductName and Code specified

                $pName,$pCode = Get-MsiProductEntry -Path $Path

                if (
                    ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name))  `
                -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode))
                )
                {
                    Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode)
                }

                $startInfo.Arguments = '/i "{0}"' -f $Path
            }
            else
            {
                $product = Get-ProductEntry $Name $identifyingNumber
                $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID
                $startInfo.Arguments = ("/x{0}" -f $id)
            }

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

            $startInfo.Arguments += " /quiet"

            if($Arguments)
            {
                $startInfo.Arguments += " " + $Arguments
            }
        }
        else #EXE
        {
            Trace-Message "The binary is an EXE"
            $startInfo.FileName = $Path
            $startInfo.Arguments = $Arguments
            if($LogPath)
            {
                Trace-Message "User has requested logging, need to attach event handlers to the process"
                $startInfo.RedirectStandardError = $true
                $startInfo.RedirectStandardOutput = $true
                Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath
                Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath
            }
        }

        Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments)

        if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null))
        {
            try
            {
                $exitCode = 0

                if($PSBoundParameters.ContainsKey("RunAsCredential"))
                {
                    CallPInvoke
                    [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, `
                        $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, `
                        $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode)
                }
                else
                {
                    $process.Start() | Out-Null

                    if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath
                    {
                        $process.BeginOutputReadLine();
                        $process.BeginErrorReadLine();
                    }

                    $process.WaitForExit()

                    if($process)
                    {
                        $exitCode = $process.ExitCode
                    }
                }
            }
            catch
            {
                Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_
            }


            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 $errLogPath
                $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 $errLogPath
            }

            if(-not ($ReturnCode -contains $exitCode))
            {
                Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString())
            }
        }
    }
    finally
    {
        if($psdrive)
        {
            Remove-PSDrive -Force $psdrive
        }

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

    if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null))
    {
        #This is deliberately not in the Finally block. We want to leave the downloaded file on disk
        #in the error case as a debugging aid for the user
        rm $downloadedFileName
    }

    $operationString = $LocalizedData.PackageUninstalled
    if($Ensure -eq "Present")
    {
        $operationString = $LocalizedData.PackageInstalled
    }

    if($CreateCheckRegValue -eq $true)
    {
        $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName
        Write-Verbose ($LocalizedData.CreatingRegistryValue -f $registryValueString)
        Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData
    }

    # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs
    $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks
    $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore
    if(($featureData -and $featureData.RequiresReboot) -or $regData)
    {
        Write-Verbose $LocalizedData.MachineRequiresReboot
        $global:DSCMachineStatus = 1
    }

    if($Ensure -eq "Present")
    {
        $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData
        if(-not $productEntry)
        {
            Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath)
        }
    }

    Write-Verbose $operationString
    Write-Verbose $LocalizedData.PackageConfigurationComplete
}

function CallPInvoke
{
$script: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"
}

function ValidateFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string] $Path,

        [string] $FileHash,
        [string] $HashAlgorithm,
        [string] $SignerThumbprint,
        [string] $SignerSubject
    )

    if ($FileHash)
    {
        ValidateFileHash -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm
    }

    if ($SignerThumbprint -or $SignerSubject)
    {
        ValidateFileSignature -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject
    }
}

function ValidateFileHash
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string] $Path,

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

        [string] $Algorithm
    )

    if ([string]::IsNullOrEmpty($Algorithm)) { $Algorithm = 'SHA256' }

    Trace-Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm)

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

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

function ValidateFileSignature
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string] $Path,

        [string] $Thumbprint,

        [string] $Subject
    )

    Trace-Message ($LocalizedData.CheckingFileSignature -f $Path)

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

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

    if ($Subject -and ($signature.SignerCertificate.Subject -notlike $Subject))
    {
        throw ($LocalizedData.WrongSignerSubject -f $Path, $Subject)
    }

    if ($Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint))
    {
        throw ($LocalizedData.WrongSignerThumbprint -f $Path, $Thumbprint)
    }
}

Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource