安装-VSCode远程服务/安装-VSCode远程服务.psm1

# 远程安装脚本模板路径(模块级变量)
$script:远程安装脚本路径 = Join-Path $PSScriptRoot 'private\远程安装脚本.ps1'

function 安装-VSCode远程服务 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [Alias('目标服务器', '计算机名', 'IP', '主机')]
        [string]$远程主机,

        [string]$远程账户,

        [int]$SSH端口 = 22,

        [ValidateSet('预览版', '稳定版')]
        [string]$本地版本,

        [int]$轮询秒数 = 3,

        [int]$最大恢复次数 = 5
    )

    function 取-本地VSCode信息 {
        param(
            [string]$指定版本
        )

        function 取-单个版本信息 {
            param(
                [string]$命令名,
                [string]$版本名,
                [string]$发布通道
            )

            $命令对象 = Get-Command $命令名 -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($null -eq $命令对象) {
                return $null
            }

            $命令路径 = if ($命令对象.Path) { $命令对象.Path } else { $命令对象.Source }
            if ([string]::IsNullOrWhiteSpace($命令路径)) {
                return $null
            }

            $版本输出 = & $命令路径 --version 2>$null
            if ($LASTEXITCODE -ne 0 -or $版本输出.Count -lt 2) {
                return $null
            }

            $提交号 = [string]($版本输出 | Select-Object -Skip 1 -First 1)
            $提交号 = $提交号.Trim()
            if ($提交号 -notmatch '^[0-9a-f]{40}$') {
                return $null
            }

            return [pscustomobject]@{
                版本名 = $版本名
                命令路径 = $命令路径
                命令名称 = $命令对象.Name
                发布通道 = $发布通道
                提交号 = $提交号
            }
        }

        $预览版信息 = 取-单个版本信息 -命令名 'code-insiders' -版本名 '预览版' -发布通道 'insider'
        $稳定版信息 = 取-单个版本信息 -命令名 'code' -版本名 '稳定版' -发布通道 'stable'

        if (-not [string]::IsNullOrWhiteSpace($指定版本)) {
            if ($指定版本 -eq '预览版') {
                if ($null -eq $预览版信息) {
                    throw '用户指定了预览版,但本机未安装可用的 VS Code Insiders。'
                }

                return $预览版信息
            }

            if ($null -eq $稳定版信息) {
                throw '用户指定了稳定版,但本机未安装可用的 VS Code Stable。'
            }

            return $稳定版信息
        }

        if ($null -ne $预览版信息 -and $null -ne $稳定版信息) {
            throw '本机同时安装了预览版和稳定版。请通过 -本地版本 显式指定要使用的版本。'
        }

        if ($null -ne $预览版信息) {
            return $预览版信息
        }

        if ($null -ne $稳定版信息) {
            return $稳定版信息
        }

        throw '本机未检测到可用的 VS Code 预览版或稳定版。'
    }

    function 取-SSH连接目标 {
        param(
            [string]$主机,
            [string]$账户
        )

        if ([string]::IsNullOrWhiteSpace($账户)) {
            return $主机
        }

        return ('{0}@{1}' -f $账户, $主机)
    }

    function 执行-SSH命令 {
        param(
            [string]$连接目标,
            [int]$端口,
            [string]$命令文本
        )

        $ssh命令 = Get-Command ssh -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($null -eq $ssh命令) {
            throw '未找到 ssh 命令,无法连接远程服务器。'
        }

        & $ssh命令.Source '-p' $端口 $连接目标 $命令文本
        if ($LASTEXITCODE -ne 0) {
            throw ('SSH 执行失败,退出码: {0}' -f $LASTEXITCODE)
        }
    }

    function 上传-文件到远程 {
        param(
            [string]$本地路径,
            [string]$连接目标,
            [int]$端口,
            [string]$远程路径
        )

        $SCP命令 = Get-Command scp -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($null -eq $SCP命令) {
            throw '未找到 scp 命令,无法上传远程安装脚本。'
        }

        & $SCP命令.Source '-P' $端口 $本地路径 ('{0}:{1}' -f $连接目标, $远程路径)
        if ($LASTEXITCODE -ne 0) {
            throw ('SCP 上传失败,退出码: {0}' -f $LASTEXITCODE)
        }
    }

    function 执行-远程安装脚本 {
        param(
            [string]$连接目标,
            [int]$端口,
            [string]$脚本文本
        )

        $本地临时脚本路径 = Join-Path $env:TEMP ('临时安装-VSCode远程服务-{0}.ps1' -f ([guid]::NewGuid().ToString('N')))
        $远程临时脚本文件名 = 'vscode-server-install-temp.ps1'
        $远程临时脚本路径 = ('./{0}' -f $远程临时脚本文件名)

        try {
            [System.IO.File]::WriteAllText($本地临时脚本路径, $脚本文本, [System.Text.UTF8Encoding]::new($false))
            上传-文件到远程 -本地路径 $本地临时脚本路径 -连接目标 $连接目标 -端口 $端口 -远程路径 $远程临时脚本路径
            执行-SSH命令 -连接目标 $连接目标 -端口 $端口 -命令文本 ('powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\{0}"' -f $远程临时脚本文件名)
        } finally {
            if (Test-Path $本地临时脚本路径) {
                Remove-Item $本地临时脚本路径 -Force -ErrorAction SilentlyContinue
            }

            try {
                执行-SSH命令 -连接目标 $连接目标 -端口 $端口 -命令文本 ('del /q "%USERPROFILE%\{0}"' -f $远程临时脚本文件名)
            } catch {
            }
        }
    }

    $ErrorActionPreference = 'Stop'
    $本地信息 = 取-本地VSCode信息 -指定版本 $本地版本
    $连接目标 = 取-SSH连接目标 -主机 $远程主机 -账户 $远程账户

    # 从私有脚本文件中读取远程安装脚本模板并替换占位符
    $远程安装脚本 = Get-Content -Path $script:远程安装脚本路径 -Raw -Encoding UTF8
    $远程安装脚本 = $远程安装脚本.Replace('__提交号__', $本地信息.提交号)
    $远程安装脚本 = $远程安装脚本.Replace('__发布通道__', $本地信息.发布通道)
    $远程安装脚本 = $远程安装脚本.Replace('__轮询秒数__', [string]$轮询秒数)
    $远程安装脚本 = $远程安装脚本.Replace('__最大恢复次数__', [string]$最大恢复次数)

    Write-Host ('本机自动检测到的 VS Code 命令: {0}' -f $本地信息.命令路径)
    Write-Host ('本机自动检测到的发布通道: {0}' -f $本地信息.发布通道)
    Write-Host ('本机自动检测到的提交号: {0}' -f $本地信息.提交号)
    Write-Host ('远程连接目标: {0}' -f $连接目标)

    执行-远程安装脚本 -连接目标 $连接目标 -端口 $SSH端口 -脚本文本 $远程安装脚本

