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 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. '@ } Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 Import-Module "$PSScriptRoot\..\RunAsHelper.psm1" function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[string[]]$newArgumentNames) { $returnValue=@{} for($i=0;$i -lt $argumentNames.Count;$i++) { $argumentName=$argumentNames[$i] if($newArgumentNames -eq $null) { $newArgumentName=$argumentName } else { $newArgumentName=$newArgumentNames[$i] } if($functionBoundParameters.ContainsKey($argumentName)) { $null=$returnValue.Add($newArgumentName,$functionBoundParameters[$argumentName]) } } return $returnValue } function IsRunFromLocalSystemUser() { (New-Object Security.Principal.WindowsPrincipal ( [Security.Principal.WindowsIdentity]::GetCurrent())).Identity.IsSystem } function Get-TargetResource { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [System.String] $Arguments, [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] $Credential ) $Path=(ResolvePath $Path) $PSBoundParameters["Path"] = $Path $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $processes = @(GetWin32_Process @getArguments) if($processes.Count -eq 0) { return @{ Path=$Path Arguments=$Arguments Ensure='Absent' } } foreach($process in $processes) { # 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) @{ Path=$process.Path Arguments=(GetProcessArgumentsFromCommandLine $process.CommandLine) PagedMemorySize=$gpsProcess.PagedMemorySize64 NonPagedMemorySize=$gpsProcess.NonpagedSystemMemorySize64 VirtualMemorySize=$gpsProcess.VirtualMemorySize64 HandleCount=$gpsProcess.HandleCount Ensure='Present' ProcessId=$process.ProcessId } } } function Set-TargetResource { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [System.String] $Arguments, [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] $Credential, [System.String] [ValidateSet("Present", "Absent")] $Ensure="Present", [System.String] $StandardOutputPath, [System.String] $StandardErrorPath, [System.String] $StandardInputPath, [System.String] $WorkingDirectory ) $Path=ResolvePath $Path $PSBoundParameters["Path"] = $Path $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $processes = @(GetWin32_Process @getArguments) if($Ensure -eq 'Absent') { "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory" | AssertParameterIsNotSpecified $PSBoundParameters if ($processes.Count -gt 0) { $processIds=$processes.ProcessId $err=Stop-Process -Id $processIds -force 2>&1 if($err -eq $null) { Write-Log ($LocalizedData.ProcessesStopped -f $Path,($processIds -join ",")) } else { Write-Log ($LocalizedData.ErrorStopping -f $Path,($processIds -join ","),($err | out-string)) throw $err } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work if (!(WaitForProcessCount @getArguments -waitCount 0)) { $message = $LocalizedData.ErrorStopping -f $Path,($processIds -join ","),$LocalizedData.FailureWaitingForProcessesToStop Write-Log $message ThrowInvalidArgumentError "FailureWaitingForProcessesToStop" $message } } else { Write-Log ($LocalizedData.ProcessAlreadyStopped -f $Path) } } else { "StandardInputPath","WorkingDirectory" | AssertAbsolutePath $PSBoundParameters -Exist "StandardOutputPath","StandardErrorPath" | AssertAbsolutePath $PSBoundParameters if ($processes.Count -eq 0) { $startArguments = ExtractArguments $PSBoundParameters ` ("Path", "Arguments", "Credential", "StandardOutputPath", "StandardErrorPath", "StandardInputPath", "WorkingDirectory") ` ("FilePath", "ArgumentList", "Credential", "RedirectStandardOutput", "RedirectStandardError", "RedirectStandardInput", "WorkingDirectory") if([string]::IsNullOrEmpty($Arguments)) { $null=$startArguments.Remove("ArgumentList") } 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 (IsRunFromLocalSystemUser)) { if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) { $exception = New-Object System.ArgumentException $LocalizedData.ErrorParametersNotSupportedWithCredential $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null } else { $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 } } } else { $err=Start-Process @startArguments 2>&1 } if($err -eq $null) { Write-Log ($LocalizedData.ProcessStarted -f $Path) } else { Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) throw $err } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work if (!(WaitForProcessCount @getArguments -waitCount 1)) { $message = $LocalizedData.ErrorStarting -f $Path,$LocalizedData.FailureWaitingForProcessesToStart Write-Log $message ThrowInvalidArgumentError "FailureWaitingForProcessesToStart" $message } } } else { Write-Log ($LocalizedData.ProcessAlreadyStarted -f $Path) } } } function Test-TargetResource { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [parameter(Mandatory = $true)] [AllowEmptyString()] [System.String] $Arguments, [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] $Credential, [System.String] [ValidateSet("Present", "Absent")] $Ensure="Present", [System.String] $StandardOutputPath, [System.String] $StandardErrorPath, [System.String] $StandardInputPath, [System.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 } $Path=ResolvePath $Path $PSBoundParameters["Path"] = $Path $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $processes = @(GetWin32_Process @getArguments) if($Ensure -eq 'Absent') { return ($processes.Count -eq 0) } else { return ($processes.Count -gt 0) } } function GetWin32ProcessOwner { param ( [parameter(Mandatory = $true)] [ValidateNotNull()] $process ) # if the process was killed by the time this is called, GetOwner # will throw a WMIMethodException "Not found" try { $owner = Invoke-CimMethod -InputObject $process -MethodName GetOwner } catch { } if($owner.Domain -ne $null) { return $owner.Domain + "\" + $owner.User } else { return $owner.User } } function WaitForProcessCount { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [System.String] $Arguments, [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] $Credential, [parameter(Mandatory=$true)] $waitCount ) $start = [DateTime]::Now do { $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $value = @(GetWin32_Process @getArguments).Count -eq $waitCount } while(!$value -and ([DateTime]::Now - $start).TotalMilliseconds -lt 2000) return $value } function GetWin32_Process { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [System.String] $Arguments, [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] $Credential, $useWmiObjectCount=8 ) $fileName = [io.path]::GetFileNameWithoutExtension($Path) $gpsProcesses = @(get-process -Name $fileName -ErrorAction SilentlyContinue) if($gpsProcesses.Count -ge $useWmiObjectCount) { # if there are many processes it is faster to perform a Get-WmiObject # in order to get Win32_Process objects for all processes $Path=WQLEscape $Path $filter = "ExecutablePath = '$Path'" $processes = Get-CimInstance Win32_Process -Filter $filter } else { # if there are few processes, building a Win32_Process for # each matching result of get-process is faster $processes = foreach($gpsProcess in $gpsProcesses) { if(!($gpsProcess.Path -ieq $Path)) { continue } try { Write-Verbose ($LocalizedData.VerboseInProcessHandle -f $gpsProcess.Id) Get-CimInstance Win32_Process -Filter "ProcessId=$($gpsProcess.Id)" } catch { #ignore if could not retrieve process } } } if($PSBoundParameters.ContainsKey('Credential')) { $Domain, $UserName = Get-DomainAndUserName $Credential # Since there are credentials we need to call the GetOwner method in each process to search for matches $processes = $processes | where { (GetWin32ProcessOwner $_) -eq "$Domain\$UserName" } } if($Arguments -eq $null) {$Arguments = ""} $processes = $processes | where { (GetProcessArgumentsFromCommandLine $_.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". #> function GetProcessArgumentsFromCommandLine { param ( [System.String] $commandLine ) if($commandLine -eq $null) { return "" } $commandLine=$commandLine.Trim() if($commandLine.Length -eq 0) { return "" } if($commandLine[0] -eq '"') { $charToLookfor=[char]'"' } else { $charToLookfor=[char]' ' } $endofCommand=$commandLine.IndexOf($charToLookfor ,1) if($endofCommand -eq -1) { return "" } return $commandLine.Substring($endofCommand+1).Trim() } <# .Synopsis Escapes a string to be used in a WQL filter as the one passed to get-wmiobject #> function WQLEscape { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $query ) return $query.Replace("\","\\").Replace('"','\"').Replace("'","\'") } 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 } function ResolvePath { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path ) $Path = [Environment]::ExpandEnvironmentVariables($Path) if(IsRootedPath $Path) { if(!(Test-Path $Path -PathType Leaf)) { ThrowInvalidArgumentError "CannotFindRootedPath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) } return $Path } if([string]::IsNullOrEmpty($env:Path)) { ThrowInvalidArgumentError "EmptyEnvironmentPath" ($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) { ThrowInvalidArgumentError "NotAbsolutePathOrFileName" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.AbsolutePathOrFileName) } foreach($rawSegment in $env:Path.Split(";")) { $segment = [Environment]::ExpandEnvironmentVariables($rawSegment) # if an exception causes $segmentedRooted not to be set, we will consider it $false $segmentRooted = $false 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) } catch {} if(!$segmentRooted) { continue } $candidate = join-path $segment $Path if(Test-Path $candidate -PathType Leaf) { return $candidate } } ThrowInvalidArgumentError "CannotFindRelativePath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) } function AssertAbsolutePath { [CmdletBinding()] param ( $ParentBoundParameters, [System.String] [Parameter (ValueFromPipeline=$true)] $ParameterName, [switch] $Exist ) Process { if(!$ParentBoundParameters.ContainsKey($ParameterName)) { return } $path=$ParentBoundParameters[$ParameterName] if(!(IsRootedPath $Path)) { ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldBeAbsolute) } if(!$Exist.IsPresent) { return } if(!(Test-Path $Path)) { ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldExist) } } } function AssertParameterIsNotSpecified { [CmdletBinding()] param ( $ParentBoundParameters, [System.String] [Parameter (ValueFromPipeline=$true)] $ParameterName ) Process { if($ParentBoundParameters.ContainsKey($ParameterName)) { ThrowInvalidArgumentError "ParameterShouldNotBeSpecified" ($LocalizedData.ParameterShouldNotBeSpecified -f $ParameterName) } } } function IsRootedPath { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path ) try { return [IO.Path]::IsPathRooted($Path) } catch { # if the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on ThrowInvalidArgumentError "CannotGetIsPathRooted" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $_.Exception.Message) } } function Write-Log { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message ) if ($PSCmdlet.ShouldProcess($Message, $null, $null)) { Write-Verbose $Message } } Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource