- 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
1617 lines
52 KiB
Plaintext
1617 lines
52 KiB
Plaintext
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: '' }
|
||
]
|
||
}
|
||
} |