Using custom DLLs on a Module

This topic contains 2 replies, has 2 voices, and was last updated by  Dalmiro Grañas 2 years, 6 months ago.

  • Author
    Posts
  • #25815

    Dalmiro Grañas
    Participant

    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

  • #25817

    Dave Wyatt
    Moderator

    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))\\?"
    }
    
  • #25923

    Dalmiro Grañas
    Participant

    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.

You must be logged in to reply to this topic.