Private/Logic/RuntimeKernel/Facts/Manifested.RuntimeFacts.Collectors.ps1

function ConvertTo-ManifestedFlexibleVersionObject {
    [CmdletBinding()]
    param(
        [string]$VersionText
    )

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

    $match = [regex]::Match($VersionText, '(\d+(?:\.\d+){1,3})')
    if (-not $match.Success) {
        return $null
    }

    return [version]$match.Groups[1].Value
}

function Get-ManifestedMachineInstallerCachePathFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $cacheRoot = Get-ManifestedArtifactCacheRootFromDefinition -Definition $Definition -Layout $layout
    if ([string]::IsNullOrWhiteSpace($cacheRoot)) {
        return $null
    }

    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'directDownload'
    if (-not $supplyBlock -or -not $supplyBlock.PSObject.Properties.Match('fileName').Count -or [string]::IsNullOrWhiteSpace($supplyBlock.fileName)) {
        return $null
    }

    return (Join-Path $cacheRoot ([string]$supplyBlock.fileName))
}

function Get-ManifestedMachineInstallerInfoFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [pscustomobject]$Artifact,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    $factsBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'facts' -BlockName 'machinePrerequisite'
    $supplyBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'supply' -BlockName 'directDownload'
    $installerPath = if ($Artifact -and $Artifact.PSObject.Properties['Path'] -and -not [string]::IsNullOrWhiteSpace($Artifact.Path)) {
        [string]$Artifact.Path
    }
    else {
        Get-ManifestedMachineInstallerCachePathFromDefinition -Definition $Definition -LocalRoot $LocalRoot
    }

    $version = if ($Artifact -and $Artifact.PSObject.Properties['Version'] -and -not [string]::IsNullOrWhiteSpace($Artifact.Version)) { [string]$Artifact.Version } else { $null }
    $versionObject = if ($Artifact -and $Artifact.PSObject.Properties['VersionObject'] -and $Artifact.VersionObject) { $Artifact.VersionObject } else { $null }
    $signatureStatus = if ($Artifact -and $Artifact.PSObject.Properties['SignatureStatus'] -and -not [string]::IsNullOrWhiteSpace($Artifact.SignatureStatus)) { [string]$Artifact.SignatureStatus } else { $null }
    $signerSubject = if ($Artifact -and $Artifact.PSObject.Properties['SignerSubject'] -and -not [string]::IsNullOrWhiteSpace($Artifact.SignerSubject)) { [string]$Artifact.SignerSubject } else { $null }

    if (-not [string]::IsNullOrWhiteSpace($installerPath) -and (Test-Path -LiteralPath $installerPath)) {
        try {
            $item = Get-Item -LiteralPath $installerPath -ErrorAction Stop
            if ([string]::IsNullOrWhiteSpace($version)) {
                $version = [string]$item.VersionInfo.FileVersion
            }
            if (-not $versionObject) {
                $versionObject = ConvertTo-ManifestedFlexibleVersionObject -VersionText $version
            }
        }
        catch {
        }

        if ([string]::IsNullOrWhiteSpace($signatureStatus) -or [string]::IsNullOrWhiteSpace($signerSubject)) {
            try {
                $signature = Get-AuthenticodeSignature -FilePath $installerPath
                $signatureStatus = $signature.Status.ToString()
                $signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null }
            }
            catch {
                $signatureStatus = $null
                $signerSubject = $null
            }
        }
    }

    return [pscustomobject]@{
        Architecture    = if ($factsBlock -and $factsBlock.PSObject.Properties.Match('architecture').Count -gt 0) { [string]$factsBlock.architecture } else { $null }
        FileName        = if ($Artifact -and $Artifact.PSObject.Properties['FileName'] -and -not [string]::IsNullOrWhiteSpace($Artifact.FileName)) { [string]$Artifact.FileName } elseif ($supplyBlock -and $supplyBlock.PSObject.Properties.Match('fileName').Count -gt 0) { [string]$supplyBlock.fileName } elseif (-not [string]::IsNullOrWhiteSpace($installerPath)) { Split-Path -Leaf $installerPath } else { $null }
        Path            = $installerPath
        Version         = $version
        VersionObject   = $versionObject
        Source          = if ($Artifact -and $Artifact.PSObject.Properties['Source'] -and -not [string]::IsNullOrWhiteSpace($Artifact.Source)) { [string]$Artifact.Source } elseif (-not [string]::IsNullOrWhiteSpace($installerPath) -and (Test-Path -LiteralPath $installerPath)) { 'cache' } else { $null }
        Action          = if ($Artifact -and $Artifact.PSObject.Properties['Action'] -and -not [string]::IsNullOrWhiteSpace($Artifact.Action)) { [string]$Artifact.Action } elseif (-not [string]::IsNullOrWhiteSpace($installerPath) -and (Test-Path -LiteralPath $installerPath)) { 'SelectedCache' } else { $null }
        DownloadUrl     = if ($Artifact -and $Artifact.PSObject.Properties['DownloadUrl'] -and -not [string]::IsNullOrWhiteSpace($Artifact.DownloadUrl)) { [string]$Artifact.DownloadUrl } elseif ($supplyBlock -and $supplyBlock.PSObject.Properties.Match('downloadUrl').Count -gt 0) { [string]$supplyBlock.downloadUrl } else { $null }
        SignatureStatus = $signatureStatus
        SignerSubject   = $signerSubject
    }
}

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

    $factsBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'facts' -BlockName 'machinePrerequisite'
    $architecture = if ($factsBlock -and $factsBlock.PSObject.Properties.Match('architecture').Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($factsBlock.architecture)) {
        ([string]$factsBlock.architecture).ToLowerInvariant()
    }
    else {
        'x64'
    }

    $subKeyPaths = @(
        ('SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\' + $architecture),
        ('SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\' + $architecture)
    )
    $views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) | Select-Object -Unique

    foreach ($view in $views) {
        $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view)
        try {
            foreach ($subKeyPath in $subKeyPaths) {
                $subKey = $baseKey.OpenSubKey($subKeyPath)
                if (-not $subKey) {
                    continue
                }

                try {
                    $installed = [int]$subKey.GetValue('Installed', 0)
                    $versionText = [string]$subKey.GetValue('Version', '')
                    $versionObject = ConvertTo-ManifestedFlexibleVersionObject -VersionText $versionText

                    if (-not $versionObject) {
                        $major = $subKey.GetValue('Major', $null)
                        $minor = $subKey.GetValue('Minor', $null)
                        $build = $subKey.GetValue('Bld', $null)
                        $revision = $subKey.GetValue('Rbld', $null)

                        if ($null -ne $major -and $null -ne $minor -and $null -ne $build -and $null -ne $revision) {
                            $versionObject = [version]::new([int]$major, [int]$minor, [int]$build, [int]$revision)
                            $versionText = $versionObject.ToString()
                        }
                    }

                    if ($installed -eq 1) {
                        return [pscustomobject]@{
                            Installed     = $true
                            Architecture  = $architecture
                            Version       = $versionText
                            VersionObject = $versionObject
                            KeyPath       = $subKeyPath
                            RegistryView  = $view.ToString()
                        }
                    }
                }
                finally {
                    $subKey.Dispose()
                }
            }
        }
        finally {
            $baseKey.Dispose()
        }
    }

    return [pscustomobject]@{
        Installed     = $false
        Architecture  = $architecture
        Version       = $null
        VersionObject = $null
        KeyPath       = $null
        RegistryView  = $null
    }
}

