Author Posts

May 29, 2015 at 2:41 pm

Hi there folks,

I'm working on a module that needs 3 dll's to work. I'm adding these with the following lines of code on my psm1 file

Add-Type -Path "$PSScriptRoot\bin\Newtonsoft.Json.dll"
Add-Type -Path "$PSScriptRoot\bin\Octopus.Client.dll"
Add-Type -Path "$PSScriptRoot\bin\Octopus.Platform.dll"

The problem comes when i release a new version of my module. My module gets installed programatically using chocolatey and a chocolateyinstall.ps1 script. When this script is run, i get an error saying those dlls cannot be deleted/overwritten because they are in use (the GAC i asume)

– What would be the best practice to load dlls as part of a module?
– and to unload them as part of a script, to update the module?

Many thanks in advance

Dalmiro

May 29, 2015 at 8:57 pm

If these dlls are dependencies that are seldom updated, you can modify your build script to only copy those files if they're not already present, or check the source / destination hashes before trying to copy, that sort of thing.

Unfortunately, this is a common problem with PowerShell modules that have binaries. If there are any instances of powershell running that have your module loaded, you won't be able to overwrite the files. Once a dll is loaded, you can't unload it without closing the powershell process. (This can be even more awkward if your module contains DSC resources, as then the LCM may be keeping the modules open as well.)

Here's some code that I use in one of my build scripts to get around this problem. It has a similar setup, relying on Security.Cryptography.dll (which never changes; just needs to be distributed with the module.) This particular code assumes you're running PowerShell 4.0 or later, for the Get-FileHash cmdlet, but it can be modified to support older versions as well.

function Copy-Folder
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory)]
        [ValidateScript({
            if (-not (Test-Path -LiteralPath $_) -or
                (Get-Item -LiteralPath $_) -isnot [System.IO.DirectoryInfo])
            {
                throw "Path '$_' does not refer to a Directory on the FileSystem provider."
            }

            return $true
        })]
        [string] $Source,

        [Parameter(Mandatory)]
        [ValidateScript({
            if (Test-Path -LiteralPath $_)
            {
                $destFolder = Get-Item -LiteralPath $_ -ErrorAction Stop -Force

                if ($destFolder -isnot [System.IO.DirectoryInfo])
                {
                    throw "Destination '$_' exists, and is not a directory on the file system."
                }
            }

            return $true
        })]
        [string] $Destination
    )

    # Everything here that's destructive is done via cmdlets that already support ShouldProcess, so we don't need to make our own calls
    # to it here.  Those cmdlets will inherit our local $WhatIfPreference / $ConfirmPreference anyway.

    $sourceFolder = Get-Item -LiteralPath $Source
    $sourceRootPath = $sourceFolder.FullName

    if (Test-Path -LiteralPath $Destination)
    {
        $destFolder = Get-Item -LiteralPath $Destination -ErrorAction Stop -Force

        # ValidateScript already made sure that we're looking at a [DirectoryInfo], but just in case there's a weird race condition
        # with some other process, we'll check again here to be sure.
        
        if ($destFolder -isnot [System.IO.DirectoryInfo])
        {
            throw "Destination '$Destination' exists, and is not a directory on the file system."
        }

        # First, clear out anything in the destination that doesn't exist in the source.  By doing this first, we can ensure that
        # there aren't existing directories with the name of a file we need to copy later, or vice versa.

        foreach ($fsInfo in Get-ChildItem -LiteralPath $destFolder.FullName -Recurse -Force)
        {
            # just in case we've already nuked the parent folder of something earlier in the loop.
            if (-not $fsInfo.Exists) { continue }

            $fsInfoRelativePath = Get-RelativePath -Path $fsInfo.FullName -RelativeTo $destFolder.FullName
            $sourcePath = Join-Path $sourceRootPath $fsInfoRelativePath

            if ($fsInfo -is [System.IO.DirectoryInfo])
            {
                $pathType = 'Container'
            }
            else
            {
                $pathType = 'Leaf'
            }

            if (-not (Test-Path -LiteralPath $sourcePath -PathType $pathType))
            {
                Remove-Item $fsInfo.FullName -Force -Recurse -ErrorAction Stop
            }
        }
    }

    # Now copy over anything from source that's either missing or different.
    foreach ($fsInfo in Get-ChildItem -LiteralPath $sourceRootPath -Recurse -Force)
    {
        $fsInfoRelativePath = Get-RelativePath -Path $fsInfo.FullName -RelativeTo $sourceRootPath
        $targetPath = Join-Path $Destination $fsInfoRelativePath
        $parentPath = Split-Path $targetPath -Parent

        if ($fsInfo -is [System.IO.FileInfo])
        {
            EnsureFolderExists -Path $parentPath

            if (-not (Test-Path -LiteralPath $targetPath) -or
                -not (FilesAreIdentical $fsInfo.FullName $targetPath))
            {
                Copy-Item -LiteralPath $fsInfo.FullName -Destination $targetPath -Force -ErrorAction Stop
            }
        }
        else
        {
            EnsureFolderExists -Path $targetPath
        }
    }
}

function EnsureFolderExists([string] $Path)
{
    if (-not (Test-Path -LiteralPath $Path -PathType Container))
    {
        $null = New-Item -Path $Path -ItemType Directory -ErrorAction Stop
    }
}

function FilesAreIdentical([string] $FirstPath, [string] $SecondPath)
{
    $first = Get-Item -LiteralPath $FirstPath -Force -ErrorAction Stop
    $second = Get-Item -LiteralPath $SecondPath -Force -ErrorAction Stop

    if ($first.Length -ne $second.Length) { return $false }

    $firstHash = Get-FileHash -LiteralPath $FirstPath -Algorithm SHA512 -ErrorAction Stop
    $secondHash = Get-FileHash -LiteralPath $SecondPath -Algorithm SHA512 -ErrorAction Stop

    return $firstHash.Hash -eq $secondHash.Hash
}

function Get-RelativePath([string] $Path, [string]$RelativeTo )
{
    $RelativeTo = $RelativeTo -replace '\\+$'
    return $Path -replace "^$([regex]::Escape($RelativeTo))\\?"
}

June 3, 2015 at 6:51 pm

Thanks for that reply Dave! Glad to hear i'm not alone with this issue.

I was relying on a ChocolateInstall.ps1 script to automatically install my module, by deleting the current version (and with it the dlls) and then copying the new one. I ended up giving up on this approach and started using Install-Module (from WMF preview 5) which apparently solves this issue by creating a folder for each version under the module directory:

[blockquote]
Directory: C:\Program Files\WindowsPowerShell\Modules\Octoposh

Mode LastWriteTime Length Name
—- ————- —— —-
d—– 6/3/2015 9:46 PM 0.2.50
d—– 6/3/2015 11:21 PM 0.2.52
[/blockquote]

I didn't knew this could be done. I'm gonna do some more research about this.

Thanks a lot for that code snippet. I'm gonna keep it on my toolset, as i'm quite sure i'm gonna need it one of these days.