1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/lib/ansible/modules/windows/win_package.ps1

1327 lines
43 KiB
PowerShell

#!powershell
# (c) 2014, Trond Hindenes <trond@hindenes.com>, and others
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# WANT_JSON
# POWERSHELL_COMMON
#region DSC
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
'@
}
$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
)
if ($errorRecord)
{
$exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
}
Else
{
$exception = new-object "System.InvalidOperationException" $Message
}
$errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null
throw $errorRecord
}
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 occurred 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"
$TestGuid = [system.guid]::NewGuid()
#Check to see if the productid is a guid
if ([guid]::TryParse($ProductId, [ref]$TestGuid))
{
$identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper()
Trace-Message "Parsed $ProductId as $identifyingNumber (is guid)"
}
Else
{
$identifyingNumber = $ProductId
Trace-Message "Parsed $ProductId as $identifyingNumber (is not guid)"
}
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] $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 LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64
}
if($installValue -eq $null)
{
$installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32
}
if($installValue)
{
if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData)
{
return @{
Installed = $true
}
}
}
}
return $null
}
function Test-TargetResource
{
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,
[int[]] $ReturnCode,
[string] $LogPath,
[pscredential] $RunAsCredential,
[string] $InstalledCheckRegKey,
[string] $InstalledCheckRegValueName,
[string] $InstalledCheckRegValueData
)
$uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name
$product = Get-ProductEntry $Name $identifyingNumber $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
{
param
(
[parameter(Mandatory = $true)]
[AllowEmptyString()]
[string] $Name,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[parameter(Mandatory = $true)]
[AllowEmptyString()]
[string] $ProductId,
[string] $InstalledCheckRegKey,
[string] $InstalledCheckRegValueName,
[string] $InstalledCheckRegValueData
)
#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
InstalledCheckRegKey = $InstalledCheckRegKey
InstalledCheckRegValueName = $InstalledCheckRegValueName
InstalledCheckRegValueData = $InstalledCheckRegValueData
}
}
if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData)
{
return @{
Ensure = "Present"
Name = $Name
ProductId = $identifyingNumber
Installed = $true
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.PackageResource `
-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,
[int[]] $ReturnCode,
[string] $LogPath,
[pscredential] $RunAsCredential,
[string] $InstalledCheckRegKey,
[string] $InstalledCheckRegValueName,
[string] $InstalledCheckRegValueData
)
$ErrorActionPreference = "Stop"
if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId `
-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")
{
Trace-Message "Ignoring bad certificates"
$request.ServerCertificateValidationCallBack = {$true}
}
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
}
}
#At this point the Path ought to be valid unless it's an MSI uninstall case
if(-not (Test-Path -PathType Leaf $Path) -and -not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi"))
{
Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path)
}
$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
Set-Attr -obj $result -name "exit_code" -value $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
}
# 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 $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"
}
#endregion
$params = Parse-Args $args;
$result = New-Object psobject;
Set-Attr $result "changed" $false;
$path = Get-Attr -obj $params -name path -failifempty $true -resultobj $result
$name = Get-Attr -obj $params -name name -default $path
$productid = Get-Attr -obj $params -name productid
if ($productid -eq $null)
{
#Alias added for backwards compat.
$productid = Get-Attr -obj $params -name product_id -failifempty $true -resultobj $result
}
$arguments = Get-Attr -obj $params -name arguments
$ensure = Get-Attr -obj $params -name state -default "present"
if ($ensure -eq $null)
{
$ensure = Get-Attr -obj $params -name ensure -default "present"
}
$username = Get-Attr -obj $params -name user_name
$password = Get-Attr -obj $params -name user_password
$return_code = Get-Attr -obj $params -name expected_return_code -default 0
#Construct the DSC param hashtable
$dscparams = @{
name=$name
path=$path
productid = $productid
arguments = $arguments
ensure = $ensure
returncode = $return_code
}
if (($username -ne $null) -and ($password -ne $null))
{
#Add network credential to the list
$secpassword = $password | ConvertTo-SecureString -AsPlainText -Force
$credential = New-Object pscredential -ArgumentList $username, $secpassword
$dscparams.add("Credential",$credential)
}
#Always return the name
set-attr -obj $result -name "name" -value $name
$testdscresult = Test-TargetResource @dscparams
if ($testdscresult -eq $true)
{
Exit-Json -obj $result
}
Else
{
try
{
set-TargetResource @dscparams
}
catch
{
$errormsg = $_
Fail-Json -obj $result -message $errormsg.ToString()
}
#Check if DSC thinks the computer needs a reboot:
if ((get-variable DSCMachinestatus -Scope Global -ea 0) -and ($global:DSCMachineStatus -eq 1))
{
Set-Attr $result "restart_required" $true
}
#Set-TargetResource did its job. We can assume a change has happened
Set-Attr $result "changed" $true
Exit-Json -obj $result
}