function Get-ManifestedMachinePrerequisiteFactsFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
        return (New-ManifestedRuntimeFacts -RuntimeName $Definition.runtimeName -CommandName $Definition.commandName -RuntimeKind 'MachinePrerequisite' -LocalRoot $LocalRoot -Layout $null -PlatformSupported:$false -BlockedReason 'Only Windows hosts are supported by this VC runtime bootstrap.')
    }

    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $artifact = Get-ManifestedCachedInstallerArtifactFromDefinition -Definition $Definition -LocalRoot $layout.LocalRoot
    $installerInfo = Get-ManifestedMachineInstallerInfoFromDefinition -Definition $Definition -Artifact $artifact -LocalRoot $layout.LocalRoot
    $partialPaths = @()
    if (-not [string]::IsNullOrWhiteSpace($installerInfo.Path)) {
        $downloadPath = Get-ManifestedDownloadPath -TargetPath $installerInfo.Path
        if (Test-Path -LiteralPath $downloadPath) {
            $partialPaths += $downloadPath
        }
    }

    $installedRuntime = Get-ManifestedInstalledMachinePrerequisiteRuntime -Definition $Definition
    $runtimeValidation = [pscustomobject]@{
        Exists          = [bool]$installedRuntime.Installed
        IsInstalled     = [bool]$installedRuntime.Installed
        RestartRequired = $false
        FailureReason   = if ($installedRuntime.Installed) { $null } else { 'NotInstalled' }
        Architecture    = $installedRuntime.Architecture
        Version         = $installedRuntime.Version
        VersionObject   = $installedRuntime.VersionObject
        KeyPath         = $installedRuntime.KeyPath
        RegistryView    = $installedRuntime.RegistryView
    }
    $managedRuntime = if ($installedRuntime.Installed) {
        [pscustomobject]@{
            Version       = $installedRuntime.Version
            VersionObject = $installedRuntime.VersionObject
            Validation    = $runtimeValidation
        }
    }
    else {
        $null
    }
    $artifactForFacts = if (-not [string]::IsNullOrWhiteSpace($installerInfo.Path) -and (Test-Path -LiteralPath $installerInfo.Path)) {
        $installerInfo
    }
    else {
        $null
    }

    return (New-ManifestedRuntimeFacts -RuntimeName $Definition.runtimeName -CommandName $Definition.commandName -RuntimeKind 'MachinePrerequisite' -LocalRoot $layout.LocalRoot -Layout $layout -ManagedRuntime $managedRuntime -Artifact $artifactForFacts -PartialPaths $partialPaths -Version $(if ($installedRuntime.Version) { $installedRuntime.Version } elseif ($installerInfo.Version) { $installerInfo.Version } else { $null }) -RuntimeHome $null -RuntimeSource $(if ($installedRuntime.Installed) { 'Managed' } else { $null }) -ExecutablePath $null -RuntimeValidation $runtimeValidation -AdditionalProperties @{
            InstalledRuntime = $installedRuntime
            Runtime          = $runtimeValidation
            Installer        = $installerInfo
            InstallerPath    = $installerInfo.Path
        })
}

