From 05710161d6b59d53dd4a9d1611af73dc4925be49 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Wed, 13 Jul 2016 16:07:30 -0700 Subject: [PATCH 1/6] Starting to merge Process. --- DSCResources/CommonResourceHelper.psm1 | 52 ++ .../MSFT_xProcessResource.psm1 | 754 ++++++------------ .../MSFT_xProcessResource.TestHelper.psm1 | 9 +- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 463 +++++++++++ 4 files changed, 780 insertions(+), 498 deletions(-) create mode 100644 Tests/Unit/MSFT_xProcessResource.Tests.ps1 diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 544b9127b..dcb8c3ac6 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -84,6 +84,58 @@ function New-InvalidOperationException throw $errorRecordToThrow } +<# +# The goal of this function is to get domain and username from PSCredential +# without calling GetNetworkCredential() method. +# Call to GetNetworkCredential() expose password as a plain text in memory. +#> +function Get-DomainAndUserName([PSCredential]$Credential) +{ + # + # Supported formats: DOMAIN\username, username@domain + # + $wrongFormat = $false + if ($Credential.UserName.Contains('\')) + { + $segments = $Credential.UserName.Split('\') + if ($segments.Length -gt 2) + { + # i.e. domain\user\foo + $wrongFormat = $true + } else { + $Domain = $segments[0] + $UserName = $segments[1] + } + } + elseif ($Credential.UserName.Contains('@')) + { + $segments = $Credential.UserName.Split('@') + if ($segments.Length -gt 2) + { + # i.e. user@domain@foo + $wrongFormat = $true + } else { + $UserName = $segments[0] + $Domain = $segments[1] + } + } + else + { + # support for default domain (localhost) + return @( $env:COMPUTERNAME, $Credential.UserName ) + } + + if ($wrongFormat) + { + $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName + Write-Verbose $message + $exception = New-Object System.ArgumentException $message + throw New-Object System.Management.Automation.ErrorRecord $exception, "InvalidUserName", InvalidArgument, $null + } + + return @( $Domain, $UserName ) +} + Export-ModuleMember -Function ` Test-IsNanoServer, ` Throw-InvalidArgumentException, ` diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 64db5a61a..a54c35d3f 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -2,38 +2,54 @@ 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 +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 -# Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1 +# Commented out until more languages are supported +# Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 -function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[string[]]$newArgumentNames) +function Convert-ArgumentNames { + [CmdletBinding()] + param + ( + $functionBoundParameters, + + [String[]] + $argumentNames, + + [String[]] + $newArgumentNames + ) + $returnValue=@{} for($i=0;$i -lt $argumentNames.Count;$i++) { $argumentName=$argumentNames[$i] if($newArgumentNames -eq $null) - { + { $newArgumentName=$argumentName } else @@ -50,29 +66,39 @@ function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[str return $returnValue } +function IsRunFromLocalSystemUser() +{ + (New-Object Security.Principal.WindowsPrincipal ( [Security.Principal.WindowsIdentity]::GetCurrent())).Identity.IsSystem +} + 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=(ResolvePath $Path) + + $Path = Expand-Path -Path $Path $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") + + $getArguments = @{ + + } + $processes = @(GetWin32_Process @getArguments) if($processes.Count -eq 0) @@ -103,7 +129,6 @@ function Get-TargetResource } } - function Set-TargetResource { [CmdletBinding(SupportsShouldProcess=$true)] @@ -154,7 +179,7 @@ function Set-TargetResource $processIds=$processes.ProcessId $err=Stop-Process -Id $processIds -force 2>&1 - + if($err -eq $null) { Write-Log ($LocalizedData.ProcessesStopped -f $Path,($processIds -join ",")) @@ -196,39 +221,38 @@ function Set-TargetResource if($PSCmdlet.ShouldProcess($Path,$LocalizedData.StartingProcessWhatif)) { - if($PSBoundParameters.ContainsKey("Credential")) + # + # 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 (IsRunFromLocalSystemUser)) { - $argumentError = $false - try + if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) { - 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) - } + $exception = New-Object System.ArgumentException $LocalizedData.ErrorParametersNotSupportedWithCredential + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null } - catch + else { - $exception = New-Object System.ArgumentException $_; - if($argumentError) + $Domain, $UserName = Get-DomainAndUserName $Credential + try { - $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"Invalid combination of arguments", $errorCategory, $null + # + # 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 ) } - else + catch { - $errorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "Win32Exception", $errorCategory, $null + throw New-Object System.Management.Automation.ErrorRecord $_.Exception, "Win32Exception", OperationStopped, $null } - $err = $errorRecord } - } else { @@ -262,7 +286,6 @@ function Set-TargetResource function Test-TargetResource { - [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] @@ -296,6 +319,23 @@ function Test-TargetResource $WorkingDirectory ) + if($PsDscContext.RunAsUser) + { + if($PSBoundParameters.ContainsKey("Credential")) + { + $exception = New-Object System.ArgumentException ($LocalizedData.ErrorCredentialParameterNotSupportedWithRunAsCredential -f $PsDscContext.RunAsUser) + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidArgument", InvalidArgument, $null + } + else + { + $exception = New-Object System.ArgumentException ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null + } + + Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) + throw $err + } + $Path=ResolvePath $Path $PSBoundParameters["Path"] = $Path $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") @@ -312,30 +352,35 @@ function Test-TargetResource } } -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 } @@ -368,140 +413,150 @@ function WaitForProcessCount $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $value = @(GetWin32_Process @getArguments).Count -eq $waitCount } while(!$value -and ([DateTime]::Now - $start).TotalMilliseconds -lt 2000) - + return $value } -function GetWin32_Process +<# + If there are many processes it is faster to perform a Get-WmiObject in order to get + Win32_Process objects for all processes. + #> +<# + If there are less processes than the threshold, building a Win32_Process for each matching result of get-process is faster + #> +function Get-Win32Process { - [CmdletBinding(SupportsShouldProcess=$true)] + [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) - - $gpsProcesses = @(get-process -Name $fileName -ErrorAction SilentlyContinue) + $processes = @() - if($gpsProcesses.Count -ge $useWmiObjectCount) + if ($getProcessResult.Count -ge $UseGetWmiObjectThreshold) { - # 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 + if ($process.Path -ieq $Path) { - Write-Verbose "in process handle, $($gpsProcess.Id)" - [wmi]"Win32_Process.Handle='$($gpsProcess.Id)'" - } - catch - { - #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 } + $Domain, $UserName = Get-DomainAndUserName $Credential + $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$Domain\$UserName" } + } + + if ($null -eq $Arguments) + { + $Arguments = [String]::Empty } - if($Arguments -eq $null) {$Arguments = ""} - $processes = $processes | where { (GetProcessArgumentsFromCommandLine $_.CommandLine) -eq $Arguments } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput $_.CommandLine) -eq $Arguments } return $processes } <# -.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 + .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. + + .PARAMETER FilterString + The string to convert. #> -function WQLEscape +function ConvertTo-EscapedStringForWqlFilter { + [OutputType([String])] + [CmdletBinding()] param ( - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $query + [String] + $FilterString ) - return $query.Replace("\","\\").Replace('"','\"').Replace("'","\'") + return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") } function ThrowInvalidArgumentError @@ -509,7 +564,7 @@ function ThrowInvalidArgumentError [CmdletBinding()] param ( - + [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] @@ -527,72 +582,82 @@ function ThrowInvalidArgumentError throw $errorRecord } -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 (Test-IsRootedPath -Path $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)) + if ([String]::IsNullOrEmpty($env:Path)) { - ThrowInvalidArgumentError "EmptyEnvironmentPath" ($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) } - # 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) + <# + 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) { - 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.AbsolutePathOrFileName) } - foreach($rawSegment in $env:Path.Split(";")) + foreach ($rawEnvPathSegment in $env:Path.Split(';')) { - $segment = [Environment]::ExpandEnvironmentVariables($rawSegment) - - # if an exception causes $segmentedRooted not to be set, we will consider it $false - $segmentRooted = $false + $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) + + if ($envPathSegmentRooted) { - continue - } - - $candidate = join-path $segment $Path - - if(Test-Path $candidate -PathType Leaf) - { - 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) } - function AssertAbsolutePath { [CmdletBinding()] @@ -610,16 +675,16 @@ function AssertAbsolutePath Process { - if(!$ParentBoundParameters.ContainsKey($ParameterName)) + if(!$ParentBoundParameters.ContainsKey($ParameterName)) { return } $path=$ParentBoundParameters[$ParameterName] - + if(!(IsRootedPath $Path)) { - ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), + ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldBeAbsolute) } @@ -630,7 +695,7 @@ function AssertAbsolutePath if(!(Test-Path $Path)) { - ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), + ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldExist) } } @@ -650,20 +715,29 @@ function AssertParameterIsNotSpecified Process { - if($ParentBoundParameters.ContainsKey($ParameterName)) + if($ParentBoundParameters.ContainsKey($ParameterName)) { ThrowInvalidArgumentError "ParameterShouldNotBeSpecified" ($LocalizedData.ParameterShouldNotBeSpecified -f $ParameterName) } } } -function IsRootedPath +<# + .SYNOPSIS + Tests is the given path is rooted. + + .PARAMETER Path + The path to test. +#> +function Test-IsRootedPath { + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path ) @@ -673,319 +747,9 @@ function IsRootedPath } 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) + # If the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $_.Exception.Message) } } -function Write-Log -{ - [CmdletBinding(SupportsShouldProcess=$true)] - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Message - ) - - if ($PSCmdlet.ShouldProcess($Message, $null, $null)) - { - Write-Verbose $Message - } -} - -function CallPInvoke -{ -$script:ProgramSource = @" -using System; -using System.Collections.Generic; -using System.Text; -using System.Security; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Security.Principal; -using System.ComponentModel; -using System.IO; - -namespace Source -{ - [SuppressUnmanagedCodeSecurity] - public static class NativeMethods - { - //The following structs and enums are used by the various Win32 API's that are used in the code below - - [StructLayout(LayoutKind.Sequential)] - public struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public Int32 dwProcessID; - public Int32 dwThreadID; - } - - [Flags] - public enum LogonType - { - LOGON32_LOGON_INTERACTIVE = 2, - LOGON32_LOGON_NETWORK = 3, - LOGON32_LOGON_BATCH = 4, - LOGON32_LOGON_SERVICE = 5, - LOGON32_LOGON_UNLOCK = 7, - LOGON32_LOGON_NETWORK_CLEARTEXT = 8, - LOGON32_LOGON_NEW_CREDENTIALS = 9 - } - - [Flags] - public enum LogonProvider - { - LOGON32_PROVIDER_DEFAULT = 0, - LOGON32_PROVIDER_WINNT35, - LOGON32_PROVIDER_WINNT40, - LOGON32_PROVIDER_WINNT50 - } - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public Int32 Length; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } - - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - public enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct TokPriv1Luid - { - public int Count; - public long Luid; - public int Attr; - } - - public const int GENERIC_ALL_ACCESS = 0x10000000; - public const int CREATE_NO_WINDOW = 0x08000000; - internal const int SE_PRIVILEGE_ENABLED = 0x00000002; - internal const int TOKEN_QUERY = 0x00000008; - internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; - internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; - - [DllImport("kernel32.dll", - EntryPoint = "CloseHandle", SetLastError = true, - CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern bool CloseHandle(IntPtr handle); - - [DllImport("advapi32.dll", - EntryPoint = "CreateProcessAsUser", SetLastError = true, - CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] - public static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandle, - Int32 dwCreationFlags, - IntPtr lpEnvrionment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - ref PROCESS_INFORMATION lpProcessInformation - ); - - [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] - public static extern bool DuplicateTokenEx( - IntPtr hExistingToken, - Int32 dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - Int32 ImpersonationLevel, - Int32 dwTokenType, - ref IntPtr phNewToken - ); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern Boolean LogonUser( - String lpszUserName, - String lpszDomain, - String lpszPassword, - LogonType dwLogonType, - LogonProvider dwLogonProvider, - out IntPtr phToken - ); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool AdjustTokenPrivileges( - IntPtr htok, - bool disall, - ref TokPriv1Luid newst, - int len, - IntPtr prev, - IntPtr relen - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GetCurrentProcess(); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool OpenProcessToken( - IntPtr h, - int acc, - ref IntPtr phtok - ); - - [DllImport("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); - } - } - } - } -} - -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} - -Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource - +Export-ModuleMember -Function *-TargetResource 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..a39cd28a1 --- /dev/null +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -0,0 +1,463 @@ +Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" + +$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 + } + + AfterAll { + Stop-ProcessByName $script:cmdProcessShortName + + if (Test-Path -Path $script:cmdProcessFullPath) + { + Remove-Item -Path $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + } + } + + BeforeEach { + Stop-ProcessByName -ProcessName $script:cmdProcessShortName + } + + Context 'Get-TargetResource' { + It 'Should return the correct properties' -Pending { + $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 -GetTargetResourceProperties $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 + } + } + + Context 'Set-TargetResource' { + It 'Should start and stop a process with no arguments' -Pending { + 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 with WhatIf is specified' -Pending { + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $script:cmdProcessFullPath + TestWhatif $script $script:cmdProcessFullPath + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $script:cmdProcessFullPath + TestWhatif $script $script:cmdProcessFullPath + } + + It 'TestWhatifStop' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments '') + { + throw "before set, there should be no process" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments '' + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $exePath + TestWhatif $script $exePath + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $exePath + TestWhatif $script $exePath + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure "Absent" -Arguments '' + } + } + + <# + .Synopsis + tests input, output and error streams are hooked up as well as the working directory + #> + It 'TestStreamsAndWorkingDirectory' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } + + $errorPath="$PWD\TestStreamsError.txt" + $outputPath="$PWD\TestStreamsOutput.txt" + $inputPath="$PWD\TestStreamsInput.txt" + + Remove-Item $errorPath -Force -ErrorAction SilentlyContinue + Remove-Item $outputPath -Force -ErrorAction SilentlyContinue + + "ECHO Testing ProcessTest.exe ` + dir volumeSyntaxError:\ ` + set /p waitforinput=Press [y/n]?: " | out-file $inputPath -Encoding ascii + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -WorkingDirectory $processTestPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments "" + + if(!(TryForAWhile ([scriptblock]::create("(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments '').Ensure -eq 'Absent'")))) + { + throw "process did not terminate" + } + + # Race condition exists for retrieving contents of the process error stream file + Start-Sleep -Seconds 2 + + $errorFile=get-content $errorPath -Raw + $outputFile=get-content $outputPath -Raw + + if((Get-Culture).Name -ieq 'en-us') + { + Assert ($errorFile.Contains("The filename, directory name, or volume label syntax is incorrect.")) "no stdErr string in file" + Assert ($outputFile.Contains("Press [y/n]?:")) "no stdOut string in file" + Assert ($outputFile.ToLower().Contains($processTestPath.ToLower())) "working directory: $processTestPath, not in output file" + } + else + { + Assert ($errorFile.Length -gt 0) "no stdErr string in file" + Assert ($outputFile.Length -gt 0) "no stdOut string in file" + } + } + } + + It 'TestCannotWritePropertiesWithAbsent' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Absent";Arguments=""} + $null=$args.Add($writeProperty,"anything") + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set when using Absent" -f $writeProperty) + } + } + } + + It 'TestCannotUseInvalidPath' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + $relativePath = "..\ExistingFile.txt" + "something" > $relativePath + + $nonExistingPath = "$processTestPath\IDoNotExist.Really.I.Do.Not" + del $nonExistingPath -ErrorAction SilentlyContinue + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $null=$args.Add($writeProperty,$relativePath) + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set to relative path" -f $writeProperty) + } + + + foreach($writeProperty in "StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $args[$writeProperty] = $nonExistingPath + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set to nonexisting path" -f $writeProperty) + } + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath") + { + # Paths that need not exist so no exception should be thrown + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $null=$args.Add($writeProperty,$nonExistingPath) + MSFT_ProcessResource\Set-TargetResource @args + get-process ProcessTest | stop-process + del $nonExistingPath -ErrorAction SilentlyContinue + } + } + } + + It 'TestGetWmiObject' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "" + MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "abc" + $r=@(GetWin32_Process $exePath -useWmiObjectCount 0) + AssertEquals $r.Count 1 "get-wmiobject with filter" + $a=@(GetWin32_Process $exePath -useWmiObjectCount 5) + AssertEquals $a.Count 1 "through get-process" + MSFT_ProcessResource\Set-TargetResource $exePath -Ensure Absent -Arguments "" + } + } + } + + Context 'Test-TargetResource' { + It 'TestTestWithDirectoryArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + $exists = MSFT_ProcessResource\Test-TargetResource $exePath -WorkingDirectory "something" -StandardOutputPath "something" ` + -StandardErrorPath "something" -StandardInputPath "something" -Arguments "" + Assert !$exists "there should be no process" + } + } + } + + + + + <# + SPLIT into 3 + + .Synopsis + Tests a process with an argument. + .DESCRIPTION + - Starts a process with an argument + - Ensures Test with "" as Arguments is false + - Ensures Test with the wrong arguments is false + - Ensures Test with no Arguments key is true + - Ensures Test with the right Arguments key is true + - Ensures Get with no arguments key is 1 Present + - Ensures Get with "" as Arguments is 1 Absent + + - Starts process with no arguments + - Ensures Get with no arguments key is 2 + - Ensures Get with null arguments is 1 Present with no Arguments + - Ensures Get with argument is 1 Present with the Arguments + - Set All process to be absent + - Ensure none are left + #> + It 'TestGetSetProcessResourceWithArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set, cannot find process with no arguments" + } + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") + { + throw "after set, cannot find process with arguments that were not the ones we set" + } + + if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) + { + throw "after set, there should be a process if we specify arguments" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') + { + throw "there should be no process without arguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with no argument" + } + + if($processes[0].Arguments.length -ne 0) + { + throw "there should no arguments in the process" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" + } + + if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') + { + throw "the argument should be TestGetSetProcessResourceWithArguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set absent, there should be no process" + } + } + } + + Context 'WQLEscape' { + It 'TestWQLEscape' -Pending { + WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + } + } + + Context 'Get-ProcessArgumentsFromCommandLine' { + It 'TestGetProcessArgumentsFromCommandLine' -Pending { + $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) + foreach($testCase in $testCases) + { + $test=$testCase[0] + $expected=$testCase[1] + $actual = GetProcessArgumentsFromCommandLine $test + + $actual | Should Be $expected + } + } + } + + + + + + + + # + # Tests for Get-DomainAndUserName function. + # + Context 'Get-DomainAndUserName' { + It 'TestDomainUserNameParseAt' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "user@domain", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } + } + + It 'TestDomainUserNameParseSlash' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } + } + + It 'TestDomainUserNameParseImplicitDomain' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "localuser", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" + Assert ($UserName -eq "localuser") "wrong user $UserName" + } + } + + Context 'TestDomainUserNameParseSlashFail.Context' { + + BeforeEach { + Invoke-Remotely { + $script:originalErrorActionPreference = $ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + } + } + + AfterEach { + Invoke-Remotely { + $global:ErrorActionPreference = $script:originalErrorActionPreference + } + } + + It 'TestDomainUserNameParseSlashFail' { + Invoke-Remotely { + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user\foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" + } + Assert ($exceptionThrown) "no exception thrown" + } + } + + It 'TestDomainUserNameParseAtFail' { + Invoke-Remotely { + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain@user@foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" + } + Assert ($exceptionThrown) "no exception thrown" + } + } + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} From 06313963ad861ad2d1c44e23b75fa5a4f916be03 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Thu, 21 Jul 2016 15:47:34 -0700 Subject: [PATCH 2/6] Updating xProcess and tests. --- .../MSFT_xProcessResource.psm1 | 645 ++++++++++-------- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 404 ++++++----- 2 files changed, 602 insertions(+), 447 deletions(-) diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index a54c35d3f..49b3676e9 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -29,46 +29,99 @@ ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredenti # Commented out until more languages are supported # Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 -function Convert-ArgumentNames +Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1" + +<# + .SYNOPSIS + Tests if the current user is from the local system. +#> +function Test-IsRunFromLocalSystemUser { + [OutputType([Boolean])] [CmdletBinding()] - param - ( - $functionBoundParameters, + param () - [String[]] - $argumentNames, - [String[]] - $newArgumentNames + $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 ) - $returnValue=@{} - for($i=0;$i -lt $argumentNames.Count;$i++) - { - $argumentName=$argumentNames[$i] + $wrongFormat = $false - if($newArgumentNames -eq $null) - { - $newArgumentName=$argumentName - } + if ($Credential.UserName.Contains('\')) + { + $credentialSegments = $Credential.UserName.Split('\') + + if ($credentialSegments.Length -gt 2) + { + # 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 + } - return $returnValue -} + if ($wrongFormat) + { + $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName + + Write-Verbose -Message $message -function IsRunFromLocalSystemUser() -{ - (New-Object Security.Principal.WindowsPrincipal ( [Security.Principal.WindowsIdentity]::GetCurrent())).Identity.IsSystem + New-InvalidArgumentException -ArgumentName 'Credential' -Message $message + } + + return @{ + Domain = $domain + UserName = $userName + } } function Get-TargetResource @@ -93,262 +146,323 @@ function Get-TargetResource ) $Path = Expand-Path -Path $Path - $PSBoundParameters["Path"] = $Path - $getArguments = @{ + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential } - $processes = @(GetWin32_Process @getArguments) + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) - if($processes.Count -eq 0) + 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 ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential + } - if($Ensure -eq 'Absent') + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) + + if ($Ensure -eq 'Absent') { - "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory" | AssertParameterIsNotSpecified $PSBoundParameters + Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - if ($processes.Count -gt 0) + if ($win32Processes.Count -gt 0) { - $processIds=$processes.ProcessId + $processIds = $win32Processes.ProcessId - $err=Stop-Process -Id $processIds -force 2>&1 + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 - if($err -eq $null) + 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 ($null -ne $PSBoundParameters[$shouldBeRootedPathArgument]) + { + Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] + } + } - if([string]::IsNullOrEmpty($Arguments)) + $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($shouldExistPathArgument in $shouldExistPathArguments) + { + if ($null -ne $PSBoundParameters[$shouldExistPathArgument]) { - $null=$startArguments.Remove("ArgumentList") + Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] + } + } + + if ($win32Processes.Count -eq 0) + { + $startProcessArguments = @{ + FilePath = $Path } - if($PSCmdlet.ShouldProcess($Path,$LocalizedData.StartingProcessWhatif)) + $startProcessOptionalArgumentMap = @{ + Credential = 'Credential' + RedirectStandardOutput = 'StandardOutputPath' + RedirectStandardError = 'StandardErrorPath' + RedirectStandardInput = 'StandardInputPath' + WorkingDirectory = 'WorkingDirectory' + } + + foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - # - # 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 (IsRunFromLocalSystemUser)) + if ($null -ne $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]) { - if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) + $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')) { - $exception = New-Object System.ArgumentException $LocalizedData.ErrorParametersNotSupportedWithCredential - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null + New-InvalidArgumentException -ArgumentName 'StandardOutputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential } - else + + if ($PSBoundParameters.ContainsKey('StandardInputPath')) { - $Domain, $UserName = Get-DomainAndUserName $Credential - try - { - # - # 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 - { - throw New-Object System.Management.Automation.ErrorRecord $_.Exception, "Win32Exception", OperationStopped, $null - } + New-InvalidArgumentException -ArgumentName 'StandardInputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + if ($PSBoundParameters.ContainsKey('WorkingDirectory')) + { + New-InvalidArgumentException -ArgumentName 'WorkingDirectory' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + $splitCredentialResult = Split-Credential $Credential + try + { + <# + 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 + { + 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([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 ) - if($PsDscContext.RunAsUser) - { - if($PSBoundParameters.ContainsKey("Credential")) - { - $exception = New-Object System.ArgumentException ($LocalizedData.ErrorCredentialParameterNotSupportedWithRunAsCredential -f $PsDscContext.RunAsUser) - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidArgument", InvalidArgument, $null - } - else - { - $exception = New-Object System.ArgumentException ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null - } - - Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) - throw $err + if ($null -ne $PsDscContext.RunAsUser) + { + New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) } - $Path=ResolvePath $Path - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + $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) } } @@ -386,50 +500,68 @@ function Get-Win32ProcessOwner } } -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)] - [ValidateNotNullOrEmpty()] - [System.String] - $Path, - - [System.String] - $Arguments, - + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] - $Credential, + [Hashtable] + $ProcessSettings, - [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) + $actualProcessCount = @( Get-Win32Process @ProcessSettings ).Count + } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt 2000) - return $value + return $actualProcessCount -eq $ProcessCount } <# - If there are many processes it is faster to perform a Get-WmiObject in order to get - Win32_Process objects for all processes. - #> -<# - If there are less processes than the threshold, building a Win32_Process for each matching result of get-process is faster - #> + .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 { + [OutputType([Object[]])] [CmdletBinding(SupportsShouldProcess = $true)] param ( - [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] @@ -453,7 +585,7 @@ function Get-Win32Process $processes = @() - if ($getProcessResult.Count -ge $UseGetWmiObjectThreshold) + if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) { $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path @@ -475,9 +607,9 @@ function Get-Win32Process if ($PSBoundParameters.ContainsKey('Credential')) { - $Domain, $UserName = Get-DomainAndUserName $Credential + $splitCredentialResult = Split-Credenital -Credential $Credential - $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$Domain\$UserName" } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$($splitCredentialResult.Domain)\$($splitCredentialResult.UserName)" } } if ($null -eq $Arguments) @@ -485,7 +617,7 @@ function Get-Win32Process $Arguments = [String]::Empty } - $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput $_.CommandLine) -eq $Arguments } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput -CommandLineInput ($_.CommandLine)) -eq $Arguments } return $processes } @@ -559,29 +691,6 @@ function ConvertTo-EscapedStringForWqlFilter return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") } -function ThrowInvalidArgumentError -{ - [CmdletBinding()] - param - ( - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $errorId, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $errorMessage - ) - - $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 -} - <# .SYNOPSIS Expands a shortened path into a full, rooted path. @@ -603,7 +712,7 @@ function Expand-Path $Path = [Environment]::ExpandEnvironmentVariables($Path) - if (Test-IsRootedPath -Path $Path) + if ([IO.Path]::IsPathRooted($Path)) { if (-not (Test-Path -Path $Path -PathType 'Leaf')) { @@ -612,6 +721,10 @@ function Expand-Path return $Path } + else + { + New-InvalidArgumentException -ArgumentName 'Path' + } if ([String]::IsNullOrEmpty($env:Path)) { @@ -658,97 +771,97 @@ function Expand-Path New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } -function AssertAbsolutePath +<# + .SYNOPSIS + Throws an error if the given path argument is not rooted. + + .PARAMETER PathArgumentName + The name of the path argument that should be rooted. + + .PARAMETER PathArgument + The path arguments that should be rooted. +#> +function Assert-PathArgumentRooted { [CmdletBinding()] param ( - $ParentBoundParameters, - - [System.String] - [Parameter (ValueFromPipeline=$true)] - $ParameterName, - - [switch] - $Exist + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Hashtable] + $PathArguments ) - - Process + + foreach ($pathArgumentName in $PathArguments.Keys) { - 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)) + if (-not ([IO.Path]::IsPathRooted($PathArguments[$pathArgumentName]))) { - ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), - $LocalizedData.PathShouldExist) + New-InvalidArgumentException -ArgumentName $pathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $pathArgumentName, $PathArguments[$pathArgumentName]), $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) } } <# .SYNOPSIS - Tests is the given path is rooted. + Throws an exception if the given hashtable contains the given key(s). - .PARAMETER Path - The path to test. + .PARAMETER Hashtable + The hashtable to check the keys of. + + .PARAMETER Key + The key(s) that should not be in the hashtable. #> -function Test-IsRootedPath +function Assert-HashtableDoesNotContainKey { - [OutputType([Boolean])] [CmdletBinding()] param ( + [Hashtable] + $Hashtable, + [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] - $Path + [String[]] + $Key ) - try - { - return [IO.Path]::IsPathRooted($Path) - } - catch + foreach ($keyName in $Key) { - # If the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on - New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $_.Exception.Message) + if ($Hashtable.ContainsKey($keyName)) + { + New-InvalidArgumentException -ArgumentName $keyName -Message ($LocalizedData.ParameterShouldNotBeSpecified -f $keyName) + } } } diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index a39cd28a1..e41bb0711 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -19,7 +19,7 @@ try } AfterAll { - Stop-ProcessByName $script:cmdProcessShortName + Stop-ProcessByName -ProcessName $script:cmdProcessShortName if (Test-Path -Path $script:cmdProcessFullPath) { @@ -32,7 +32,21 @@ try } Context 'Get-TargetResource' { - It 'Should return the correct properties' -Pending { + It 'Should return the correct properties for a process that is absent' { + $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 present' { $processArguments = 'TestGetProperties' Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments @@ -40,7 +54,7 @@ try $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) - Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceProperties $getTargetResourceProperties + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false $getTargetResourceResult.Arguments | Should Be $processArguments @@ -52,10 +66,45 @@ try $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' -Pending { + 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 '' @@ -238,220 +287,213 @@ try } Context 'Test-TargetResource' { - It 'TestTestWithDirectoryArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - $exists = MSFT_ProcessResource\Test-TargetResource $exePath -WorkingDirectory "something" -StandardOutputPath "something" ` - -StandardErrorPath "something" -StandardInputPath "something" -Arguments "" - Assert !$exists "there should be no process" - } - } - } + It 'Should return correct value based on Arguments' { + $actualArguments = 'TestProcessResourceWithArguments' - - + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments - <# - SPLIT into 3 - - .Synopsis - Tests a process with an argument. - .DESCRIPTION - - Starts a process with an argument - - Ensures Test with "" as Arguments is false - - Ensures Test with the wrong arguments is false - - Ensures Test with no Arguments key is true - - Ensures Test with the right Arguments key is true - - Ensures Get with no arguments key is 1 Present - - Ensures Get with "" as Arguments is 1 Absent - - - Starts process with no arguments - - Ensures Get with no arguments key is 2 - - Ensures Get with null arguments is 1 Present with no Arguments - - Ensures Get with argument is 1 Present with the Arguments - - Set All process to be absent - - Ensure none are left - #> - It 'TestGetSetProcessResourceWithArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "before set, there should be no process" - } + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotTheOriginalArguments' | Should Be $false - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set, cannot find process with no arguments" + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments | Should Be $true } - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") - { - throw "after set, cannot find process with arguments that were not the ones we set" + + 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 } + } + + <# + SPLIT into 3 + + .Synopsis + Tests a process with an argument. + .DESCRIPTION + - Starts a process with an argument + - Ensures Test with "" as Arguments is false + - Ensures Test with the wrong arguments is false + - Ensures Test with no Arguments key is true + - Ensures Test with the right Arguments key is true + - Ensures Get with no arguments key is 1 Present + - Ensures Get with "" as Arguments is 1 Absent + + - Starts process with no arguments + - Ensures Get with no arguments key is 2 + - Ensures Get with null arguments is 1 Present with no Arguments + - Ensures Get with argument is 1 Present with the Arguments + - Set All process to be absent + - Ensure none are left + #> + It 'TestGetSetProcessResourceWithArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath - if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) - { - throw "after set, there should be a process if we specify arguments" - } + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') - { - throw "there should be no process without arguments" - } + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set, cannot find process with no arguments" + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") + { + throw "after set, cannot find process with arguments that were not the ones we set" + } - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with no argument" - } + if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) + { + throw "after set, there should be a process if we specify arguments" + } - if($processes[0].Arguments.length -ne 0) - { - throw "there should no arguments in the process" - } + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') + { + throw "there should be no process without arguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with no argument" + } + + if($processes[0].Arguments.length -ne 0) + { + throw "there should no arguments in the process" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" - } + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" + } - if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') - { - throw "the argument should be TestGetSetProcessResourceWithArguments" - } + if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') + { + throw "the argument should be TestGetSetProcessResourceWithArguments" + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set absent, there should be no process" + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set absent, there should be no process" + } } } - } - Context 'WQLEscape' { - It 'TestWQLEscape' -Pending { - WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + Context 'WQLEscape' { + It 'TestWQLEscape' -Pending { + WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + } } - } - Context 'Get-ProcessArgumentsFromCommandLine' { - It 'TestGetProcessArgumentsFromCommandLine' -Pending { - $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) - foreach($testCase in $testCases) - { - $test=$testCase[0] - $expected=$testCase[1] - $actual = GetProcessArgumentsFromCommandLine $test - - $actual | Should Be $expected + Context 'Get-ProcessArgumentsFromCommandLine' { + It 'TestGetProcessArgumentsFromCommandLine' -Pending { + $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) + foreach($testCase in $testCases) + { + $test=$testCase[0] + $expected=$testCase[1] + $actual = GetProcessArgumentsFromCommandLine $test + + $actual | Should Be $expected + } } } - } - - - - - - - # - # Tests for Get-DomainAndUserName function. - # - Context 'Get-DomainAndUserName' { - It 'TestDomainUserNameParseAt' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "user@domain", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseSlash' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseImplicitDomain' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "localuser", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" - Assert ($UserName -eq "localuser") "wrong user $UserName" + Context 'Get-DomainAndUserName' { + It 'TestDomainUserNameParseAt' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "user@domain", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } } - } - Context 'TestDomainUserNameParseSlashFail.Context' { - - BeforeEach { + It 'TestDomainUserNameParseSlash' -Pending { Invoke-Remotely { - $script:originalErrorActionPreference = $ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" } } - - AfterEach { + + It 'TestDomainUserNameParseImplicitDomain' -Pending { Invoke-Remotely { - $global:ErrorActionPreference = $script:originalErrorActionPreference + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "localuser", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" + Assert ($UserName -eq "localuser") "wrong user $UserName" } } - - It 'TestDomainUserNameParseSlashFail' { - Invoke-Remotely { - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user\foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + + It 'TestDomainUserNameParseSlashFail' -Pending { + $originalErrorActionPreference = $global:ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user\foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" } + Assert ($exceptionThrown) "no exception thrown" + + $global:ErrorActionPreference = $script:originalErrorActionPreference } - It 'TestDomainUserNameParseAtFail' { - Invoke-Remotely { - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain@user@foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" - } + It 'TestDomainUserNameParseAtFail' -Pending { + $originalErrorActionPreference = $global:ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain@user@foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" } + Assert ($exceptionThrown) "no exception thrown" + + $global:ErrorActionPreference = $script:originalErrorActionPreference } } } From 81172924e9c7dbb0830714e576a6256f8734b26d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:07:25 -0700 Subject: [PATCH 3/6] Updating xProcess resource and tests. --- .../MSFT_xProcessResource.psm1 | 37 +- Tests/CommonTestHelper.psm1 | 85 +++ Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 591 +++++++++--------- 3 files changed, 397 insertions(+), 316 deletions(-) diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 49b3676e9..11403b890 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -13,6 +13,7 @@ ProcessAlreadyStopped = Process matching path '{0}' not found running and no act 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 @@ -244,7 +245,7 @@ function Set-TargetResource { Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - if ($win32Processes.Count -gt 0) + if ($win32Processes.Count -gt 0 -and $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif)) { $processIds = $win32Processes.ProcessId @@ -281,7 +282,7 @@ function Set-TargetResource foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) { - if ($null -ne $PSBoundParameters[$shouldBeRootedPathArgument]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) { Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] } @@ -291,7 +292,7 @@ function Set-TargetResource foreach ($shouldExistPathArgument in $shouldExistPathArguments) { - if ($null -ne $PSBoundParameters[$shouldExistPathArgument]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) { Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] } @@ -313,7 +314,7 @@ function Set-TargetResource foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - if ($null -ne $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]])) { $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]] } @@ -617,9 +618,17 @@ function Get-Win32Process $Arguments = [String]::Empty } - $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput -CommandLineInput ($_.CommandLine)) -eq $Arguments } + $processesWithMatchingArguments = @() - return $processes + foreach ($process in $processes) + { + if ((Get-ArgumentsFromCommandLineInput -CommandLineInput ($process.CommandLine)) -eq $Arguments) + { + $processesWithMatchingArguments += $process + } + } + + return $processesWithMatchingArguments } <# @@ -788,16 +797,18 @@ function Assert-PathArgumentRooted ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [Hashtable] - $PathArguments + [String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument ) - foreach ($pathArgumentName in $PathArguments.Keys) + if (-not ([IO.Path]::IsPathRooted($PathArgument))) { - if (-not ([IO.Path]::IsPathRooted($PathArguments[$pathArgumentName]))) - { - New-InvalidArgumentException -ArgumentName $pathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $pathArgumentName, $PathArguments[$pathArgumentName]), $LocalizedData.PathShouldBeAbsolute) - } + New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldBeAbsolute) } } diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 0e987b0d8..9813bd523 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -555,6 +555,90 @@ 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. @@ -649,5 +733,6 @@ Export-ModuleMember -Function ` Test-User, ` Wait-ScriptBlockReturnTrue, ` Test-IsFileLocked, ` + Test-SetTargetResourceWithWhatIf, ` Enter-DscResourceTestEnvironment, ` Exit-DscResourceTestEnvironment diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index e41bb0711..d3346eb0f 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -1,4 +1,4 @@ -Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" +Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" -Force $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName 'xPSDesiredStateConfiguration' ` @@ -16,6 +16,17 @@ try $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 { @@ -25,6 +36,13 @@ try { Remove-Item -Path $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force } + + Pop-Location + + if (Test-Path -Path $script:processTestFolder) + { + Remove-Item -Path $script:processTestFolder -Recurse -Force + } } BeforeEach { @@ -32,7 +50,7 @@ try } Context 'Get-TargetResource' { - It 'Should return the correct properties for a process that is absent' { + It 'Should return the correct properties for a process that is absent with Arguments' { $processArguments = 'TestGetProperties' $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments @@ -46,7 +64,21 @@ try $getTargetResourceResult.Count | Should Be 3 } - It 'Should return the correct properties for a process that is present' { + 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 @@ -67,6 +99,27 @@ try $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' @@ -116,172 +169,194 @@ try Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false } - It 'Should have correct output with WhatIf is specified' -Pending { - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $script:cmdProcessFullPath - TestWhatif $script $script:cmdProcessFullPath + 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 = '' + } - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $script:cmdProcessFullPath - TestWhatif $script $script:cmdProcessFullPath + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput '' } - It 'TestWhatifStop' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath + It 'Should have correct output for existing process with WhatIf specified and Ensure Absent' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments '') - { - throw "before set, there should be no process" - } + $setTargetResourceParameters = @{ + Ensure = 'Absent' + Path = $script:cmdProcessFullPath + Arguments = '' + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments '' + $expectedWhatIfOutput = @( $LocalizedData.StoppingProcessWhatif, $script:cmdProcessFullPath ) - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $exePath - TestWhatif $script $exePath + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput $expectedWhatIfOutput - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $exePath - TestWhatif $script $exePath - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure "Absent" -Arguments '' + if ($setTargetResourceParameters.ContainsKey('WhatIf')) + { + $setTargetResourceParameters.Remove('WhatIf') } + + $testTargetResourceResult = Test-TargetResource @setTargetResourceParameters + $testTargetResourceResult | Should Be $false } - <# - .Synopsis - tests input, output and error streams are hooked up as well as the working directory - #> - It 'TestStreamsAndWorkingDirectory' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + 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) { - throw "before set, there should be no process" + Remove-Item -Path $path -Recurse -Force } - - $errorPath="$PWD\TestStreamsError.txt" - $outputPath="$PWD\TestStreamsOutput.txt" - $inputPath="$PWD\TestStreamsInput.txt" + } + + New-Item -Path $workingDirectoryPath -ItemType 'Directory' | Out-Null - Remove-Item $errorPath -Force -ErrorAction SilentlyContinue - Remove-Item $outputPath -Force -ErrorAction SilentlyContinue + $inputFileText = "ECHO Testing ProcessTest.exe ` + dir volumeSyntaxError:\ ` + set /p waitforinput=Press [y/n]?: " - "ECHO Testing ProcessTest.exe ` - dir volumeSyntaxError:\ ` - set /p waitforinput=Press [y/n]?: " | out-file $inputPath -Encoding ascii + Out-File -FilePath $inputPath -InputObject $inputFileText -Encoding 'ASCII' - MSFT_ProcessResource\Set-TargetResource -Path $exePath -WorkingDirectory $processTestPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments "" + Set-TargetResource -Path $script:cmdProcessFullPath -WorkingDirectory $workingDirectoryPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments '' - if(!(TryForAWhile ([scriptblock]::create("(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments '').Ensure -eq 'Absent'")))) - { - throw "process did not terminate" - } + Wait-ScriptBlockReturnTrue -ScriptBlock { (Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '').Ensure -ieq 'Absent' } -TimeoutSeconds 10 - # Race condition exists for retrieving contents of the process error stream file - Start-Sleep -Seconds 2 + Wait-ScriptBlockReturnTrue -ScriptBlock { Test-IsFileLocked -Path $errorPath } -TimeoutSeconds 2 - $errorFile=get-content $errorPath -Raw - $outputFile=get-content $outputPath -Raw + $errorFileContent = Get-Content -Path $errorPath -Raw + $errorFileContent | Should Not Be $null - if((Get-Culture).Name -ieq 'en-us') - { - Assert ($errorFile.Contains("The filename, directory name, or volume label syntax is incorrect.")) "no stdErr string in file" - Assert ($outputFile.Contains("Press [y/n]?:")) "no stdOut string in file" - Assert ($outputFile.ToLower().Contains($processTestPath.ToLower())) "working directory: $processTestPath, not in output file" - } - else - { - Assert ($errorFile.Length -gt 0) "no stdErr string in file" - Assert ($outputFile.Length -gt 0) "no stdOut string in file" - } + 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 'TestCannotWritePropertiesWithAbsent' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Absent";Arguments=""} - $null=$args.Add($writeProperty,"anything") - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set when using Absent" -f $writeProperty) - } - } - } + It 'Should throw when trying to specify streams or working directory with Ensure Absent' { + $invalidPropertiesWithAbsent = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - It 'TestCannotUseInvalidPath' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - $relativePath = "..\ExistingFile.txt" - "something" > $relativePath + foreach ($invalidPropertyWithAbsent in $invalidPropertiesWithAbsent) + { + $setTargetResourceArguments = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Absent' + Arguments = '' + $invalidPropertyWithAbsent = 'Something' + } + + { Set-TargetResource @setTargetResourceArguments } | Should Throw ($LocalizedData.ParameterShouldNotBeSpecified -f $invalidPropertyWithAbsent) + } + } - $nonExistingPath = "$processTestPath\IDoNotExist.Really.I.Do.Not" - del $nonExistingPath -ErrorAction SilentlyContinue + It 'Should throw when passing a relative path to stream or working directory parameters' { + $invalidRelativePath = '..\RelativePath' + $pathParameters = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $null=$args.Add($writeProperty,$relativePath) - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set to relative path" -f $writeProperty) + 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' - foreach($writeProperty in "StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $args[$writeProperty] = $nonExistingPath - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set to nonexisting path" -f $writeProperty) - } + if (Test-Path -Path $invalidNonexistentPath) + { + Remove-Item -Path $invalidNonexistentPath -Recurse -Force + } - foreach($writeProperty in "StandardOutputPath","StandardErrorPath") - { - # Paths that need not exist so no exception should be thrown - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $null=$args.Add($writeProperty,$nonExistingPath) - MSFT_ProcessResource\Set-TargetResource @args - get-process ProcessTest | stop-process - del $nonExistingPath -ErrorAction SilentlyContinue + $pathMustExistParameters = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($pathMustExistParameter in $pathMustExistParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathMustExistParameter = $invalidNonexistentPath } + + { Set-TargetResource @setTargetResourceParameters } | Should Throw $LocalizedData.PathShouldExist } } - It 'TestGetWmiObject' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "" - MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "abc" - $r=@(GetWin32_Process $exePath -useWmiObjectCount 0) - AssertEquals $r.Count 1 "get-wmiobject with filter" - $a=@(GetWin32_Process $exePath -useWmiObjectCount 5) - AssertEquals $a.Count 1 "through get-process" - MSFT_ProcessResource\Set-TargetResource $exePath -Ensure Absent -Arguments "" + 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 } } } @@ -295,7 +370,7 @@ try 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 } @@ -310,190 +385,100 @@ try $testTargetResourceResult | Should Be $false } - } - - <# - SPLIT into 3 - - .Synopsis - Tests a process with an argument. - .DESCRIPTION - - Starts a process with an argument - - Ensures Test with "" as Arguments is false - - Ensures Test with the wrong arguments is false - - Ensures Test with no Arguments key is true - - Ensures Test with the right Arguments key is true - - Ensures Get with no arguments key is 1 Present - - Ensures Get with "" as Arguments is 1 Absent - - - Starts process with no arguments - - Ensures Get with no arguments key is 2 - - Ensures Get with null arguments is 1 Present with no Arguments - - Ensures Get with argument is 1 Present with the Arguments - - Set All process to be absent - - Ensure none are left - #> - It 'TestGetSetProcessResourceWithArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "before set, there should be no process" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set, cannot find process with no arguments" - } - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") - { - throw "after set, cannot find process with arguments that were not the ones we set" - } - - if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) - { - throw "after set, there should be a process if we specify arguments" - } - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') - { - throw "there should be no process without arguments" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with no argument" - } - - if($processes[0].Arguments.length -ne 0) - { - throw "there should no arguments in the process" - } - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") - - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" - } - - if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') - { - throw "the argument should be TestGetSetProcessResourceWithArguments" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set absent, there should be no process" - } - } + } - Context 'WQLEscape' { - It 'TestWQLEscape' -Pending { - WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + 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-ProcessArgumentsFromCommandLine' { - It 'TestGetProcessArgumentsFromCommandLine' -Pending { - $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) - foreach($testCase in $testCases) + + 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) { - $test=$testCase[0] - $expected=$testCase[1] - $actual = GetProcessArgumentsFromCommandLine $test + $commandLineInput = $testCase.CommandLineInput + $expectedArguments = $testCase.ExpectedArguments + $actualArguments = Get-ArgumentsFromCommandLineInput -CommandLineInput $commandLineInput - $actual | Should Be $expected + $actualArguments | Should Be $expectedArguments } } } - Context 'Get-DomainAndUserName' { - It 'TestDomainUserNameParseAt' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "user@domain", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseSlash' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseImplicitDomain' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "localuser", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" - Assert ($UserName -eq "localuser") "wrong user $UserName" - } + 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 'TestDomainUserNameParseSlashFail' -Pending { - $originalErrorActionPreference = $global:ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + 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) - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user\foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + $splitCredentialResult = Split-Credential -Credential $testCredential - $global:ErrorActionPreference = $script:originalErrorActionPreference + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' } - It 'TestDomainUserNameParseAtFail' -Pending { - $originalErrorActionPreference = $global:ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + 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) - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain@user@foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + $splitCredentialResult = Split-Credential -Credential $testCredential - $global:ErrorActionPreference = $script:originalErrorActionPreference + $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 } } } From 06371a49769992cf113eddbc9450067ca7bd6f06 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:09:31 -0700 Subject: [PATCH 4/6] Updating README and HQRM plan. --- HighQualityResourceModulePlan.md | 4 ++-- README.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/HighQualityResourceModulePlan.md b/HighQualityResourceModulePlan.md index 634bb12b4..36f4082cb 100644 --- a/HighQualityResourceModulePlan.md +++ b/HighQualityResourceModulePlan.md @@ -40,8 +40,8 @@ The PSDesiredStateConfiguration High Quality Resource Module will consist of the - [x] Archive - [x] Group - [x] Package - - [ ] Process (In Progress) - - [ ] Registry + - [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 3734da8e1..ddcf82148 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,7 @@ These parameters will be the same for each Windows optional feature in the set. * 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. +* Merged xProcess with in-box Process resource and added tests. ### 3.12.0.0 From 71336d5673764c25d391fae52b5caaf15650b04d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:12:19 -0700 Subject: [PATCH 5/6] Removing extra function from CommonResourceHelper. --- DSCResources/CommonResourceHelper.psm1 | 52 ------------------- .../MSFT_xProcessResource.psm1 | 2 +- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 587de66ff..39dc0b203 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -84,58 +84,6 @@ function New-InvalidOperationException throw $errorRecordToThrow } -<# -# The goal of this function is to get domain and username from PSCredential -# without calling GetNetworkCredential() method. -# Call to GetNetworkCredential() expose password as a plain text in memory. -#> -function Get-DomainAndUserName([PSCredential]$Credential) -{ - # - # Supported formats: DOMAIN\username, username@domain - # - $wrongFormat = $false - if ($Credential.UserName.Contains('\')) - { - $segments = $Credential.UserName.Split('\') - if ($segments.Length -gt 2) - { - # i.e. domain\user\foo - $wrongFormat = $true - } else { - $Domain = $segments[0] - $UserName = $segments[1] - } - } - elseif ($Credential.UserName.Contains('@')) - { - $segments = $Credential.UserName.Split('@') - if ($segments.Length -gt 2) - { - # i.e. user@domain@foo - $wrongFormat = $true - } else { - $UserName = $segments[0] - $Domain = $segments[1] - } - } - else - { - # support for default domain (localhost) - return @( $env:COMPUTERNAME, $Credential.UserName ) - } - - if ($wrongFormat) - { - $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName - Write-Verbose $message - $exception = New-Object System.ArgumentException $message - throw New-Object System.Management.Automation.ErrorRecord $exception, "InvalidUserName", InvalidArgument, $null - } - - return @( $Domain, $UserName ) -} - Export-ModuleMember -Function ` Test-IsNanoServer, ` New-InvalidArgumentException, ` diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 11403b890..15fc1cbcf 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -28,7 +28,7 @@ ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredenti } # Commented out until more languages are supported -# Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 +# Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1 Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1" From d539b0f1eb36bd873f682dd567b157a647ff9867 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 1 Aug 2016 15:38:23 -0700 Subject: [PATCH 6/6] Suppressing PSSA rule in tests. --- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index d3346eb0f..be65087e5 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -1,3 +1,6 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] +param () + Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" -Force $script:testEnvironment = Enter-DscResourceTestEnvironment `