<#
.SYNOPSIS
通过 SSH 在远程 Windows 主机上安装与本机 VS Code 对应版本的 VS Code Server。
 
.DESCRIPTION
此模块会根据用户指定的本机版本类型选择 VS Code 预览版或稳定版,自动检测本机提交号,
再通过 SSH 和 SCP 将远程安装脚本上传到目标 Windows 主机,使用 BITS 断点续传下载对应
版本的 VS Code Server 压缩包,并完成解压、目录整理与清理。
 
.PARAMETER 远程主机
要安装 VS Code 远程服务的目标服务器,可以是 SSH 配置名、计算机名或 IP 地址。
 
.PARAMETER 远程账户
可选的远程登录账户名。不提供时直接使用当前 SSH 配置或默认账户。
 
.PARAMETER SSH端口
远程 SSH 端口,默认值为 22。
 
.PARAMETER 本地版本
指定本机使用预览版还是稳定版。取值只能是"预览版"或"稳定版"。
 
.PARAMETER 轮询秒数
轮询远程 BITS 下载任务状态的时间间隔,默认值为 3 秒。
 
.PARAMETER 最大恢复次数
远程 BITS 下载任务在中断后允许自动恢复的最大次数,默认值为 5。
 
.EXAMPLE
安装-VSCode远程服务 上科大A服务器 -本地版本 预览版
 
.EXAMPLE
安装-VSCode远程服务 192.168.1.10 -远程账户 admin -SSH端口 22 -本地版本 稳定版
#>

}

Export-ModuleMember -Function '安装-VSCode远程服务'