function Test-ManifestedNpmCliRuntimeHome {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [Parameter(Mandatory = $true)]
        [string]$RuntimeHome
    )

    $factsBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'facts' -BlockName 'npmCli'
    $commandFileName = if ($factsBlock.PSObject.Properties.Match('commandFileName').Count -gt 0) { $factsBlock.commandFileName } else { $null }
    $packageJsonRelativePath = if ($factsBlock.PSObject.Properties.Match('packageJsonRelativePath').Count -gt 0) { $factsBlock.packageJsonRelativePath } else { $null }

    $commandPath = if (-not [string]::IsNullOrWhiteSpace($commandFileName)) { Join-Path $RuntimeHome $commandFileName } else { $null }
    $packageJsonPath = if (-not [string]::IsNullOrWhiteSpace($packageJsonRelativePath)) { Join-Path $RuntimeHome $packageJsonRelativePath } else { $null }
    $packageVersion = $null
    $reportedVersion = $null
    $exists = (Test-Path -LiteralPath $RuntimeHome)
    $hasRequiredFiles = $false
    $versionsAligned = $false
    $failureReason = $null

    if (-not $exists) {
        $failureReason = 'RuntimeHomeMissing'
    }
    elseif ([string]::IsNullOrWhiteSpace($commandPath) -or [string]::IsNullOrWhiteSpace($packageJsonPath) -or -not (Test-Path -LiteralPath $commandPath) -or -not (Test-Path -LiteralPath $packageJsonPath)) {
        $failureReason = 'RequiredFilesMissing'
    }
    else {
        $hasRequiredFiles = $true

        try {
            $packageDocument = Get-Content -LiteralPath $packageJsonPath -Raw -ErrorAction Stop | ConvertFrom-Json
            $packageVersion = ConvertTo-ManifestedSemanticVersionText -VersionText ([string]$packageDocument.version)
        }
        catch {
            $packageVersion = $null
        }

        try {
            $reportedVersion = (& $commandPath --version 2>$null | Select-Object -First 1)
            if ($reportedVersion) {
                $reportedVersion = ConvertTo-ManifestedSemanticVersionText -VersionText $reportedVersion.ToString().Trim()
            }
        }
        catch {
            $reportedVersion = $null
        }

        if (-not [string]::IsNullOrWhiteSpace($packageVersion) -and -not [string]::IsNullOrWhiteSpace($reportedVersion) -and $packageVersion -eq $reportedVersion) {
            $versionsAligned = $true
        }
        else {
            $failureReason = 'VersionMismatch'
        }
    }

    return [pscustomobject]@{
        Exists           = $exists
        HasRequiredFiles = $hasRequiredFiles
        VersionsAligned  = $versionsAligned
        IsUsable         = ($exists -and $hasRequiredFiles -and $versionsAligned)
        FailureReason    = $failureReason
        RuntimeHome      = $RuntimeHome
        CommandPath      = $commandPath
        PackageJsonPath  = $packageJsonPath
        PackageVersion   = $packageVersion
        ReportedVersion  = $reportedVersion
    }
}

