DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1

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
StoppingProcessWhatIf = Stop-Process
ProcessNotFound = Process matching path '{0}' not found
PathShouldBeAbsolute = The path should be absolute
PathShouldExist = The path should exist
ParameterShouldNotBeSpecified = Parameter {0} should not be specified.
FailureWaitingForProcessesToStart = Failed to wait for processes to start
FailureWaitingForProcessesToStop = Failed to wait for processes to stop
ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, StandardInputPath or WorkingDirectory when trying to run a process under a user context.
VerboseInProcessHandle = In process handle {0}
ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter.
ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredential parameter is not supported by the Process resource, and cannot be used with the Credential parameter. To start the process with user '{0}', use only the Credential parameter, not the PsDscRunAsCredential parameter.
'@

}

# Commented out until more languages are supported
# Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1

Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1"

<#
    .SYNOPSIS
        Tests if the current user is from the local system.
#>

function Test-IsRunFromLocalSystemUser
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param ()


    $currentUser = (New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList @( [Security.Principal.WindowsIdentity]::GetCurrent() ))

    return $currenUser.Identity.IsSystem
}

<#
    .SYNOPSIS
        Splits a credential into a username and domain wihtout calling GetNetworkCredential.
        Calls to GetNetworkCredential expose the password as plain text in memory.
 
    .PARAMETER Credential
        The credential to pull the username and domain out of.
 
    .NOTES
        Supported formats: DOMAIN\username, username@domain
#>

function Split-Credential
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential
    )

    $wrongFormat = $false

    if ($Credential.UserName.Contains('\')) 
    {
        $credentialSegments = $Credential.UserName.Split('\')
        
        if ($credentialSegments.Length -gt 2)
        {
            # i.e. domain\user\foo
            $wrongFormat = $true
        } 
        else
        {
            $domain = $credentialSegments[0]
            $userName = $credentialSegments[1]
        }
    } 
    elseif ($Credential.UserName.Contains('@')) 
    {
        $credentialSegments = $Credential.UserName.Split('@')

        if ($credentialSegments.Length -gt 2)
        {
            # i.e. user@domain@foo
            $wrongFormat = $true
        }
        else
        {
            $UserName = $credentialSegments[0]
            $Domain = $credentialSegments[1]
        }
    }
    else 
    {
        # support for default domain (localhost)
        $domain = $env:computerName
        $userName = $Credential.UserName
    }

    if ($wrongFormat) 
    {
        $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName
        
        Write-Verbose -Message $message

        New-InvalidArgumentException -ArgumentName 'Credential' -Message $message
    }

    return @{
        Domain = $domain
        UserName = $userName
    }
}

function Get-TargetResource
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [String]
        $Arguments,

        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential
    )
    
    $Path = Expand-Path -Path $Path

    $getWin32ProcessArguments = @{
        Path = $Path
        Arguments = $Arguments
    }

    if ($null -ne $Credential)
    {
        $getWin32ProcessArguments['Credential'] = $Credential
    }

    $win32Processes = @( Get-Win32Process @getWin32ProcessArguments )

    if ($win32Processes.Count -eq 0)
    {
        return @{
            Path = $Path
            Arguments = $Arguments
            Ensure ='Absent'
        }
    }

    foreach ($win32Process in $win32Processes)
    {
        $getProcessResult = Get-Process -ID $win32Process.ProcessId -ErrorAction 'Ignore'

        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)]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [String]
        $Arguments,

        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [String]
        $StandardOutputPath,

        [String]
        $StandardErrorPath,

        [String]
        $StandardInputPath,

        [String]
        $WorkingDirectory
    )

    if ($null -ne $PsDscContext.RunAsUser)
    {
        New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser)
    } 

    $Path = Expand-Path -Path $Path

    $getWin32ProcessArguments = @{
        Path = $Path
        Arguments = $Arguments
    }

    if ($null -ne $Credential)
    {
        $getWin32ProcessArguments['Credential'] = $Credential
    }

    $win32Processes = @( Get-Win32Process @getWin32ProcessArguments )

    if ($Ensure -eq 'Absent')
    {
        Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' )

        if ($win32Processes.Count -gt 0 -and $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif))
        {
           $processIds = $win32Processes.ProcessId

           $stopProcessError = Stop-Process -Id $processIds -Force 2>&1
           
           if ($null -eq $stopProcessError)
           {
               Write-Verbose -Message ($LocalizedData.ProcessesStopped -f $Path, ($processIds -join ','))
           }
           else
           {
               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 (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 0))
           {
                $message = $LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), $LocalizedData.FailureWaitingForProcessesToStop
                
                Write-Verbose -Message $message

                New-InvalidOperationException -Message $message
           }
        }
        else
        {
            Write-Verbose -Message ($LocalizedData.ProcessAlreadyStopped -f $Path)
        }
    }
    else
    {
        $shouldBeRootedPathArguments = @( 'StandardInputPath', 'WorkingDirectory', 'StandardOutputPath', 'StandardErrorPath' )

        foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments)
        {
            if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument]))
            {
                Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument]
            }
        }

        $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' )

        foreach ($shouldExistPathArgument in $shouldExistPathArguments)
        {
            if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument]))
            {
                Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument]
            }
        }

        if ($win32Processes.Count -eq 0)
        {
            $startProcessArguments = @{
                FilePath = $Path
            }

            $startProcessOptionalArgumentMap = @{
                Credential = 'Credential'
                RedirectStandardOutput = 'StandardOutputPath'
                RedirectStandardError = 'StandardErrorPath'
                RedirectStandardInput = 'StandardInputPath'
                WorkingDirectory = 'WorkingDirectory'
            }

            foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys)
            {
                if (-not [String]::IsNullOrEmpty($PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]))
                {
                    $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]
                }
            }

            if (-not [String]::IsNullOrEmpty($Arguments))
            {
                $startProcessArguments['ArgumentList'] = $Arguments
            }

            if ($PSCmdlet.ShouldProcess($Path, $LocalizedData.StartingProcessWhatif))
            {
                <#
                    Start-Process calls .net Process.Start()
                    If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx
                    CreateProcessWithLogonW cannot be called as LocalSystem user.
                    Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003)
                 
                    In this case we call another api.
                #>

                if ($PSBoundParameters.ContainsKey('Credential') -and (Test-IsRunFromLocalSystemUser))
                {
                    if ($PSBoundParameters.ContainsKey('StandardOutputPath'))
                    {
                        New-InvalidArgumentException -ArgumentName 'StandardOutputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential
                    }
                    
                    if ($PSBoundParameters.ContainsKey('StandardInputPath'))
                    {
                        New-InvalidArgumentException -ArgumentName 'StandardInputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential
                    }
                    
                    if ($PSBoundParameters.ContainsKey('WorkingDirectory'))
                    {
                        New-InvalidArgumentException -ArgumentName 'WorkingDirectory' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential
                    }

                    $splitCredentialResult = Split-Credential $Credential
                    try
                    {
                        <#
                            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
                {
                    $startProcessError = Start-Process @startProcessArguments 2>&1
                }

                if ($null -eq $startProcessError)
                {
                    Write-Verbose -Message ($LocalizedData.ProcessStarted -f $Path)
                }
                else
                {
                    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 (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 1))
                {
                    $message = $LocalizedData.ErrorStarting -f $Path, $LocalizedData.FailureWaitingForProcessesToStart
                    
                    Write-Verbose -Message $message
                    
                    New-InvalidOperationException -Message $message
                }
            }
        }
        else
        {
            Write-Verbose -Message ($LocalizedData.ProcessAlreadyStarted -f $Path)
        }
    }
}

