diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 544b9127b..39dc0b203 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -86,5 +86,5 @@ function New-InvalidOperationException Export-ModuleMember -Function ` Test-IsNanoServer, ` - Throw-InvalidArgumentException, ` - Throw-TerminatingError + New-InvalidArgumentException, ` + New-InvalidOperationException diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 0f65f9c14..f9884d7d1 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -363,10 +363,18 @@ function Set-TargetResourceOnFullSKU if ($Ensure -eq 'Present') { + $actualMembersAsPrincipals = $null + if ($groupOriginallyExists) { $disposables.Add($group) | Out-Null $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation) + + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` + -Group $group ` + -PrincipalContexts $principalContexts ` + -Disposables $disposables ` + -Credential $Credential ) } else { @@ -433,14 +441,47 @@ function Set-TargetResourceOnFullSKU -Disposables $disposables ` -Credential $Credential - if ($membersAsPrincipals.Length -gt 0) + if ($membersAsPrincipals.Count -gt 0) { - $group.Members.Clear() + if ($null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Count -gt 0) + { + $membersToAdd = @() + $membersToRemove = @() - # Set the members of the group - if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersAsPrincipals) + foreach ($membersAsPrincipal in $membersAsPrincipals) + { + if ($actualMembersAsPrincipals -notcontains $membersAsPrincipal) + { + $membersToAdd += $membersAsPrincipal + } + } + + foreach ($actualMembersAsPrincipal in $actualMembersAsPrincipals) + { + if ($membersAsPrincipals -notcontains $actualMembersAsPrincipal) + { + $membersToRemove += $actualMembersAsPrincipal + } + } + + # Set the members of the group + if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersToAdd) + { + $saveChanges = $true + } + + if (Remove-GroupMembers -Group $group -MembersAsPrincipals $membersToRemove) + { + $saveChanges = $true + } + } + else { - $saveChanges = $true + # Set the members of the group + if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersAsPrincipals) + { + $saveChanges = $true + } } } else @@ -495,13 +536,13 @@ function Set-TargetResourceOnFullSKU } } - if ($membersToIncludeAsPrincipals.Length -eq 0 -and $membersToExcludeAsPrincipals.Length -eq 0) + if ($membersToIncludeAsPrincipals.Count -eq 0 -and $membersToExcludeAsPrincipals.Count -eq 0) { New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' -Message ($LocalizedData.IncludeAndExcludeAreEmpty) } } - if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Length -eq 0) + if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Count -gt 0) { if (Remove-GroupMembers -Group $group -MembersAsPrincipals $membersToExcludeAsPrincipals) { @@ -509,7 +550,7 @@ function Set-TargetResourceOnFullSKU } } - if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Length -eq 0) + if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Count -gt 0) { if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersToIncludeAsPrincipals) { @@ -651,6 +692,15 @@ function Test-TargetResourceOnFullSKU return ($Ensure -eq 'Absent') } + if ($null -ne $group.Members) + { + $actualGroupMembers = @($group.Members) + } + else + { + $actualGroupMembers = $null + } + $disposables.Add($group) | Out-Null Write-Verbose -Message ($LocalizedData.GroupExists -f $GroupName) @@ -686,7 +736,7 @@ function Test-TargetResourceOnFullSKU if ($Members.Count -eq 0) { - return ($group.Members.Count -eq 0) + return ($null -eq $actualGroupMembers -or $actualGroupMembers.Count -eq 0) } else { @@ -694,23 +744,23 @@ function Test-TargetResourceOnFullSKU $Members = @( Remove-DuplicateMembers -Members $Members ) # Resolve the names to actual principal objects. - $expectedMembersAsPrincipals = ConvertTo-Principals ` + $expectedMembersAsPrincipals = @( ConvertTo-Principals ` -MemberNames $Members ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) - if ($expectedMembersAsPrincipals.Length -ne $group.Members.Count) + if ($expectedMembersAsPrincipals.Count -ne $actualGroupMembers.Count) { - Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembersAsPrincipals.Length, $group.Members.Count) + Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembersAsPrincipals.Count, $actualGroupMembers.Count) return $false } - $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` -Group $group ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) # Compare the two member lists. foreach ($expectedMemberAsPrincipal in $expectedMembersAsPrincipals) @@ -723,13 +773,13 @@ function Test-TargetResourceOnFullSKU } } } - else + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) { - $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` -Group $group ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) if ($PSBoundParameters.ContainsKey('MembersToInclude')) { @@ -971,7 +1021,7 @@ function Set-TargetResourceOnNanoServer # Remove duplicate names as strings. $Members = @( Remove-DuplicateMembers -Members $Members ) - if ($Members.Length -gt 0) + if ($Members.Count -gt 0) { # Get current members $groupMembers = Get-MembersOnNanoServer -Group $group @@ -1013,7 +1063,7 @@ function Set-TargetResourceOnNanoServer } } - if ($MembersToInclude.Length -eq 0 -and $MembersToExclude.Length -eq 0) + if ($MembersToInclude.Count -eq 0 -and $MembersToExclude.Count -eq 0) { New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' -Message ($LocalizedData.IncludeAndExcludeAreEmpty) } @@ -1180,8 +1230,6 @@ function Test-TargetResourceOnNanoServer if ($PSBoundParameters.ContainsKey('Members')) { - Write-Verbose 'Testing Members...' - if ($PSBoundParameters.ContainsKey('MembersToInclude')) { New-InvalidArgumentException -ArgumentName 'MembersToInclude' -Message ($LocalizedData.MembersAndIncludeExcludeConflict -f 'Members', 'MembersToInclude') @@ -1198,9 +1246,9 @@ function Test-TargetResourceOnNanoServer # Get current members $groupMembers = Get-MembersOnNanoServer -Group $group - if ($expectedMembers.Length -ne $groupMembers.Length) + if ($expectedMembers.Count -ne $groupMembers.Count) { - Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembers.Length, $groupMembers.Length) + Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembers.Count, $groupMembers.Count) return $false } @@ -1378,7 +1426,7 @@ function Get-MembersOnFullSKU $members = New-Object -TypeName 'System.Collections.ArrayList' - $membersAsPrincipals = Get-MembersAsPrincipals -Group $Group -PrincipalContexts $PrincipalContexts -Disposables $Disposables -Credential $Credential + $membersAsPrincipals = @( Get-MembersAsPrincipals -Group $Group -PrincipalContexts $PrincipalContexts -Disposables $Disposables -Credential $Credential ) foreach ($membersAsPrincipal in $membersAsPrincipals) { @@ -1518,7 +1566,7 @@ function Get-MembersAsPrincipals # The account is domain qualified - credential required to resolve it. elseif ($null -ne $Credential -or $null -ne $principalContext) { - Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $scope, $accountName) + Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $accountName, $scope) } else { @@ -1858,7 +1906,14 @@ function Get-PrincipalContext elseif ($null -ne $Credential) { # Create a PrincipalContext targeting $Scope using the network credentials that were passed in. - $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + if ($Credential.Domain) + { + $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + } + else + { + $principalContextName = $Credential.UserName + } $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope, $principalContextName, $Credential.Password ) # Cache the PrincipalContext for this scope for subsequent calls. @@ -2211,7 +2266,16 @@ function Get-Group -Disposables $Disposables ` -Scope $env:computerName - return [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $GroupName) + try + { + $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $GroupName) + } + catch + { + $group = $null + } + + return $group } <# @@ -2236,7 +2300,10 @@ function Assert-GroupNameValid if ($GroupName.IndexOfAny($invalidCharacters) -ne -1) { - ThrowInvalidArgumentError -ErrorId 'GroupNameHasInvalidCharacter' -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [String]::Join(' ', $invalidCharacters)) + New-InvalidArgumentException ` + -ArgumentName 'GroupNameHasInvalidCharacter' ` + -Message ($LocalizedData.InvalidGroupName ` + -f $GroupName, [String]::Join(' ', $invalidCharacters)) } $nameContainsOnlyWhitspaceOrDots = $true @@ -2253,7 +2320,10 @@ function Assert-GroupNameValid if ($nameContainsOnlyWhitspaceOrDots) { - ThrowInvalidArgumentError -ErrorId 'GroupNameHasOnlyWhiteSpacesAndDots' -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [String]::Join(' ', $invalidCharacters)) + New-InvalidArgumentException ` + -ErrorId 'GroupNameHasOnlyWhiteSpacesAndDots' ` + -Message ($LocalizedData.InvalidGroupName ` + -f $GroupName, [String]::Join(' ', $invalidCharacters)) } } diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 3385953cc..fc653af7f 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -1,562 +1,711 @@ data LocalizedData { - # culture="en-US" + # 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}. -RemovingRegistryValue=Removing package registry value of {0}. + ConvertFrom-StringData -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 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}. +RemovingRegistryValue = Removing package registry value of {0}. +ValidateStandardArgumentsPathwasPath = Validate-StandardArguments, Path was {0} +TheurischemewasuriScheme = The uri scheme was {0} +ThepathextensionwaspathExt = The path extension was {0} +ParsingProductIdasanidentifyingNumber = Parsing {0} as an identifyingNumber +ParsedProductIdasidentifyingNumber = Parsed {0} as {1} +EnsureisEnsure = Ensure is {0} +productisproduct = product {0} found +productasbooleanis = product as boolean is {0} +Creatingcachelocation = Creating cache location +NeedtodownloadfilefromschemedestinationwillbedestName = Need to download file from {0}, destination will be {1} +Creatingthedestinationcachefile = Creating the destination cache file +Creatingtheschemestream = Creating the {0} stream +Settingdefaultcredential = Setting default credential +Settingauthenticationlevel = Setting authentication level +Ignoringbadcertificates = Ignoring bad certificates +Gettingtheschemeresponsestream = Getting the {0} response stream +ErrorOutString = Error: {0} +Copyingtheschemestreambytestothediskcache = Copying the {0} stream bytes to the disk cache +Redirectingpackagepathtocachefilelocation = Redirecting package path to cache file location +ThebinaryisanEXE = The binary is an EXE +Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process +StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1} +ProvideParameterForRegistryCheck = Please provide the {0} parameter in order to check for installation status from a registry key. +ErrorSettingRegistryValue = An error occured while attempting to set the registry key {0} value {1} to {2} +ErrorRemovingRegistryValue = An error occured while attempting to remove the registry key {0} value {1} +ExeCouldNotBeUninstalled = The .exe file found at {0} could not be uninstalled. The uninstall functionality may not be implemented in this .exe file. '@ } -$Debug = $true -Function Trace-Message -{ - param([string] $Message) - if($Debug) - { - Write-Verbose $Message - } -} +# Commented-out until more languages are supported +# Import-LocalizedData -BindingVariable 'LocalizedData' -FileName 'MSFT_xPackageResource.strings.psd1' -$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" +Import-Module -Name "$PSScriptRoot\..\CommonResourceHelper.psm1" -Force -Function Throw-InvalidArgumentException -{ - param( - [string] $Message, - [string] $ParamName - ) +$script:packageCacheLocation = "$env:programData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xPackageResource" +$script:msiTools = $null - $exception = new-object System.ArgumentException $Message,$ParamName - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null - throw $errorRecord -} +<# + .SYNOPSIS + Asserts that the path extension is valid. -Function Throw-InvalidNameOrIdException + .PARAMETER Path + The path to validate the extension of. +#> +function Assert-PathExtensionValid { - param( - [string] $Message + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path ) - $exception = new-object System.ArgumentException $Message - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null - throw $errorRecord + $pathExtension = [System.IO.Path]::GetExtension($Path) + Write-Verbose -Message ($LocalizedData.ThePathExtensionWasPathExt -f $pathExtension) + + $validPathExtensions = @( '.msi', '.exe' ) + + if ($validPathExtensions -notcontains $pathExtension.ToLower()) + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidBinaryType -f $Path) + } } -Function Throw-TerminatingError -{ - param( - [string] $Message, - [System.Management.Automation.ErrorRecord] $ErrorRecord - ) +<# + .SYNOPSIS + Retrieves the product ID as an identifying number. - $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 + .PARAMETER ProductId + The product id to retrieve as an identifying number. +#> +function Convert-ProductIdToIdentifyingNumber { + [OutputType([String])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Key, - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $Value, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Data + [String] + $ProductId ) 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) + Write-Verbose -Message ($LocalizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId) + $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + + Write-Verbose -Message ($LocalizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber) + return $identifyingNumber } catch { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Set-RegistryValue: $exceptionText" + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) } } -Function Remove-RegistryValue +<# + .SYNOPSIS + Converts the given path to a URI. + Throws an exception if the path's scheme as a URI is not valid. + + .PARAMETER Path + The path to retrieve as a URI. +#> +function Convert-PathToUri { + [OutputType([Uri])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $Key, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Value + [String] + $Path ) try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) - $subKey = $baseKey.OpenSubKey($Key, $true) ## Opens the subkey with write access - $subKey.DeleteValue($Value) + $uri = [Uri] $Path } catch { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Remove-RegistryValue: $exceptionText" + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path) } + + $validUriSchemes = @( 'file', 'http', 'https' ) + + if ($validUriSchemes -notcontains $uri.Scheme) + { + Write-Verbose -Message ($Localized.TheUriSchemeWasUriScheme -f $uri.Scheme) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path) + } + + return $uri } -Function Get-RegistryValueIgnoreError +<# + .SYNOPSIS + Retrieves a value from a registry without throwing errors. + + .PARAMETER Key + The key of the registry to get the value from. + + .PARAMETER Value + The name of the value to retrieve. + + .PARAMETER RegistryHive + The registry hive to retrieve the value from. + + .PARAMETER RegistyView + The registry view to retrieve the value from. +#> +function Get-RegistryValueWithErrorsIgnored { + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] - [System.String] + [Parameter(Mandatory = $true)] + [String] $Key, - [parameter(Mandatory = $true)] - [System.String] + [Parameter(Mandatory = $true)] + [String] $Value, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryView] $RegistryView ) + $registryValue = $null + try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) - $subKey = $baseKey.OpenSubKey($Key) - if($subKey -ne $null) + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key) + + if ($null -ne $subRegistryKey) { - return $subKey.GetValue($Value) + $registryValue = $subRegistryKey.GetValue($Value) } } catch { $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText" + Write-Verbose -Message "An exception occured while attempting to retrieve a registry value: $exceptionText" } - return $null + + return $registryValue } -Function Validate-StandardArguments -{ - param( - $Path, - $ProductId, - $Name - ) +<# + .SYNOPSIS + Retrieves the product entry for the package with the given name and/or identifying number. - Trace-Message "Validate-StandardArguments, Path was $Path" - $uri = $null - try - { - $uri = [uri] $Path - } - catch - { - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" - } + .PARAMETER Name + The name of the product entry to retrieve. - if(-not @("file", "http", "https") -contains $uri.Scheme) - { - Trace-Message "The uri scheme was $uri.Scheme" - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" - } + .PARAMETER CreateCheckRegValue + Indicates whether or not to retrieve the package installation status from a registry. - $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" - } + .PARAMETER IdentifyingNumber + The identifying number of the product entry to retrieve. - $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 - } - } + .PARAMETER InstalledCheckRegHive + The registry hive to check for package installation status. - return $uri, $identifyingNumber -} + .PARAMETER InstalledCheckRegKey + The registry key to open to check for package installation status. + + .PARAMETER InstalledCheckRegValueName + The registry value name to check for package installation status. -Function Get-ProductEntry + .PARAMETER InstalledCheckRegValueData + The value to compare against the retrieved registry value to check for package installation. +#> +function Get-ProductEntry { + [CmdletBinding()] param ( - [string] $Name, - [string] $IdentifyingNumber, - [string] $InstalledCheckRegHive = 'LocalMachine', - [string] $InstalledCheckRegKey, - [string] $InstalledCheckRegValueName, - [string] $InstalledCheckRegValueData + [String] + $Name, + + [String] + $IdentifyingNumber, + + [Switch] + $CreateCheckRegValue, + + [ValidateSet('LocalMachine', 'CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData ) - $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" - $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' - if($IdentifyingNumber) + $productEntry = $null + + if (-not [String]::IsNullOrEmpty($IdentifyingNumber)) { - $keyLocation = "$uninstallKey\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue - if(-not $item) + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber + + $productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + + if ($null -eq $productEntry) { - $keyLocation = "$uninstallKeyWow64\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber + $productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue' } - - return $item } - - foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) + else { - if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) + foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' )) { - return $item + if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName')) + { + $productEntry = $registryKeyEntry + break + } } } - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + if ($null -eq $productEntry) { - $installValue = $null - - #if 64bit OS, check 64bit registry view first - if ((Get-WmiObject -Class Win32_OperatingSystem -ea 0).OSArchitecture -eq '64-bit') + if ($CreateCheckRegValue) { - $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 - } + $installValue = $null + + $win32OperatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue' - if($installValue -eq $null) - { - $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 - } + # If 64-bit OS, check 64-bit registry view first + if ($win32OperatingSystem.OSArchitecture -ieq '64-bit') + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry64' + } - if($installValue) - { - if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + if ($null -eq $installValue) + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry32' + } + + if ($null -ne $installValue) { - return @{ - Installed = $true + if ($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + { + $productEntry = @{ + Installed = $true + } } } } } - return $null + return $productEntry } function Test-TargetResource { - [OutputType([System.Boolean])] + [OutputType([Boolean])] + [CmdletBinding()] param ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, + [String] + $ProductId, - [string] $Arguments, + [String] + $Arguments, - [pscredential] $Credential, + [PSCredential] + $Credential, - [System.UInt32[]] $ReturnCode, + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [ValidateNotNullOrEmpty()] + [UInt32[]] + $ReturnCode = @( 0, 1641, 3010 ), - [string] $LogPath, + [String] + $LogPath, - [pscredential] $RunAsCredential, + [String] + $FileHash, - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [String] + $HashAlgorithm, - [string] $InstalledCheckRegKey, + [String] + $SignerSubject, - [string] $InstalledCheckRegValueName, + [String] + $SignerThumbprint, - [string] $InstalledCheckRegValueData, + [String] + $ServerCertificateValidationCallback, - [boolean] $CreateCheckRegValue, + [Boolean] + $CreateCheckRegValue = $false, - [string] $FileHash, + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', - [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] - [string] $HashAlgorithm, + [String] + $InstalledCheckRegKey, - [string] $SignerSubject, - [string] $SignerThumbprint, + [String] + $InstalledCheckRegValueName, - [string] $ServerCertificateValidationCallback + [String] + $InstalledCheckRegValueData ) - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - Trace-Message "Ensure is $Ensure" - if($product) + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path + + if (-not [String]::IsNullOrEmpty($ProductId)) { - Trace-Message "product found" + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } - else + + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) { - Trace-Message "product installation cannot be determined" + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData + $getProductEntryParameters += $checkRegistryValueParameters } - 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) + $productEntry = Get-ProductEntry @getProductEntryParameters + + Write-Verbose -Message ($LocalizedData.EnsureIsEnsure -f $Ensure) + + if ($null -eq $productEntry) { - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) + Write-Verbose -Message ($LocalizedData.ProductIsProduct -f $productEntry) } else { - if ($product -ne $null) + Write-Verbose -Message 'Product installation cannot be determined' + } + + Write-Verbose -Message ($LocalizedData.ProductAsBooleanIs -f [Boolean]$productEntry) + + if ($null -ne $productEntry) + { + if ($CreateCheckRegValue) { - $name = Get-LocalizableRegKeyValue $product "DisplayName" - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) + Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $Name) } else { - $displayName = $null - if($Name) - { - $displayName = $Name - } - else - { - $displayName = $ProductId - } - - Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $displayName) } + } + else + { + $displayName = $null + if (-not [String]::IsNullOrEmpty($Name)) + { + $displayName = $Name + } + else + { + $displayName = $ProductId + } + + Write-Verbose -Message ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) } - return $res + return ($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent') } -function Get-LocalizableRegKeyValue +<# + .SYNOPSIS + Retrieves a localized registry key value. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER ValueName + The name of the value to retrieve. +#> +function Get-LocalizedRegistryKeyValue { - param( - [object] $RegKey, - [string] $ValueName + [CmdletBinding()] + param + ( + [Object] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ValueName ) - $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) - if(-not $res) + $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName) + + if ($null -eq $localizedRegistryKeyValue) { - $res = $RegKey.GetValue($ValueName) + $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName) } - return $res + return $localizedRegistryKeyValue } function Get-TargetResource { - [OutputType([System.Collections.Hashtable])] + [OutputType([Hashtable])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, + [String] + $ProductId, - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', + [Boolean] + $CreateCheckRegValue = $false, - [string] $InstalledCheckRegKey, + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', - [string] $InstalledCheckRegValueName, + [String] + $InstalledCheckRegKey, - [string] $InstalledCheckRegValueData, + [String] + $InstalledCheckRegValueName, - [boolean] $CreateCheckRegValue + [String] + $InstalledCheckRegValueData ) - #If the user gave the ProductId then we derive $identifyingNumber - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path + + if (-not [String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } + else + { + $identifyingNumber = $ProductId + } + + $packageResourceResult = @{} - $localMsi = $uri.IsFile -and -not $uri.IsUnc + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } - if(-not $product) + if ($CreateCheckRegValue) { - return @{ - Ensure = "Absent" + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData + + $getProductEntryParameters += $checkRegistryValueParameters + $packageResourceResult += $checkRegistryValueParameters + } + + $productEntry = Get-ProductEntry @getProductEntryParameters + + if ($null -eq $productEntry) + { + $packageResourceResult += @{ + Ensure = 'Absent' Name = $Name ProductId = $identifyingNumber + Path = $Path Installed = $false - InstalledCheckRegHive = $InstalledCheckRegHive - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData } - } - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + return $packageResourceResult + } + elseif ($CreateCheckRegValue) { - return @{ - Ensure = "Present" + $packageResourceResult += @{ + Ensure = 'Present' Name = $Name ProductId = $identifyingNumber + Path = $Path Installed = $true - InstalledCheckRegHive = $InstalledCheckRegHive - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData } + + return $packageResourceResult } - #$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) + <# + Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE). + If the user gave a product ID just pass it through, otherwise get it from the product. + #> + if ($null -eq $identifyingNumber -and $null -ne $productEntry.Name) { - $identifyingNumber = Split-Path -Leaf $product.Name + $identifyingNumber = Split-Path -Path $productEntry.Name -Leaf } - $date = $product.GetValue("InstallDate") - if($date) + $installDate = $productEntry.GetValue('InstallDate') + + if ($null -ne $installDate) { try { - $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date + $installDate = '{0:d}' -f [DateTime]::ParseExact($installDate, 'yyyyMMdd',[System.Globalization.CultureInfo]::CurrentCulture).Date } catch { - $date = $null + $installDate = $null } } - $publisher = Get-LocalizableRegKeyValue $product "Publisher" - $size = $product.GetValue("EstimatedSize") - if($size) + $publisher = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'Publisher' + + $estimatedSize = $productEntry.GetValue('EstimatedSize') + + if ($null -ne $estimatedSize) { - $size = $size/1024 + $estimatedSize = $estimatedSize / 1024 } - $version = $product.GetValue("DisplayVersion") - $description = $product.GetValue("Comments") - $name = Get-LocalizableRegKeyValue $product "DisplayName" - return @{ - Ensure = "Present" - Name = $name + $displayVersion = $productEntry.GetValue('DisplayVersion') + + $comments = $productEntry.GetValue('Comments') + + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + + $packageResourceResult += @{ + Ensure = 'Present' + Name = $displayName Path = $Path - InstalledOn = $date + InstalledOn = $installDate ProductId = $identifyingNumber - Size = $size + Size = $estimatedSize Installed = $true - Version = $version - PackageDescription = $description + Version = $displayVersion + PackageDescription = $comments Publisher = $publisher } + + return $packageResourceResult } -Function Get-MsiTools +<# + .SYNOPSIS + Retrieves the MSI tools type. +#> +function Get-MsiTools { - if($script:MsiTools) + [OutputType([System.Type])] + [CmdletBinding()] + param () + + if ($null -ne $script:msiTools) { - return $script:MsiTools + return $script:msiTools } - $sig = @' + $msiToolsCodeDefinition = @' [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] - private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); + private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct); [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] private static extern uint MsiCloseHandle(IntPtr hAny); @@ -569,7 +718,7 @@ Function Get-MsiTools IntPtr MsiHandle = IntPtr.Zero; try { - var res = MsiOpenPackageW(msi, out MsiHandle); + var res = MsiOpenPackageExW(msi, 1, out MsiHandle); if (res != 0) { return null; @@ -598,354 +747,618 @@ Function Get-MsiTools return GetPackageProperty(msi, "ProductName"); } '@ - $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.xPackageResource ` - -Name MsiTools -Using System.Text -MemberDefinition $sig - return $script:MsiTools + + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + $script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + $script:msiTools = Add-Type ` + -Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' ` + -Name 'MsiTools' ` + -Using 'System.Text' ` + -MemberDefinition $msiToolsCodeDefinition ` + -PassThru + } + + return $script:msiTools +} + +<# + .SYNOPSIS + Retrieves the name of a product from an msi. + + .PARAMETER Path + The path to the msi to retrieve the name from. +#> +function Get-MsiProductName +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path + ) + + $msiTools = Get-MsiTools + + $productName = $msiTools::GetProductName($Path) + + return $productName } +<# + .SYNOPSIS + Retrieves the code of a product from an msi. -Function Get-MsiProductEntry + .PARAMETER Path + The path to the msi to retrieve the code from. +#> +function Get-MsiProductCode { + [OutputType([String])] + [CmdletBinding()] param ( - [string] $Path + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path ) - if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) + $msiTools = Get-MsiTools + + $productCode = $msiTools::GetProductCode($Path) + + return $productCode +} + +<# + .SYNOPSIS + Asserts that the InstalledCheckRegKey, InstalledCheckRegValueName, and + InstalledCheckRegValueData parameter required for retrieving package installation status + from a registry are not null or empty. + + .PARAMETER InstalledCheckRegKey + The InstalledCheckRegKey parameter to check. + + .PARAMETER InstalledCheckRegValueName + The InstalledCheckRegValueName parameter to check. + + .PARAMETER InstalledCheckRegValueData + The InstalledCheckRegValueData parameter to check. + + .NOTES + This could be done with parameter validation. + It is implemented this way to provide a clearer error message. +#> +function Assert-RegistryParametersValid +{ + [CmdletBinding()] + param + ( + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData + ) + + foreach ($parameter in $PSBoundParameters.Keys) { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + if ([String]::IsNullOrEmpty($PSBoundParameters[$parameter])) + { + New-InvalidArgumentException -ArgumentName $parameter -Message ($LocalizedData.ProvideParameterForRegistryCheck -f $parameter) + } } +} + +<# + .SYNOPSIS + Sets the value of a registry key to the specified data. + + .PARAMETER Key + The registry key that contains the value to set. + + .PARAMETER Value + The value name of the registry key value to set. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to set. + + .PARAMETER Data + The data to set the registry key value to. +#> +function Set-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Key, + + [Parameter(Mandatory = $true)] + [String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, - $tools = Get-MsiTools + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Data + ) - $pn = $tools::GetProductName($Path) + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) - $pc = $tools::GetProductCode($Path) + # Opens the subkey with write access + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) - return $pn,$pc + if ($null -eq $subRegistryKey) + { + Write-Verbose "Key: '$Key'" + $subRegistryKey = $baseRegistryKey.CreateSubKey($Key) + } + + $subRegistryKey.SetValue($Value, $Data) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($LocalizedData.ErrorSettingRegistryValue -f $Key, $Value, $Data) -ErrorRecord $_ + } } +<# + .SYNOPSIS + Removes the specified value of a registry key. + + .PARAMETER Key + The registry key that contains the value to remove. + + .PARAMETER Value + The value name of the registry key value to remove. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to remove. +#> +function Remove-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Key, + + [Parameter(Mandatory = $true)] + [String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive + ) + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) + + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) + $subRegistryKey.DeleteValue($Value) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($LocalizedData.ErrorRemovingRegistryValue -f $Key, $Value) -ErrorRecord $_ + } +} function Set-TargetResource { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, + [String] + $ProductId, - [string] $Arguments, + [String] + $Arguments, - [pscredential] $Credential, + [PSCredential] + $Credential, - [System.UInt32[]] $ReturnCode, + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [ValidateNotNullOrEmpty()] + [UInt32[]] + $ReturnCode = @( 0, 1641, 3010 ), - [string] $LogPath, + [String] + $LogPath, - [pscredential] $RunAsCredential, + [String] + $FileHash, - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [String] + $HashAlgorithm, - [string] $InstalledCheckRegKey, + [String] + $SignerSubject, - [string] $InstalledCheckRegValueName, + [String] + $SignerThumbprint, - [string] $InstalledCheckRegValueData, - - [boolean] $CreateCheckRegValue, + [String] + $ServerCertificateValidationCallback, - [string] $FileHash, + [Boolean] + $CreateCheckRegValue = $false, - [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] - [string] $HashAlgorithm, + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, - [string] $SignerSubject, - [string] $SignerThumbprint, + [String] + $InstalledCheckRegValueName, - [string] $ServerCertificateValidationCallback + [String] + $InstalledCheckRegValueData ) - $ErrorActionPreference = "Stop" + $ErrorActionPreference = 'Stop' - if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` - -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` - -InstalledCheckRegValueData $InstalledCheckRegValueData)) + if (Test-TargetResource @PSBoundParameters) { 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 + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path - Write-Verbose $LocalizedData.PackageConfigurationStarting - if(-not $ReturnCode) + if (-not [String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } + else { - $ReturnCode = @(0) + $identifyingNumber = $ProductId } + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + <# + Path gets overwritten in the download code path. Retain the user's original Path in case + the install succeeded but the named package wasn't present on the system afterward so we + can give a better error message. + #> + $originalPath = $Path + + Write-Verbose -Message $LocalizedData.PackageConfigurationStarting + $logStream = $null - $psdrive = $null + $psDrive = $null $downloadedFileName = $null + try { $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() - if($LogPath) + if (-not [String]::IsNullOrEmpty($LogPath)) { try { - if($fileExtension -eq ".msi") + 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)) + <# + We want to pre-verify the log path exists and is writable ahead of time + even in the MSI case, as detecting WHY the MSI log path doesn't exist would + be rather problematic for the user. + #> + if ((Test-Path -Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile, $null, $null)) { - rm $LogPath + Remove-Item -Path $LogPath } - if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + if ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { - New-Item -Type File $LogPath | Out-Null + New-Item -Path $LogPath -Type 'File' | Out-Null } } - elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + elseif ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { - $logStream = new-object "System.IO.StreamWriter" $LogPath,$false + $logStream = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $LogPath, $false ) } } catch { - Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ + New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_ } } - #Download or mount file as necessary - if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) + # 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)) + if ($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) { - $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)} - if($Credential) + $psDriveArgs = @{ + Name = [Guid]::NewGuid() + PSProvider = 'FileSystem' + Root = Split-Path -Path $uri.LocalPath + } + + # If we pass a null for Credential, a dialog will pop up. + if ($null -ne $Credential) { - #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 + $psDriveArgs['Credential'] = $Credential } - $psdrive = New-PSDrive @psdriveArgs - $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary? + $psDrive = New-PSDrive @psDriveArgs + $Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) } - elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) + elseif (@( 'http', 'https' ) -contains $uri.Scheme -and $Ensure -eq 'Present' -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) { - $scheme = $uri.Scheme + $uriScheme = $uri.Scheme $outStream = $null $responseStream = $null try { - Trace-Message "Creating cache location" + Write-Verbose -Message ($LocalizedData.CreatingCacheLocation) - if(-not (Test-Path -PathType Container $CacheLocation)) + if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container')) { - mkdir $CacheLocation | Out-Null + New-Item -Path $script:packageCacheLocation -ItemType 'Directory' | Out-Null } - $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) + $destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) - Trace-Message "Need to download file from $scheme, destination will be $destName" + Write-Verbose -Message ($LocalizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $uriScheme, $destinationPath) try { - Trace-Message "Creating the destination cache file" - $outStream = New-Object System.IO.FileStream $destName, "Create" + Write-Verbose -Message ($LocalizedData.CreatingTheDestinationCacheFile) + $outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' ) } catch { - #Should never happen since we own the cache directory - Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ + # Should never happen since we own the cache directory + New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_ } 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") + Write-Verbose -Message ($LocalizedData.CreatingTheSchemeStream -f $uriScheme) + $webRequest = [System.Net.WebRequest]::Create($uri) + + Write-Verbose -Message ($LocalizedData.SettingDefaultCredential) + $webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials + + if ($uriScheme -eq 'http') { - Trace-Message "Setting authentication level" - # default value is MutualAuthRequested, which applies to https scheme - $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + # Default value is MutualAuthRequested, which applies to the https scheme + Write-Verbose -Message ($LocalizedData.SettingAuthenticationLevel) + $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None } - if ($scheme -eq "https" -and -not [string]::IsNullOrEmpty($ServerCertificateValidationCallback)) + elseif ($uriScheme -eq 'https' -and -not [String]::IsNullOrEmpty($ServerCertificateValidationCallback)) { - Trace-Message "Assigning user-specified certificate verification callback" - $scriptBlock = [scriptblock]::Create($ServerCertificateValidationCallback) - $request.ServerCertificateValidationCallBack = $scriptBlock + Write-Verbose -Message 'Assigning user-specified certificate verification callback' + $serverCertificateValidationScriptBlock = [ScriptBlock]::Create($ServerCertificateValidationCallback) + $webRequest.ServerCertificateValidationCallBack = $serverCertificateValidationScriptBlock } - Trace-Message "Getting the $scheme response stream" - $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() + + Write-Verbose -Message ($LocalizedData.Gettingtheschemeresponsestream -f $uriScheme) + $responseStream = (([System.Net.HttpWebRequest]$webRequest).GetResponse()).GetResponseStream() } catch { - Trace-Message ("Error: " + ($_ | Out-String)) - Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ + Write-Verbose -Message ($LocalizedData.ErrorOutString -f ($_ | Out-String)) + New-InvalidOperationException -Message ($LocalizedData.CouldNotGetHttpStream -f $uriScheme, $Path) -ErrorRecord $_ } try { - Trace-Message "Copying the $scheme stream bytes to the disk cache" + Write-Verbose -Message ($LocalizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $uriScheme) $responseStream.CopyTo($outStream) $responseStream.Flush() $outStream.Flush() } catch { - Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ + New-InvalidOperationException -Message ($LocalizedData.ErrorCopyingDataToFile -f $Path, $destinationPath) -ErrorRecord $_ } } finally { - if($outStream) + if ($null -ne $outStream) { $outStream.Close() } - if($responseStream) + if ($null -ne $responseStream) { $responseStream.Close() } } - Trace-Message "Redirecting package path to cache file location" - $Path = $downloadedFileName = $destName + + Write-Verbose -Message ($LocalizedData.RedirectingPackagePathToCacheFileLocation) + $Path = $destinationPath + $downloadedFileName = $destinationPath } - } - 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)) + # At this point the Path ought to be valid unless it's a MSI uninstall case + if (-not (Test-Path -Path $Path -PathType 'Leaf')) { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + New-InvalidOperationException -Message ($LocalizedData.PathDoesNotExist -f $Path) } - ValidateFile -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint + Assert-FileValid -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 + $startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' + + # Necessary for I/O redirection and just generally a good idea + $startInfo.UseShellExecute = $false + + $process = New-Object -TypeName 'System.Diagnostics.Process' $process.StartInfo = $startInfo - $errLogPath = $LogPath + ".err" #Concept only, will never touch disk - if($fileExtension -eq ".msi") + + # Concept only, will never touch disk + $errorLogPath = $LogPath + ".err" + + if ($fileExtension -eq '.msi') { - $startInfo.FileName = "$env:windir\system32\msiexec.exe" - if($Ensure -eq "Present") + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + if ($Ensure -eq 'Present') { - # check if Msi package contains the ProductName and Code specified + # Check if the MSI package specifies the ProductName and Code + $productName = Get-MsiProductName -Path $Path + $productCode = Get-MsiProductCode -Path $Path - $pName,$pCode = Get-MsiProductEntry -Path $Path + if ((-not [String]::IsNullOrEmpty($Name)) -and ($productName -ne $Name)) + { + New-InvalidArgumentException -ArgumentName 'Name' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) + } - if ( - ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` - -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) - ) + if ((-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode)) { - Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) } $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) + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + # We may have used the Name earlier, now we need the actual ID + $id = Split-Path -Path $productEntry.Name -Leaf + $startInfo.Arguments = '/x{0}' -f $id } - if($LogPath) + if ($LogPath) { $startInfo.Arguments += ' /log "{0}"' -f $LogPath } $startInfo.Arguments += " /quiet" - if($Arguments) + if ($Arguments) { - $startInfo.Arguments += " " + $Arguments + $startInfo.Arguments += "$Arguments" } } - else #EXE + else { - Trace-Message "The binary is an EXE" - $startInfo.FileName = $Path - $startInfo.Arguments = $Arguments - if($LogPath) + # EXE + Write-Verbose -Message $LocalizedData.TheBinaryIsAnExe + + if ($Ensure -eq 'Present') { - 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 + $startInfo.FileName = $Path + $startInfo.Arguments = $Arguments + + if ($LogPath) + { + Write-Verbose -Message ($LocalizedData.UserHasRequestedLoggingNeedToAttachEventHandlersToTheProcess) + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardOutput = $true + + Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -SourceIdentifier $LogPath + Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -SourceIdentifier $errorLogPath + } + } + else + { + # Absent case + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + # We may have used the Name earlier, now we need the actual ID + if ($null -eq $productEntry.Name) + { + $id = $Path + } + else + { + $id = Split-Path -Path $productEntry.Name -Leaf + } + + $startInfo.Arguments = "/x $id /quiet /norestart" + + if ($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + if ($Arguments) + { + $startInfo.Arguments += "$Arguments" + } } } - Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) + Write-Verbose -Message ($LocalizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) - if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) + if ($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) { try { $exitCode = 0 - if($PSBoundParameters.ContainsKey("RunAsCredential")) + $process.Start() | Out-Null + + # Identical to $fileExtension -eq '.exe' -and $logPath + if ($logStream) { - CallPInvoke - [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` - $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` - $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() } - else - { - $process.Start() | Out-Null - - if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath - { - $process.BeginOutputReadLine(); - $process.BeginErrorReadLine(); - } + + $process.WaitForExit() - $process.WaitForExit() - - if($process) - { - $exitCode = $process.ExitCode - } + if ($process) + { + $exitCode = $process.ExitCode } } catch { - Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ + New-InvalidOperationException -Message ($LocalizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_ } - if($logStream) + 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 @@ -965,44 +1378,54 @@ function Set-TargetResource Remove-Event -SourceIdentifier $errLogPath } - if(-not ($ReturnCode -contains $exitCode)) + if (-not ($ReturnCode -contains $exitCode)) { - Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + # Some .exe files do not support uninstall + if ($Ensure -eq 'Absent' -and $fileExtension -eq '.exe' -and $exitCode -eq '1620') + { + Write-Warning -Message ($LocalizedData.ExeCouldNotBeUninstalled -f $Path) + } + else + { + New-InvalidOperationException ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + } } } } finally { - if($psdrive) + if ($psDrive) { - Remove-PSDrive -Force $psdrive + Remove-PSDrive -Name $psDrive -Force } - if($logStream) + if ($logStream) { $logStream.Dispose() } } - if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) + 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 + <# + This is deliberately not in the finally block because we want to leave the downloaded + file on disk if an error occurred as a debugging aid for the user. + #> + Remove-Item -Path $downloadedFileName } - $operationString = $LocalizedData.PackageUninstalled - if($Ensure -eq "Present") + $operationMessageString = $LocalizedData.PackageUninstalled + if ($Ensure -eq 'Present') { - $operationString = $LocalizedData.PackageInstalled + $operationMessageString = $LocalizedData.PackageInstalled } - if($CreateCheckRegValue -eq $true) + if ($CreateCheckRegValue) { $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName - if($Ensure -eq 'Present') + if ($Ensure -eq 'Present') { - Write-Verbose ($LocalizedData.CreatingRegistryValue -f $registryValueString) + Write-Verbose -Message ($LocalizedData.CreatingRegistryValue -f $registryValueString) Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData } else @@ -1012,386 +1435,142 @@ function Set-TargetResource } } - # 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) + <# + Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is + missing on some client SKUs (worked on both Server and Client Skus in Windows 10). + #> + + $serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -Arguments @{ BatchSize = 256 } -ErrorAction 'Ignore' + $registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore' + + if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641) { Write-Verbose $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 + if ($Ensure -eq 'Present') { - //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; + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber } - [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 + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData } - [Flags] - public enum LogonProvider + if ($CreateCheckRegValue) { - 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; + $getProductEntryParameters += $checkRegistryValueParameters } - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } + $productEntry = Get-ProductEntry @getProductEntryParameters - public enum TOKEN_TYPE + if ($null -eq $productEntry) { - TokenPrimary = 1, - TokenImpersonation + New-InvalidOperationException -Message ($LocalizedData.PostValidationError -f $originalPath) } + } - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct TokPriv1Luid - { - public int Count; - public long Luid; - public int Attr; - } + Write-Verbose -Message $operationMessageString + Write-Verbose -Message $LocalizedData.PackageConfigurationComplete +} - 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()); - } +<# + .SYNOPSIS + Asserts that the file at the given path is valid. - 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()); - } + .PARAMETER Path + The path to the file to check. - int status = WaitForSingleObject(pi.hProcess, -1); - if(status == -1) - { - throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); - } + .PARAMETER FileHash + The hash that should match the hash of the file. - 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); - } - } - } - } -} + .PARAMETER HashAlgorithm + The algorithm to use to retrieve the file hash. -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} + .PARAMETER SignerThumbprint + The certificate thumbprint that should match the file's signer certificate. -function ValidateFile + .PARAMETER SignerSubject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileValid { [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, + + [String] + $FileHash, + + [String] + $HashAlgorithm, + + [String] + $SignerThumbprint, - [string] $FileHash, - [string] $HashAlgorithm, - [string] $SignerThumbprint, - [string] $SignerSubject + [String] + $SignerSubject ) - if ($FileHash) + if (-not [String]::IsNullOrEmpty($FileHash)) { - ValidateFileHash -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm + Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm } - if ($SignerThumbprint -or $SignerSubject) + if (-not [String]::IsNullOrEmpty($SignerThumbprint) -or -not [String]::IsNullOrEmpty($SignerSubject)) { - ValidateFileSignature -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject + Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject } } -function ValidateFileHash +<# + .SYNOPSIS + Asserts that the hash of the file at the given path matches the given hash. + + .PARAMETER Path + The path to the file to check the hash of. + + .PARAMETER Hash + The hash to check against. + + .PARAMETER Algorithm + The algorithm to use to retrieve the file's hash. +#> +function Assert-FileHashValid { [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, [Parameter(Mandatory)] - [string] $Hash, + [String] + $Hash, - [string] $Algorithm + [String] + $Algorithm = 'SHA256' ) - if ([string]::IsNullOrEmpty($Algorithm)) { $Algorithm = 'SHA256' } + if ([String]::IsNullOrEmpty($Algorithm)) + { + $Algorithm = 'SHA256' + } - Trace-Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) + Write-Verbose -Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) - $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction Stop + $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction 'Stop' if ($fileHash.Hash -ne $Hash) { @@ -1399,21 +1578,38 @@ function ValidateFileHash } } -function ValidateFileSignature +<# + .SYNOPSIS + Asserts that the signature of the file at the given path is valid. + + .PARAMETER Path + The path to the file to check the signature of + + .PARAMETER Thumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER Subject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileSignatureValid { [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, - [string] $Thumbprint, + [String] + $Thumbprint, - [string] $Subject + [String] + $Subject ) - Trace-Message ($LocalizedData.CheckingFileSignature -f $Path) + Write-Verbose -Message ($LocalizedData.CheckingFileSignature -f $Path) - $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction Stop + $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction 'Stop' if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { @@ -1421,18 +1617,18 @@ function ValidateFileSignature } else { - Trace-Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) + Write-Verbose -Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) } - if ($Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) + if ($null -ne $Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) { throw ($LocalizedData.WrongSignerSubject -f $Path, $Subject) } - if ($Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) + if ($null -ne $Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) { throw ($LocalizedData.WrongSignerThumbprint -f $Path, $Thumbprint) } } -Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 57b72c3e4..d143c4751 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof @@ -1,33 +1,28 @@ - -[ClassVersion("1.0.0"),FriendlyName("xPackage")] -class MSFT_xPackageResource : OMI_BaseResource -{ - [write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; - [Key] string Name; - [required] string Path; - [Key] string ProductId; - [write] string Arguments; - [write,EmbeddedInstance("MSFT_Credential")] string Credential; - [write] uint32 ReturnCode[]; - [write] string LogPath; - [read] string PackageDescription; - [read] string Publisher; - [read] string InstalledOn; - [read] uint32 Size; - [read] string Version; - [read] boolean Installed; - [write,EmbeddedInstance("MSFT_Credential")] string RunAsCredential; - [write] string FileHash; - [write,ValueMap{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"},Values{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"}] string HashAlgorithm; - [write] string SignerSubject; - [write] string SignerThumbprint; - [write] string ServerCertificateValidationCallback; - [write,ValueMap{"LocalMachine", "CurrentUser"},Values{"LocalMachine", "CurrentUser"}] string InstalledCheckRegHive; - [write] string InstalledCheckRegKey; - [write] string InstalledCheckRegValueName; - [write] string InstalledCheckRegValueData; - [write] boolean CreateCheckRegValue; -}; - - - +[ClassVersion("1.0.0"),FriendlyName("xPackage")] +class MSFT_xPackageResource : OMI_BaseResource +{ + [write,ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] string Ensure; + [Key] string Name; + [required] string Path; + [Key] string ProductId; + [write] string Arguments; + [write,EmbeddedInstance("MSFT_Credential")] string Credential; + [write] uint32 ReturnCode[]; + [write] string LogPath; + [read] string PackageDescription; + [read] string Publisher; + [read] string InstalledOn; + [read] uint32 Size; + [read] string Version; + [read] boolean Installed; + [write] string FileHash; + [write,ValueMap{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"},Values{"SHA1","SHA256","SHA384","SHA512","MD5","RIPEMD160"}] string HashAlgorithm; + [write] string SignerSubject; + [write] string SignerThumbprint; + [write] string ServerCertificateValidationCallback; + [write,ValueMap{"LocalMachine","CurrentUser"},Values{"LocalMachine","CurrentUser"}] string InstalledCheckRegHive; + [write] string InstalledCheckRegKey; + [write] string InstalledCheckRegValueName; + [write] string InstalledCheckRegValueData; + [write] boolean CreateCheckRegValue; +}; diff --git a/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 b/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 deleted file mode 100644 index 4b65c10b3..000000000 --- a/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -#requires -Version 4.0 - -Remove-Module MSFT_xPackageResource -ErrorAction Ignore -$module = Import-Module $PSScriptRoot\..\MSFT_xPackageResource.psm1 -Force -PassThru -ErrorAction Stop - -Describe 'Get-MsiTools' { - It 'Uses Add-Type with a name that does not conflict with the original Package resource' { - InModuleScope MSFT_xPackageResource { - $hash = @{ Namespace = 'Mock not called' } - Mock Add-Type { $hash['Namespace'] = $Namespace } - $null = Get-MsiTools - - $hash['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' - } - } -} diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 64db5a61a..15fc1cbcf 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -2,990 +2,878 @@ data LocalizedData { # culture="en-US" ConvertFrom-StringData @' -FileNotFound=File not found in the environment path. -AbsolutePathOrFileName=Absolute path or file name expected. -InvalidArgument=Invalid argument: '{0}' with value: '{1}'. -InvalidArgumentAndMessage={0} {1} -ProcessStarted=Process matching path '{0}' started -ProcessesStopped=Proceses matching path '{0}' with Ids '({1})' stopped. -ProcessAlreadyStarted=Process matching path '{0}' found running and no action required. -ProcessAlreadyStopped=Process matching path '{0}' not found running and no action required. -ErrorStopping=Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. -ErrorStarting=Failure starting process matching path '{0}'. Message: {1}. -StartingProcessWhatif=Start-Process -ProcessNotFound=Process matching path '{0}' not found -PathShouldBeAbsolute="The path should be absolute" -PathShouldExist="The path should exist" -ParameterShouldNotBeSpecified="Parameter {0} should not be specified." -FailureWaitingForProcessesToStart="Failed to wait for processes to start" -FailureWaitingForProcessesToStop="Failed to wait for processes to stop" +FileNotFound = File not found in the environment path. +AbsolutePathOrFileName = Absolute path or file name expected. +InvalidArgument = Invalid argument: '{0}' with value: '{1}'. +InvalidArgumentAndMessage = {0} {1} +ProcessStarted = Process matching path '{0}' started +ProcessesStopped = Proceses matching path '{0}' with Ids '({1})' stopped. +ProcessAlreadyStarted = Process matching path '{0}' found running and no action required. +ProcessAlreadyStopped = Process matching path '{0}' not found running and no action required. +ErrorStopping = Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. +ErrorStarting = Failure starting process matching path '{0}'. Message: {1}. +StartingProcessWhatif = Start-Process +StoppingProcessWhatIf = Stop-Process +ProcessNotFound = Process matching path '{0}' not found +PathShouldBeAbsolute = The path should be absolute +PathShouldExist = The path should exist +ParameterShouldNotBeSpecified = Parameter {0} should not be specified. +FailureWaitingForProcessesToStart = Failed to wait for processes to start +FailureWaitingForProcessesToStop = Failed to wait for processes to stop +ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, StandardInputPath or WorkingDirectory when trying to run a process under a user context. +VerboseInProcessHandle = In process handle {0} +ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter. +ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredential parameter is not supported by the Process resource, and cannot be used with the Credential parameter. To start the process with user '{0}', use only the Credential parameter, not the PsDscRunAsCredential parameter. '@ } -# Commented-out until more languages are supported +# Commented out until more languages are supported # Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1 -function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[string[]]$newArgumentNames) +Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1" + +<# + .SYNOPSIS + Tests if the current user is from the local system. +#> +function Test-IsRunFromLocalSystemUser { - $returnValue=@{} - for($i=0;$i -lt $argumentNames.Count;$i++) - { - $argumentName=$argumentNames[$i] + [OutputType([Boolean])] + [CmdletBinding()] + param () + + + $currentUser = (New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList @( [Security.Principal.WindowsIdentity]::GetCurrent() )) + + return $currenUser.Identity.IsSystem +} + +<# + .SYNOPSIS + Splits a credential into a username and domain wihtout calling GetNetworkCredential. + Calls to GetNetworkCredential expose the password as plain text in memory. + + .PARAMETER Credential + The credential to pull the username and domain out of. + + .NOTES + Supported formats: DOMAIN\username, username@domain +#> +function Split-Credential +{ + [OutputType([Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSCredential] + $Credential + ) + + $wrongFormat = $false - if($newArgumentNames -eq $null) + if ($Credential.UserName.Contains('\')) + { + $credentialSegments = $Credential.UserName.Split('\') + + if ($credentialSegments.Length -gt 2) { - $newArgumentName=$argumentName - } + # i.e. domain\user\foo + $wrongFormat = $true + } else { - $newArgumentName=$newArgumentNames[$i] + $domain = $credentialSegments[0] + $userName = $credentialSegments[1] } + } + elseif ($Credential.UserName.Contains('@')) + { + $credentialSegments = $Credential.UserName.Split('@') - if($functionBoundParameters.ContainsKey($argumentName)) + if ($credentialSegments.Length -gt 2) { - $null=$returnValue.Add($newArgumentName,$functionBoundParameters[$argumentName]) + # i.e. user@domain@foo + $wrongFormat = $true } + else + { + $UserName = $credentialSegments[0] + $Domain = $credentialSegments[1] + } + } + else + { + # support for default domain (localhost) + $domain = $env:computerName + $userName = $Credential.UserName + } + + if ($wrongFormat) + { + $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName + + Write-Verbose -Message $message + + New-InvalidArgumentException -ArgumentName 'Credential' -Message $message } - return $returnValue + return @{ + Domain = $domain + UserName = $userName + } } function Get-TargetResource { - [OutputType([System.Collections.Hashtable])] + [OutputType([Hashtable])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential ) + + $Path = Expand-Path -Path $Path - $Path=(ResolvePath $Path) - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } - if($processes.Count -eq 0) + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential + } + + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) + + if ($win32Processes.Count -eq 0) { return @{ - Path=$Path - Arguments=$Arguments - Ensure='Absent' + Path = $Path + Arguments = $Arguments + Ensure ='Absent' } } - foreach($process in $processes) + foreach ($win32Process in $win32Processes) { - # in case the process was killed between GetWin32_Process and this point, we should - # ignore errors which will generate empty entries in the return - $gpsProcess = (get-process -id $process.ProcessId -ErrorAction Ignore) + $getProcessResult = Get-Process -ID $win32Process.ProcessId -ErrorAction 'Ignore' - @{ - Path=$process.Path - Arguments=(GetProcessArgumentsFromCommandLine $process.CommandLine) - PagedMemorySize=$gpsProcess.PagedMemorySize64 - NonPagedMemorySize=$gpsProcess.NonpagedSystemMemorySize64 - VirtualMemorySize=$gpsProcess.VirtualMemorySize64 - HandleCount=$gpsProcess.HandleCount - Ensure='Present' - ProcessId=$process.ProcessId + return @{ + Path = $win32Process.Path + Arguments = (Get-ArgumentsFromCommandLineInput -CommandLineInput $win32Process.CommandLine) + PagedMemorySize = $getProcessResult.PagedMemorySize64 + NonPagedMemorySize = $getProcessResult.NonpagedSystemMemorySize64 + VirtualMemorySize = $getProcessResult.VirtualMemorySize64 + HandleCount = $getProcessResult.HandleCount + Ensure = 'Present' + ProcessId = $win32Process.ProcessId } } } - function Set-TargetResource { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - [System.String] - [ValidateSet("Present", "Absent")] - $Ensure="Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [System.String] + [String] $StandardOutputPath, - [System.String] + [String] $StandardErrorPath, - [System.String] + [String] $StandardInputPath, - [System.String] + [String] $WorkingDirectory ) - $Path=ResolvePath $Path - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + if ($null -ne $PsDscContext.RunAsUser) + { + New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + + $Path = Expand-Path -Path $Path + + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } - if($Ensure -eq 'Absent') + if ($null -ne $Credential) { - "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory" | AssertParameterIsNotSpecified $PSBoundParameters + $getWin32ProcessArguments['Credential'] = $Credential + } - if ($processes.Count -gt 0) - { - $processIds=$processes.ProcessId + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) + + if ($Ensure -eq 'Absent') + { + Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - $err=Stop-Process -Id $processIds -force 2>&1 + if ($win32Processes.Count -gt 0 -and $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif)) + { + $processIds = $win32Processes.ProcessId - if($err -eq $null) + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 + + if ($null -eq $stopProcessError) { - Write-Log ($LocalizedData.ProcessesStopped -f $Path,($processIds -join ",")) + Write-Verbose -Message ($LocalizedData.ProcessesStopped -f $Path, ($processIds -join ',')) } else { - Write-Log ($LocalizedData.ErrorStopping -f $Path,($processIds -join ","),($err | out-string)) - throw $err + Write-Verbose -Message ($LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), ($stopProcessError | Out-String)) + throw $stopProcessError } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work - if (!(WaitForProcessCount @getArguments -waitCount 0)) + if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 0)) { - $message = $LocalizedData.ErrorStopping -f $Path,($processIds -join ","),$LocalizedData.FailureWaitingForProcessesToStop - Write-Log $message - ThrowInvalidArgumentError "FailureWaitingForProcessesToStop" $message + $message = $LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), $LocalizedData.FailureWaitingForProcessesToStop + + Write-Verbose -Message $message + + New-InvalidOperationException -Message $message } } else { - Write-Log ($LocalizedData.ProcessAlreadyStopped -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessAlreadyStopped -f $Path) } } else { - "StandardInputPath","WorkingDirectory" | AssertAbsolutePath $PSBoundParameters -Exist - "StandardOutputPath","StandardErrorPath" | AssertAbsolutePath $PSBoundParameters + $shouldBeRootedPathArguments = @( 'StandardInputPath', 'WorkingDirectory', 'StandardOutputPath', 'StandardErrorPath' ) - if ($processes.Count -eq 0) + foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) { - $startArguments = ExtractArguments $PSBoundParameters ` - ("Path", "Arguments", "Credential", "StandardOutputPath", "StandardErrorPath", "StandardInputPath", "WorkingDirectory") ` - ("FilePath", "ArgumentList", "Credential", "RedirectStandardOutput", "RedirectStandardError", "RedirectStandardInput", "WorkingDirectory") + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) + { + Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] + } + } - if([string]::IsNullOrEmpty($Arguments)) + $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($shouldExistPathArgument in $shouldExistPathArguments) + { + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) { - $null=$startArguments.Remove("ArgumentList") + Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] } + } - if($PSCmdlet.ShouldProcess($Path,$LocalizedData.StartingProcessWhatif)) + if ($win32Processes.Count -eq 0) + { + $startProcessArguments = @{ + FilePath = $Path + } + + $startProcessOptionalArgumentMap = @{ + Credential = 'Credential' + RedirectStandardOutput = 'StandardOutputPath' + RedirectStandardError = 'StandardErrorPath' + RedirectStandardInput = 'StandardInputPath' + WorkingDirectory = 'WorkingDirectory' + } + + foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - if($PSBoundParameters.ContainsKey("Credential")) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]])) { - $argumentError = $false + $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]] + } + } + + if (-not [String]::IsNullOrEmpty($Arguments)) + { + $startProcessArguments['ArgumentList'] = $Arguments + } + + if ($PSCmdlet.ShouldProcess($Path, $LocalizedData.StartingProcessWhatif)) + { + <# + Start-Process calls .net Process.Start() + If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + CreateProcessWithLogonW cannot be called as LocalSystem user. + Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003) + + In this case we call another api. + #> + if ($PSBoundParameters.ContainsKey('Credential') -and (Test-IsRunFromLocalSystemUser)) + { + if ($PSBoundParameters.ContainsKey('StandardOutputPath')) + { + New-InvalidArgumentException -ArgumentName 'StandardOutputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + if ($PSBoundParameters.ContainsKey('StandardInputPath')) + { + New-InvalidArgumentException -ArgumentName 'StandardInputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + if ($PSBoundParameters.ContainsKey('WorkingDirectory')) + { + New-InvalidArgumentException -ArgumentName 'WorkingDirectory' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + $splitCredentialResult = Split-Credential $Credential try { - if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) - { - $argumentError = $true - $errorMessage = "Can't specify StandardOutptPath, StandardInputPath or WorkingDirectory when trying to run a process under a user context" - throw $errorMessage - } - else - { - CallPInvoke - [Source.NativeMethods]::CreateProcessAsUser(("$Path "+$Arguments), $Credential.GetNetworkCredential().Domain, $Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password) - } + <# + Internally we use win32 api LogonUser() with dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + It grants process ability for second-hop. + #> + Import-DscNativeMethods + + [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $domain, $userName, $Credential.Password, $false, [Ref]$null ) } catch { - $exception = New-Object System.ArgumentException $_; - if($argumentError) - { - $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"Invalid combination of arguments", $errorCategory, $null - } - else - { - $errorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "Win32Exception", $errorCategory, $null - } - $err = $errorRecord + throw (New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList @( $_.Exception, 'Win32Exception', 'OperationStopped', $null )) } - } else { - $err=Start-Process @startArguments 2>&1 + $startProcessError = Start-Process @startProcessArguments 2>&1 } - if($err -eq $null) + + if ($null -eq $startProcessError) { - Write-Log ($LocalizedData.ProcessStarted -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessStarted -f $Path) } else { - Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) - throw $err + Write-Verbose -Message ($LocalizedData.ErrorStarting -f $Path, ($startProcessError | Out-String)) + throw $startProcessError } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work - if (!(WaitForProcessCount @getArguments -waitCount 1)) + if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 1)) { - $message = $LocalizedData.ErrorStarting -f $Path,$LocalizedData.FailureWaitingForProcessesToStart - Write-Log $message - ThrowInvalidArgumentError "FailureWaitingForProcessesToStart" $message + $message = $LocalizedData.ErrorStarting -f $Path, $LocalizedData.FailureWaitingForProcessesToStart + + Write-Verbose -Message $message + + New-InvalidOperationException -Message $message } } } else { - Write-Log ($LocalizedData.ProcessAlreadyStarted -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessAlreadyStarted -f $Path) } } } function Test-TargetResource { - [OutputType([System.Boolean])] + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - [System.String] - [ValidateSet("Present", "Absent")] - $Ensure="Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [System.String] + [String] $StandardOutputPath, - [System.String] + [String] $StandardErrorPath, - [System.String] + [String] $StandardInputPath, - [System.String] + [String] $WorkingDirectory ) - $Path=ResolvePath $Path - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + if ($null -ne $PsDscContext.RunAsUser) + { + New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + + $Path = Expand-Path -Path $Path + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential + } + + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) - if($Ensure -eq 'Absent') + if ($Ensure -eq 'Absent') { - return ($processes.Count -eq 0) + return ($win32Processes.Count -eq 0) } else { - return ($processes.Count -gt 0) + return ($win32Processes.Count -gt 0) } } -function GetWin32ProcessOwner +<# + .SYNOPSIS + Retrieves the owner of a Win32_Process. + + .PARAMETER Process + The Win32_Process to retrieve the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-Win32ProcessOwner { + [OutputType([String])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] - $process + $Process ) - # if the process was killed by the time this is called, GetOwner - # will throw a WMIMethodException "Not found" - try - { - $owner = $process.GetOwner() - } - catch - { - } - - if($owner.Domain -ne $null) + $owner = Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' -ErrorAction 'SilentlyContinue' + + if ($null -ne $owner.Domain) { - return $owner.Domain + "\" + $owner.User + return $owner.Domain + '\' + $owner.User } - else + else { return $owner.User } } -function WaitForProcessCount +<# + .SYNOPSIS + Waits for the given number of processes with the given settings to be running. + + .PARAMETER ProcessSettings + The settings of the running process(s) to get the count of. + + .PARAMETER ProcessCount + The number of processes running to wait for. +#> +function Wait-ProcessCount { - [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $Path, + [Hashtable] + $ProcessSettings, - [System.String] - $Arguments, - - [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] - $Credential, - - [parameter(Mandatory=$true)] - $waitCount + [Parameter(Mandatory = $true)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $ProcessCount ) - $start = [DateTime]::Now + $startTime = [DateTime]::Now + do { - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $value = @(GetWin32_Process @getArguments).Count -eq $waitCount - } while(!$value -and ([DateTime]::Now - $start).TotalMilliseconds -lt 2000) - - return $value + $actualProcessCount = @( Get-Win32Process @ProcessSettings ).Count + } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt 2000) + + return $actualProcessCount -eq $ProcessCount } -function GetWin32_Process +<# + .SYNOPSIS + Retrieves any Win32_Process objects that match the given path, arguments, and credential. + + .PARAMETER Path + The path that should match the retrieved process. + + .PARAMETER Arguments + The arguments that should match the retrieved process. + + .PARAMETER Credential + The credential whose user name should match the owner of the process. + + .PARAMETER UseGetCimInstanceThreshold + If the number of processes returned by the Get-Process method is greater than or equal to + this value, this function will retrieve all processes at the executable path. This will + help the function execute faster. Otherwise, this function will retrieve each Win32_Process + objects with the product ids returned from Get-Process. +#> +function Get-Win32Process { - [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([Object[]])] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - $useWmiObjectCount=8 + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $UseGetCimInstanceThreshold = 8 ) + $processName = [IO.Path]::GetFileNameWithoutExtension($Path) + $getProcessResult = @( Get-Process -Name $processName -ErrorAction 'SilentlyContinue' ) - $fileName = [io.path]::GetFileNameWithoutExtension($Path) + $processes = @() - $gpsProcesses = @(get-process -Name $fileName -ErrorAction SilentlyContinue) - - if($gpsProcesses.Count -ge $useWmiObjectCount) + if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) { - # if there are many processes it is faster to perform a Get-WmiObject - # in order to get Win32_Process objects for all processes - Write-Verbose "When gpsprocess.count is greater than usewmiobjectcount" - $Path=WQLEscape $Path - $filter = "ExecutablePath = '$Path'" - $processes = Get-WmiObject Win32_Process -Filter $filter + + $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path + $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'" + + $processes = Get-CimInstance -ClassName 'Win32_Process' -Filter $wqlFilter } else { - # if there are few processes, building a Win32_Process for - # each matching result of get-process is faster - $processes = foreach($gpsProcess in $gpsProcesses) + foreach ($process in $getProcessResult) { - if(!($gpsProcess.Path -ieq $Path)) - { - continue - } - - try - { - Write-Verbose "in process handle, $($gpsProcess.Id)" - [wmi]"Win32_Process.Handle='$($gpsProcess.Id)'" - } - catch + if ($process.Path -ieq $Path) { - #ignore if could not retrieve process + Write-Verbose -Message ($LocalizedData.VerboseInProcessHandle -f $process.Id) + $processes += Get-CimInstance -ClassName 'Win32_Process' -Filter "ProcessId = $($process.Id)" -ErrorAction 'SilentlyContinue' } } } - if($PSBoundParameters.ContainsKey('Credential')) + if ($PSBoundParameters.ContainsKey('Credential')) { - # Since there are credentials we need to call the GetOwner method in each process to search for matches - $processes = $processes | where { (GetWin32ProcessOwner $_) -eq $Credential.UserName } + $splitCredentialResult = Split-Credenital -Credential $Credential + + $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$($splitCredentialResult.Domain)\$($splitCredentialResult.UserName)" } + } + if ($null -eq $Arguments) + { + $Arguments = [String]::Empty } - if($Arguments -eq $null) {$Arguments = ""} - $processes = $processes | where { (GetProcessArgumentsFromCommandLine $_.CommandLine) -eq $Arguments } + $processesWithMatchingArguments = @() - return $processes + foreach ($process in $processes) + { + if ((Get-ArgumentsFromCommandLineInput -CommandLineInput ($process.CommandLine)) -eq $Arguments) + { + $processesWithMatchingArguments += $process + } + } + + return $processesWithMatchingArguments } <# -.Synopsis - Strips the Arguments part of a commandLine. In "c:\temp\a.exe X Y Z" the Arguments part is "X Y Z". + .SYNOPSIS + Retrieves the 'arguments' part of command line input. + + .PARAMETER CommandLineInput + The command line input to retrieve the arguments from. + + .EXAMPLE + Get-ArgumentsFromCommandLineInput -CommandLineInput 'C:\temp\a.exe X Y Z' + Returns 'X Y Z'. #> -function GetProcessArgumentsFromCommandLine +function Get-ArgumentsFromCommandLineInput { + [OutputType([String])] + [CmdletBinding()] param ( - [System.String] - $commandLine + [String] + $CommandLineInput ) - if($commandLine -eq $null) + if ([String]::IsNullOrWhitespace($CommandLineInput)) { - return "" + return [String]::Empty } + + $CommandLineInput = $CommandLineInput.Trim() - $commandLine=$commandLine.Trim() - - if($commandLine.Length -eq 0) + if ($CommandLineInput.StartsWith('"')) { - return "" - } - - if($commandLine[0] -eq '"') - { - $charToLookfor=[char]'"' + $endOfCommandChar = [Char]'"' } else { - $charToLookfor=[char]' ' + $endOfCommandChar = [Char]' ' } - $endofCommand=$commandLine.IndexOf($charToLookfor ,1) - if($endofCommand -eq -1) + $endofCommandIndex = $CommandLineInput.IndexOf($endOfCommandChar, 1) + if ($endofCommandIndex -eq -1) { - return "" + return [String]::Empty } - return $commandLine.Substring($endofCommand+1).Trim() + return $CommandLineInput.Substring($endofCommandIndex + 1).Trim() } <# -.Synopsis - Escapes a string to be used in a WQL filter as the one passed to get-wmiobject -#> -function WQLEscape -{ - param - ( - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $query - ) - - return $query.Replace("\","\\").Replace('"','\"').Replace("'","\'") -} + .SYNOPSIS + Converts a string to an escaped string to be used in a WQL filter such as the one passed in + the Filter parameter of Get-WmiObject. -function ThrowInvalidArgumentError + .PARAMETER FilterString + The string to convert. +#> +function ConvertTo-EscapedStringForWqlFilter { + [OutputType([String])] [CmdletBinding()] param ( - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $errorId, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $errorMessage + [String] + $FilterString ) - $errorCategory=[System.Management.Automation.ErrorCategory]::InvalidArgument - $exception = New-Object System.ArgumentException $errorMessage; - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null - throw $errorRecord + return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") } -function ResolvePath +<# + .SYNOPSIS + Expands a shortened path into a full, rooted path. + + .PARAMETER Path + The shortened path to expand. +#> +function Expand-Path { + [OutputType([String])] [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path ) $Path = [Environment]::ExpandEnvironmentVariables($Path) - if(IsRootedPath $Path) + if ([IO.Path]::IsPathRooted($Path)) { - if(!(Test-Path $Path -PathType Leaf)) + if (-not (Test-Path -Path $Path -PathType 'Leaf')) { - ThrowInvalidArgumentError "CannotFindRootedPath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } return $Path } - - if([string]::IsNullOrEmpty($env:Path)) + else { - ThrowInvalidArgumentError "EmptyEnvironmentPath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' } - # This will block relative paths. The statement is only true id $Path contains a plain file name. - # Checking a relative path against segments of the $env:Path does not make sense - if((Split-Path $Path -Leaf) -ne $Path) + if ([String]::IsNullOrEmpty($env:Path)) { - ThrowInvalidArgumentError "NotAbsolutePathOrFileName" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.AbsolutePathOrFileName) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } - foreach($rawSegment in $env:Path.Split(";")) + <# + This will block relative paths. The statement is only true when $Path contains a plain file name. + Checking a relative path against segments of $env:Path does not make sense. + #> + if ((Split-Path -Path $Path -Leaf) -ne $Path) { - $segment = [Environment]::ExpandEnvironmentVariables($rawSegment) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.AbsolutePathOrFileName) + } - # if an exception causes $segmentedRooted not to be set, we will consider it $false - $segmentRooted = $false + foreach ($rawEnvPathSegment in $env:Path.Split(';')) + { + $envPathSegment = [Environment]::ExpandEnvironmentVariables($rawEnvPathSegment) + + # If an exception causes $envPathSegmentRooted not to be set, we will consider it $false + $envPathSegmentRooted = $false + + <# + If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have + invalid characters, so the segment has no invalid characters and will not throw as well. + #> try { - # If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have - # invalid characters, so segment has no invalid characters and will not throw as well - $segmentRooted=[IO.Path]::IsPathRooted($segment) + $envPathSegmentRooted = [IO.Path]::IsPathRooted($envPathSegment) } catch {} - - if(!$segmentRooted) - { - continue - } - - $candidate = join-path $segment $Path - - if(Test-Path $candidate -PathType Leaf) + + if ($envPathSegmentRooted) { - return $candidate + $fullPathCandidate = Join-Path -Path $envPathSegment -ChildPath $Path + + if (Test-Path -Path $fullPathCandidate -PathType 'Leaf') + { + return $fullPathCandidate + } } } - ThrowInvalidArgumentError "CannotFindRelativePath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } +<# + .SYNOPSIS + Throws an error if the given path argument is not rooted. + + .PARAMETER PathArgumentName + The name of the path argument that should be rooted. -function AssertAbsolutePath + .PARAMETER PathArgument + The path arguments that should be rooted. +#> +function Assert-PathArgumentRooted { [CmdletBinding()] param ( - $ParentBoundParameters, - - [System.String] - [Parameter (ValueFromPipeline=$true)] - $ParameterName, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgumentName, - [switch] - $Exist + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument ) - - Process + + if (-not ([IO.Path]::IsPathRooted($PathArgument))) { - if(!$ParentBoundParameters.ContainsKey($ParameterName)) - { - return - } - - $path=$ParentBoundParameters[$ParameterName] - - if(!(IsRootedPath $Path)) - { - ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), - $LocalizedData.PathShouldBeAbsolute) - } - - if(!$Exist.IsPresent) - { - return - } - - if(!(Test-Path $Path)) - { - ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), - $LocalizedData.PathShouldExist) - } + New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldBeAbsolute) } } -function AssertParameterIsNotSpecified +<# + .SYNOPSIS + Throws an error if the given path argument does not exist. + + .PARAMETER PathArgumentName + The name of the path argument that should exist. + + .PARAMETER PathArgument + The path argument that should exist. +#> +function Assert-PathArgumentExists { [CmdletBinding()] param ( - $ParentBoundParameters, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgumentName, - [System.String] - [Parameter (ValueFromPipeline=$true)] - $ParameterName + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument ) - Process + if (-not (Test-Path -Path $PathArgument)) { - if($ParentBoundParameters.ContainsKey($ParameterName)) - { - ThrowInvalidArgumentError "ParameterShouldNotBeSpecified" ($LocalizedData.ParameterShouldNotBeSpecified -f $ParameterName) - } + New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldExist) } } -function IsRootedPath -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Path - ) +<# + .SYNOPSIS + Throws an exception if the given hashtable contains the given key(s). - try - { - return [IO.Path]::IsPathRooted($Path) - } - catch - { - # if the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on - ThrowInvalidArgumentError "CannotGetIsPathRooted" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $_.Exception.Message) - } -} + .PARAMETER Hashtable + The hashtable to check the keys of. -function Write-Log + .PARAMETER Key + The key(s) that should not be in the hashtable. +#> +function Assert-HashtableDoesNotContainKey { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Message - ) + [Hashtable] + $Hashtable, - if ($PSCmdlet.ShouldProcess($Message, $null, $null)) - { - Write-Verbose $Message - } -} + [Parameter(Mandatory = $true)] + [String[]] + $Key + ) -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 + foreach ($keyName in $Key) { - //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 + if ($Hashtable.ContainsKey($keyName)) { - 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("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) - { - 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("The user could not be logged on. Ensure that the user has an existing profile on the machine and that correct credentials are provided. 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("Error in looking up privilege of the process. This should not happen if DSC is running as LocalSystem 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("The process could not be created. Create process as user 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); - } - } + New-InvalidArgumentException -ArgumentName $keyName -Message ($LocalizedData.ParameterShouldNotBeSpecified -f $keyName) } } } -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} - -Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource - +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 b/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 index 3bc8311e2..20bd67648 100644 --- a/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 +++ b/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 @@ -135,7 +135,7 @@ function Test-TargetResource [String] $DisplayName, - [ValidateNotNullOrEmpty()] + [ValidateNotNull()] [String] $Description, @@ -245,7 +245,7 @@ function Set-TargetResource [String] $DisplayName, - [ValidateNotNullOrEmpty()] + [ValidateNotNull()] [String] $Description, diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 b/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 rename to Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCreds_xPackage.ps1 b/Examples/Sample_InstallExeCreds_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCreds_xPackage.ps1 rename to Examples/Sample_InstallExeCreds_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSIProductId_xPackage.ps1 b/Examples/Sample_InstallMSIProductId_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSIProductId_xPackage.ps1 rename to Examples/Sample_InstallMSIProductId_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSI_xPackage.ps1 b/Examples/Sample_InstallMSI_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSI_xPackage.ps1 rename to Examples/Sample_InstallMSI_xPackage.ps1 diff --git a/Examples/Sample_xArchive_ExpandArchive.ps1 b/Examples/Sample_xArchive_ExpandArchive.ps1 index 5854205d4..c4d4f1166 100644 --- a/Examples/Sample_xArchive_ExpandArchive.ps1 +++ b/Examples/Sample_xArchive_ExpandArchive.ps1 @@ -5,20 +5,11 @@ Configuration Sample_xArchive_ExpandArchive ( [parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] - [string[]] $Path, + [string] $Path, [parameter (mandatory=$true)] [ValidateNotNullOrEmpty()] - [string] $Destination, - - [parameter (mandatory=$false)] - [ValidateSet("Optimal","NoCompression","Fastest")] - [string] - $CompressionLevel = "Optimal", - - [parameter (mandatory=$false)] - [boolean] - $MatchSource = $false + [string] $Destination ) Import-DscResource -ModuleName xPSDesiredStateConfiguration @@ -28,9 +19,6 @@ Configuration Sample_xArchive_ExpandArchive { Path = $Path Destination = $Destination - CompressionLevel = $CompressionLevel - DestinationType="Directory" - MatchSource=$MatchSource } } } diff --git a/HighQualityResourceModulePlan.md b/HighQualityResourceModulePlan.md index 9c320a892..36f4082cb 100644 --- a/HighQualityResourceModulePlan.md +++ b/HighQualityResourceModulePlan.md @@ -39,9 +39,9 @@ The PSDesiredStateConfiguration High Quality Resource Module will consist of the - [ ] [2. Merge In-Box & Open-Source Resources](#merge-in-box-and-open-source-resources) - [x] Archive - [x] Group - - [ ] Package (In Progress) - - [ ] Process - - [ ] Registry + - [x] Package + - [X] Process + - [ ] Registry (In Progress) - [x] Service - [ ] WindowsOptionalFeature - [ ] [3. Resolve Nano Server vs. Full Server Resources](#resolve-nano-server-vs-full-server-resources) diff --git a/README.md b/README.md index 58032226c..1a883d4e4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please check out common DSC Resources [contributing guidelines](https://github.c * **xEnvironment** configures and manages environment variables. * **xWindowsFeature** provides a mechanism to ensure that roles and features are added or removed on a target node. * **xScript** provides a mechanism to run Windows PowerShell script blocks on target nodes. -* **xGroupSet** configures multiple xGroups with common settings but different names. +* **xGroupSet** configures multiple xGroups with common settings but different names. * **xProcessSet** allows starting and stopping of a group of windows processes with no arguments. * **xServiceSet** allows starting, stopping and change in state or account type for a group of services. * **xWindowsFeatureSet** allows installation and uninstallation of a group of Windows features and their subfeatures. @@ -105,8 +105,6 @@ For a complete list of properties, please use Get-DscResource ### xPackage -For a complete list, please use Get-DscResource. - * **Ensure**: Ensures that the package is **Present** or **Absent**. * **Name**: The name of the package. * **Path**: The source path of the package. @@ -115,17 +113,20 @@ For a complete list, please use Get-DscResource. * **Credential**: PSCredential needed to access Path. * **ReturnCode**: An array of return codes that are returned after a successful installation. * **LogPath**: The destination path of the log. +* **FileHash**: The hash that should match the hash of the package file. +* **HashAlgorithm**: The algorithm to use to get the hash of the package file. + - Supported values: SHA1, SHA256, SHA384, SHA512, MD5, RIPEMD160 +* **SignerSubject**: The certificate subject that should match that of the package file's signing certificate. +* **SignerThumbprint**: The certificate thumbprint that should match that of the package file's signing certificate. +* **ServerCertificateValidationCallback**: A callback function to validate the server certificate. + +Read-Only Properties: * **PackageDescription**: A text description of the package being installed. * **Publisher**: Publisher's name. * **InstalledOn**: Date of installation. * **Size**: Size of the installation. * **Version**: Version of the package. * **Installed**: Is the package installed? -* **RunAsCredential**: Credentials to use when installing the package. -* **InstalledCheckRegKey**: Registry key to open to check for package installation status. -* **InstalledCheckRegValueName**: Registry value name to check for package installation status. -* **InstalledCheckRegValueData**: Value to compare against the retrieved value to check for package installation. -* **CreateCheckRegValue**: Creates the InstallCheckRegValueName registry value/data after successful package installation. ### xGroup @@ -335,6 +336,25 @@ These parameters will be the same for each Windows optional feature in the set. ### Unreleased +### 3.13.0.0 + +* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* Updated appveyor.yml to use the default image. +* Merged xPackage with in-box Package resource and added tests. +* xPackage: Re-implemented parameters for installation check from registry key value. +* xGroup: + * Fixed Verbose output in Get-MembersAsPrincipals function. + * Fixed bug when credential parameter passed does not contain local or domain context. + * Fixed logic bug in MembersToInclude and MembersToExclude. + * Fixed bug when trying to include the built-in Administrator in Members. + * Fixed bug where Test-TargetResource would check for members when none specified. + * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bugs in Assert-GroupNameValid to throw correct exception. +* xService + * Updated xService resource to allow empty string for Description parameter. +* Merged xProcess with in-box Process resource and added tests. + ### 3.12.0.0 * Removed localization for now so that resources can run on non-English systems. diff --git a/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 b/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 index eaa161f32..629a8a217 100644 --- a/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 +++ b/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 @@ -1,4 +1,4 @@ -ipmo xDSCResourceDesigner +Import-Module -Name 'xDSCResourceDesigner' $resProperties = @{ Name = New-xDscResourceProperty -Description 'Name of the PS Remoting Endpoint' ` diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 14423abfc..a4446f8e0 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -555,10 +555,184 @@ function Test-IsFileLocked } } +<# + .SYNOPSIS + Tests that calling the Set-TargetResource cmdlet with the WhatIf parameter specified produces output that contains all the given expected output. + If empty or null expected output is specified, this cmdlet will check that there was no output from Set-TargetResource with WhatIf specified. + Uses Pester. + + .PARAMETER Parameters + The parameters to pass to Set-TargetResource. + These parameters do not need to contain that WhatIf parameter, but if they do, + this function will run Set-TargetResource with WhatIf = $true no matter what is in the Parameters Hashtable. + + .PARAMETER ExpectedOutput + The output expected to be in the output from running WhatIf with the Set-TargetResource cmdlet. + If this parameter is empty or null, this cmdlet will check that there was no output from Set-TargetResource with WhatIf specified. +#> +function Test-SetTargetResourceWithWhatIf +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Hashtable] + $Parameters, + + [String[]] + $ExpectedOutput + ) + + $transcriptPath = Join-Path -Path (Get-Location) -ChildPath 'WhatIfTestTranscript.txt' + if (Test-Path -Path $transcriptPath) + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} -TimeoutSeconds 10 + Remove-Item -Path $transcriptPath -Force + } + + $Parameters['WhatIf'] = $true + + try + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} + + Start-Transcript -Path $transcriptPath + Set-TargetResource @Parameters + Stop-Transcript + + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} + + $transcriptContent = Get-Content -Path $transcriptPath -Raw + $transcriptContent | Should Not Be $null + + $regexString = '\*+[^\*]*\*+' + + # Removing transcript diagnostic logging at top and bottom of file + $selectedString = Select-String -InputObject $transcriptContent -Pattern $regexString -AllMatches + + foreach ($match in $selectedString.Matches) + { + $transcriptContent = $transcriptContent.Replace($match.Captures, '') + } + + $transcriptContent = $transcriptContent.Replace("`r`n", "").Replace("`n", "") + + if ($null -eq $ExpectedOutput -or $ExpectedOutput.Count -eq 0) + { + [String]::IsNullOrEmpty($transcriptContent) | Should Be $true + } + else + { + foreach ($expectedOutputPiece in $ExpectedOutput) + { + $transcriptContent.Contains($expectedOutputPiece) | Should Be $true + } + } + } + finally + { + if (Test-Path -Path $transcriptPath) + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} -TimeoutSeconds 10 + Remove-Item -Path $transcriptPath -Force + } + } +} + +<# + .SYNOPSIS + Enters a DSC Resource test environment. + + .PARAMETER DscResourceModuleName + The name of the module that contains the DSC Resource to test. + + .PARAMETER DscResourceName + The name of the DSC resource to test. + + .PARAMETER TestType + Specifies whether the test environment will run a Unit test or an Integration test. +#> +function Enter-DscResourceTestEnvironment +{ + [OutputType([PSObject])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DscResourceModuleName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DscResourceName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Unit', 'Integration')] + [String] + $TestType + ) + + if ((-not (Test-Path -Path "$PSScriptRoot\..\DSCResource.Tests")) -or (-not (Test-Path -Path "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1"))) + { + Push-Location "$PSScriptRoot\.." + git clone https://github.com/PowerShell/DscResource.Tests.git --quiet + Pop-Location + } + else + { + $gitInstalled = $null -ne (Get-Command -Name 'git' -ErrorAction 'SilentlyContinue') + + if ($gitInstalled) + { + Push-Location "$PSScriptRoot\..\DSCResource.Tests" + git pull origin master --quiet + Pop-Location + } + else + { + Write-Verbose -Message "Git not installed. Leaving current DSCResource.Tests as is." + } + } + + Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" + + return Initialize-TestEnvironment ` + -DSCModuleName $DscResourceModuleName ` + -DSCResourceName $DscResourceName ` + -TestType $TestType +} + +<# + .SYNOPSIS + Exits the specified DSC Resource test environment. + + .PARAMETER TestEnvironment + The test environment to exit. +#> +function Exit-DscResourceTestEnvironment +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSObject]$TestEnvironment + ) + + Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" + + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + Export-ModuleMember -Function ` Test-GetTargetResourceResult, ` New-User, ` Remove-User, ` Test-User, ` Wait-ScriptBlockReturnTrue, ` - Test-IsFileLocked + Test-IsFileLocked, ` + Test-SetTargetResourceWithWhatIf, ` + Enter-DscResourceTestEnvironment, ` + Exit-DscResourceTestEnvironment diff --git a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 new file mode 100644 index 000000000..eadd44846 --- /dev/null +++ b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 @@ -0,0 +1,108 @@ +Import-Module "$PSScriptRoot\..\..\DSCResource.Tests\TestHelper.psm1" -Force + +Initialize-TestEnvironment ` + -DSCModuleName 'xPSDesiredStateConfiguration' ` + -DSCResourceName 'MSFT_xGroupResource' ` + -TestType Integration ` + | Out-Null + +Describe 'xGroup Integration Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xGroupResource.TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\..\DSCResources\CommonResourceHelper.psm1" -Force + } + + It 'Should create an empty group' { + $configurationName = 'CreateEmptyGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + + $resourceName = 'EmptyGroup' + $groupName = 'Empty' + + try + { + Configuration $configurationName + { + param () + + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + + xGroup $resourceName + { + Ensure = 'Present' + GroupName = $groupName + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null + } + else + { + Test-GroupExists -GroupName $groupName | Should Be $true + } + } + finally + { + Remove-Group -GroupName $groupName + + if (Test-Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + It 'Should create an Administrators group with the built-in Administrator' { + $configurationName = 'CreateAdministratorsGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + + $resourceName = 'AdministratorsGroup' + $groupName = 'Administrators' + $builtInAdministratorUsername = 'Administrator' + + try + { + Configuration $configurationName + { + param () + + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + + xGroup $resourceName + { + GroupName = $groupName + MembersToInclude = $builtInAdministratorUsername + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null + } + else + { + Test-GroupExists -GroupName $groupName | Should Be $true + } + } + finally + { + if (Test-Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } +} diff --git a/Tests/Integration/MSFT_xPackageResource.Tests.ps1 b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 new file mode 100644 index 000000000..8de93a967 --- /dev/null +++ b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 @@ -0,0 +1,108 @@ +Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xPackageResource' ` + -TestType 'Integration' +try +{ + Describe "xPackage Integration Tests" { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xPackageResource.TestHelper.psm1" -Force + + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null + + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName + + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + + New-TestMsi -DestinationPath $script:msiLocation | Out-Null + + Clear-xPackageCache | Out-Null + } + + BeforeEach { + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Package could not be removed.' + } + } + + AfterAll { + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } + } + + It "Install a .msi package" { + $configurationName = "EnsurePackageIsPresent" + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + $errorPath = Join-Path -Path (Get-Location) -ChildPath "StdErrorPath.txt" + $outputPath = Join-Path -Path (Get-Location) -ChildPath "StdOutputPath.txt" + + try + { + Configuration $configurationName + { + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xPackage Package1 + { + Path = $script:msiLocation + Ensure = "Present" + Name = $script:packageName + ProductId = $script:packageId + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Force -Verbose + + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + } + finally + { + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/Tests/MSFT_xPackageResource.tests.ps1 b/Tests/MSFT_xPackageResource.tests.ps1 deleted file mode 100644 index 25b0df112..000000000 --- a/Tests/MSFT_xPackageResource.tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.summary - Test suite for MSFT_xPackageResource.psm1 -#> -[CmdletBinding()] -param() - -Import-Module $PSScriptRoot\..\DSCResources\MSFT_xPackageResource\MSFT_xPackageResource.psm1 - -$ErrorActionPreference = 'stop' -Set-StrictMode -Version latest - - -function Suite.BeforeAll { - # Remove any leftovers from previous test runs - Suite.AfterAll - -} - -function Suite.AfterAll { - Remove-Module MSFT_xPackageResource -} - -function Suite.BeforeEach { -} - -try -{ - InModuleScope MSFT_xPackageResource { - Describe 'Get-RegistryValueIgnoreError' { - - It 'Should get values from HKLM' { - $installValue = Get-RegistryValueIgnoreError 'LocalMachine' "SOFTWARE\Microsoft\Windows\CurrentVersion" "ProgramFilesDir" Registry64 - $installValue | should be $env:programfiles - } - It 'Should get values from HKCU' { - $installValue = Get-RegistryValueIgnoreError 'CurrentUser' "Environment" "Temp" Registry64 - $installValue.length -gt 3 | should be $true - $installValue | should match $env:username - # comparing $installValue with $env:temp may fail if the username is longer than 8 characters - } - } - } -} -finally -{ - Suite.AfterAll -} - diff --git a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 index 3c5335701..d4c9b5d1d 100644 --- a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 @@ -98,6 +98,140 @@ InModuleScope 'MSFT_xGroupResource' { } Context 'Set-TargetResource' { + It 'Should create an empty group' { + $testGroupName = 'LocalTestGroup' + + try + { + $setTargetResourceResult = Set-TargetResource -GroupName $testGroupName -Ensure 'Present' + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Members'].Count | Should Be 0 + } + finally + { + Remove-Group -GroupName $testGroupName + } + } + + It 'Should create a group with 2 users using Members' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -Members @( $testUserName1, $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 2 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + + It 'Should create a group with 2 users using MembersToInclude' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -MembersToInclude @( $testUserName1, $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 2 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + + It 'Should remove a member from a group with MembersToExclude' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + New-Group -GroupName $testGroupName -Description $testDescription -MemberUserNames @( $testUserName1, $testUserName2 ) + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -MembersToExclude @( $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 1 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + It 'Should not remove an existing group when Ensure is Present' { $testUserName1 = 'LocalTestUser1' $testUserName2 = 'LocalTestUser2' diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 new file mode 100644 index 000000000..6f87fbd09 --- /dev/null +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -0,0 +1,795 @@ +<# + .SYNOPSIS + Clears the xPackage cache. +#> +function Clear-xPackageCache +{ + [CmdletBinding()] + param () + + $xPackageCacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\" + ` + "BuiltinProvCache\MSFT_xPackageResource" + + Remove-Item -Path $xPackageCacheLocation -ErrorAction 'SilentlyContinue' -Recurse +} + +<# + .SYNOPSIS + Tests if the package with the given name is installed. + + .PARAMETER Name + The name of the package to test for. +#> +function Test-PackageInstalledByName +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [String] + $Name + ) + + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + + $productEntry = $null + + foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' )) + { + if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName')) + { + $productEntry = $registryKeyEntry + break + } + } + + return ($null -ne $productEntry) +} + +<# + .SYNOPSIS + Mimics a simple http or https file server. + + .PARAMETER FilePath + The path to the file to add on the mock file server. + + .PARAMETER Https + Indicates that the new file server should use https. + Otherwise the new file server will use http. +#> +function New-MockFileServer +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FilePath, + + [Switch] + $Https + ) + + if ($null -eq (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) + { + New-NetFirewallRule -DisplayName 'UnitTestRule' -Direction 'Inbound' -Program "$PSHome\powershell.exe" -Authentication 'NotRequired' -Action 'Allow' + } + + netsh advfirewall set allprofiles state off + + Start-Job -ArgumentList @( $FilePath ) -ScriptBlock { + + # Create certificate + $certificate = Get-ChildItem -Path 'Cert:\LocalMachine\My' -Recurse | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -eq 'Server Authentication' } + + if ($certificate.Count -gt 1) + { + # Just use the first one + $certificate = $certificate[0] + } + elseif ($certificate.count -eq 0) + { + # Create a self-signed one + $certificate = New-SelfSignedCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -DnsName $env:computerName + } + + $hash = $certificate.Thumbprint + + # Use net shell command to directly bind certificate to designated testing port + netsh http add sslcert ipport=0.0.0.0:1243 certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable + + # Start listening endpoints + $httpListener = New-Object -TypeName 'System.Net.HttpListener' + + if ($Https) + { + $httpListener.Prefixes.Add([Uri]'https://localhost:1243') + } + else + { + $httpListener.Prefixes.Add([Uri]'http://localhost:1242') + } + + $httpListener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Negotiate + $httpListener.Start() + + # Create a pipe to flag http/https client + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.Connect() + $pipe.Dispose() + + # Prepare binary buffer for http/https response + $fileInfo = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList @( $args[0] ) + $numBytes = $fileInfo.Length + $fileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $args[0], 'Open' ) + $binaryReader = New-Object -TypeName 'System.IO.BinaryReader' -ArgumentList @( $fileStream ) + [Byte[]] $buf = $binaryReader.ReadBytes($numBytes) + $fileStream.Close() + + # Send response + $response = ($httpListener.GetContext()).Response + $response.ContentType = 'application/octet-stream' + $response.ContentLength64 = $buf.Length + $response.OutputStream.Write($buf, 0, $buf.Length) + $response.OutputStream.Flush() + + # Wait for client to finish downloading + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.WaitForConnection() + $pipe.Dispose() + + $response.Dispose() + $httpListener.Stop() + $httpListener.Close() + + # Close pipe + + # Use net shell command to clean up the certificate binding + netsh http delete sslcert ipport=0.0.0.0:1243 + } + + netsh advfirewall set allprofiles state on +} + +<# + .SYNOPSIS + Creates a new test executable. + + .PARAMETER DestinationPath + The path at which to create the test executable. +#> +function New-TestExecutable +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String]$DestinationPath + ) + + if (Test-Path -Path $DestinationPath) + { + Write-Verbose -Message "Removing old executable at $DestinationPath..." + Remove-Item -Path $DestinationPath -Force + } + + $testExecutableCode = @' + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management; + using System.Text; + using System.Threading.Tasks; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using System.Runtime.InteropServices; + namespace Providers.Package.UnitTests.MySuite + { + class ExeTestClass + { + public static void Main(string[] args) + { + string cmdline = System.Environment.CommandLine; + Console.WriteLine("Cmdline was " + cmdline); + int endIndex = cmdline.IndexOf("\"", 1); + string self = cmdline.Substring(0, endIndex); + string other = cmdline.Substring(self.Length + 1); + string msiexecpath = System.IO.Path.Combine(System.Environment.SystemDirectory, "msiexec.exe"); + + self = self.Replace("\"", ""); + string packagePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(self), "DSCSetupProject.msi"); + + string msiexecargs = String.Format("/i {0} {1}", packagePath, other); + System.Diagnostics.Process.Start(msiexecpath, msiexecargs).WaitForExit(); + } + } + } +'@ + + Add-Type -TypeDefinition $testExecutableCode -OutputAssembly $DestinationPath -OutputType 'ConsoleApplication' +} + +<# + .SYNOPSIS + Creates a new MSI package for testing. + + .PARAMETER DestinationPath + The path at which to create the test msi file. +#> +function New-TestMsi +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DestinationPath + ) + + #region msiContentInBase64 + $msiContentInBase64 = '0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgAEAP7/DAAGAAAAAAAAAAEAAAABAAAAAQA' + ` + 'AAAAAAAAAEAAAAgAAAAEAAAD+////AAAAAAAAAAD/////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '////////////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP3////+/////v///wYAAAD+////BAAAAP7////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '////////////////////////////////////////////////////////////9SAG8AbwB0ACAARQBuAHQAcgB' + ` + '5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAFAP//////////CQAAAIQQ' + ` + 'DAAAAAAAwAAAAAAAAEYAAAAAAAAAAAAAAADwRqG1qh/OAQMAAAAAEwAAAAAAAAUAUwB1AG0AbQBhAHIAeQBJA' + ` + 'G4AZgBvAHIAbQBhAHQAaQBvAG4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAIA////////////////AA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwCAAAAAAAAQEj/P+RD7EHkRaxEMUgAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAgETAAAABAAAAP////8A' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAOAcAAAAAAABASMpBMEOxOztCJkY3QhxCN' + ` + 'EZoRCZCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAACAQsAAAAKAAAA/////w' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACYAAAAwAAAAAAAAAEBIykEwQ7E/Ej8oRThCsUE' + ` + 'oSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAIBDAAAAP//////////' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJwAAABgAAAAAAAAAQEjKQflFzkaoQfhFKD8oR' + ` + 'ThCsUEoSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAgD///////////////' + ` + '8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAKgAAAAAAAABASIxE8ERyRGhEN0gAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgACAP//////////////' + ` + '/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAAMAAAAAAAAAEBIDUM1QuZFckU8SAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAIADgAAAAIAAAD///' + ` + '//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgAAABIAAAAAAAAAQEgPQuRFeEUoSAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgD/////////////' + ` + '//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArAAAAEAAAAAAAAABASA9C5EV4RSg7MkSzR' + ` + 'DFC8UU2SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgACAQcAAAADAAAA//' + ` + '///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAAAAAAEBIUkT2ReRDrzs7QiZ' + ` + 'GN0IcQjRGaEQmQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAAIBBQAAAAEAAAD/' + ` + '////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALQAAAHIAAAAAAAAAQEhSRPZF5EOvPxI/K' + ` + 'EU4QrFBKEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAgH///////////' + ` + '////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvAAAAMAAAAAAAAABASBVBeETmQoxE8UH' + ` + 'sRaxEMUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAACAP//////////' + ` + '/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAEAAAAAAAAAEBIWUXyRGhFN0cAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAIBDwAAAP////' + ` + '//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQAAACQAAAAAAAAAQEgbQipD9kU1RwA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgEQAAAADQAA' + ` + 'AP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyAAAADAAAAAAAAABASN5EakXkQShIA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAACAP////////' + ` + '///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMAAAAgAAAAAAAAAEBIfz9kQS9CNkg' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAIBEQAAAAgA' + ` + 'AAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAACAAAAAAAAAAQEg/O/JDOESxR' + ` + 'QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgD///////' + ` + '////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1AAAAWAIAAAAAAABASD8/d0VsRGo' + ` + '+skQvSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAACAP//////' + ` + '/////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAYAwAAAAAAAEBIPz93RWxEa' + ` + 'jvkRSRIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAIBBgAAAB' + ` + 'IAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAFAaAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////' + ` + '//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////' + ` + '///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////' + ` + '////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///' + ` + '////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//' + ` + '/////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//' + ` + '//////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/' + ` + '//////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP' + ` + '///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + '////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'D///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQA' + ` + 'AAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAD+////CgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEA' + ` + 'AAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhA' + ` + 'AAAIgAAACMAAAAkAAAAJQAAAP7////+/////v////7////+/////v////7////+////LgAAAP7////+/////v' + ` + '////7////+/////v////7///82AAAANwAAADgAAAA5AAAAOgAAADsAAAA8AAAAPQAAAD4AAAD+////QAAAAEE' + ` + 'AAABCAAAAQwAAAEQAAABFAAAARgAAAEcAAABIAAAASQAAAEoAAABLAAAA/v//////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '///////////////////7/AAAGAQIAAAAAAAAAAAAAAAAAAAAAAAEAAADghZ/y+U9oEKuRCAArJ7PZMAAAAAwC' + ` + 'AAAOAAAAAQAAAHgAAAACAAAAgAAAAAMAAACgAAAABAAAAMQAAAAFAAAA9AAAAAYAAAAIAQAABwAAAGwBAAAJA' + ` + 'AAAgAEAAAwAAACwAQAADQAAALwBAAAOAAAAyAEAAA8AAADQAQAAEgAAANgBAAATAAAABAIAAAIAAADkBAAAHg' + ` + 'AAABYAAABJbnN0YWxsYXRpb24gRGF0YWJhc2UAAAAeAAAAGwAAAEEgcGFja2FnZSBmb3IgdW5pdCB0ZXN0aW5' + ` + 'nAAAeAAAAKAAAAE1pY3Jvc29mdCBVbml0IFRlc3RpbmcgR3VpbGQgb2YgQW1lcmljYQAeAAAACgAAAEluc3Rh' + ` + 'bGxlcgAAAB4AAABcAAAAVGhpcyBpbnN0YWxsZXIgZGF0YWJhc2UgY29udGFpbnMgdGhlIGxvZ2ljIGFuZCBkY' + ` + 'XRhIHJlcXVpcmVkIHRvIGluc3RhbGwgRFNDVW5pdFRlc3RQYWNrYWdlLgAeAAAACwAAAEludGVsOzEwMzMAAB' + ` + '4AAAAnAAAAe0YxN0FGREExLUREMEItNDRFNi1CNDczLTlFQkUyREJEOUVBOX0AAEAAAAAAAOO0qh/OAUAAAAA' + ` + 'AAOO0qh/OAQMAAADIAAAAAwAAAAIAAAAeAAAAIwAAAFdpbmRvd3MgSW5zdGFsbGVyIFhNTCAoMy43LjEyMDQu' + ` + 'MCkAAAMAAAACAAAAAAAAAAYABgAGAAYABgAGAAYABgAGAAYACgAKACIAIgAiACkAKQApACoAKgAqACsAKwArA' + ` + 'CsAKwArADEAMQAxAD4APgA+AD4APgA+AD4APgBNAE0AUgBSAFIAUgBSAFIAUgBSAGAAYABgAGEAYQBhAGIAYg' + ` + 'BmAGYAZgBmAGYAZgByAHIAdgB2AHYAdgB2AHYAgACAAIAAgACAAIAAgAACAAUACwAMAA0ADgAPABAAEQASAAc' + ` + 'ACQAjACUAJwAjACUAJwAjACUAJwAlACsALQAwADMANgAxADoAPAALADAAMwA+AEAAQgBFAEcATgBQACcAMwBQ' + ` + 'AFIAVQBYAFoAXAAjACUAJwAjACUAJwALACUAZwBpAGsAbQBvAHEABwByAAEABwBQAHYAeAB6ADMAXACBAIMAh' + ` + 'QCJAIsACAAIABgAGAAYABgAGAAIABgAGAAIAAgACAAYABgACAAYABgACAAYABgAGAAIABgACAAIABgACAAYAA' + ` + 'gAGAAYAAgACAAYABgAGAAIAAgACAAIABgACAAIAAgACAAYABgACAAYABgACAAYABgACAAIAAgACAAYABgAGAA' + ` + 'YAAgACAAYABgACAAIAAgACAAIABgACAAYABgAGAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAgAEAAAAAAAAA' + ` + 'AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAA' + ` + 'ABAACAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAA' + ` + 'AAQAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////fwAAAAAAAACAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAACA/////wAAAAAAAAAA/////wAAA' + ` + 'AAAAAAAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/38AgP9/AIAAAAAAAAAAAP//////fwCAAAA' + ` + 'AAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCA/////wAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAACAAAAAAP////8AAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxAAAANw' + ` + 'AAADEAAAAAADEAAAAAAD4AAAAAAAAAPgArAAAAAAArAAAAAAAAAFIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAArAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAGAAAABgAAAAAABgAAAAAABgAAAAAAAAAGAAYAAAAAAAYAAAAAAA' + ` + 'AABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAB' + ` + 'MAEwAfAB8AAAAAAAAAAAATAAAAAAAAABMAJQAAABMAJQAAABMAJQAAACUAEwAuABMAAAATABMAEwA8AB8ASQA' + ` + 'AABMAEwAfAAAAAAATABMAAAAAABMAEwBWAAAAWgBcABMAJQAAABMAJQAAAGQAJQAAAAAAHwBtAB8AcgAfABMA' + ` + 'ZABkABMAEwAAAHsAAABcAC4AHwAfAGQASQAAAAAAAAAAAB0AAAAAABYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAVACEAIAAeABw' + ` + 'AGgAXABsAGQAAAAAAJAAmACgAJAAmACgAJAAmACgANQAsAC8AMgA0ADgAOQA7AD0ARABKAEwAPwBBAEMARgBI' + ` + 'AE8AUQBfAF4AVABTAFcAWQBbAF0AJAAmACgAJAAmACgAZQBjAGgAagBsAG4AcABzAHUAdAB9AH4AfwB3AHkAf' + ` + 'ACIAIcAggCEAIYAigCMAAAAAAAAAAAAjQCOAI8AkACRAJIAkwCUAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IN4hd' + ` + 'yFPI+gj8iZAAAAAAAAAAAAAAAAAAAAAI0AjgCPAJUAAAAAAAAAAAAgg4SD6IMUhQAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACNAI8AkACRAJQAlgCXAAAAAAAAAAAAAAAAAAAAIIPog3iF3IXImZyY' + ` + 'AJkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACZAJoABIAAAJsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoAnACeAJwAngAAAJ0AnwCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChAAAAogAAAAKAAYAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoQCYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI0AjgCPAJAAkQCUAJYAlwCjAKQApQCmAKcAqACpAKoAqwCsAK0AA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IN4hdyFyJmcmACZGYBkgLyCsIRAhg' + ` + 'iHKIqIk3CX1Jd5hQAAAAAAAAAAAAAAAAAAjQCOAI8AlQCjAKQApQCmAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IM' + ` + 'UhRmAZIC8grCEAAAAAAAAAAAAAAAAAAAAAK4ArwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBALAAsgC0ALYAuAC6AL0AvwC8ALEAswC1ALcAuQC7AL4AwAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmwACgMEAwgDDAJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwAvAAAALsAuwAAAAAAAAABAACAAgAAgAAAAADEAMUAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGACIAKQAqACsAMQA+AE0AUgBgAGEAYgBmAHIAdgCAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYABgAGAAYABgAGAAYABgAiACIAIgApACkAKQAqACoAK' + ` + 'gArACsAKwArACsAKwAxADEAMQA+AD4APgA+AD4APgA+AD4ATQBNAFIAUgBSAFIAUgBSAFIAUgBgAGAAYABhAG' + ` + 'EAYQBiAGIAZgBmAGYAZgBmAGYAcgByAHYAdgB2AHYAdgB2AIAAgACAAIAAgACAAIAAAYACgAOABIAFgAaAB4A' + ` + 'IgAmACoABgAKAA4ABgAKAA4ABgAKAA4ABgAKAA4AEgAWABoABgAKAA4ABgAKAA4AEgAWABoAHgAiAAYACgAGA' + ` + 'AoADgASABYAGgAeACIABgAKAA4ABgAKAA4ABgAKAAYACgAOABIAFgAaAAYACgAGAAoADgASABYAGgAGAAoADg' + ` + 'ASABYAGgAeAAgAFABAAEgAPABEADgANAAwACwAjACUAJwAjACUAJwAjACUAJwArAC0AMAAzACUANgAxADoAPA' + ` + 'A+AEAAQgALAEUARwAwADMATgBQAFIAUABVAFgAWgBcADMAJwAjACUAJwAjACUAJwAlAAsAZwBpAGsAbQBvAHE' + ` + 'AcgAHAHYAeAB6AAEABwBQAIEAgwCFAFwAMwCJAIsAIK0grQSNBJEEkf+dApUgnf+d/51Irf+dApVIrf+dApVI' + ` + 'rf+dApVIrSadSI0Chf+dSJ1IrUid/48mrSadQJ//nwKVAoVInQKFJq1IrUitSI3/jwSBSJ0UnQKVBIFIrf+dA' + ` + 'pVIrf+dApX/rf+PAqUEgUCf/50gnUidSK0Aj0itAoX/j/+fAJ9IjSatFL0Uvf+9BKH/nUiNAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIABQACAAAAAAAAAAAABgACAAsAFQAFAAUAAQA' + ` + 'mAAoAAQATAAIACwAGAAMAAgAIAAIACQACAAgAAgBudGVnZXIgdG8gZGV0ZXJtaW5lIHNvcnQgb3JkZXIgZm9y' + ` + 'IHRhYmxlLkxhc3RTZXF1ZW5jZUZpbGUgc2VxdWVuY2UgbnVtYmVyIGZvciB0aGUgbGFzdCBmaWxlIGZvciB0a' + ` + 'GlzIG1lZGlhLkRpc2tQcm9tcHREaXNrIG5hbWU6IHRoZSB2aXNpYmxlIHRleHQgYWN0dWFsbHkgcHJpbnRlZC' + ` + 'BvbiB0aGUgZGlzay4gIFRoaXMgd2lsbCBiZSB1c2VkIHRvIHByb21wdCB0aGUgdXNlciB3aGVuIHRoaXMgZGl' + ` + 'zayBuZWVkcyB0byBiZSBpbnNlcnRlZC5DYWJpbmV0SWYgc29tZSBvciBhbGwgb2YgdGhlIGZpbGVzIHN0b3Jl' + ` + 'ZCBvbiB0aGUgbWVkaWEgYXJlIGNvbXByZXNzZWQgaW4gYSBjYWJpbmV0LCB0aGUgbmFtZSBvZiB0aGF0IGNhY' + ` + 'mluZXQuVm9sdW1lTGFiZWxUaGUgbGFiZWwgYXR0cmlidXRlZCB0byB0aGUgdm9sdW1lLlNvdXJjZVByb3Blcn' + ` + 'R5VGhlIHByb3BlcnR5IGRlZmluaW5nIHRoZSBsb2NhdGlvbiBvZiB0aGUgY2FiaW5ldCBmaWxlLk5hbWUgb2Y' + ` + 'gcHJvcGVydHksIHVwcGVyY2FzZSBpZiBzZXR0YWJsZSBieSBsYXVuY2hlciBvciBsb2FkZXIuU3RyaW5nIHZh' + ` + 'bHVlIGZvciBwcm9wZXJ0eS4gIE5ldmVyIG51bGwgb3IgZW1wdHkuUmVnaXN0cnlQcmltYXJ5IGtleSwgbm9uL' + ` + 'WxvY2FsaXplZCB0b2tlbi5Sb290VGhlIHByZWRlZmluZWQgcm9vdCBrZXkgZm9yIHRoZSByZWdpc3RyeSB2YW' + ` + 'x1ZSwgb25lIG9mIHJya0VudW0uS2V5UmVnUGF0aFRoZSBrZXkgZm9yIHRoZSByZWdpc3RyeSB2YWx1ZS5UaGU' + ` + 'gcmVnaXN0cnkgdmFsdWUgbmFtZS5UaGUgcmVnaXN0cnkgdmFsdWUuRm9yZWlnbiBrZXkgaW50byB0aGUgQ29t' + ` + 'cG9uZW50IHRhYmxlIHJlZmVyZW5jaW5nIGNvbXBvbmVudCB0aGF0IGNvbnRyb2xzIHRoZSBpbnN0YWxsaW5nI' + ` + 'G9mIHRoZSByZWdpc3RyeSB2YWx1ZS5VcGdyYWRlVXBncmFkZUNvZGVUaGUgVXBncmFkZUNvZGUgR1VJRCBiZW' + ` + 'xvbmdpbmcgdG8gdGhlIHByb2R1Y3RzIGluIHRoaXMgc2V0LlZlcnNpb25NaW5UaGUgbWluaW11bSBQcm9kdWN' + ` + '0VmVyc2lvbiBvZiB0aGUgcHJvZHVjdHMgaW4gdGhpcyBzZXQuICBUaGUgc2V0IG1heSBvciBtYXkgbm90IGlu' + ` + 'Y2x1ZGUgcHJvZHVjdHMgd2l0aCB0aGlzIHBhcnRpY3VsYXIgdmVyc2lvbi5WZXJzaW9uTWF4VGhlIG1heGltd' + ` + 'W0gUHJvZHVjdFZlcnNpb24gb2YgdGhlIHByb2R1Y3RzIGluIHRoaXMgc2V0LiAgVGhlIHNldCBtYXkgb3IgbW' + ` + 'F5IG5vdCBpbmNsdWRlIHByb2R1Y3RzIHdpdGggdGhpcyBwYXJ0aWN1bGFyIHZlcnNpb24uQSBjb21tYS1zZXB' + ` + 'hcmF0ZWQgbGlzdCBvZiBsYW5ndWFnZXMgZm9yIGVpdGhlciBwcm9kdWN0cyBpbiB0aGlzIHNldCBvciBwcm9k' + ` + 'dWN0cyBub3QgaW4gdGhpcyBzZXQuVGhlIGF0dHJpYnV0ZXMgb2YgdGhpcyBwcm9kdWN0IHNldC5SZW1vdmVUa' + ` + 'GUgbGlzdCBvZiBmZWF0dXJlcyB0byByZW1vdmUgd2hlbiB1bmluc3RhbGxpbmcgYSBwcm9kdWN0IGZyb20gdG' + ` + 'hpcyBzZXQuICBUaGUgZGVmYXVsdCBpcyAiQUxMIi5BY3Rpb25Qcm9wZXJ0eVRoZSBwcm9wZXJ0eSB0byBzZXQ' + ` + 'gd2hlbiBhIHByb2R1Y3QgaW4gdGhpcyBzZXQgaXMgZm91bmQuQ29zdEluaXRpYWxpemVGaWxlQ29zdENvc3RG' + ` + 'aW5hbGl6ZUluc3RhbGxWYWxpZGF0ZUluc3RhbGxJbml0aWFsaXplSW5zdGFsbEFkbWluUGFja2FnZUluc3Rhb' + ` + 'GxGaWxlc0luc3RhbGxGaW5hbGl6ZUV4ZWN1dGVBY3Rpb25QdWJsaXNoRmVhdHVyZXNQdWJsaXNoUHJvZHVjdF' + ` + 'Byb2R1Y3RDb21wb25lbnR7OTg5QjBFRDgtREVBRC01MjhELUI4RTMtN0NBRTQxODYyNEQ1fUlOU1RBTExGT0x' + ` + 'ERVJEdW1teUZsYWdWYWx1ZVByb2dyYW1GaWxlc0ZvbGRlcnE0cGZqNHo3fERTQ1NldHVwUHJvamVjdFRBUkdF' + ` + 'VERJUi5Tb3VyY2VEaXJQcm9kdWN0RmVhdHVyZURTQ1NldHVwUHJvamVjdEZpbmRSZWxhdGVkUHJvZHVjdHNMY' + ` + 'XVuY2hDb25kaXRpb25zVmFsaWRhdGVQcm9kdWN0SURNaWdyYXRlRmVhdHVyZVN0YXRlc1Byb2Nlc3NDb21wb2' + ` + '5lbnRzVW5wdWJsaXNoRmVhdHVyZXNSZW1vdmVSZWdpc3RyeVZhbHVlc1dyaXRlUmVnaXN0cnlWYWx1ZXNSZWd' + ` + 'pc3RlclVzZXJSZWdpc3RlclByb2R1Y3RSZW1vdmVFeGlzdGluZ1Byb2R1Y3RzTk9UIFdJWF9ET1dOR1JBREVf' + ` + 'REVURUNURURBIG5ld2VyIHZlcnNpb24gb2YgW1Byb2R1Y3ROYW1lXSBpcyBhbHJlYWR5IGluc3RhbGxlZC5BT' + ` + 'ExVU0VSUzFNYW51ZmFjdHVyZXJNaWNyb3NvZnQgVW5pdCBUZXN0aW5nIEd1aWxkIG9mIEFtZXJpY2FQcm9kdW' + ` + 'N0Q29kZXtERUFEQkVFRi04MEM2LTQxRTYtQTFCOS04QkRCOEEwNTAyN0Z9UHJvZHVjdExhbmd1YWdlMTAzM1B' + ` + 'yb2R1Y3ROYW1lRFNDVW5pdFRlc3RQYWNrYWdlUHJvZHVjdFZlcnNpb24xLjIuMy40ezgzQkMzNzkyLTgwQzYt' + ` + 'NDFFNi1BMUI5LThCREI4QTA1MDI3Rn1TZWN1cmVDdXN0b21Qcm9wZXJ0aWVzV0lYX0RPV05HUkFERV9ERVRFQ' + ` + '1RFRDtXSVhfVVBHUkFERV9ERVRFQ1RFRFdpeFBkYlBhdGhDOlxVc2Vyc1xiZWNhcnJcRG9jdW1lbnRzXFZpc3' + ` + 'VhbCBTdHVkaW8gMjAxMFxQcm9qZWN0c1xEU0NTZXR1cFByb2plY3RcRFNDU2V0dXBQcm9qZWN0XGJpblxEZWJ' + ` + '1Z1xEU0NTZXR1cFByb2plY3Qud2l4cGRiU29mdHdhcmVcRFNDVGVzdERlYnVnRW50cnlbfl1EVU1NWUZMQUc9' + ` + 'W0RVTU1ZRkxBR11bfl1XSVhfVVBHUkFERV9ERVRFQ1RFRFdJWF9ET1dOR1JBREVfREVURUNURURzZWQgdG8gZ' + ` + 'm9yY2UgYSBzcGVjaWZpYyBkaXNwbGF5IG9yZGVyaW5nLkxldmVsVGhlIGluc3RhbGwgbGV2ZWwgYXQgd2hpY2' + ` + 'ggcmVjb3JkIHdpbGwgYmUgaW5pdGlhbGx5IHNlbGVjdGVkLiBBbiBpbnN0YWxsIGxldmVsIG9mIDAgd2lsbCB' + ` + 'kaXNhYmxlIGFuIGl0ZW0gYW5kIHByZXZlbnQgaXRzIGRpc3BsYXkuVXBwZXJDYXNlVGhlIG5hbWUgb2YgdGhl' + ` + 'IERpcmVjdG9yeSB0aGF0IGNhbiBiZSBjb25maWd1cmVkIGJ5IHRoZSBVSS4gQSBub24tbnVsbCB2YWx1ZSB3a' + ` + 'WxsIGVuYWJsZSB0aGUgYnJvd3NlIGJ1dHRvbi4wOzE7Mjs0OzU7Njs4Ozk7MTA7MTY7MTc7MTg7MjA7MjE7Mj' + ` + 'I7MjQ7MjU7MjY7MzI7MzM7MzQ7MzY7Mzc7Mzg7NDg7NDk7NTA7NTI7NQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATmFtZVRhYmxlQ29sdW1uX1Zh' + ` + 'bGlkYXRpb25WYWx1ZU5Qcm9wZXJ0eUlkX1N1bW1hcnlJbmZvcm1hdGlvbkRlc2NyaXB0aW9uU2V0Q2F0ZWdvc' + ` + 'nlLZXlDb2x1bW5NYXhWYWx1ZU51bGxhYmxlS2V5VGFibGVNaW5WYWx1ZUlkZW50aWZpZXJOYW1lIG9mIHRhYm' + ` + 'xlTmFtZSBvZiBjb2x1bW5ZO05XaGV0aGVyIHRoZSBjb2x1bW4gaXMgbnVsbGFibGVZTWluaW11bSB2YWx1ZSB' + ` + 'hbGxvd2VkTWF4aW11bSB2YWx1ZSBhbGxvd2VkRm9yIGZvcmVpZ24ga2V5LCBOYW1lIG9mIHRhYmxlIHRvIHdo' + ` + 'aWNoIGRhdGEgbXVzdCBsaW5rQ29sdW1uIHRvIHdoaWNoIGZvcmVpZ24ga2V5IGNvbm5lY3RzVGV4dDtGb3JtY' + ` + 'XR0ZWQ7VGVtcGxhdGU7Q29uZGl0aW9uO0d1aWQ7UGF0aDtWZXJzaW9uO0xhbmd1YWdlO0lkZW50aWZpZXI7Qm' + ` + 'luYXJ5O1VwcGVyQ2FzZTtMb3dlckNhc2U7RmlsZW5hbWU7UGF0aHM7QW55UGF0aDtXaWxkQ2FyZEZpbGVuYW1' + ` + 'lO1JlZ1BhdGg7Q3VzdG9tU291cmNlO1Byb3BlcnR5O0NhYmluZXQ7U2hvcnRjdXQ7Rm9ybWF0dGVkU0RETFRl' + ` + 'eHQ7SW50ZWdlcjtEb3VibGVJbnRlZ2VyO1RpbWVEYXRlO0RlZmF1bHREaXJTdHJpbmcgY2F0ZWdvcnlUZXh0U' + ` + '2V0IG9mIHZhbHVlcyB0aGF0IGFyZSBwZXJtaXR0ZWREZXNjcmlwdGlvbiBvZiBjb2x1bW5BZG1pbkV4ZWN1dG' + ` + 'VTZXF1ZW5jZUFjdGlvbk5hbWUgb2YgYWN0aW9uIHRvIGludm9rZSwgZWl0aGVyIGluIHRoZSBlbmdpbmUgb3I' + ` + 'gdGhlIGhhbmRsZXIgRExMLkNvbmRpdGlvbk9wdGlvbmFsIGV4cHJlc3Npb24gd2hpY2ggc2tpcHMgdGhlIGFj' + ` + 'dGlvbiBpZiBldmFsdWF0ZXMgdG8gZXhwRmFsc2UuSWYgdGhlIGV4cHJlc3Npb24gc3ludGF4IGlzIGludmFsa' + ` + 'WQsIHRoZSBlbmdpbmUgd2lsbCB0ZXJtaW5hdGUsIHJldHVybmluZyBpZXNCYWRBY3Rpb25EYXRhLlNlcXVlbm' + ` + 'NlTnVtYmVyIHRoYXQgZGV0ZXJtaW5lcyB0aGUgc29ydCBvcmRlciBpbiB3aGljaCB0aGUgYWN0aW9ucyBhcmU' + ` + 'gdG8gYmUgZXhlY3V0ZWQuICBMZWF2ZSBibGFuayB0byBzdXBwcmVzcyBhY3Rpb24uQWRtaW5VSVNlcXVlbmNl' + ` + 'QWR2dEV4ZWN1dGVTZXF1ZW5jZUNvbXBvbmVudFByaW1hcnkga2V5IHVzZWQgdG8gaWRlbnRpZnkgYSBwYXJ0a' + ` + 'WN1bGFyIGNvbXBvbmVudCByZWNvcmQuQ29tcG9uZW50SWRHdWlkQSBzdHJpbmcgR1VJRCB1bmlxdWUgdG8gdG' + ` + 'hpcyBjb21wb25lbnQsIHZlcnNpb24sIGFuZCBsYW5ndWFnZS5EaXJlY3RvcnlfRGlyZWN0b3J5UmVxdWlyZWQ' + ` + 'ga2V5IG9mIGEgRGlyZWN0b3J5IHRhYmxlIHJlY29yZC4gVGhpcyBpcyBhY3R1YWxseSBhIHByb3BlcnR5IG5h' + ` + 'bWUgd2hvc2UgdmFsdWUgY29udGFpbnMgdGhlIGFjdHVhbCBwYXRoLCBzZXQgZWl0aGVyIGJ5IHRoZSBBcHBTZ' + ` + 'WFyY2ggYWN0aW9uIG9yIHdpdGggdGhlIGRlZmF1bHQgc2V0dGluZyBvYnRhaW5lZCBmcm9tIHRoZSBEaXJlY3' + ` + 'RvcnkgdGFibGUuQXR0cmlidXRlc1JlbW90ZSBleGVjdXRpb24gb3B0aW9uLCBvbmUgb2YgaXJzRW51bUEgY29' + ` + 'uZGl0aW9uYWwgc3RhdGVtZW50IHRoYXQgd2lsbCBkaXNhYmxlIHRoaXMgY29tcG9uZW50IGlmIHRoZSBzcGVj' + ` + 'aWZpZWQgY29uZGl0aW9uIGV2YWx1YXRlcyB0byB0aGUgJ1RydWUnIHN0YXRlLiBJZiBhIGNvbXBvbmVudCBpc' + ` + 'yBkaXNhYmxlZCwgaXQgd2lsbCBub3QgYmUgaW5zdGFsbGVkLCByZWdhcmRsZXNzIG9mIHRoZSAnQWN0aW9uJy' + ` + 'BzdGF0ZSBhc3NvY2lhdGVkIHdpdGggdGhlIGNvbXBvbmVudC5LZXlQYXRoRmlsZTtSZWdpc3RyeTtPREJDRGF' + ` + '0YVNvdXJjZUVpdGhlciB0aGUgcHJpbWFyeSBrZXkgaW50byB0aGUgRmlsZSB0YWJsZSwgUmVnaXN0cnkgdGFi' + ` + 'bGUsIG9yIE9EQkNEYXRhU291cmNlIHRhYmxlLiBUaGlzIGV4dHJhY3QgcGF0aCBpcyBzdG9yZWQgd2hlbiB0a' + ` + 'GUgY29tcG9uZW50IGlzIGluc3RhbGxlZCwgYW5kIGlzIHVzZWQgdG8gZGV0ZWN0IHRoZSBwcmVzZW5jZSBvZi' + ` + 'B0aGUgY29tcG9uZW50IGFuZCB0byByZXR1cm4gdGhlIHBhdGggdG8gaXQuVW5pcXVlIGlkZW50aWZpZXIgZm9' + ` + 'yIGRpcmVjdG9yeSBlbnRyeSwgcHJpbWFyeSBrZXkuIElmIGEgcHJvcGVydHkgYnkgdGhpcyBuYW1lIGlzIGRl' + ` + 'ZmluZWQsIGl0IGNvbnRhaW5zIHRoZSBmdWxsIHBhdGggdG8gdGhlIGRpcmVjdG9yeS5EaXJlY3RvcnlfUGFyZ' + ` + 'W50UmVmZXJlbmNlIHRvIHRoZSBlbnRyeSBpbiB0aGlzIHRhYmxlIHNwZWNpZnlpbmcgdGhlIGRlZmF1bHQgcG' + ` + 'FyZW50IGRpcmVjdG9yeS4gQSByZWNvcmQgcGFyZW50ZWQgdG8gaXRzZWxmIG9yIHdpdGggYSBOdWxsIHBhcmV' + ` + 'udCByZXByZXNlbnRzIGEgcm9vdCBvZiB0aGUgaW5zdGFsbCB0cmVlLkRlZmF1bHREaXJUaGUgZGVmYXVsdCBz' + ` + 'dWItcGF0aCB1bmRlciBwYXJlbnQncyBwYXRoLkZlYXR1cmVQcmltYXJ5IGtleSB1c2VkIHRvIGlkZW50aWZ5I' + ` + 'GEgcGFydGljdWxhciBmZWF0dXJlIHJlY29yZC5GZWF0dXJlX1BhcmVudE9wdGlvbmFsIGtleSBvZiBhIHBhcm' + ` + 'VudCByZWNvcmQgaW4gdGhlIHNhbWUgdGFibGUuIElmIHRoZSBwYXJlbnQgaXMgbm90IHNlbGVjdGVkLCB0aGV' + ` + 'uIHRoZSByZWNvcmQgd2lsbCBub3QgYmUgaW5zdGFsbGVkLiBOdWxsIGluZGljYXRlcyBhIHJvb3QgaXRlbS5U' + ` + 'aXRsZVNob3J0IHRleHQgaWRlbnRpZnlpbmcgYSB2aXNpYmxlIGZlYXR1cmUgaXRlbS5Mb25nZXIgZGVzY3Jpc' + ` + 'HRpdmUgdGV4dCBkZXNjcmliaW5nIGEgdmlzaWJsZSBmZWF0dXJlIGl0ZW0uRGlzcGxheU51bWVyaWMgc29ydC' + ` + 'BvcmRlciwgdXNlZCB0byBmb3JjZSBhIHNwZWNpZmljIGRpc3BsYXkgb3JkZXJpbmcuTGV2ZWxUaGUgaW5zdGF' + ` + 'sbCBsZXZlbCBhdCB3aGljaCByZWNvcmQgd2lsbCBiZSBpbml0aWFsbHkgc2VsZWN0ZWQuIEFuIGluc3RhbGwg' + ` + 'bGV2ZWwgb2YgMCB3aWxsIGRpc2FibGUgYW4gaXRlbSBhbmQgcHJldmVudCBpdHMgZGlzcGxheS5VcHBlckNhc' + ` + '2VUaGUgbmFtZSBvZiB0aGUgRGlyZWN0b3J5IHRoYXQgY2FuIGJlIGNvbmZpZ3VyZWQgYnkgdGhlIFVJLiBBIG' + ` + '5vbi1udWxsIHZhbHVlIHdpbGwgZW5hYmxlIHRoZSBicm93c2UgYnV0dG9uLjA7MTsyOzQ7NTs2Ozg7OTsxMDs' + ` + 'xNjsxNzsxODsyMDsyMTsyMjsyNDsyNTsyNjszMjszMzszNDszNjszNzszODs0ODs0OTs1MDs1Mjs1Mzs1NEZl' + ` + 'YXR1cmUgYXR0cmlidXRlc0ZlYXR1cmVDb21wb25lbnRzRmVhdHVyZV9Gb3JlaWduIGtleSBpbnRvIEZlYXR1c' + ` + 'mUgdGFibGUuQ29tcG9uZW50X0ZvcmVpZ24ga2V5IGludG8gQ29tcG9uZW50IHRhYmxlLkZpbGVQcmltYXJ5IG' + ` + 'tleSwgbm9uLWxvY2FsaXplZCB0b2tlbiwgbXVzdCBtYXRjaCBpZGVudGlmaWVyIGluIGNhYmluZXQuICBGb3I' + ` + 'gdW5jb21wcmVzc2VkIGZpbGVzLCB0aGlzIGZpZWxkIGlzIGlnbm9yZWQuRm9yZWlnbiBrZXkgcmVmZXJlbmNp' + ` + 'bmcgQ29tcG9uZW50IHRoYXQgY29udHJvbHMgdGhlIGZpbGUuRmlsZU5hbWVGaWxlbmFtZUZpbGUgbmFtZSB1c' + ` + '2VkIGZvciBpbnN0YWxsYXRpb24sIG1heSBiZSBsb2NhbGl6ZWQuICBUaGlzIG1heSBjb250YWluIGEgInNob3' + ` + 'J0IG5hbWV8bG9uZyBuYW1lIiBwYWlyLkZpbGVTaXplU2l6ZSBvZiBmaWxlIGluIGJ5dGVzIChsb25nIGludGV' + ` + 'nZXIpLlZlcnNpb25WZXJzaW9uIHN0cmluZyBmb3IgdmVyc2lvbmVkIGZpbGVzOyAgQmxhbmsgZm9yIHVudmVy' + ` + 'c2lvbmVkIGZpbGVzLkxhbmd1YWdlTGlzdCBvZiBkZWNpbWFsIGxhbmd1YWdlIElkcywgY29tbWEtc2VwYXJhd' + ` + 'GVkIGlmIG1vcmUgdGhhbiBvbmUuSW50ZWdlciBjb250YWluaW5nIGJpdCBmbGFncyByZXByZXNlbnRpbmcgZm' + ` + 'lsZSBhdHRyaWJ1dGVzICh3aXRoIHRoZSBkZWNpbWFsIHZhbHVlIG9mIGVhY2ggYml0IHBvc2l0aW9uIGluIHB' + ` + 'hcmVudGhlc2VzKVNlcXVlbmNlIHdpdGggcmVzcGVjdCB0byB0aGUgbWVkaWEgaW1hZ2VzOyBvcmRlciBtdXN0' + ` + 'IHRyYWNrIGNhYmluZXQgb3JkZXIuSW5zdGFsbEV4ZWN1dGVTZXF1ZW5jZUluc3RhbGxVSVNlcXVlbmNlTGF1b' + ` + 'mNoQ29uZGl0aW9uRXhwcmVzc2lvbiB3aGljaCBtdXN0IGV2YWx1YXRlIHRvIFRSVUUgaW4gb3JkZXIgZm9yIG' + ` + 'luc3RhbGwgdG8gY29tbWVuY2UuRm9ybWF0dGVkTG9jYWxpemFibGUgdGV4dCB0byBkaXNwbGF5IHdoZW4gY29' + ` + 'uZGl0aW9uIGZhaWxzIGFuZCBpbnN0YWxsIG11c3QgYWJvcnQuTWVkaWFEaXNrSWRQcmltYXJ5IGtleSwgaQgA' + ` + 'AgAIAAIACAACAAoAFgANAAEADgABAAMAAQAeAAEAAQAnABUAAQAVAAEANgABACQAAQD1AAEADwABAAQACQAgA' + ` + 'AEAFQABABQABwAGAAoAQgAFAAkAFQCfAAUACAAMAG8ABQAPAAcAEwAHAAkAEgA7AAEACwACAAQAAgA+AAEACg' + ` + 'AEAAkADADSAAEACgAIACcAAQDoAAEABwACABwAAQDjAAEAhgABABAAAgCmAAEACgADACkAAQAHABUAOQABAA4' + ` + 'AAgCUAAEABQACAC4AAQA6AAEABwACAD4AAQAFAAIAgQABAAkAAgBrAAEAUQABABIAAQARAAUACAACAB8AAQAK' + ` + 'AAYAIQABAAQAFABzAAEAOQABAAgAAgAIAAEAYwABAAgAAgAlAAEABwADAEEAAQAIAAYAPwABAHYAAQBKAAEAF' + ` + 'gAHABEABwAPAAUASAABAAkABABIAAEABQANAAYAAgA3AAEADAACADYAAQAKAAIAhAABAAcAAwBmAAEACwACAC' + ` + 'MAAQAGAAIACAAIADcAAQA+AAEAMAABAAgADwAhAAEABAACAD8AAQADAAIABwABAB8AAQAYAAEAEwABAG4AAQA' + ` + 'HAA8ACwADADsAAQAKAAIAfgABAAoAAgB+AAEAYAABACMAAQAGAAIAYAABAA4AAgA4AAEADgAFAAgABAAMAAUA' + ` + 'DwADABEAAwATAAEADAABAA8AAwANAAIADwACAA4AAgAQAAMAJgABAA0AAgAOAAIAEgACABgAAQAJAAIAAQABA' + ` + 'AkAAQAOAAIADwABABMAAgAQAAIAEQACABQAAgARAAEAEQABABQAAQATAAEADAABAA8AAQAWAAEAGgABADYAAQ' + ` + 'AIAAEAAQABAAwAAQAnAAEACwABACYAAQAPAAEABAABAAsAAQASAAEADgABAAcAAwAmAAMAFgABACsAAQAKAAE' + ` + 'AdgABABAAAQAKAAEAGwABABQAAQAWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAA=' + #endregion + + $msiContentInBytes = [System.Convert]::FromBase64String($msiContentInBase64) + + Set-Content -Path $DestinationPath -Value $msiContentInBytes -Encoding 'Byte' | Out-Null +} + +<# + .SYNOPSIS + Retrieves a localized registry key value. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER ValueName + The name of the value to retrieve. +#> +function Get-LocalizedRegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Object] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ValueName + ) + + $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName) + + if ($null -eq $localizedRegistryKeyValue) + { + $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName) + } + + return $localizedRegistryKeyValue +} + +Export-ModuleMember -Function ` + New-TestMsi, ` + Clear-xPackageCache, ` + New-MockFileServer, ` + New-TestExecutable, ` + Test-PackageInstalledByName diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 new file mode 100644 index 000000000..a2ca433b4 --- /dev/null +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -0,0 +1,497 @@ +Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xPackageResource' ` + -TestType 'Unit' + +try +{ + InModuleScope 'MSFT_xPackageResource' { + Describe 'MSFT_xPackageResource Unit Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" + + $script:skipHttpsTest = $true + + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null + + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName + + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + + New-TestMsi -DestinationPath $script:msiLocation | Out-Null + + $script:testExecutablePath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestExecutable.exe' + New-TestExecutable -DestinationPath $script:testExecutablePath | Out-Null + + Clear-xPackageCache | Out-Null + } + + BeforeEach { + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Package could not be removed.' + } + } + + AfterAll { + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } + } + + Context 'Get-TargetResource' { + It 'Should return only basic properties for absent package' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + } + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + + It 'Should return basic and registry properties for present package with registry check parameters specified and CreateCheckRegValue true' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'CreateCheckRegValue', 'InstalledCheckRegHive', 'InstalledCheckRegKey', 'InstalledCheckRegValueName', 'InstalledCheckRegValueData' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + + It 'Should return full package properties for present package with registry check parameters specified and CreateCheckRegValue false' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + CreateCheckRegValue = $false + InstalledCheckRegKey = '' + InstalledCheckRegValueName = '' + InstalledCheckRegValueData = '' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'Path', 'InstalledOn', 'Size', 'Version', 'PackageDescription', 'Publisher' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + + It 'Should return full package properties for present package without registry check parameters specified' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + } + + Set-TargetResource -Ensure 'Present' @packageParameters + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'Path', 'InstalledOn', 'Size', 'Version', 'PackageDescription', 'Publisher' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + } + + Context 'Test-TargetResource' { + It 'Should return correct value when package is absent' { + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $true + } + + It 'Should return correct value when package is present without registry parameters' { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + Clear-xPackageCache + + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $false + } + + $existingPackageParameters = @{ + Path = $script:testExecutablePath + Name = [String]::Empty + ProductId = [String]::Empty + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + It 'Should return present with existing exe and matching registry parameters' { + Set-TargetResource -Ensure 'Present' @existingPackageParameters + + try + { + $testTargetResourceResult = Test-TargetResource -Ensure 'Present' @existingPackageParameters + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource -Ensure 'Absent' @existingPackageParameters + $testTargetResourceResult | Should Be $false + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($existingPackageParameters.InstalledCheckRegKey) + } + } + + $parametersToMismatchCheck = @( 'InstalledCheckRegKey', 'InstalledCheckRegValueName', 'InstalledCheckRegValueData' ) + + foreach ($parameterToMismatchCheck in $parametersToMismatchCheck) + { + It "Should return not present with existing exe and mismatching parameter $parameterToMismatchCheck" { + Set-TargetResource -Ensure 'Present' @existingPackageParameters + + try + { + $mismatchingParameters = $existingPackageParameters.Clone() + $mismatchingParameters[$parameterToMismatchCheck] = 'not original value' + + Write-Verbose -Message "Test target resource parameters: $( Out-String -InputObject $mismatchingParameters)" + + $testTargetResourceResult = Test-TargetResource -Ensure 'Present' @mismatchingParameters + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource -Ensure 'Absent' @mismatchingParameters + $testTargetResourceResult | Should Be $true + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($existingPackageParameters.InstalledCheckRegKey) + } + } + } + } + + Context 'Set-TargetResource' { + It 'Should correctly install and remove a .msi package without registry parameters' { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + $getTargetResourceResult.Version | Should Be '1.2.3.4' + $getTargetResourceResult.InstalledOn | Should Be ("{0:d}" -f [DateTime]::Now.Date) + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $script:packageId + $getTargetResourceResult.Path | Should Be $script:msiLocation + + # Can't figure out how to set this within the MSI. + # $getTargetResourceResult.PackageDescription | Should Be 'A package for unit testing' + + [Math]::Round($getTargetResourceResult.Size, 2) | Should Be 0.03 + + Set-TargetResource -Ensure 'Absent' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + Test-PackageInstalledByName -Name $script:packageName | Should Be $false + } + + It 'Should correctly install and remove a .msi package with registry parameters' { + $packageParameters = @{ + Path = $script:msiLocation + Name = [String]::Empty + ProductId = $script:packageId + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + $getTargetResourceResult = Get-TargetResource @packageParameters + + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $packageParameters.ProductId + $getTargetResourceResult.Path | Should Be $packageParameters.Path + $getTargetResourceResult.Name | Should Be $packageParameters.Name + $getTargetResourceResult.CreateCheckRegValue | Should Be $packageParameters.CreateCheckRegValue + $getTargetResourceResult.InstalledCheckRegHive | Should Be $packageParameters.InstalledCheckRegHive + $getTargetResourceResult.InstalledCheckRegKey | Should Be $packageParameters.InstalledCheckRegKey + $getTargetResourceResult.InstalledCheckRegValueName | Should Be $packageParameters.InstalledCheckRegValueName + $getTargetResourceResult.InstalledCheckRegValueData | Should Be $packageParameters.InstalledCheckRegValueData + + Set-TargetResource -Ensure 'Absent' @packageParameters + + Test-PackageInstalledByName -Name $script:packageName | Should Be $false + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + + It 'Should correctly install and remove a .exe package with registry parameters' { + $packageParameters = @{ + Path = $script:testExecutablePath + Name = [String]::Empty + ProductId = [String]::Empty + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Test-TargetResource -Ensure 'Present' @packageParameters | Should Be $true + + $getTargetResourceResult = Get-TargetResource @packageParameters + + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $packageParameters.ProductId + $getTargetResourceResult.Path | Should Be $packageParameters.Path + $getTargetResourceResult.Name | Should Be $packageParameters.Name + $getTargetResourceResult.CreateCheckRegValue | Should Be $packageParameters.CreateCheckRegValue + $getTargetResourceResult.InstalledCheckRegHive | Should Be $packageParameters.InstalledCheckRegHive + $getTargetResourceResult.InstalledCheckRegKey | Should Be $packageParameters.InstalledCheckRegKey + $getTargetResourceResult.InstalledCheckRegValueName | Should Be $packageParameters.InstalledCheckRegValueName + $getTargetResourceResult.InstalledCheckRegValueData | Should Be $packageParameters.InstalledCheckRegValueData + + Set-TargetResource -Ensure 'Absent' @packageParameters + + Test-TargetResource -Ensure 'Absent' @packageParameters | Should Be $true + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + + It 'Should throw with incorrect product id' { + $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' + + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $wrongPackageId -Name ([String]::Empty) } | Should Throw + } + + It 'Should throw with incorrect name' { + $wrongPackageName = 'WrongPackageName' + + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId ([String]::Empty) -Name $wrongPackageName } | Should Throw + } + + It 'Should correctly install and remove a package from a HTTP URL' { + $baseUrl = 'http://localhost:1242/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation + + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw + + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should Be $false + + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() + } + + It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { + $baseUrl = 'https://localhost:1243/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation -Https + + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw + + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalledByName -Name $script:packageName | Should Be $false + + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() + } + + It 'Should write to the specified log path' { + $logPath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestMsiLog.txt' + + if (Test-Path -Path $logPath) + { + Remove-Item -Path $logPath -Force + } + + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -Name $script:packageName -LogPath $logPath -ProductId ([string]::Empty) + + Test-Path -Path $logPath | Should Be $true + Get-Content -Path $logPath | Should Not Be $null + } + } + + Context 'Get-MsiTools' { + It 'Should add MSI tools in the Microsoft.Windows.DesiredStateConfiguration.xPackageResource namespace' { + $addTypeResult = @{ Namespace = 'Mock not called' } + Mock -CommandName 'Add-Type' -MockWith { $addTypeResult['Namespace'] = $Namespace } + + $msiTools = Get-MsiTools + + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + Assert-MockCalled -CommandName 'Add-Type' -Times 0 + + $msiTools | Should Be ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + Assert-MockCalled -CommandName 'Add-Type' -Times 1 + + $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' + $msiTools | Should Be $null + } + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 b/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 index eb1c7a0af..5ac5a225e 100644 --- a/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 @@ -2,7 +2,10 @@ Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" <# .SYNOPSIS - Stops all instances of a process using the process name. + Stops all instances of the process with the given name. + + .PARAMETER ProcessName + The name of the process to stop. #> function Stop-ProcessByName { @@ -13,8 +16,8 @@ function Stop-ProcessByName $ProcessName ) - Stop-Process -Name $ProcessName -Force -ErrorAction SilentlyContinue - Wait-ScriptBlockReturnTrue -ScriptBlock {$null -eq (Get-Process -Name $ProcessName -ErrorAction SilentlyContinue)} -TimeoutSeconds 15 + Stop-Process -Name $ProcessName -ErrorAction 'SilentlyContinue' -Force + Wait-ScriptBlockReturnTrue -ScriptBlock { return $null -eq (Get-Process -Name $ProcessName -ErrorAction 'SilentlyContinue') } -TimeoutSeconds 15 } Export-ModuleMember -Function Stop-ProcessByName diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 new file mode 100644 index 000000000..be65087e5 --- /dev/null +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -0,0 +1,493 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] +param () + +Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xProcessResource' ` + -TestType 'Unit' + +try +{ + InModuleScope 'MSFT_xProcessResource' { + Describe 'MSFT_xProcessResource Unit Tests' { + BeforeAll { + Import-Module -Name "$PSScriptRoot\MSFT_xProcessResource.TestHelper.psm1" -Force + + $script:cmdProcessShortName = 'ProcessTest' + $script:cmdProcessFullName = 'ProcessTest.exe' + $script:cmdProcessFullPath = "$env:winDir\system32\ProcessTest.exe" + Copy-Item -Path "$env:winDir\system32\cmd.exe" -Destination $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + + $script:processTestFolder = Join-Path -Path (Get-Location) -ChildPath 'ProcessTestFolder' + + if (Test-Path -Path $script:processTestFolder) + { + Remove-Item -Path $script:processTestFolder -Recurse -Force + } + + New-Item -Path $script:processTestFolder -ItemType 'Directory' | Out-Null + + Push-Location -Path $script:processTestFolder + } + + AfterAll { + Stop-ProcessByName -ProcessName $script:cmdProcessShortName + + if (Test-Path -Path $script:cmdProcessFullPath) + { + Remove-Item -Path $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + } + + Pop-Location + + if (Test-Path -Path $script:processTestFolder) + { + Remove-Item -Path $script:processTestFolder -Recurse -Force + } + } + + BeforeEach { + Stop-ProcessByName -ProcessName $script:cmdProcessShortName + } + + Context 'Get-TargetResource' { + It 'Should return the correct properties for a process that is absent with Arguments' { + $processArguments = 'TestGetProperties' + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'Arguments', 'Ensure', 'Path' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.Path -icontains $script:cmdProcessFullPath | Should Be $true + $getTargetResourceResult.Count | Should Be 3 + } + + It 'Should return the correct properties for a process that is absent without Arguments' { + $processArguments = '' + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'Arguments', 'Ensure', 'Path' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.Path -icontains $script:cmdProcessFullPath | Should Be $true + $getTargetResourceResult.Count | Should Be 3 + } + + It 'Should return the correct properties for a process that is present with Arguments' { + $processArguments = 'TestGetProperties' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Path.IndexOf("ProcessTest.exe",[Stringcomparison]::OrdinalIgnoreCase) -le 0 | Should Be $false + $getTargetResourceResult.NonPagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.HandleCount -le 0 | Should Be $false + $getTargetResourceResult.ProcessId -le 0 | Should Be $false + $getTargetResourceResult.Count | Should Be 8 + } + + It 'Should return the correct properties for a process that is present without Arguments' { + $processArguments = '' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Path.IndexOf("ProcessTest.exe",[Stringcomparison]::OrdinalIgnoreCase) -le 0 | Should Be $false + $getTargetResourceResult.NonPagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.HandleCount -le 0 | Should Be $false + $getTargetResourceResult.ProcessId -le 0 | Should Be $false + $getTargetResourceResult.Count | Should Be 8 + } + + It 'Should return correct Ensure value based on Arguments parameter with multiple processes' { + $actualArguments = 'TestProcessResourceWithArguments' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Absent' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments) + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotOrginalArguments') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Absent' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + $processes[0].Arguments.Length | Should Be 0 + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments) + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + $processes[0].Arguments | Should Be $actualArguments + } + } + + Context 'Set-TargetResource' { + It 'Should start and stop a process with no arguments' { + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $true + + Set-TargetResource -Path $script:cmdProcessFullPath -Ensure 'Absent' -Arguments '' + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false + } + + It 'Should have correct output for absent process with WhatIf specified and default Ensure' { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Arguments = '' + } + + $expectedWhatIfOutput = @( $LocalizedData.StartingProcessWhatif, $script:cmdProcessFullPath ) + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput $expectedWhatIfOutput + + if ($setTargetResourceParameters.ContainsKey('WhatIf')) + { + $setTargetResourceParameters.Remove('WhatIf') + } + + $testTargetResourceResult = Test-TargetResource @setTargetResourceParameters + $testTargetResourceResult | Should Be $false + } + + It 'Should have no output for absent process with WhatIf specified and Ensure Absent' { + $setTargetResourceParameters = @{ + Ensure = 'Absent' + Path = $script:cmdProcessFullPath + Arguments = '' + } + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput '' + } + + It 'Should have correct output for existing process with WhatIf specified and Ensure Absent' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + $setTargetResourceParameters = @{ + Ensure = 'Absent' + Path = $script:cmdProcessFullPath + Arguments = '' + } + + $expectedWhatIfOutput = @( $LocalizedData.StoppingProcessWhatif, $script:cmdProcessFullPath ) + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput $expectedWhatIfOutput + + if ($setTargetResourceParameters.ContainsKey('WhatIf')) + { + $setTargetResourceParameters.Remove('WhatIf') + } + + $testTargetResourceResult = Test-TargetResource @setTargetResourceParameters + $testTargetResourceResult | Should Be $false + } + + It 'Should have no output for existing process with WhatIf specified and default Ensure' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Arguments = '' + } + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput '' + } + + It 'Should provide correct error output to the specified error and output streams when using invalid input from the specified input stream' { + $errorPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsError.txt' + $outputPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsOutput.txt' + $inputPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsInput.txt' + + $workingDirectoryPath = Join-Path -Path (Get-Location) -ChildPath 'TestWorkingDirectory' + + foreach ($path in @( $errorPath, $outputPath, $inputPath, $workingDirectoryPath )) + { + if (Test-Path -Path $path) + { + Remove-Item -Path $path -Recurse -Force + } + } + + New-Item -Path $workingDirectoryPath -ItemType 'Directory' | Out-Null + + $inputFileText = "ECHO Testing ProcessTest.exe ` + dir volumeSyntaxError:\ ` + set /p waitforinput=Press [y/n]?: " + + Out-File -FilePath $inputPath -InputObject $inputFileText -Encoding 'ASCII' + + Set-TargetResource -Path $script:cmdProcessFullPath -WorkingDirectory $workingDirectoryPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments '' + + Wait-ScriptBlockReturnTrue -ScriptBlock { (Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '').Ensure -ieq 'Absent' } -TimeoutSeconds 10 + + Wait-ScriptBlockReturnTrue -ScriptBlock { Test-IsFileLocked -Path $errorPath } -TimeoutSeconds 2 + + $errorFileContent = Get-Content -Path $errorPath -Raw + $errorFileContent | Should Not Be $null + + Wait-ScriptBlockReturnTrue -ScriptBlock { Test-IsFileLocked -Path $outputPath } -TimeoutSeconds 2 + + $outputFileContent = Get-Content -Path $outputPath -Raw + $outputFileContent | Should Not Be $null + + if ((Get-Culture).Name -ieq 'en-us') + { + $errorFileContent.Contains('The filename, directory name, or volume label syntax is incorrect.') | Should Be $true + $outputFileContent.Contains('Press [y/n]?:') | Should Be $true + $outputFileContent.ToLower().Contains($workingDirectoryPath.ToLower()) | Should Be $true + } + else + { + $errorFileContent.Length -gt 0 | Should Be $true + $outputFileContent.Length -gt 0 | Should Be $true + } + } + + It 'Should throw when trying to specify streams or working directory with Ensure Absent' { + $invalidPropertiesWithAbsent = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($invalidPropertyWithAbsent in $invalidPropertiesWithAbsent) + { + $setTargetResourceArguments = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Absent' + Arguments = '' + $invalidPropertyWithAbsent = 'Something' + } + + { Set-TargetResource @setTargetResourceArguments } | Should Throw ($LocalizedData.ParameterShouldNotBeSpecified -f $invalidPropertyWithAbsent) + } + } + + It 'Should throw when passing a relative path to stream or working directory parameters' { + $invalidRelativePath = '..\RelativePath' + $pathParameters = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) + + foreach($pathParameter in $pathParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathParameter = $invalidRelativePath + } + + { Set-TargetResource @setTargetResourceParameters } | Should Throw $LocalizedData.PathShouldBeAbsolute + } + } + + It 'Should throw when providing a nonexistent path for StandardInputPath or WorkingDirectory' { + $invalidNonexistentPath = Join-Path -Path (Get-Location) -ChildPath 'NonexistentPath' + + if (Test-Path -Path $invalidNonexistentPath) + { + Remove-Item -Path $invalidNonexistentPath -Recurse -Force + } + + $pathMustExistParameters = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($pathMustExistParameter in $pathMustExistParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathMustExistParameter = $invalidNonexistentPath + } + + { Set-TargetResource @setTargetResourceParameters } | Should Throw $LocalizedData.PathShouldExist + } + } + + It 'Should not throw when providing a nonexistent path for StandardOutputPath or StandardErrorPath' { + $invalidNonexistentPath = Join-Path -Path (Get-Location) -ChildPath 'NonexistentPath' + + if (Test-Path -Path $invalidNonexistentPath) + { + Remove-Item -Path $invalidNonexistentPath -Recurse -Force + } + + $pathNotNeedExistParameters = @( 'StandardOutputPath', 'StandardErrorPath' ) + + foreach ($pathNotNeedExistParameter in $pathNotNeedExistParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathNotNeedExistParameter = $invalidNonexistentPath + } + + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + } + } + + Context 'Test-TargetResource' { + It 'Should return correct value based on Arguments' { + $actualArguments = 'TestProcessResourceWithArguments' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotTheOriginalArguments' | Should Be $false + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments | Should Be $true + } + + It 'Should return false for absent process with directory arguments' { + $testTargetResourceResult = Test-TargetResource ` + -Path $script:cmdProcessFullPath ` + -WorkingDirectory 'something' ` + -StandardOutputPath 'something' ` + -StandardErrorPath 'something' ` + -StandardInputPath 'something' ` + -Arguments '' + + $testTargetResourceResult | Should Be $false + } + + } + + Context 'Get-Win32Process' { + It 'Should only return one process when arguments were changed for that process' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments 'abc' + + $processes = @( Get-Win32Process -Path $script:cmdProcessFullPath -UseGetCimInstanceThreshold 0 ) + $processes.Count | Should Be 1 + + $processes = @( Get-Win32Process -Path $script:cmdProcessFullPath -UseGetCimInstanceThreshold 5 ) + $processes.Count | Should Be 1 + } + } + + Context 'Get-ArgumentsFromCommandLineInput' { + It 'Should retrieve expected arguments from command line input' { + $testCases = @( @{ + CommandLineInput = "c a " + ExpectedArguments = "a" + }, + @{ + CommandLineInput = '"c b d" e ' + ExpectedArguments = "e" + }, + @{ + CommandLineInput = " a b" + ExpectedArguments = "b" + }, + @{ + CommandLineInput = " abc " + ExpectedArguments = "" + } + ) + + foreach ($testCase in $testCases) + { + $commandLineInput = $testCase.CommandLineInput + $expectedArguments = $testCase.ExpectedArguments + $actualArguments = Get-ArgumentsFromCommandLineInput -CommandLineInput $commandLineInput + + $actualArguments | Should Be $expectedArguments + } + } + } + + Context 'Split-Credential' { + It 'Should return correct domain and username with @ seperator' { + $testUsername = 'user@domain' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' + } + + It 'Should return correct domain and username with \ seperator' { + $testUsername = 'domain\user' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' + } + + It 'Should return correct domain and username with a local user' { + $testUsername = 'localuser' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Username | Should Be 'localuser' + } + + It 'Should throw when more than one \ in username' { + $testUsername = 'user\domain\foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw + } + + It 'Should throw when more than one @ in username' { + $testUsername = 'user@domain@foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/appveyor.yml b/appveyor.yml index f6ce89caf..2408e30f7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,28 +1,13 @@ #---------------------------------# # environment configuration # #---------------------------------# -os: WMF 5 -version: 3.12.{build}.0 +version: 3.13.{build}.0 install: - git clone https://github.com/PowerShell/DscResource.Tests - - ps: Push-Location - - cd DscResource.Tests - - ps: Import-Module .\TestHelper.psm1 -force - - ps: Pop-Location - ps: | - Get-PackageProvider -Name nuget -ForceBootstrap -Force - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - (Get-date).AddHours(3).AddMinutes(-5) | Export-Clixml -Path ..\BuildTimeout.xml - $installed = $false - $retries = 1 - while($retries -lt 10 -and !$installed) - { - Write-Verbose -message "Attempting to install Pester, attempt # $retries" - $pesterModule = @(find-module 'Pester' -Repository PSGallery -ErrorAction SilentlyContinue) - Install-Module -InputObject $pesterModule -Force - $installed = ($LASTEXITCODE -eq 0) - $retries++ - } + Import-Module -Name .\DscResource.Tests\TestHelper.psm1 -Force + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name Pester -Repository PSGallery -Force #---------------------------------# # build configuration # @@ -55,7 +40,7 @@ deploy_script: # Creating project artifact $stagingDirectory = (Resolve-Path ..).Path $manifest = Join-Path $pwd "xPSDesiredStateConfiguration.psd1" - (Get-Content $manifest -Raw).Replace("3.12.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest + (Get-Content $manifest -Raw).Replace("3.13.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest $zipFilePath = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" Add-Type -assemblyname System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::CreateFromDirectory($pwd, $zipFilePath) diff --git a/xPSDesiredStateConfiguration.psd1 b/xPSDesiredStateConfiguration.psd1 index 752dc0b83..1db1f4928 100644 --- a/xPSDesiredStateConfiguration.psd1 +++ b/xPSDesiredStateConfiguration.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. -ModuleVersion = '3.12.0.0' +ModuleVersion = '3.13.0.0' # ID used to uniquely identify this module GUID = 'cc8dc021-fa5f-4f96-8ecf-dfd68a6d9d48' @@ -52,7 +52,22 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '* Removed localization for now so that resources can run on non-English systems. + ReleaseNotes = '* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* Updated appveyor.yml to use the default image. +* Merged xPackage with in-box Package resource and added tests. +* xPackage: Re-implemented parameters for installation check from registry key value. +* xGroup: + * Fixed Verbose output in Get-MembersAsPrincipals function. + * Fixed bug when credential parameter passed does not contain local or domain context. + * Fixed logic bug in MembersToInclude and MembersToExclude. + * Fixed bug when trying to include the built-in Administrator in Members. + * Fixed bug where Test-TargetResource would check for members when none specified. + * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bugs in Assert-GroupNameValid to throw correct exception. +* xService + * Updated xService resource to allow empty string for Description parameter. +* Merged xProcess with in-box Process resource and added tests. ' @@ -64,3 +79,4 @@ PrivateData = @{ +