Eigenverft.Manifested.Sandbox.Shared.CommandEnvironment.ps1

<#
    Eigenverft.Manifested.Sandbox.Shared.CommandEnvironment
#>


function Get-ManifestedPathEntryKey {
    [CmdletBinding()]
    param(
        [string]$Path
    )

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return $null
    }

    $trimmedPath = $Path.Trim().Trim('"')
    if ([string]::IsNullOrWhiteSpace($trimmedPath)) {
        return $null
    }

    $normalizedPath = $trimmedPath
    try {
        $normalizedPath = [System.IO.Path]::GetFullPath($trimmedPath)
    }
    catch {
        $normalizedPath = $trimmedPath
    }

    if ($normalizedPath.Length -gt 3) {
        $normalizedPath = $normalizedPath.TrimEnd('\')
    }

    return $normalizedPath.ToLowerInvariant()
}

function Test-ManifestedPathEntryMatch {
    [CmdletBinding()]
    param(
        [string]$LeftPath,

        [string]$RightPath
    )

    $leftKey = Get-ManifestedPathEntryKey -Path $LeftPath
    $rightKey = Get-ManifestedPathEntryKey -Path $RightPath
    if ([string]::IsNullOrWhiteSpace($leftKey) -or [string]::IsNullOrWhiteSpace($rightKey)) {
        return $false
    }

    return $leftKey -eq $rightKey
}

function Get-ManifestedPathEntries {
    [CmdletBinding()]
    param(
        [string]$PathValue
    )

    if ([string]::IsNullOrWhiteSpace($PathValue)) {
        return @()
    }

    return @(
        $PathValue -split ';' |
            ForEach-Object { $_.Trim().Trim('"') } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    )
}

function Get-ManifestedResolvedApplicationPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandName
    )

    $command = @(Get-Command -Name $CommandName -CommandType Application -All -ErrorAction SilentlyContinue | Select-Object -First 1)
    if (-not $command) {
        return $null
    }

    $commandPath = $null
    if ($command[0].PSObject.Properties['Path'] -and $command[0].Path) {
        $commandPath = $command[0].Path
    }
    elseif ($command[0].PSObject.Properties['Source'] -and $command[0].Source) {
        $commandPath = $command[0].Source
    }

    if ([string]::IsNullOrWhiteSpace($commandPath)) {
        return $null
    }

    return (Get-ManifestedFullPath -Path $commandPath)
}

function Set-ManifestedPathEntries {
    [CmdletBinding()]
    param(
        [string]$CurrentPath,

        [string[]]$DesiredLeadingEntries = @()
    )

    $entries = New-Object System.Collections.Generic.List[string]
    $seenEntries = @{}

    foreach ($entry in @($DesiredLeadingEntries)) {
        $key = Get-ManifestedPathEntryKey -Path $entry
        if ([string]::IsNullOrWhiteSpace($key) -or $seenEntries.ContainsKey($key)) {
            continue
        }

        $entries.Add((Get-ManifestedFullPath -Path $entry)) | Out-Null
        $seenEntries[$key] = $true
    }

    foreach ($entry in @(Get-ManifestedPathEntries -PathValue $CurrentPath)) {
        $key = Get-ManifestedPathEntryKey -Path $entry
        if ([string]::IsNullOrWhiteSpace($key) -or $seenEntries.ContainsKey($key)) {
            continue
        }

        $entries.Add($entry) | Out-Null
        $seenEntries[$key] = $true
    }

    [pscustomobject]@{
        Value   = ($entries -join ';')
        Entries = @($entries)
    }
}