function Test-TargetResource
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [String]
        $Arguments,

        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [String]
        $StandardOutputPath,

        [String]
        $StandardErrorPath,

        [String]
        $StandardInputPath,

        [String]
        $WorkingDirectory
    )

    if ($null -ne $PsDscContext.RunAsUser)
    {
        New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser)
    }    

    $Path = Expand-Path -Path $Path

    $getWin32ProcessArguments = @{
        Path = $Path
        Arguments = $Arguments
    }

    if ($null -ne $Credential)
    {
        $getWin32ProcessArguments['Credential'] = $Credential
    }

    $win32Processes = @( Get-Win32Process @getWin32ProcessArguments )

    if ($Ensure -eq 'Absent')
    {
        return ($win32Processes.Count -eq 0)
    }
    else
    {
        return ($win32Processes.Count -gt 0)
    }
}

<#
    .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)]
        [ValidateNotNull()]
        $Process
    )

    $owner = Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' -ErrorAction 'SilentlyContinue'
    
    if ($null -ne $owner.Domain)
    {
        return $owner.Domain + '\' + $owner.User
    }
    else                
    {
        return $owner.User
    }
}

<#
    .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
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Hashtable]
        $ProcessSettings,

        [Parameter(Mandatory = $true)]
        [ValidateRange(0, [Int]::MaxValue)]
        [Int]
        $ProcessCount
    )

    $startTime = [DateTime]::Now

    do
    {
        $actualProcessCount = @( Get-Win32Process @ProcessSettings ).Count
    } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt 2000)
    
    return $actualProcessCount -eq $ProcessCount
}