function Get-ManifestedNpmCliFactsFromDefinition {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Definition,

        [string]$LocalRoot = (Get-ManifestedLocalRoot)
    )

    if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
        return (New-ManifestedRuntimeFacts -RuntimeName $Definition.runtimeName -CommandName $Definition.commandName -RuntimeKind 'NpmCli' -LocalRoot $LocalRoot -Layout $null -PlatformSupported:$false -BlockedReason ('Only Windows hosts are supported by this ' + $Definition.runtimeName + ' bootstrap.'))
    }

    $factsBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'facts' -BlockName 'npmCli'
    $installBlock = Get-ManifestedDefinitionBlock -Definition $Definition -SectionName 'install' -BlockName 'npmGlobalPackage'
    $layout = Get-ManifestedLayout -LocalRoot $LocalRoot
    $toolsRoot = $layout.($installBlock.toolsRootLayoutProperty)
    $stagePrefix = if ($installBlock.PSObject.Properties.Match('stagePrefix').Count -gt 0) { $installBlock.stagePrefix } else { (($Definition.runtimeName -replace 'Runtime$', '')).ToLowerInvariant() }

    $partialPaths = @()
    $partialPaths += @(Get-ManifestedStageDirectories -Prefix $stagePrefix -Mode TemporaryShort -LegacyRootPaths @($toolsRoot) | Select-Object -ExpandProperty FullName)

    $entries = @()
    if (Test-Path -LiteralPath $toolsRoot) {
        $runtimeRoots = Get-ChildItem -LiteralPath $toolsRoot -Directory -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -notlike ('_stage_' + $stagePrefix + '_*') } |
            Sort-Object -Descending -Property @{ Expression = { ConvertTo-ManifestedSemanticVersionObject -VersionText $_.Name } }, Name

        foreach ($runtimeRoot in $runtimeRoots) {
            $validation = Test-ManifestedNpmCliRuntimeHome -Definition $Definition -RuntimeHome $runtimeRoot.FullName
            $expectedVersion = ConvertTo-ManifestedSemanticVersionText -VersionText $runtimeRoot.Name
            $runtimeVersion = if ($validation.PackageVersion) { $validation.PackageVersion } else { $expectedVersion }
            $versionMatches = (-not $expectedVersion) -or (-not $validation.PackageVersion) -or ($expectedVersion -eq $validation.PackageVersion)
            $isUsable = ($validation.IsUsable -and $versionMatches)

            $entries += [pscustomobject]@{
                Version         = $runtimeVersion
                RuntimeHome     = $runtimeRoot.FullName
                ExecutablePath  = $validation.CommandPath
                PackageJsonPath = $validation.PackageJsonPath
                Validation      = $validation
                VersionMatches  = $versionMatches
                IsUsable        = $isUsable
                Source          = 'Managed'
            }
        }
    }

    $managedRuntime = $entries | Where-Object { $_.IsUsable } | Select-Object -First 1
    $externalRuntime = $null
    if (-not $managedRuntime) {
        $candidatePaths = New-Object System.Collections.Generic.List[string]
        foreach ($commandName in @($Definition.environment.commandProjection.expectedCommands)) {
            $candidatePath = Get-ManifestedApplicationPath -CommandName $commandName -ExcludedRoots @($toolsRoot)
            if (-not [string]::IsNullOrWhiteSpace($candidatePath)) {
                $candidatePaths.Add($candidatePath) | Out-Null
            }

            if ($commandName -match '\.cmd$') {
                $alternateCommandName = [System.IO.Path]::GetFileNameWithoutExtension($commandName)
                if (-not [string]::IsNullOrWhiteSpace($alternateCommandName)) {
                    $alternateCandidatePath = Get-ManifestedApplicationPath -CommandName $alternateCommandName -ExcludedRoots @($toolsRoot)
                    if (-not [string]::IsNullOrWhiteSpace($alternateCandidatePath)) {
                        $candidatePaths.Add($alternateCandidatePath) | Out-Null
                    }
                }
            }
        }

        foreach ($candidatePath in @($candidatePaths | Select-Object -Unique)) {
            $resolvedCandidatePath = Get-ManifestedFullPath -Path $candidatePath
            if ([string]::IsNullOrWhiteSpace($resolvedCandidatePath) -or -not (Test-Path -LiteralPath $resolvedCandidatePath)) {
                continue
            }

            $runtimeHome = Split-Path -Parent $resolvedCandidatePath
            $validation = Test-ManifestedNpmCliRuntimeHome -Definition $Definition -RuntimeHome $runtimeHome
            if (-not $validation.IsUsable) {
                continue
            }

            $externalRuntime = [pscustomobject]@{
                Version         = $validation.PackageVersion
                RuntimeHome     = $runtimeHome
                ExecutablePath  = $validation.CommandPath
                PackageJsonPath = $validation.PackageJsonPath
                Validation      = $validation
                IsUsable        = $true
                Source          = 'External'
                Discovery       = 'Path'
            }
            break
        }
    }

    $currentRuntime = if ($managedRuntime) { $managedRuntime } else { $externalRuntime }
    $invalidRuntimeHomes = @($entries | Where-Object { -not $_.IsUsable } | Select-Object -ExpandProperty RuntimeHome)

    return (New-ManifestedRuntimeFacts -RuntimeName $Definition.runtimeName -CommandName $Definition.commandName -RuntimeKind 'NpmCli' -LocalRoot $layout.LocalRoot -Layout $layout -ManagedRuntime $managedRuntime -ExternalRuntime $externalRuntime -PartialPaths $partialPaths -InvalidPaths $invalidRuntimeHomes -Version $(if ($currentRuntime) { $currentRuntime.Version } else { $null }) -RuntimeHome $(if ($currentRuntime) { $currentRuntime.RuntimeHome } else { $null }) -RuntimeSource $(if ($managedRuntime) { 'Managed' } elseif ($externalRuntime) { 'External' } else { $null }) -ExecutablePath $(if ($currentRuntime) { $currentRuntime.ExecutablePath } else { $null }) -RuntimeValidation $(if ($currentRuntime) { $currentRuntime.Validation } else { $null }) -AdditionalProperties @{
            PackageJsonPath     = if ($currentRuntime) { $currentRuntime.PackageJsonPath } else { $null }
            InvalidRuntimeHomes = $invalidRuntimeHomes
        })
}