5 ConvertFrom-StringData @'
6 InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid
7 InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP
8 InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file
9 NeedsMoreInfo=Either Name or ProductId is required
10 InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported
11 CouldNotOpenLog=The specified LogPath ({0}) could not be opened
12 CouldNotStartProcess=The process {0} could not be started
13 UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct
14 PathDoesNotExist=The given Path ({0}) could not be found
15 CouldNotOpenDestFile=Could not open the file {0} for writing
16 CouldNotGetHttpStream=Could not get the {0} stream for file {1}
17 ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1}
18 PackageConfigurationComplete=Package configuration finished
19 PackageConfigurationStarting=Package configuration starting
20 InstalledPackage=Installed package
21 UninstalledPackage=Uninstalled package
22 NoChangeRequired=Package found in desired state, no action required
23 RemoveExistingLogFile=Remove existing log file
24 CreateLogFile=Create log file
25 MountSharePath=Mount share to get media
26 DownloadHTTPFile=Download the media over HTTP or HTTPS
27 StartingProcessMessage=Starting process {0} with arguments {1}
28 RemoveDownloadedFile=Remove the downloaded file
29 PackageInstalled=Package has been installed
30 PackageUninstalled=Package has been uninstalled
31 MachineRequiresReboot=The machine requires a reboot
32 PackageDoesNotAppearInstalled=The package {0} is not installed
33 PackageAppearsInstalled=The package {0} is installed
34 PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details
35 ValidateStandardArgumentsPathwasPath = Validate-StandardArguments, Path was {0}
36 TheurischemewasuriScheme = The uri scheme was {0}
37 ThepathextensionwaspathExt = The path extension was {0}
38 ParsingProductIdasanidentifyingNumber = Parsing {0} as an identifyingNumber
39 ParsedProductIdasidentifyingNumber = Parsed {0} as {1}
40 EnsureisEnsure = Ensure is {0}
41 productisproduct = product {0} found
42 productasbooleanis = product as boolean is {0}
43 Creatingcachelocation = Creating cache location
44 NeedtodownloadfilefromschemedestinationwillbedestName = Need to download file from {0}, destination will be {1}
45 Creatingthedestinationcachefile = Creating the destination cache file
46 Creatingtheschemestream = Creating the {0} stream
47 Settingdefaultcredential = Setting default credential
48 Settingauthenticationlevel = Setting authentication level
49 Ignoringbadcertificates = Ignoring bad certificates
50 Gettingtheschemeresponsestream = Getting the {0} response stream
51 ErrorOutString = Error: {0}
52 Copyingtheschemestreambytestothediskcache = Copying the {0} stream bytes to the disk cache
53 Redirectingpackagepathtocachefilelocation = Redirecting package path to cache file location
54 ThebinaryisanEXE = The binary is an EXE
55 Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process
56 StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1}
60 Import-LocalizedData LocalizedData -filename PackageProvider.psd1
64 Function Trace-Message
66 param([string] $Message)
69 Write-Verbose $Message
73 $CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource"
75 Function Throw-InvalidArgumentException
82 $exception = new-object System.ArgumentException $Message,$ParamName
83 $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null
87 Function Throw-InvalidNameOrIdException
93 $exception = new-object System.ArgumentException $Message
94 $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null
98 Function Throw-TerminatingError
102 [System.Management.Automation.ErrorRecord] $ErrorRecord
105 $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception
106 $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null
110 Function Validate-StandardArguments
118 Trace-Message ($LocalizedData.ValidateStandardArgumentsPathwasPath -f $Path)
126 Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path"
129 if(-not @("file", "http", "https") -contains $uri.Scheme)
131 Trace-Message ($Localized.TheurischemewasuriScheme -f $uri.Scheme)
132 Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path"
135 $pathExt = [System.IO.Path]::GetExtension($Path)
136 Trace-Message ($LocalizedData.ThepathextensionwaspathExt -f $pathExt)
137 if(-not @(".msi",".exe") -contains $pathExt.ToLower())
139 Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path"
142 $identifyingNumber = $null
143 if(-not $Name -and -not $ProductId)
145 #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version
146 Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId"
152 Trace-Message ($LocalizedData.ParsingProductIdasanidentifyingNumber -f $ProductId)
153 $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper()
154 Trace-Message ($LocalizedData.ParsedProductIdasidentifyingNumber -f $ProductId, $identifyingNumber)
158 Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId
162 return $uri, $identifyingNumber
165 Function Get-ProductEntry
170 [string] $IdentifyingNumber
173 $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
174 $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
176 if($IdentifyingNumber)
178 $keyLocation = "$uninstallKey\$identifyingNumber"
179 $item = Get-Item $keyLocation -EA SilentlyContinue
182 $keyLocation = "$uninstallKeyWow64\$identifyingNumber"
183 $item = Get-Item $keyLocation -EA SilentlyContinue
189 foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64))
191 if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName"))
200 function Test-TargetResource
204 [ValidateSet("Present", "Absent")]
205 [string] $Ensure = "Present",
207 [parameter(Mandatory = $true)]
211 [parameter(Mandatory = $true)]
212 [ValidateNotNullOrEmpty()]
215 [parameter(Mandatory = $true)]
221 [pscredential] $Credential,
228 $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name
229 $product = Get-ProductEntry $Name $identifyingNumber
230 Trace-Message ($LocalizedData.EnsureisEnsure -f $Ensure)
233 Trace-Message ($LocalizedData.productisproduct -f $product)
237 Trace-Message "product installation cannot be determined"
239 Trace-Message ($LocalizedData.productasbooleanis -f [boolean]$product)
240 $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent")
242 if ($product -ne $null)
244 $name = Get-LocalizableRegKeyValue $product "DisplayName"
245 Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name)
256 $displayName = $ProductId
259 Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName)
265 function Get-LocalizableRegKeyValue
272 $res = $RegKey.GetValue("{0}_Localized" -f $ValueName)
275 $res = $RegKey.GetValue($ValueName)
281 function Get-TargetResource
285 [parameter(Mandatory = $true)]
289 [parameter(Mandatory = $true)]
290 [ValidateNotNullOrEmpty()]
293 [parameter(Mandatory = $true)]
298 #If the user gave the ProductId then we derive $identifyingNumber
299 $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name
301 $localMsi = $uri.IsFile -and -not $uri.IsUnc
303 $product = Get-ProductEntry $Name $identifyingNumber
310 ProductId = $identifyingNumber
315 #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE)
316 #If the user gave a ProductId just pass it through, otherwise fill it from the product
317 if(-not $identifyingNumber)
319 $identifyingNumber = Split-Path -Leaf $product.Name
322 $date = $product.GetValue("InstallDate")
327 $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date
335 $publisher = Get-LocalizableRegKeyValue $product "Publisher"
336 $size = $product.GetValue("EstimatedSize")
342 $version = $product.GetValue("DisplayVersion")
343 $description = $product.GetValue("Comments")
344 $name = Get-LocalizableRegKeyValue $product "DisplayName"
350 ProductId = $identifyingNumber
354 PackageDescription = $description
355 Publisher = $publisher
359 Function Get-MsiTools
363 return $script:MsiTools
367 [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
368 private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct);
370 [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
371 private static extern uint MsiCloseHandle(IntPtr hAny);
373 [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)]
374 private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength);
376 private static string GetPackageProperty(string msi, string property)
378 IntPtr MsiHandle = IntPtr.Zero;
381 var res = MsiOpenPackageExW(msi, 1, out MsiHandle);
388 var buffer = new StringBuilder(length);
389 res = MsiGetPropertyW(MsiHandle, property, buffer, ref length);
390 return buffer.ToString();
394 if (MsiHandle != IntPtr.Zero)
396 MsiCloseHandle(MsiHandle);
400 public static string GetProductCode(string msi)
402 return GetPackageProperty(msi, "ProductCode");
405 public static string GetProductName(string msi)
407 return GetPackageProperty(msi, "ProductName");
410 $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.PackageResource `
411 -Name MsiTools -Using System.Text -MemberDefinition $sig
412 return $script:MsiTools
416 Function Get-MsiProductEntry
423 if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi"))
425 Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path)
428 $tools = Get-MsiTools
430 $pn = $tools::GetProductName($Path)
432 $pc = $tools::GetProductCode($Path)
438 function Set-TargetResource
440 [CmdletBinding(SupportsShouldProcess=$true)]
443 [ValidateSet("Present", "Absent")]
444 [string] $Ensure = "Present",
446 [parameter(Mandatory = $true)]
450 [parameter(Mandatory = $true)]
451 [ValidateNotNullOrEmpty()]
454 [parameter(Mandatory = $true)]
460 [pscredential] $Credential,
467 $ErrorActionPreference = "Stop"
469 if (Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId)
474 $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name
475 $product = Get-ProductEntry $Name $identifyingNumber
477 #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded
478 #but the named package wasn't present on the system afterward so we can give a better message
481 Write-Verbose $LocalizedData.PackageConfigurationStarting
484 # return code 1641 and 3010 are succeed indication when restart is requested per installation
485 $ReturnCode = @(0, 1641, 3010)
490 $downloadedFileName = $null
493 $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower()
498 if($fileExtension -eq ".msi")
500 #We want to pre-verify the path exists and is writable ahead of time
501 #even in the MSI case, as detecting WHY the MSI log doesn't exist would
502 #be rather problematic for the user
503 if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null))
508 if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
510 New-Item -Type File $LogPath | Out-Null
513 elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null))
515 $logStream = new-object "System.IO.StreamWriter" $LogPath,$false
520 Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_
524 #Download or mount file as necessary
525 if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent"))
527 if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null))
529 $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)}
532 #We need to optionally include these and then splat the hash otherwise
533 #we pass a null for Credential which causes the cmdlet to pop a dialog up
534 $psdriveArgs["Credential"] = $Credential
537 $psdrive = New-PSDrive @psdriveArgs
538 $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary?
540 elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null))
542 $scheme = $uri.Scheme
544 $responseStream = $null
548 Trace-Message ($LocalizedData.Creatingcachelocation)
550 if(-not (Test-Path -PathType Container $CacheLocation))
552 mkdir $CacheLocation | Out-Null
555 $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath)
557 Trace-Message ($LocalizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $scheme, $destName)
561 Trace-Message ($LocalizedData.Creatingthedestinationcachefile)
562 $outStream = New-Object System.IO.FileStream $destName, "Create"
566 #Should never happen since we own the cache directory
567 Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_
572 Trace-Message ($LocalizedData.Creatingtheschemestream -f $scheme)
573 $request = [System.Net.WebRequest]::Create($uri)
574 Trace-Message ($LocalizedData.Settingdefaultcredential)
575 $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials
576 if ($scheme -eq "http")
578 Trace-Message ($LocalizedData.Settingauthenticationlevel)
579 # default value is MutualAuthRequested, which applies to https scheme
580 $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None
582 if ($scheme -eq "https")
584 Trace-Message ($LocalizedData.Ignoringbadcertificates)
585 $request.ServerCertificateValidationCallBack = {$true}
587 Trace-Message ($LocalizedData.Gettingtheschemeresponsestream -f $scheme)
588 $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream()
592 Trace-Message ($LocalizedData.ErrorOutString -f ($_ | Out-String))
593 Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_
598 Trace-Message ($LocalizedData.Copyingtheschemestreambytestothediskcache -f $scheme)
599 $responseStream.CopyTo($outStream)
600 $responseStream.Flush()
605 Trace-Message ($LocalizedData.ErrorOutString -f ($_ | Out-String))
606 Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_
618 $responseStream.Close()
621 Trace-Message ($LocalizedData.Redirectingpackagepathtocachefilelocation)
622 $Path = $downloadedFileName = $destName
626 #At this point the Path ought to be valid unless it's an MSI uninstall case
627 if(-not (Test-Path -PathType Leaf $Path) -and -not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi"))
629 Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path)
632 $startInfo = New-Object System.Diagnostics.ProcessStartInfo
633 $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea
634 $process = New-Object System.Diagnostics.Process
635 $process.StartInfo = $startInfo
636 $errLogPath = $LogPath + ".err" #Concept only, will never touch disk
638 if($fileExtension -eq ".msi")
640 $startInfo.FileName = "$env:windir\system32\msiexec.exe"
641 if($Ensure -eq "Present")
643 # check if Msi package contains the ProductName and Code specified
645 $pName,$pCode = Get-MsiProductEntry -Path $Path
648 ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) `
649 -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode))
652 Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode)
655 $startInfo.Arguments = '/i "{0}"' -f $Path
659 $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID
660 $startInfo.Arguments = ("/x{0}" -f $id)
665 $startInfo.Arguments += ' /log "{0}"' -f $LogPath
668 $startInfo.Arguments += " /quiet"
672 $startInfo.Arguments += " " + $Arguments
677 Trace-Message ($LocalizedData.ThebinaryisanEXE)
678 if($Ensure -eq "Present")
680 $startInfo.FileName = $Path
681 $startInfo.Arguments = $Arguments
684 Trace-Message ($LocalizedData.Userhasrequestedloggingneedtoattacheventhandlerstotheprocess)
685 $startInfo.RedirectStandardError = $true
686 $startInfo.RedirectStandardOutput = $true
687 Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath
688 Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath
692 $startInfo.FileName = "$env:windir\system32\msiexec.exe"
693 $id = Split-Path -Leaf $product.Name
694 $startInfo.Arguments = ("/x{0} /quiet" -f $id)
695 # Never let the msiexec to restart automatically, DSC should handle reboot requests
696 $startInfo.Arguments += ' /norestart'
699 $startInfo.Arguments += ' /log "{0}"' -f $LogPath
704 $startInfo.Arguments += " " + $Arguments
709 Trace-Message ($LocalizedData.StartingwithstartInfoFileNamestartInfoArguments -f $startInfo.FileName, $startInfo.Arguments)
711 if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null))
716 $process.Start() | Out-Null
717 if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath
719 $process.BeginOutputReadLine();
720 $process.BeginErrorReadLine();
723 $process.WaitForExit()
727 $exitCode = $process.ExitCode
733 Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_
739 #We have to re-mux these since they appear to us as different streams
740 #The underlying Win32 APIs prevent this problem, as would constructing a script
741 #on the fly and executing it, but the former is highly problematic from PowerShell
742 #and the latter doesn't let us get the return code for UI-based EXEs
743 $outputEvents = Get-Event -SourceIdentifier $LogPath
744 $errorEvents = Get-Event -SourceIdentifier $errLogPath
745 $masterEvents = @() + $outputEvents + $errorEvents
746 $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated
748 foreach($event in $masterEvents)
750 $logStream.Write($event.SourceEventArgs.Data);
753 Remove-Event -SourceIdentifier $LogPath
754 Remove-Event -SourceIdentifier $errLogPath
757 if(-not ($ReturnCode -contains $exitCode))
759 Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString())
767 Remove-PSDrive -Force $psdrive
776 if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null))
778 #This is deliberately not in the Finally block. We want to leave the downloaded file on disk
779 #in the error case as a debugging aid for the user
780 rm $downloadedFileName
783 $operationString = $LocalizedData.PackageUninstalled
784 if($Ensure -eq "Present")
786 $operationString = $LocalizedData.PackageInstalled
789 # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs (Worked on both Server and Client Skus as in windows 10)
790 $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks
791 $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore
792 if(($featureData -and $featureData.RequiresReboot) -or $regData -or $exitcode -eq 3010 -or $exitcode -eq 1641)
794 Write-Verbose $LocalizedData.MachineRequiresReboot
795 $global:DSCMachineStatus = 1
798 if($Ensure -eq "Present")
800 $productEntry = Get-ProductEntry $Name $identifyingNumber
801 if(-not $productEntry)
803 Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath)
807 Write-Verbose $operationString
808 Write-Verbose $LocalizedData.PackageConfigurationComplete
811 Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource