phper/electron/services/GoManager.ts.backup
Ethanfly 9614a3d234 feat: add Go version management support
- Add GoManager service for downloading and managing Go versions
- Implement Go version detection and installation
- Add GoManager Vue component with version selection UI
- Update main process to handle Go-related IPC calls
- Add Jest testing configuration and GoManager unit tests
- Update service store to include Go management
- Add routing for Go manager page
- Include Kiro specs and steering documentation
2026-01-13 18:30:26 +08:00

1617 lines
52 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ConfigStore } from './ConfigStore'
import { exec } from 'child_process'
import { promisify } from 'util'
import { existsSync, mkdirSync, readdirSync, rmSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs'
import { join } from 'path'
import https from 'https'
import http from 'http'
import { createWriteStream, createReadStream } from 'fs'
import { sendDownloadProgress } from '../main'
import { createHash } from 'crypto'
// Import unzipper with any type to avoid type issues
const unzipper = require('unzipper')
const execAsync = promisify(exec)
interface GoVersion {
version: string // 版本号,如 "1.21.5"
path: string // 安装路径
isActive: boolean // 是否为当前活动版本
goroot: string // GOROOT 路径
gopath?: string // GOPATH 路径(可选)
installDate?: Date // 安装日期
size?: number // 安装大小(字节)
}
interface AvailableGoVersion {
version: string // 版本号
stable: boolean // 是否为稳定版本
downloadUrl: string // Windows 64位 ZIP 下载链接
size: number // 文件大小(字节)
sha256: string // SHA256 校验和
releaseDate?: string // 发布日期
}
interface GoInfo {
version: string // Go 版本
goroot: string // GOROOT 路径
gopath: string // GOPATH 路径
goversion: string // go version 命令输出
goos: string // 目标操作系统
goarch: string // 目标架构
}
interface GoRelease {
version: string
stable: boolean
files: GoFile[]
}
interface GoFile {
filename: string
os: string
arch: string
version: string
sha256: string
size: number
kind: 'archive' | 'installer' | 'source'
}
interface DownloadOptions {
url: string
dest: string
name: string
expectedSha256?: string
maxRetries?: number
timeout?: number
}
interface DownloadProgress {
percent: number
downloadedBytes: number
totalBytes: number
speed?: number
}
interface DownloadResult {
success: boolean
message: string
filePath?: string
}
export class GoManager {
private configStore: ConfigStore
private versionsCache: AvailableGoVersion[] = []
private cacheTime: number = 0
private readonly CACHE_DURATION = 5 * 60 * 1000 // 5 分钟缓存
constructor(configStore: ConfigStore) {
this.configStore = configStore
}
/**
* 获取 Go 基础安装路径
*/
getGoBasePath(): string {
return join(this.configStore.getBasePath(), 'go')
}
/**
* 获取指定版本的 Go 路径
*/
getGoPath(version: string): string {
return join(this.getGoBasePath(), `go-${version}`)
}
/**
* 获取默认 GOPATH 工作空间路径
*/
getDefaultGoPath(): string {
return join(this.getGoBasePath(), 'workspace')
}
/**
* 获取已安装的 Go 版本
*/
async getInstalledVersions(): Promise<GoVersion[]> {
const versions: GoVersion[] = []
const goBasePath = this.getGoBasePath()
if (!existsSync(goBasePath)) {
return versions
}
const dirs = readdirSync(goBasePath, { withFileTypes: true })
const activeVersion = this.configStore.get('activeGoVersion' as any) || ''
for (const dir of dirs) {
if (dir.isDirectory() && dir.name.startsWith('go-')) {
const versionDir = join(goBasePath, dir.name)
const goExe = join(versionDir, 'bin', 'go.exe')
const gofmtExe = join(versionDir, 'bin', 'gofmt.exe')
// 验证安装完整性
if (existsSync(goExe) && existsSync(gofmtExe)) {
const version = dir.name.replace('go-', '')
const goroot = versionDir
const gopath = this.getDefaultGoPath()
// 获取安装日期和大小信息
let installDate: Date | undefined
let size: number | undefined
try {
const stats = require('fs').statSync(versionDir)
installDate = stats.birthtime || stats.mtime
// 计算目录大小(简化版本,只统计主要文件)
const binDir = join(versionDir, 'bin')
if (existsSync(binDir)) {
const binFiles = readdirSync(binDir)
size = binFiles.reduce((total, file) => {
try {
const filePath = join(binDir, file)
const fileStats = require('fs').statSync(filePath)
return total + fileStats.size
} catch {
return total
}
}, 0)
}
} catch {
// 忽略统计错误
}
versions.push({
version,
path: versionDir,
isActive: version === activeVersion,
goroot,
gopath,
installDate,
size
})
}
}
}
// 按版本号排序(降序)
versions.sort((a, b) => {
const aParts = a.version.replace('go', '').split('.').map(Number)
const bParts = b.version.replace('go', '').split('.').map(Number)
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) {
return bParts[i] - aParts[i]
}
}
return 0
})
return versions
}
/**
* 获取可用的 Go 版本列表
*/
async getAvailableVersions(): Promise<AvailableGoVersion[]> {
// 检查缓存
if (this.versionsCache.length > 0 && Date.now() - this.cacheTime < this.CACHE_DURATION) {
return this.versionsCache
}
try {
const versions = await this.fetchGoVersions()
if (versions.length > 0) {
this.versionsCache = versions
this.cacheTime = Date.now()
return versions
}
} catch (error) {
console.error('获取 Go 版本列表失败:', error)
}
// 返回硬编码的版本列表作为后备
return this.getFallbackVersions()
}
/**
* 从 golang.org API 获取版本列表
*/
private async fetchGoVersions(): Promise<AvailableGoVersion[]> {
return new Promise((resolve, reject) => {
const url = 'https://golang.org/dl/?mode=json'
https.get(url, {
headers: {
'User-Agent': 'PHPer-Dev-Manager/1.0'
},
timeout: 30000
}, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
const redirectUrl = res.headers.location
if (redirectUrl) {
https.get(redirectUrl, (redirectRes) => {
this.handleVersionResponse(redirectRes, resolve, reject)
}).on('error', reject)
return
}
}
this.handleVersionResponse(res, resolve, reject)
}).on('error', reject)
.on('timeout', () => reject(new Error('请求超时')))
})
}
private handleVersionResponse(res: http.IncomingMessage, resolve: (value: AvailableGoVersion[]) => void, reject: (reason?: any) => void) {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => {
try {
const releases: GoRelease[] = JSON.parse(data)
const availableVersions: AvailableGoVersion[] = []
for (const release of releases) {
if (release.stable) {
const windowsFile = release.files.find(f =>
f.os === 'windows' && f.arch === 'amd64' && f.kind === 'archive'
)
if (windowsFile) {
availableVersions.push({
version: release.version,
stable: release.stable,
downloadUrl: `https://golang.org/dl/${windowsFile.filename}`,
size: windowsFile.size,
sha256: windowsFile.sha256
})
}
}
}
// 只返回前 20 个版本
resolve(availableVersions.slice(0, 20))
} catch (e) {
reject(e)
}
})
res.on('error', reject)
}
/**
* 安装 Go 版本
* 包含完整的安装流程:下载、解压、验证、配置
*/
async install(version: string, downloadUrl: string, expectedSha256?: string): Promise<{ success: boolean; message: string }> {
try {
const goBasePath = this.getGoBasePath()
const tempPath = this.configStore.getTempPath()
const zipPath = join(tempPath, `go-${version}.zip`)
const extractDir = join(goBasePath, `go-${version}`)
// 确保目录存在
if (!existsSync(goBasePath)) {
mkdirSync(goBasePath, { recursive: true })
}
if (!existsSync(tempPath)) {
mkdirSync(tempPath, { recursive: true })
}
// 重复安装检测和防护
if (await this.isVersionInstalled(version)) {
return { success: false, message: `Go ${version} 已安装,无需重复安装` }
}
console.log(`开始安装 Go ${version}...`)
try {
// 下载(使用增强的下载管理器,包含 SHA256 验证)
console.log(`正在下载 Go ${version}...`)
await this.downloadFile(downloadUrl, zipPath, `go-${version}`, expectedSha256)
// ZIP 文件解压到目标目录
console.log(`正在解压 Go ${version}...`)
await this.extractGoArchive(zipPath, goBasePath, version)
// 安装后验证和完整性检查
console.log(`正在验证 Go ${version} 安装...`)
const validationResult = await this.performInstallationValidation(version)
if (!validationResult.isValid) {
// 如果验证失败,清理安装目录
await this.cleanupFailedInstallation(extractDir)
return { success: false, message: `安装验证失败: ${validationResult.error}` }
}
// 更新配置
await this.updateInstallationConfig(version)
// 如果是第一个版本,设为默认
const installedVersions = await this.getInstalledVersions()
if (installedVersions.length === 1) {
await this.setActive(version)
}
return { success: true, message: `Go ${version} 安装成功` }
} finally {
// 清理临时文件(无论成功还是失败)
await this.cleanupTempFiles(zipPath)
}
} catch (error: any) {
console.error(`Go ${version} 安装失败:`, error)
return { success: false, message: `安装失败: ${error.message}` }
}
}
/**
* 检查版本是否已安装(重复安装检测)
*/
private async isVersionInstalled(version: string): Promise<boolean> {
const extractDir = join(this.getGoBasePath(), `go-${version}`)
const goExe = join(extractDir, 'bin', 'go.exe')
const gofmtExe = join(extractDir, 'bin', 'gofmt.exe')
// 检查目录和关键文件是否存在
if (!existsSync(extractDir) || !existsSync(goExe) || !existsSync(gofmtExe)) {
return false
}
// 进一步验证安装完整性
try {
const { stdout } = await execAsync(`"${goExe}" version`, { timeout: 5000 })
return stdout.includes(version)
} catch {
return false
}
}
/**
* 解压 Go 归档文件
*/
private async extractGoArchive(zipPath: string, destDir: string, version: string): Promise<void> {
const extractDir = join(destDir, `go-${version}`)
try {
// 先解压到临时位置
await this.extractZip(zipPath, destDir)
// Go 的 ZIP 文件解压后会创建一个 'go' 目录,需要重命名
const extractedGoDir = join(destDir, 'go')
if (existsSync(extractedGoDir)) {
// 如果目标目录已存在,先删除
if (existsSync(extractDir)) {
rmSync(extractDir, { recursive: true, force: true })
}
// 重命名为版本特定的目录
require('fs').renameSync(extractedGoDir, extractDir)
} else {
throw new Error('解压后未找到 Go 目录')
}
} catch (error: any) {
throw new Error(`解压失败: ${error.message}`)
}
}
/**
* 执行安装后验证和完整性检查
*/
private async performInstallationValidation(version: string): Promise<{ isValid: boolean; error?: string }> {
const goPath = this.getGoPath(version)
const goExe = join(goPath, 'bin', 'go.exe')
const gofmtExe = join(goPath, 'bin', 'gofmt.exe')
try {
// 1. 检查核心可执行文件是否存在
if (!existsSync(goExe)) {
return { isValid: false, error: 'go.exe 文件不存在' }
}
if (!existsSync(gofmtExe)) {
return { isValid: false, error: 'gofmt.exe 文件不存在' }
}
// 2. 验证 go version 命令
const { stdout: versionOutput } = await execAsync(`"${goExe}" version`, { timeout: 10000 })
if (!versionOutput.includes(version)) {
return { isValid: false, error: `版本验证失败,期望 ${version},实际 ${versionOutput.trim()}` }
}
// 3. 验证 go env 命令
await execAsync(`"${goExe}" env GOROOT`, { timeout: 10000 })
// 4. 验证 go mod 功能
await execAsync(`"${goExe}" help mod`, { timeout: 10000 })
// 5. 验证 go build 命令可用
await execAsync(`"${goExe}" help build`, { timeout: 10000 })
// 6. 检查标准库目录
const srcDir = join(goPath, 'src')
if (!existsSync(srcDir)) {
return { isValid: false, error: '标准库源码目录不存在' }
}
return { isValid: true }
} catch (error: any) {
return { isValid: false, error: `验证过程出错: ${error.message}` }
}
}
/**
* 清理失败的安装
*/
private async cleanupFailedInstallation(installDir: string): Promise<void> {
try {
if (existsSync(installDir)) {
console.log(`清理失败的安装目录: ${installDir}`)
rmSync(installDir, { recursive: true, force: true })
}
} catch (error) {
console.warn(`清理安装目录失败: ${error}`)
}
}
/**
* 更新安装配置
*/
private async updateInstallationConfig(version: string): Promise<void> {
const goVersions = this.configStore.get('goVersions' as any) || []
if (!goVersions.includes(version)) {
goVersions.push(version)
this.configStore.set('goVersions' as any, goVersions)
}
}
/**
* 清理临时文件
*/
private async cleanupTempFiles(zipPath: string): Promise<void> {
try {
// 清理下载的 ZIP 文件
if (existsSync(zipPath)) {
unlinkSync(zipPath)
console.log(`已清理临时文件: ${zipPath}`)
}
// 清理部分下载文件
const partialFile = `${zipPath}.partial`
if (existsSync(partialFile)) {
unlinkSync(partialFile)
console.log(`已清理部分下载文件: ${partialFile}`)
}
} catch (error) {
console.warn(`清理临时文件时出错: ${error}`)
// 不抛出错误,因为这不应该影响安装结果
}
}
/**
* 卸载 Go 版本
* 包含完整的文件系统清理、环境变量清理和配置状态更新
* Requirements: 2.2, 2.3, 2.4, 2.5
*/
async uninstall(version: string): Promise<{ success: boolean; message: string }> {
try {
console.log(`开始卸载 Go ${version}...`)
const goBasePath = this.getGoBasePath()
const versionDir = join(goBasePath, `go-${version}`)
// 检查版本是否存在
if (!existsSync(versionDir)) {
return { success: false, message: `Go ${version} 未安装` }
}
// 获取当前活动版本
const activeVersion = this.configStore.get('activeGoVersion' as any)
const isActiveVersion = activeVersion === version
console.log(`版本目录: ${versionDir}`)
console.log(`是否为活动版本: ${isActiveVersion}`)
// 如果是当前活动版本,执行环境变量清理
if (isActiveVersion) {
console.log('清理活动版本的环境变量...')
await this.cleanupActiveVersionEnvironment(version)
}
// 执行文件系统清理和目录删除
console.log('执行文件系统清理...')
await this.performFileSystemCleanup(versionDir, version)
// 更新配置状态
console.log('更新配置状态...')
await this.updateUninstallConfiguration(version, isActiveVersion)
console.log(`Go ${version} 卸载完成`)
return { success: true, message: `Go ${version} 已成功卸载` }
} catch (error: any) {
console.error(`Go ${version} 卸载失败:`, error)
return { success: false, message: `卸载失败: ${error.message}` }
}
}
/**
* 清理活动版本的环境变量
* 移除 GOROOT、GOPATH 和 PATH 中的相关配置
* Requirements: 2.3
*/
private async cleanupActiveVersionEnvironment(version: string): Promise<void> {
try {
const versionDir = this.getGoPath(version)
console.log(`清理活动版本 ${version} 的环境变量`)
// 使用专门的环境变量清理脚本
const cleanupScript = this.generateActiveVersionCleanupScript()
await this.executePowerShellScript(cleanupScript)
console.log(`活动版本环境变量清理完成`)
} catch (error: any) {
console.error(`环境变量清理失败: ${error.message}`)
throw new Error(`环境变量清理失败: ${error.message}`)
}
}
/**
* 执行文件系统清理和目录删除
* 完全删除版本目录及其所有内容
* Requirements: 2.2
*/
private async performFileSystemCleanup(versionDir: string, version: string): Promise<void> {
try {
console.log(`开始删除目录: ${versionDir}`)
// 检查目录是否存在
if (!existsSync(versionDir)) {
console.log(`目录不存在,跳过删除: ${versionDir}`)
return
}
// 获取目录信息用于日志记录
const dirStats = this.getDirectoryStats(versionDir)
console.log(`目录统计: ${dirStats.fileCount} 个文件, ${dirStats.dirCount} 个子目录`)
// 尝试删除目录(使用强制删除)
rmSync(versionDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 })
// 验证删除是否成功
if (existsSync(versionDir)) {
throw new Error(`目录删除失败,目录仍然存在: ${versionDir}`)
}
console.log(`目录删除成功: ${versionDir}`)
} catch (error: any) {
console.error(`文件系统清理失败: ${error.message}`)
throw new Error(`文件系统清理失败: ${error.message}`)
}
}
/**
* 更新卸载后的配置状态
* 从已安装版本列表中移除,清除活动版本配置
* Requirements: 2.4, 2.5
*/
private async updateUninstallConfiguration(version: string, wasActiveVersion: boolean): Promise<void> {
try {
console.log(`更新配置状态: 移除版本 ${version}`)
// 从已安装版本列表中移除
const goVersions = this.configStore.get('goVersions' as any) || []
const index = goVersions.indexOf(version)
if (index > -1) {
goVersions.splice(index, 1)
this.configStore.set('goVersions' as any, goVersions)
console.log(`已从版本列表中移除: ${version}`)
} else {
console.log(`版本不在配置列表中: ${version}`)
}
// 如果卸载的是活动版本,清除活动版本配置
if (wasActiveVersion) {
this.configStore.set('activeGoVersion' as any, '')
console.log(`已清除活动版本配置`)
// 如果还有其他版本,可以提示用户选择新的活动版本
const remainingVersions = await this.getInstalledVersions()
if (remainingVersions.length > 0) {
console.log(`提示: 还有 ${remainingVersions.length} 个版本可用,可以设置新的活动版本`)
}
}
console.log(`配置状态更新完成`)
} catch (error: any) {
console.error(`配置状态更新失败: ${error.message}`)
throw new Error(`配置状态更新失败: ${error.message}`)
}
}
/**
* 生成活动版本清理的 PowerShell 脚本
* 清除 GOROOT、GOPATH 和 PATH 中的所有 Go 相关配置
*/
private generateActiveVersionCleanupScript(): string {
return `
# Go 活动版本环境变量清理脚本
Write-Host "开始清理 Go 活动版本环境变量..."
try {
# 获取当前用户 PATH
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($userPath) {
Write-Host "当前 PATH: $userPath"
$pathArray = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne $null }
# 移除所有 Go 相关路径(更全面的清理)
$filteredPaths = $pathArray | Where-Object {
-not ($_ -like '*\\go\\go-*\\bin' -or
$_ -like '*\\go-*\\bin' -or
$_ -like '*\\golang\\*\\bin' -or
$_ -like '*\\Go\\*\\bin' -or
$_ -like '*\\go\\bin' -or
$_ -like '*\\golang\\bin')
}
$finalPath = ($filteredPaths | Where-Object { $_ -ne '' -and $_ -ne $null } | Select-Object -Unique) -join ';'
[Environment]::SetEnvironmentVariable('PATH', $finalPath, 'User')
Write-Host "已清理 PATH 中的所有 Go 路径"
}
# 清除 GOROOT
$currentGoRoot = [Environment]::GetEnvironmentVariable('GOROOT', 'User')
if ($currentGoRoot) {
[Environment]::SetEnvironmentVariable('GOROOT', $null, 'User')
Write-Host "已清除 GOROOT: $currentGoRoot"
}
# 清除 GOPATH
$currentGoPath = [Environment]::GetEnvironmentVariable('GOPATH', 'User')
if ($currentGoPath) {
[Environment]::SetEnvironmentVariable('GOPATH', $null, 'User')
Write-Host "已清除 GOPATH: $currentGoPath"
}
Write-Host "活动版本环境变量清理完成"
} catch {
Write-Error "活动版本环境变量清理失败: $_"
throw $_
}
`
}
/**
* 获取目录统计信息
* 用于日志记录和验证
*/
private getDirectoryStats(dirPath: string): { fileCount: number; dirCount: number; totalSize: number } {
let fileCount = 0
let dirCount = 0
let totalSize = 0
try {
const items = readdirSync(dirPath, { withFileTypes: true })
for (const item of items) {
const itemPath = join(dirPath, item.name)
if (item.isDirectory()) {
dirCount++
const subStats = this.getDirectoryStats(itemPath)
fileCount += subStats.fileCount
dirCount += subStats.dirCount
totalSize += subStats.totalSize
} else if (item.isFile()) {
fileCount++
try {
const stats = statSync(itemPath)
totalSize += stats.size
} catch {
// 忽略无法访问的文件
}
}
}
} catch (error) {
console.warn(`获取目录统计信息失败: ${dirPath}`, error)
}
return { fileCount, dirCount, totalSize }
}
/**
* 设置活动的 Go 版本
* 包含版本激活、环境变量更新和切换后的功能验证
* Requirements: 3.2, 3.3, 3.4
*/
async setActive(version: string): Promise<{ success: boolean; message: string }> {
try {
console.log(`开始切换到 Go ${version}`)
// 1. 验证版本是否已安装
const goBasePath = this.getGoBasePath()
const versionDir = join(goBasePath, `go-${version}`)
const goExe = join(versionDir, 'bin', 'go.exe')
if (!existsSync(goExe)) {
return { success: false, message: `Go ${version} 未安装,请先安装该版本` }
}
// 2. 执行版本激活和环境变量更新
console.log(`更新环境变量为 Go ${version}`)
await this.updateEnvironmentVariables(version)
// 3. 更新配置存储
const previousActiveVersion = this.configStore.get('activeGoVersion' as any) || ''
this.configStore.set('activeGoVersion' as any, version)
console.log(`配置已更新: ${previousActiveVersion} -> ${version}`)
// 4. 切换后的功能验证
console.log(`验证 Go ${version} 切换结果`)
const validationResult = await this.validateVersionSwitch(version)
if (!validationResult.isValid) {
// 如果验证失败,尝试回滚
console.warn(`版本切换验证失败,尝试回滚到 ${previousActiveVersion}`)
if (previousActiveVersion) {
try {
await this.updateEnvironmentVariables(previousActiveVersion)
this.configStore.set('activeGoVersion' as any, previousActiveVersion)
} catch (rollbackError) {
console.error('回滚失败:', rollbackError)
}
}
return {
success: false,
message: `版本切换失败: ${validationResult.errors.join('; ')}`
}
}
// 5. 系统级 Go 命令验证
const systemValidation = await this.validateSystemGoCommand(version)
if (!systemValidation.isValid) {
console.warn('系统级 Go 命令验证失败,但环境变量已更新')
return {
success: true,
message: `Go ${version} 已设为默认版本,但系统级验证失败: ${systemValidation.error}。请重启终端或重新登录以使环境变量生效。`
}
}
console.log(`Go ${version} 切换成功`)
// 记录成功的版本切换
await this.recordVersionSwitch(version, true)
return {
success: true,
message: `已成功将 Go ${version} 设为默认版本`
}
} catch (error: any) {
console.error(`Go ${version} 切换失败:`, error)
// 记录失败的版本切换
await this.recordVersionSwitch(version, false)
return {
success: false,
message: `版本切换失败: ${error.message}`
}
}
}
/**
* 验证版本切换结果
* 检查环境变量是否正确设置
*/
private async validateVersionSwitch(version: string): Promise<{ isValid: boolean; errors: string[] }> {
const errors: string[] = []
try {
// 验证环境变量设置
const envValidation = await this.validateEnvironmentVariables(version)
if (!envValidation.isValid) {
errors.push(...envValidation.errors)
}
// 验证安装完整性
const installationValid = await this.validateInstallation(version)
if (!installationValid) {
errors.push(`Go ${version} 安装验证失败`)
}
// 验证 Go 信息获取
const goInfo = await this.getGoInfo(version)
if (!goInfo) {
errors.push(`无法获取 Go ${version} 的环境信息`)
} else {
// 验证 GOROOT 是否匹配
const expectedGoRoot = this.getGoPath(version)
if (goInfo.goroot !== expectedGoRoot) {
errors.push(`GOROOT 不匹配: 期望 ${expectedGoRoot}, 实际 ${goInfo.goroot}`)
}
}
} catch (error: any) {
errors.push(`版本切换验证过程出错: ${error.message}`)
}
return {
isValid: errors.length === 0,
errors
}
}
/**
* 验证系统级 Go 命令
* 检查在新进程中执行 go version 是否返回正确版本
*/
private async validateSystemGoCommand(expectedVersion: string): Promise<{ isValid: boolean; error?: string }> {
try {
// 使用新的进程环境执行 go version
const { stdout } = await execAsync('go version', {
timeout: 10000,
env: { ...process.env } // 使用当前环境变量
})
const versionOutput = stdout.trim()
console.log(`系统 go version 输出: ${versionOutput}`)
// 检查输出是否包含期望的版本
if (!versionOutput.includes(expectedVersion)) {
return {
isValid: false,
error: `go version 输出不匹配: 期望包含 '${expectedVersion}', 实际 '${versionOutput}'`
}
}
// 验证 go env 命令
const { stdout: goEnvOutput } = await execAsync('go env GOROOT', { timeout: 5000 })
const actualGoRoot = goEnvOutput.trim()
const expectedGoRoot = this.getGoPath(expectedVersion)
if (actualGoRoot !== expectedGoRoot) {
return {
isValid: false,
error: `go env GOROOT 不匹配: 期望 '${expectedGoRoot}', 实际 '${actualGoRoot}'`
}
}
return { isValid: true }
} catch (error: any) {
return {
isValid: false,
error: `系统级 Go 命令验证失败: ${error.message}`
}
}
}
/**
* 获取版本切换历史
* 返回最近的版本切换记录
*/
async getVersionSwitchHistory(): Promise<Array<{ version: string; timestamp: Date; success: boolean }>> {
try {
const history = this.configStore.get('goVersionSwitchHistory' as any) || []
return history.map((entry: any) => ({
version: entry.version,
timestamp: new Date(entry.timestamp),
success: entry.success
}))
} catch {
return []
}
}
/**
* 记录版本切换历史
*/
private async recordVersionSwitch(version: string, success: boolean): Promise<void> {
try {
const history = this.configStore.get('goVersionSwitchHistory' as any) || []
const newEntry = {
version,
timestamp: new Date().toISOString(),
success
}
// 保留最近 10 条记录
const updatedHistory = [newEntry, ...history].slice(0, 10)
this.configStore.set('goVersionSwitchHistory' as any, updatedHistory)
} catch (error) {
console.warn('记录版本切换历史失败:', error)
}
}
/**
* 验证 Go 安装
*/
async validateInstallation(version: string): Promise<boolean> {
const goPath = this.getGoPath(version)
const goExe = join(goPath, 'bin', 'go.exe')
const gofmtExe = join(goPath, 'bin', 'gofmt.exe')
try {
// 检查核心可执行文件
if (!existsSync(goExe)) return false
if (!existsSync(gofmtExe)) return false
// 验证版本
const { stdout } = await execAsync(`"${goExe}" version`)
if (!stdout.includes(version)) return false
// 验证环境和基本命令
await execAsync(`"${goExe}" env GOROOT`)
// 验证 go mod 功能
await execAsync(`"${goExe}" help mod`)
return true
} catch {
return false
}
}
/**
* 获取 Go 环境信息
*/
async getGoInfo(version: string): Promise<GoInfo | null> {
const goPath = this.getGoPath(version)
const goExe = join(goPath, 'bin', 'go.exe')
if (!existsSync(goExe)) {
return null
}
try {
const { stdout: goVersion } = await execAsync(`"${goExe}" version`, { timeout: 5000 })
const { stdout: goRoot } = await execAsync(`"${goExe}" env GOROOT`, { timeout: 5000 })
const { stdout: goOs } = await execAsync(`"${goExe}" env GOOS`, { timeout: 5000 })
const { stdout: goArch } = await execAsync(`"${goExe}" env GOARCH`, { timeout: 5000 })
return {
version,
goroot: goRoot.trim(),
gopath: this.getDefaultGoPath(),
goversion: goVersion.trim(),
goos: goOs.trim(),
goarch: goArch.trim()
}
} catch (error) {
return null
}
}
/**
* 检测系统已安装的 Go 版本
*/
async detectSystemGoVersion(): Promise<GoVersion | null> {
try {
// 尝试执行系统的 go 命令
const { stdout: goVersion } = await execAsync('go version', { timeout: 5000 })
const { stdout: goRoot } = await execAsync('go env GOROOT', { timeout: 5000 })
const { stdout: goPath } = await execAsync('go env GOPATH', { timeout: 5000 })
// 解析版本号
const versionMatch = goVersion.match(/go(\d+\.\d+\.\d+)/)
if (!versionMatch) return null
const version = versionMatch[1]
const systemGoRoot = goRoot.trim()
const systemGoPath = goPath.trim()
return {
version: `go${version}`,
path: systemGoRoot,
isActive: false, // 系统版本不算作我们管理的活动版本
goroot: systemGoRoot,
gopath: systemGoPath,
installDate: undefined,
size: undefined
}
} catch (error) {
// 系统没有安装 Go 或者不在 PATH 中
return null
}
}
// ==================== 私有方法 ====================
/**
* 增强的文件下载管理器
* 支持 HTTPS 下载、进度跟踪、SHA256 校验、下载中断和重试
*/
private async downloadFile(url: string, dest: string, name: string, expectedSha256?: string): Promise<void> {
const options: DownloadOptions = {
url,
dest,
name,
expectedSha256,
maxRetries: 3,
timeout: 600000 // 10 分钟超时
}
const result = await this.downloadWithRetry(options)
if (!result.success) {
throw new Error(result.message)
}
}
/**
* 带重试机制的下载
*/
private async downloadWithRetry(options: DownloadOptions): Promise<DownloadResult> {
const { maxRetries = 3 } = options
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`下载尝试 ${attempt}/${maxRetries}: ${options.name}`)
// 检查是否存在部分下载的文件
const partialFile = `${options.dest}.partial`
let resumeFrom = 0
if (existsSync(partialFile)) {
try {
const stats = statSync(partialFile)
resumeFrom = stats.size
console.log(`检测到部分下载文件,从 ${resumeFrom} 字节处恢复下载`)
} catch (e) {
// 如果无法读取部分文件,删除它
try {
unlinkSync(partialFile)
} catch {}
resumeFrom = 0
}
}
await this.performDownload(options, resumeFrom)
// 下载成功后验证 SHA256如果提供
if (options.expectedSha256) {
const isValid = await this.verifySha256(options.dest, options.expectedSha256)
if (!isValid) {
// SHA256 验证失败,删除文件并重试
try {
unlinkSync(options.dest)
} catch {}
throw new Error('SHA256 校验失败')
}
}
// 清理部分下载文件
try {
unlinkSync(partialFile)
} catch {}
return {
success: true,
message: '下载完成',
filePath: options.dest
}
} catch (error: any) {
lastError = error
console.error(`下载尝试 ${attempt} 失败:`, error.message)
if (attempt < maxRetries) {
// 等待一段时间后重试(指数退避)
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000)
console.log(`等待 ${delay}ms 后重试...`)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
return {
success: false,
message: `下载失败(已重试 ${maxRetries} 次): ${lastError?.message || '未知错误'}`
}
}
/**
* 执行实际的下载操作
*/
private async performDownload(options: DownloadOptions, resumeFrom: number = 0): Promise<void> {
return new Promise((resolve, reject) => {
const protocol = options.url.startsWith('https') ? https : http
const partialFile = `${options.dest}.partial`
const isResume = resumeFrom > 0
const requestOptions: any = {
headers: {
'User-Agent': 'PHPer-Dev-Manager/1.0'
},
timeout: options.timeout || 600000
}
// 如果是断点续传,添加 Range 头
if (isResume) {
requestOptions.headers['Range'] = `bytes=${resumeFrom}-`
}
const request = protocol.get(options.url, requestOptions, (response) => {
// 处理重定向
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location
if (redirectUrl) {
const redirectOptions = { ...options, url: redirectUrl }
this.performDownload(redirectOptions, resumeFrom).then(resolve).catch(reject)
return
}
}
// 检查响应状态码
const expectedStatus = isResume ? 206 : 200 // 206 for partial content
if (response.statusCode !== expectedStatus && response.statusCode !== 200) {
reject(new Error(`下载失败: HTTP ${response.statusCode}`))
return
}
const totalSize = isResume
? resumeFrom + parseInt(response.headers['content-length'] || '0', 10)
: parseInt(response.headers['content-length'] || '0', 10)
let downloadedSize = resumeFrom
// 创建写入流(追加模式如果是断点续传)
const file = createWriteStream(isResume ? partialFile : options.dest,
isResume ? { flags: 'a' } : undefined)
let lastProgressTime = 0
let lastDownloadedSize = downloadedSize
let downloadSpeed = 0
response.on('data', (chunk) => {
downloadedSize += chunk.length
const now = Date.now()
// 每500ms更新一次进度和速度
if (now - lastProgressTime > 500) {
const timeDiff = (now - lastProgressTime) / 1000
const sizeDiff = downloadedSize - lastDownloadedSize
downloadSpeed = sizeDiff / timeDiff // bytes per second
const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0
sendDownloadProgress('go', progress, downloadedSize, totalSize)
lastProgressTime = now
lastDownloadedSize = downloadedSize
}
})
response.pipe(file)
file.on('finish', () => {
file.close()
// 如果是部分下载文件,重命名为最终文件
if (isResume) {
try {
if (existsSync(options.dest)) {
unlinkSync(options.dest)
}
require('fs').renameSync(partialFile, options.dest)
} catch (e) {
reject(new Error(`重命名文件失败: ${e}`))
return
}
}
sendDownloadProgress('go', 100, totalSize, totalSize)
resolve()
})
file.on('error', (err) => {
try {
if (!isResume) {
unlinkSync(options.dest)
}
} catch {}
reject(err)
})
})
request.on('error', (err) => {
reject(new Error(`网络错误: ${err.message}`))
})
request.on('timeout', () => {
request.destroy()
reject(new Error('下载超时'))
})
})
}
/**
* 验证文件的 SHA256 校验和
*/
private async verifySha256(filePath: string, expectedSha256: string): Promise<boolean> {
return new Promise((resolve) => {
try {
const hash = createHash('sha256')
const stream = createReadStream(filePath)
stream.on('data', (data) => {
hash.update(data)
})
stream.on('end', () => {
const actualSha256 = hash.digest('hex').toLowerCase()
const expected = expectedSha256.toLowerCase()
resolve(actualSha256 === expected)
})
stream.on('error', () => {
resolve(false)
})
} catch {
resolve(false)
}
})
}
private async extractZip(zipPath: string, destDir: string): Promise<void> {
return new Promise((resolve, reject) => {
const readStream = require('fs').createReadStream(zipPath)
readStream
.pipe(unzipper.Extract({ path: destDir }))
.on('close', resolve)
.on('error', reject)
})
}
/**
* 更新环境变量以支持指定的 Go 版本
* 设置 GOROOT、GOPATH 和 PATH清理旧版本的路径配置
* Requirements: 3.1, 6.1, 6.2, 6.3, 6.4
*/
private async updateEnvironmentVariables(goVersion: string): Promise<void> {
const goRoot = this.getGoPath(goVersion)
const goBin = join(goRoot, 'bin')
const goPath = this.getDefaultGoPath()
console.log(`更新环境变量为 Go ${goVersion}`)
console.log(`GOROOT: ${goRoot}`)
console.log(`GOPATH: ${goPath}`)
console.log(`Go Binary Path: ${goBin}`)
// 确保 GOPATH 工作空间目录存在
await this.ensureGoPathStructure(goPath)
// 使用 PowerShell 脚本更新用户环境变量
const psScript = this.generateEnvironmentUpdateScript(goRoot, goPath, goBin)
try {
await this.executePowerShellScript(psScript)
console.log(`环境变量更新成功: Go ${goVersion}`)
} catch (error: any) {
console.error(`环境变量更新失败: ${error.message}`)
throw new Error(`环境变量更新失败: ${error.message}`)
}
}
/**
* 确保 GOPATH 工作空间目录结构存在
* 创建标准的 Go 工作空间结构src, pkg, bin
*/
private async ensureGoPathStructure(goPath: string): Promise<void> {
try {
if (!existsSync(goPath)) {
mkdirSync(goPath, { recursive: true })
console.log(`创建 GOPATH 目录: ${goPath}`)
}
// 创建标准的 Go 工作空间结构
const subdirs = ['src', 'pkg', 'bin']
for (const subdir of subdirs) {
const dirPath = join(goPath, subdir)
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true })
console.log(`创建 GOPATH 子目录: ${dirPath}`)
}
}
} catch (error: any) {
throw new Error(`创建 GOPATH 结构失败: ${error.message}`)
}
}
/**
* 生成环境变量更新的 PowerShell 脚本
* 包含 GOROOT、GOPATH 设置和 PATH 清理更新
*/
private generateEnvironmentUpdateScript(goRoot: string, goPath: string, goBin: string): string {
// 转义路径中的反斜杠
const escapedGoRoot = goRoot.replace(/\\/g, '\\\\')
const escapedGoPath = goPath.replace(/\\/g, '\\\\')
const escapedGoBin = goBin.replace(/\\/g, '\\\\')
return `
# Go 环境变量更新脚本
Write-Host "开始更新 Go 环境变量..."
try {
# 设置 GOROOT
Write-Host "设置 GOROOT: ${escapedGoRoot}"
[Environment]::SetEnvironmentVariable('GOROOT', '${escapedGoRoot}', 'User')
# 设置 GOPATH
Write-Host "设置 GOPATH: ${escapedGoPath}"
[Environment]::SetEnvironmentVariable('GOPATH', '${escapedGoPath}', 'User')
# 更新 PATH
Write-Host "更新 PATH 环境变量..."
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if (-not $userPath) { $userPath = '' }
$pathArray = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne $null }
# 移除所有旧的 Go 路径(更全面的清理)
$filteredPaths = $pathArray | Where-Object {
-not ($_ -like '*\\go\\go-*\\bin' -or
$_ -like '*\\go-*\\bin' -or
$_ -like '*\\golang\\*\\bin' -or
$_ -like '*\\Go\\*\\bin')
}
# 添加新的 Go 路径到开头
$newPathArray = @('${escapedGoBin}') + $filteredPaths
$finalPath = ($newPathArray | Where-Object { $_ -ne '' -and $_ -ne $null } | Select-Object -Unique) -join ';'
Write-Host "新的 PATH: $finalPath"
[Environment]::SetEnvironmentVariable('PATH', $finalPath, 'User')
Write-Host "环境变量更新完成"
} catch {
Write-Error "环境变量更新失败: $_"
throw $_
}
`
}
/**
* 从环境变量中移除指定 Go 版本的路径
* 清理 GOROOT、GOPATH 和 PATH 中的相关配置
*/
private async removeFromPath(goPath: string): Promise<void> {
const goBin = join(goPath, 'bin')
console.log(`从环境变量中移除 Go 路径: ${goBin}`)
const psScript = this.generateEnvironmentCleanupScript(goBin)
try {
await this.executePowerShellScript(psScript)
console.log(`环境变量清理成功`)
} catch (error: any) {
console.error(`环境变量清理失败: ${error.message}`)
throw new Error(`环境变量清理失败: ${error.message}`)
}
}
/**
* 生成环境变量清理的 PowerShell 脚本
* 移除指定的 Go 路径并清理 GOROOT、GOPATH
*/
private generateEnvironmentCleanupScript(goBin: string): string {
const escapedGoBin = goBin.replace(/\\/g, '\\\\')
return `
# Go 环境变量清理脚本
Write-Host "开始清理 Go 环境变量..."
try {
# 获取当前用户 PATH
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($userPath) {
$pathArray = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne $null }
# 移除指定的 Go 路径
$filteredPaths = $pathArray | Where-Object {
$_ -ne '${escapedGoBin}'
}
$finalPath = ($filteredPaths | Where-Object { $_ -ne '' -and $_ -ne $null } | Select-Object -Unique) -join ';'
[Environment]::SetEnvironmentVariable('PATH', $finalPath, 'User')
Write-Host "已从 PATH 中移除: ${escapedGoBin}"
}
# 清除 GOROOT 和 GOPATH如果没有其他活动版本
[Environment]::SetEnvironmentVariable('GOROOT', $null, 'User')
[Environment]::SetEnvironmentVariable('GOPATH', $null, 'User')
Write-Host "已清除 GOROOT 和 GOPATH"
Write-Host "环境变量清理完成"
} catch {
Write-Error "环境变量清理失败: $_"
throw $_
}
`
}g): string {
const escapedGoBin = goBin.replace(/\\/g, '\\\\')
return `
# Go 环境变量清理脚本
Write-Host "开始清理 Go 环境变量..."
try {
# 获取当前用户 PATH
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($userPath) {
$pathArray = $userPath -split ';' | Where-Object { $_ -ne '' -and $_ -ne $null }
# 移除指定的 Go 路径
$filteredPaths = $pathArray | Where-Object {
$_ -ne '${escapedGoBin}'
}
$finalPath = ($filteredPaths | Where-Object { $_ -ne '' -and $_ -ne $null } | Select-Object -Unique) -join ';'
[Environment]::SetEnvironmentVariable('PATH', $finalPath, 'User')
Write-Host "已从 PATH 中移除: ${escapedGoBin}"
}
# 清除 GOROOT 和 GOPATH如果没有其他活动版本
[Environment]::SetEnvironmentVariable('GOROOT', $null, 'User')
[Environment]::SetEnvironmentVariable('GOPATH', $null, 'User')
Write-Host "已清除 GOROOT 和 GOPATH"
Write-Host "环境变量清理完成"
} catch {
Write-Error "环境变量清理失败: $_"
throw $_
}
`
}
/**
* 验证环境变量是否正确设置
* 检查 GOROOT、GOPATH 和 PATH 中的 Go 路径
*/
async validateEnvironmentVariables(expectedVersion: string): Promise<{ isValid: boolean; errors: string[] }> {
const errors: string[] = []
const expectedGoRoot = this.getGoPath(expectedVersion)
const expectedGoPath = this.getDefaultGoPath()
const expectedGoBin = join(expectedGoRoot, 'bin')
try {
// 检查 GOROOT
const { stdout: actualGoRoot } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'GOROOT\', \'User\')"', { timeout: 5000 })
const cleanGoRoot = actualGoRoot.trim()
if (cleanGoRoot !== expectedGoRoot) {
errors.push(`GOROOT 不匹配: 期望 '${expectedGoRoot}', 实际 '${cleanGoRoot}'`)
}
// 检查 GOPATH
const { stdout: actualGoPath } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'GOPATH\', \'User\')"', { timeout: 5000 })
const cleanGoPath = actualGoPath.trim()
if (cleanGoPath !== expectedGoPath) {
errors.push(`GOPATH 不匹配: 期望 '${expectedGoPath}', 实际 '${cleanGoPath}'`)
}
// 检查 PATH 中是否包含 Go 二进制路径
const { stdout: userPath } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'PATH\', \'User\')"', { timeout: 5000 })
const pathEntries = userPath.trim().split(';').filter(p => p.trim() !== '')
const hasGoBin = pathEntries.some(p => p.trim() === expectedGoBin)
if (!hasGoBin) {
errors.push(`PATH 中缺少 Go 二进制路径: '${expectedGoBin}'`)
}
// 验证是否能够执行 go version 命令
try {
const { stdout: goVersionOutput } = await execAsync('go version', { timeout: 10000 })
if (!goVersionOutput.includes(expectedVersion)) {
errors.push(`go version 输出不匹配: 期望包含 '${expectedVersion}', 实际 '${goVersionOutput.trim()}'`)
}
} catch (error: any) {
errors.push(`无法执行 go version 命令: ${error.message}`)
}
} catch (error: any) {
errors.push(`环境变量验证过程出错: ${error.message}`)
}
return {
isValid: errors.length === 0,
errors
}
}
/**
* 获取当前环境变量状态
* 返回 GOROOT、GOPATH 和 PATH 的当前值
*/
async getCurrentEnvironmentState(): Promise<{ goroot: string; gopath: string; pathEntries: string[] }> {
try {
const { stdout: goroot } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'GOROOT\', \'User\')"', { timeout: 5000 })
const { stdout: gopath } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'GOPATH\', \'User\')"', { timeout: 5000 })
const { stdout: userPath } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'PATH\', \'User\')"', { timeout: 5000 })
const pathEntries = userPath.trim().split(';').filter(p => p.trim() !== '')
return {
goroot: goroot.trim(),
gopath: gopath.trim(),
pathEntries
}
} catch (error: any) {
console.error('获取环境变量状态失败:', error)
return {
goroot: '',
gopath: '',
pathEntries: []
}
}
}
const tempPs1 = join(this.configStore.getTempPath(), 'update_go_env.ps1')
writeFileSync(tempPs1, script, 'utf-8')
try {
await execAsync(`powershell -ExecutionPolicy Bypass -File "${tempPs1}"`, { timeout: 30000 })
} finally {
try {
unlinkSync(tempPs1)
} catch (e) {
// 忽略
}
}
}
/**
* 执行 PowerShell 脚本
* 创建临时脚本文件并执行,完成后清理
*/
private async executePowerShellScript(script: string): Promise<void> {
const tempPs1 = join(this.configStore.getTempPath(), `go_env_${Date.now()}.ps1`)
try {
// 确保临时目录存在
const tempDir = this.configStore.getTempPath()
if (!existsSync(tempDir)) {
mkdirSync(tempDir, { recursive: true })
}
// 写入脚本文件
writeFileSync(tempPs1, script, 'utf-8')
// 执行 PowerShell 脚本
const command = `powershell -ExecutionPolicy Bypass -File "${tempPs1}"`
await execAsync(command, { timeout: 30000 })
} catch (error: any) {
console.error('PowerShell 脚本执行失败:', error)
throw new Error(`PowerShell 脚本执行失败: ${error.message}`)
} finally {
// 清理临时脚本文件
try {
if (existsSync(tempPs1)) {
unlinkSync(tempPs1)
}
} catch (e) {
console.warn('清理临时脚本文件失败:', e)
}
}
}
private getFallbackVersions(): AvailableGoVersion[] {
return [
{ version: 'go1.22.0', stable: true, downloadUrl: 'https://golang.org/dl/go1.22.0.windows-amd64.zip', size: 0, sha256: '' },
{ version: 'go1.21.6', stable: true, downloadUrl: 'https://golang.org/dl/go1.21.6.windows-amd64.zip', size: 0, sha256: '' },
{ version: 'go1.21.5', stable: true, downloadUrl: 'https://golang.org/dl/go1.21.5.windows-amd64.zip', size: 0, sha256: '' },
{ version: 'go1.20.13', stable: true, downloadUrl: 'https://golang.org/dl/go1.20.13.windows-amd64.zip', size: 0, sha256: '' },
{ version: 'go1.20.12', stable: true, downloadUrl: 'https://golang.org/dl/go1.20.12.windows-amd64.zip', size: 0, sha256: '' },
{ version: 'go1.19.13', stable: true, downloadUrl: 'https://golang.org/dl/go1.19.13.windows-amd64.zip', size: 0, sha256: '' }
]
}
}