Private/Common/Install-SilkTCOModule.ps1

<#
    .SYNOPSIS
    Makes sure a module is present and loaded, pulling it from the gallery if it isnt.

    .DESCRIPTION
    Returns $true if the module ended up loaded, $false if it couldnt be - the caller
    decides whether thats fatal.

    Installs to CurrentUser scope on purpose: an elevation prompt halfway through a
    collection run is worse than useless, and CurrentUser works on a locked down box.

    The install runs in a job with a timeout because Install-Module blocks forever when
    PSGallery is unreachable, which is common on exactly the sort of hardened jump box
    these assessments get run from. A hung collection is worse than a missing cost column.
#>


function Install-SilkTCOModule {
    param(
        [Parameter(Mandatory)]
        [string] $Name,
        [Parameter()]
        [int] $TimeoutSeconds = 300
    )

    # already loaded, nothing to do
    if (Get-Module -Name $Name) {
        return $true
    }

    if (-not (Get-Module -ListAvailable -Name $Name)) {
        Write-Warning "$Name module not found. Installing from PSGallery (CurrentUser scope, ${TimeoutSeconds}s limit)..."

        $job = Start-Job -ArgumentList $Name -ScriptBlock {
            param($moduleName)

            # ps5.1 on a clean box wont install anything until nuget is bootstrapped, and
            # it prompts for it interactively if we dont get there first
            if ((Get-Command Install-PackageProvider -ErrorAction SilentlyContinue) -and
                -not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
            }

            # -Force also gets us past the untrusted PSGallery prompt
            Install-Module -Name $moduleName -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
        }

        $finished = Wait-Job -Job $job -Timeout $TimeoutSeconds

        if (-not $finished) {
            Stop-Job -Job $job -ErrorAction SilentlyContinue
            Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
            Write-Warning "Gave up installing $Name after ${TimeoutSeconds}s - PSGallery is probably blocked from this host."
            Write-Warning "Install it by hand with: Install-Module $Name -Scope CurrentUser"
            return $false
        }

        $jobError = $null
        Receive-Job -Job $job -ErrorVariable jobError -ErrorAction SilentlyContinue | Out-Null
        $failed = ($job.State -eq 'Failed') -or $jobError
        Remove-Job -Job $job -Force -ErrorAction SilentlyContinue

        if ($failed) {
            Write-Warning "Could not install $Name automatically: $(($jobError | Select-Object -First 1))"
            Write-Warning "Install it by hand with: Install-Module $Name -Scope CurrentUser"
            return $false
        }

        Write-Verbose "-> installed $Name" -Verbose
    }

    try {
        Import-Module $Name -ErrorAction Stop
        return $true
    } catch {
        Write-Warning "$Name is installed but would not import: $($_.Exception.Message)"
        return $false
    }
}