PSDesiredStateConfiguration icon indicating copy to clipboard operation
PSDesiredStateConfiguration copied to clipboard

Invoke-DscResource cmdlet throws an exception when using class-based DSC Resource, when module path contains a space

Open KristiyanGK opened this issue 4 years ago • 11 comments

When using a custom class-based DSC resource, the **Invoke-DscResource **cmdlet fails. The issue seems to be in the function Invoke-DscClassBasedResource in the PSDesiredStateConfiguration.psm1 of the PSDesiredStateConfiguration module:

$script = @"
using module $path

Write-Host -Message ([$type]::new | out-string)

return [$type]::new()
"@

When $path is a string with white spaces for example: C:\My Modules\MyModule.psd1 the path gets cut off at the first space and results in C:\My. This leads to an error when trying to use the module, due to the fact that the path is invalid.

Steps to reproduce

  1. Create a Dsc Resource located in : C:\My Modules\MyDscResource.psm1 or the standard Module path: C:\Program Files\WindowsPowerShell\Modules
enum Ensure
{
    Absent
    Present
}
[DscResource()]
class FileResource
{
    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Mandatory)]
    [string] $SourcePath

    [DscProperty(NotConfigurable)]
    [Nullable[datetime]] $CreationTime

    [void] Set() {
        $fileExists = $this.TestFilePath($this.Path)
        if ($this.ensure -eq [Ensure]::Present) {
            if (-not $fileExists) {
                $this.CopyFile()
            }
        }
        else {
            if ($fileExists) {
                Write-Verbose -Message "Deleting the file $($this.Path)"
                Remove-Item -LiteralPath $this.Path -Force
            }
        }
    }
    [bool] Test() {
        $present = $this.TestFilePath($this.Path)

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        } else {
            return -not $present
        }
    }
    [FileResource] Get() {
        $present = $this.TestFilePath($this.Path)

        if ($present) {
            $file = Get-ChildItem -LiteralPath $this.Path
            $this.CreationTime = $file.CreationTime
            $this.Ensure = [Ensure]::Present
        }
        else {
            $this.CreationTime = $null
            $this.Ensure = [Ensure]::Absent
        }

        return $this
    }
    [bool] TestFilePath([string] $location) {
        $present = $true

        $item = Get-ChildItem -LiteralPath $location -ea Ignore
        if ($item -eq $null) {
            $present = $false
        }  elseif ($item.PSProvider.Name -ne "FileSystem") {
            throw "Path $($location) is not a file path."
        }
        elseif ($item.PSIsContainer) {
            throw "Path $($location) is a directory path."
        }
        return $present
    }
    [void] CopyFile() {
        if (-not $this.TestFilePath($this.SourcePath)) {
            throw "SourcePath $($this.SourcePath) is not found."
        }
        [System.IO.FileInfo] $destFileInfo = new-object System.IO.FileInfo($this.Path)
        if (-not $destFileInfo.Directory.Exists) {
            Write-Verbose -Message "Creating directory $($destFileInfo.Directory.FullName)"
            [System.IO.Directory]::CreateDirectory($destFileInfo.Directory.FullName)
        }

        if (Test-Path -LiteralPath $this.Path -PathType Container)  {
            throw "Path $($this.Path) is a directory path"
        }
        Write-Verbose -Message "Copying $($this.SourcePath) to $($this.Path)"
        Copy-Item -LiteralPath $this.SourcePath -Destination $this.Path -Force
    }
} # This module defines a class for a DSC "FileResource" provider.
  1. Invoke the DSC Resource with the Invoke-DscResource cmdlet
$invokeParams = @{
    Name = 'FileResource'
    Modulename = 'MyDscResource'
    Method = 'Test'
    Property = @{
        Path = "C:\Test\sample data.txt"
        SourcePath = "C:\Test\sample data.txt"
        Ensure = "Present"
    }
    Verbose = $true
}

Invoke-DscResource @invokeParams

Expected behavior

Should Invoke the resource correctly and return a valid result.

Actual behavior

Throws various exceptions due to not parsing the module path correctly.

image

Environment data