function Publish-ManifestedEnvironmentChange {
    [CmdletBinding()]
    param()

    if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
        return
    }

    if (-not ('EigenverftManifestedNativeMethods' -as [type])) {
        Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
 
public static class EigenverftManifestedNativeMethods
{
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessageTimeout(
        IntPtr hWnd,
        uint Msg,
        UIntPtr wParam,
        string lParam,
        uint fuFlags,
        uint uTimeout,
        out UIntPtr lpdwResult);
}
"@
 -ErrorAction SilentlyContinue | Out-Null
    }

    $broadcastHandle = [IntPtr]0xffff
    $messageId = 0x001A
    $abortIfHung = 0x0002
    $sendResult = [UIntPtr]::Zero

    try {
        [void][EigenverftManifestedNativeMethods]::SendMessageTimeout(
            $broadcastHandle,
            $messageId,
            [UIntPtr]::Zero,
            'Environment',
            $abortIfHung,
            5000,
            [ref]$sendResult
        )
    }
    catch {
        Write-Verbose ('Could not broadcast environment change notification. ' + $_.Exception.Message)
    }
}

function Get-ManifestedCommandEnvironmentSpec {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandName,

        [pscustomobject]$RuntimeState
    )

    $runtimeHome = if ($RuntimeState -and $RuntimeState.PSObject.Properties['RuntimeHome']) { $RuntimeState.RuntimeHome } else { $null }
    $executablePath = if ($RuntimeState -and $RuntimeState.PSObject.Properties['ExecutablePath']) { $RuntimeState.ExecutablePath } else { $null }
    $cliCommandPath = if ($RuntimeState -and $RuntimeState.PSObject.Properties['CliCommandPath']) { $RuntimeState.CliCommandPath } else { $null }
    $runtimeSource = if ($RuntimeState -and $RuntimeState.PSObject.Properties['RuntimeSource']) { $RuntimeState.RuntimeSource } else { $null }

    $desiredCommandDirectory = $null
    $expectedCommandPaths = [ordered]@{}

    switch ($CommandName) {
        'Initialize-NodeRuntime' {
            if (-not [string]::IsNullOrWhiteSpace($runtimeHome)) {
                $desiredCommandDirectory = $runtimeHome
            }
            elseif (-not [string]::IsNullOrWhiteSpace($executablePath)) {
                $desiredCommandDirectory = Split-Path -Parent $executablePath
            }

            if (-not [string]::IsNullOrWhiteSpace($executablePath)) {
                $expectedCommandPaths['node.exe'] = (Get-ManifestedFullPath -Path $executablePath)
            }
            if (-not [string]::IsNullOrWhiteSpace($runtimeHome)) {
                $expectedCommandPaths['npm.cmd'] = (Get-ManifestedFullPath -Path (Join-Path $runtimeHome 'npm.cmd'))
            }
        }
        'Initialize-Ps7Runtime' {
            if (-not [string]::IsNullOrWhiteSpace($executablePath)) {
                $desiredCommandDirectory = Split-Path -Parent $executablePath
                $expectedCommandPaths['pwsh.exe'] = (Get-ManifestedFullPath -Path $executablePath)
            }
        }
        'Initialize-GitRuntime' {
            if (-not [string]::IsNullOrWhiteSpace($executablePath)) {
                $desiredCommandDirectory = Split-Path -Parent $executablePath
                $expectedCommandPaths['git.exe'] = (Get-ManifestedFullPath -Path $executablePath)
            }
        }
        'Initialize-VSCodeRuntime' {
            if (-not [string]::IsNullOrWhiteSpace($cliCommandPath)) {
                $desiredCommandDirectory = Split-Path -Parent $cliCommandPath
                $expectedCommandPaths['code'] = (Get-ManifestedFullPath -Path $cliCommandPath)
                $expectedCommandPaths['code.cmd'] = (Get-ManifestedFullPath -Path $cliCommandPath)
            }
            elseif (-not [string]::IsNullOrWhiteSpace($runtimeHome)) {
                $desiredCommandDirectory = Join-Path $runtimeHome 'bin'
                $expectedCommandPaths['code'] = (Get-ManifestedFullPath -Path (Join-Path $desiredCommandDirectory 'code.cmd'))
                $expectedCommandPaths['code.cmd'] = (Get-ManifestedFullPath -Path (Join-Path $desiredCommandDirectory 'code.cmd'))
            }
        }
    }

    $applicable = (-not [string]::IsNullOrWhiteSpace($desiredCommandDirectory)) -and ($expectedCommandPaths.Count -gt 0)

    [pscustomobject]@{
        Applicable              = $applicable
        CommandName             = $CommandName
        CommandNames            = @($expectedCommandPaths.Keys)
        DesiredExecutablePath   = if (-not [string]::IsNullOrWhiteSpace($executablePath)) { Get-ManifestedFullPath -Path $executablePath } else { $null }
        DesiredCommandDirectory = if (-not [string]::IsNullOrWhiteSpace($desiredCommandDirectory)) { Get-ManifestedFullPath -Path $desiredCommandDirectory } else { $null }
        ExpectedCommandPaths    = $expectedCommandPaths
        RuntimeSource           = $runtimeSource
    }
}