<#
    .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]
        $Path,

        [String]
        $Arguments,

        [ValidateNotNullOrEmpty()]
        [PSCredential]
        $Credential,

        [ValidateRange(0, [Int]::MaxValue)]
        [Int]
        $UseGetCimInstanceThreshold = 8
    )

    $processName = [IO.Path]::GetFileNameWithoutExtension($Path)

    $getProcessResult = @( Get-Process -Name $processName -ErrorAction 'SilentlyContinue' )

    $processes = @()

    if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold)
    {
        
        $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path
        $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'"

        $processes = Get-CimInstance -ClassName 'Win32_Process' -Filter $wqlFilter
    }
    else
    {
        foreach ($process in $getProcessResult)
        {
            if ($process.Path -ieq $Path)
            {
                Write-Verbose -Message ($LocalizedData.VerboseInProcessHandle -f $process.Id)
                $processes += Get-CimInstance -ClassName 'Win32_Process' -Filter "ProcessId = $($process.Id)" -ErrorAction 'SilentlyContinue'
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
        $splitCredentialResult = Split-Credenital -Credential $Credential

        $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$($splitCredentialResult.Domain)\$($splitCredentialResult.UserName)" }
    }

    if ($null -eq $Arguments)
    {
        $Arguments = [String]::Empty
    }

    $processesWithMatchingArguments = @()

    foreach ($process in $processes)
    {
        if ((Get-ArgumentsFromCommandLineInput -CommandLineInput ($process.CommandLine)) -eq $Arguments)
        {
            $processesWithMatchingArguments += $process
        }
    }

    return $processesWithMatchingArguments
}

<#
    .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 Get-ArgumentsFromCommandLineInput
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [String]
        $CommandLineInput
    )

    if ([String]::IsNullOrWhitespace($CommandLineInput))
    {
        return [String]::Empty
    }
    
    $CommandLineInput = $CommandLineInput.Trim()

    if ($CommandLineInput.StartsWith('"'))
    {
        $endOfCommandChar = [Char]'"'
    }
    else
    {
        $endOfCommandChar = [Char]' '
    }

    $endofCommandIndex = $CommandLineInput.IndexOf($endOfCommandChar, 1)
    if ($endofCommandIndex -eq -1)
    {
        return [String]::Empty
    }

    return $CommandLineInput.Substring($endofCommandIndex + 1).Trim()
}

<#
    .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 ConvertTo-EscapedStringForWqlFilter
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $FilterString
    )

    return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'")
}

<#
    .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)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Path
    )

    $Path = [Environment]::ExpandEnvironmentVariables($Path)

    if ([IO.Path]::IsPathRooted($Path))
    {
        if (-not (Test-Path -Path $Path -PathType 'Leaf'))
        {
            New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound)
        }

        return $Path
    }
    else
    {
        New-InvalidArgumentException -ArgumentName 'Path'
    }

    if ([String]::IsNullOrEmpty($env:Path))
    {
        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 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)
    {
        New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.AbsolutePathOrFileName)
    }

    foreach ($rawEnvPathSegment in $env:Path.Split(';'))
    {
        $envPathSegment = [Environment]::ExpandEnvironmentVariables($rawEnvPathSegment)

        # If an exception causes $envPathSegmentRooted not to be set, we will consider it $false
        $envPathSegmentRooted = $false
        
        <#
            If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have
            invalid characters, so the segment has no invalid characters and will not throw as well.
        #>

        try
        {
            $envPathSegmentRooted = [IO.Path]::IsPathRooted($envPathSegment)
        }
        catch {}
        
        if ($envPathSegmentRooted)
        {
            $fullPathCandidate = Join-Path -Path $envPathSegment -ChildPath $Path
        
            if (Test-Path -Path $fullPathCandidate -PathType 'Leaf')
            {
                return $fullPathCandidate
            }
        }
    }

    New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound)
}

<#
    .SYNOPSIS
        Throws an error if the given path argument is not rooted.
 
    .PARAMETER PathArgumentName
        The name of the path argument that should be rooted.
 
    .PARAMETER PathArgument
        The path arguments that should be rooted.
#>

function Assert-PathArgumentRooted
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PathArgumentName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PathArgument
    )
    
    if (-not ([IO.Path]::IsPathRooted($PathArgument)))
    {
        New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldBeAbsolute)
    }
}

<#
    .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
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PathArgumentName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PathArgument
    )

    if (-not (Test-Path -Path $PathArgument))
    {
        New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldExist)
    }
}

<#
    .SYNOPSIS
        Throws an exception if the given hashtable contains the given key(s).
 
    .PARAMETER Hashtable
        The hashtable to check the keys of.
 
    .PARAMETER Key
        The key(s) that should not be in the hashtable.
#>

function Assert-HashtableDoesNotContainKey
{
    [CmdletBinding()]
    param
    (
        [Hashtable]
        $Hashtable,

        [Parameter(Mandatory = $true)]
        [String[]]
        $Key
    )

    foreach ($keyName in $Key)
    {
        if ($Hashtable.ContainsKey($keyName)) 
        {
            New-InvalidArgumentException -ArgumentName $keyName -Message ($LocalizedData.ParameterShouldNotBeSpecified -f $keyName)
        }
    }
}

Export-ModuleMember -Function *-TargetResource