$psversiontable info PSVersion | 7.0.3 PSEdition | Core GitCommitId | 7.0.3 OS | Microsoft Windows 10.0.18363 Platform | Win32NT PSCompatibleVersions| {1.0, 2.0, 3.0, 4.0…} PSRemotingProtocolVersion | 2.3 SerializationVersion |1.1.0.1 WSManStackVersion |3.0

KristiyanGK avatar Jul 23 '20 13:07 KristiyanGK

/cc @TravisEz13 for comments.

daxian-dbw avatar Jul 23 '20 21:07 daxian-dbw

@KristiyanGK Can you run Get-DscResource with the same module specification you used in Invoke-DscResource to see if this is a bug in Invoke or Get?

TravisEz13 avatar Aug 11 '20 21:08 TravisEz13

@TravisEz13 This is the result I get by running Get-DscResource .

image

KristiyanGK avatar Aug 12 '20 11:08 KristiyanGK

@KristiyanGK Unfortunately, this lives in a private repo. I think I should transfer this issue to that repo, which will make it invisible to you. After, I do so, feel free to file a new issue here, and mention me and I'll link them. Does that sound fair?

Hopefully, the other repo will become public, at least for issue tracking soon enough.

TravisEz13 avatar Aug 14 '20 16:08 TravisEz13

It appears to me (having done some debugging with VS Code) that (part of) the bug is here:

$listPSModuleFolders = $env:PSModulePath.Split(":")

It's splitting on :, but $PSModulePath is ;-separated on Windows, isn't it?

ghost avatar Aug 28 '20 18:08 ghost

Yup... it should be using [System.IO.Path]::PathSeparator for compatibility 😕

vexx32 avatar Aug 28 '20 21:08 vexx32

I think what I mentioned in my previous comment is actually an unrelated problem. Having done a lot more debugging, I think I've tracked the problem to Invoke-DscClassBasedResource. This function (among other things) does

    $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2()
    $powershell = [PowerShell]::Create($iss)
    $script = @"
using module $path

Write-Host -Message ([$type]::new | out-string)
return [$type]::new()
"@


    $null= $powershell.AddScript($script)
    $dscType=$powershell.Invoke() | Select-object -First 1

Essentially, it's creating an entirely new PowerShell session as if it were a host application, and then using it to execute a script with a using module (to make importing classes work) of the resource provider module which then returns an instance of the resource class.

From one perspective, it seems like it would be a lot easier to just use a script block rather than this hosting-PowerShell-in-PowerShell craziness, but the actual cause of the problem appears to be that the path is unquoted in the script, so, e.g. C:\Program Files\WindowsPowerShell\Modules\SomeModule is interpreted as using module C:\Program and then an expected semicolon or newline parse error happens.

I think the best fix for this is probably the long-overdue engine modifications to make Import-Module work with classes, and then Invoke-DscClassBasedResource can be simplified enormously. Until that can be done, though, replacing the whole PS host thing with

$script = @"
using module "$path"
return [$type]::new()
"@
$dscType = & ([ScriptBlock]::Create($script))

or similar should be a good fix. Testing it on my machine seems to indicate that it works. I would put in a PR, but as @TravisEz13 said above, PSDesiredStateConfiguration is a private repository.

ghost avatar Sep 04 '20 18:09 ghost

I should point out that, at least in my case, leaving the PowerShell-in-PowerShell thing in place and just fixing the quotes led to a bunch of other problems and crashed my script; it works fine with the script block.

ghost avatar Sep 04 '20 18:09 ghost

Hi, how is the plan here ? Also run into this

MaBauMeBad avatar May 26 '21 16:05 MaBauMeBad

This issue continues in the current version of PSDesiredStateConfiguration, and makes it impossible to use DSC correctly without locally patching the module for users who e.g. have their Documents folder redirected to OneDrive for Business (as some organizations require, and which, at least in the default configuration for US English, introduces a space into the path to the Documents folder and thus also the Documents\PowerShell\Modules directory where PowerShellGet installs per-user modules) and have installed DSC resource modules in per-user scope.

Does the PowerShell team plan to address this issue at some point in 2022?

ghost avatar Jan 18 '22 15:01 ghost

Any Update on this issue ? This is nearly 2 years old jet.

MaBauMeBad avatar Mar 18 '22 14:03 MaBauMeBad