function Get-ManifestedCommandLineEnvironmentState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Specification
    )

    if (-not $Specification.Applicable) {
        return [pscustomobject]@{
            Applicable              = $false
            Status                  = 'NotApplicable'
            ScopeApplied            = @()
            CommandNames            = if ($Specification.PSObject.Properties['CommandNames']) { @($Specification.CommandNames) } else { @() }
            DesiredExecutablePath   = if ($Specification.PSObject.Properties['DesiredExecutablePath']) { $Specification.DesiredExecutablePath } else { $null }
            DesiredCommandDirectory = if ($Specification.PSObject.Properties['DesiredCommandDirectory']) { $Specification.DesiredCommandDirectory } else { $null }
            ResolvedCommandPaths    = [ordered]@{}
            ProcessPathUpdated      = $false
            UserPathUpdated         = $false
            ProcessPathContainsDesired = $false
            UserPathContainsDesired = $false
            ProcessPathPrefersDesired = $false
            UserPathPrefersDesired  = $false
        }
    }

    $processPath = [System.Environment]::GetEnvironmentVariable('Path', 'Process')
    $userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
    $desiredDirectory = $Specification.DesiredCommandDirectory
    $processEntries = @(Get-ManifestedPathEntries -PathValue $processPath)
    $userEntries = @(Get-ManifestedPathEntries -PathValue $userPath)

    $processContainsDesired = $false
    foreach ($entry in $processEntries) {
        if (Test-ManifestedPathEntryMatch -LeftPath $entry -RightPath $desiredDirectory) {
            $processContainsDesired = $true
            break
        }
    }
    $processPrefersDesired = ($processEntries.Count -gt 0) -and (Test-ManifestedPathEntryMatch -LeftPath $processEntries[0] -RightPath $desiredDirectory)

    $userContainsDesired = $false
    foreach ($entry in $userEntries) {
        if (Test-ManifestedPathEntryMatch -LeftPath $entry -RightPath $desiredDirectory) {
            $userContainsDesired = $true
            break
        }
    }
    $userPrefersDesired = ($userEntries.Count -gt 0) -and (Test-ManifestedPathEntryMatch -LeftPath $userEntries[0] -RightPath $desiredDirectory)

    $resolvedCommandPaths = [ordered]@{}
    $allCommandsResolvedAsExpected = $true
    foreach ($commandName in @($Specification.CommandNames)) {
        $expectedPath = $Specification.ExpectedCommandPaths[$commandName]
        $resolvedPath = Get-ManifestedResolvedApplicationPath -CommandName $commandName
        $resolvedCommandPaths[$commandName] = $resolvedPath

        if (-not (Test-ManifestedPathEntryMatch -LeftPath $resolvedPath -RightPath $expectedPath)) {
            $allCommandsResolvedAsExpected = $false
        }
    }

    $status = if ($allCommandsResolvedAsExpected -and $processPrefersDesired -and $userPrefersDesired) {
        'Aligned'
    }
    else {
        'NeedsSync'
    }

    [pscustomobject]@{
        Applicable                 = $true
        Status                     = $status
        ScopeApplied               = @()
        CommandNames               = @($Specification.CommandNames)
        DesiredExecutablePath      = $Specification.DesiredExecutablePath
        DesiredCommandDirectory    = $desiredDirectory
        ResolvedCommandPaths       = $resolvedCommandPaths
        ProcessPathUpdated         = $false
        UserPathUpdated            = $false
        ProcessPathContainsDesired = $processContainsDesired
        UserPathContainsDesired    = $userContainsDesired
        ProcessPathPrefersDesired  = $processPrefersDesired
        UserPathPrefersDesired     = $userPrefersDesired
    }
}

function Get-ManifestedCommandEnvironmentResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommandName,

        [pscustomobject]$RuntimeState
    )

    $specification = Get-ManifestedCommandEnvironmentSpec -CommandName $CommandName -RuntimeState $RuntimeState
    return (Get-ManifestedCommandLineEnvironmentState -Specification $specification)
}

function Sync-ManifestedCommandLineEnvironment {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Specification
    )

    $initialState = Get-ManifestedCommandLineEnvironmentState -Specification $Specification
    if (-not $initialState.Applicable) {
        return $initialState
    }

    $processPath = [System.Environment]::GetEnvironmentVariable('Path', 'Process')
    $processUpdate = Set-ManifestedPathEntries -CurrentPath $processPath -DesiredLeadingEntries @($Specification.DesiredCommandDirectory)
    $processPathUpdated = ($processUpdate.Value -ne $processPath)
    if ($processPathUpdated) {
        [System.Environment]::SetEnvironmentVariable('Path', $processUpdate.Value, 'Process')
        $env:PATH = $processUpdate.Value
    }

    $userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
    $safeUserPath = if ($null -eq $userPath) { '' } else { $userPath }
    $userUpdate = Set-ManifestedPathEntries -CurrentPath $safeUserPath -DesiredLeadingEntries @($Specification.DesiredCommandDirectory)
    $userPathUpdated = ($userUpdate.Value -ne $safeUserPath)
    if ($userPathUpdated) {
        [System.Environment]::SetEnvironmentVariable('Path', $userUpdate.Value, 'User')
        Publish-ManifestedEnvironmentChange
    }

    $finalState = Get-ManifestedCommandLineEnvironmentState -Specification $Specification
    $scopeApplied = New-Object System.Collections.Generic.List[string]
    if ($processPathUpdated) {
        $scopeApplied.Add('Process') | Out-Null
    }
    if ($userPathUpdated) {
        $scopeApplied.Add('User') | Out-Null
    }

    $status = if ($finalState.Status -eq 'Aligned') {
        if ($scopeApplied.Count -gt 0) { 'Updated' } else { 'Aligned' }
    }
    else {
        'ValidationFailed'
    }

    $result = [pscustomobject]@{
        Applicable                 = $true
        Status                     = $status
        ScopeApplied               = @($scopeApplied)
        CommandNames               = @($finalState.CommandNames)
        DesiredExecutablePath      = $finalState.DesiredExecutablePath
        DesiredCommandDirectory    = $finalState.DesiredCommandDirectory
        ResolvedCommandPaths       = $finalState.ResolvedCommandPaths
        ProcessPathUpdated         = $processPathUpdated
        UserPathUpdated            = $userPathUpdated
        ProcessPathContainsDesired = $finalState.ProcessPathContainsDesired
        UserPathContainsDesired    = $finalState.UserPathContainsDesired
        ProcessPathPrefersDesired  = $finalState.ProcessPathPrefersDesired
        UserPathPrefersDesired     = $finalState.UserPathPrefersDesired
    }

    if ($status -eq 'ValidationFailed') {
        $resolvedPaths = @()
        foreach ($commandName in @($result.CommandNames)) {
            $resolvedPaths += ('{0}={1}' -f $commandName, $result.ResolvedCommandPaths[$commandName])
        }

        throw ('Command-line environment synchronization failed for {0}. Expected directory {1}. Resolved commands: {2}' -f $Specification.CommandName, $result.DesiredCommandDirectory, ($resolvedPaths -join ', '))
    }

    return $result
}