phper/electron/services/GoManager.ts
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

2048 lines
67 KiB
TypeScript
Raw Permalink 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)
// ==================== 错误处理系统 ====================
/**
* Go 管理器错误类型枚举
*/
enum GoErrorType {
NETWORK_ERROR = 'NETWORK_ERROR',
DISK_SPACE_ERROR = 'DISK_SPACE_ERROR',
PERMISSION_ERROR = 'PERMISSION_ERROR',
VALIDATION_ERROR = 'VALIDATION_ERROR',
FILE_SYSTEM_ERROR = 'FILE_SYSTEM_ERROR',
ENVIRONMENT_ERROR = 'ENVIRONMENT_ERROR',
DOWNLOAD_ERROR = 'DOWNLOAD_ERROR',
INSTALLATION_ERROR = 'INSTALLATION_ERROR',
UNINSTALL_ERROR = 'UNINSTALL_ERROR',
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR'
}
/**
* Go 管理器自定义错误类
* 提供结构化的错误信息和解决建议
*/
class GoManagerError extends Error {
public readonly type: GoErrorType
public readonly suggestion: string
public readonly retryable: boolean
public readonly details?: any
constructor(
type: GoErrorType,
message: string,
suggestion: string,
retryable: boolean = false,
details?: any
) {
super(message)
this.name = 'GoManagerError'
this.type = type
this.suggestion = suggestion
this.retryable = retryable
this.details = details
}
}
/**
* 操作结果接口
* 统一的操作结果格式,包含成功状态、消息和详细信息
*/
interface OperationResult {
success: boolean
message: string
suggestion?: string
retryable?: boolean
details?: any
}
/**
* 系统资源检查结果
*/
interface SystemResourceCheck {
hasPermission: boolean
hasDiskSpace: boolean
availableSpace: number
requiredSpace: number
permissionDetails?: string
}
/**
* 网络连接检查结果
*/
interface NetworkCheck {
isConnected: boolean
canReachGolang: boolean
responseTime?: number
error?: string
}
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
suggestion?: string
retryable?: boolean
details?: any
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
}
// ==================== 综合错误处理系统 ====================
/**
* 检查系统资源(磁盘空间和权限)
* Requirements: 8.2, 8.3
*/
private async checkSystemResources(requiredSpaceBytes: number = 500 * 1024 * 1024): Promise<SystemResourceCheck> {
const result: SystemResourceCheck = {
hasPermission: false,
hasDiskSpace: false,
availableSpace: 0,
requiredSpace: requiredSpaceBytes
}
try {
// 检查磁盘空间
const goBasePath = this.getGoBasePath()
const drive = goBasePath.split(':')[0] + ':'
try {
const { stdout } = await execAsync(`powershell "Get-WmiObject -Class Win32_LogicalDisk -Filter \\"DeviceID='${drive}'\\" | Select-Object FreeSpace"`, { timeout: 10000 })
const freeSpaceMatch = stdout.match(/(\d+)/)
if (freeSpaceMatch) {
result.availableSpace = parseInt(freeSpaceMatch[1], 10)
result.hasDiskSpace = result.availableSpace >= requiredSpaceBytes
}
} catch (error) {
console.warn('磁盘空间检查失败,使用默认值')
result.availableSpace = requiredSpaceBytes // 假设有足够空间
result.hasDiskSpace = true
}
// 检查写入权限
try {
const testDir = join(goBasePath, 'permission_test')
if (!existsSync(goBasePath)) {
mkdirSync(goBasePath, { recursive: true })
}
mkdirSync(testDir, { recursive: true })
rmSync(testDir, { recursive: true, force: true })
result.hasPermission = true
} catch (error: any) {
result.hasPermission = false
result.permissionDetails = error.message
}
} catch (error: any) {
console.error('系统资源检查失败:', error)
result.permissionDetails = error.message
}
return result
}
/**
* 检查网络连接
* Requirements: 8.1
*/
private async checkNetworkConnection(): Promise<NetworkCheck> {
const result: NetworkCheck = {
isConnected: false,
canReachGolang: false
}
try {
// 检查基本网络连接
const startTime = Date.now()
await new Promise<void>((resolve, reject) => {
const req = https.get('https://golang.org', { timeout: 10000 }, (res) => {
result.isConnected = true
result.canReachGolang = res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302
result.responseTime = Date.now() - startTime
resolve()
})
req.on('error', (error) => {
result.error = error.message
reject(error)
})
req.on('timeout', () => {
req.destroy()
result.error = '连接超时'
reject(new Error('连接超时'))
})
})
} catch (error: any) {
result.error = error.message
}
return result
}
/**
* 创建结构化的错误结果
* Requirements: 8.5
*/
private createErrorResult(error: GoManagerError | Error, operation: string): OperationResult {
if (error instanceof GoManagerError) {
return {
success: false,
message: `${operation}失败: ${error.message}`,
suggestion: error.suggestion,
retryable: error.retryable,
details: error.details
}
}
// 处理普通错误,提供通用建议
let suggestion = '请检查网络连接和系统权限,然后重试'
let retryable = true
// 根据错误消息提供具体建议
const errorMessage = error.message.toLowerCase()
if (errorMessage.includes('network') || errorMessage.includes('timeout') || errorMessage.includes('连接')) {
suggestion = '网络连接失败请检查网络设置并重试。如果问题持续请尝试使用VPN或更换网络环境'
retryable = true
} else if (errorMessage.includes('permission') || errorMessage.includes('access') || errorMessage.includes('权限')) {
suggestion = '权限不足,请以管理员身份运行程序,或检查目标目录的写入权限'
retryable = false
} else if (errorMessage.includes('space') || errorMessage.includes('disk') || errorMessage.includes('磁盘')) {
suggestion = '磁盘空间不足请清理磁盘空间后重试。建议至少保留500MB可用空间'
retryable = false
} else if (errorMessage.includes('sha256') || errorMessage.includes('checksum') || errorMessage.includes('校验')) {
suggestion = '文件校验失败,可能是下载过程中文件损坏。请删除临时文件并重新下载'
retryable = true
}
return {
success: false,
message: `${operation}失败: ${error.message}`,
suggestion,
retryable
}
}
/**
* 创建成功结果
* Requirements: 8.4
*/
private createSuccessResult(message: string, details?: any): OperationResult {
return {
success: true,
message,
details
}
}
/**
* 处理网络错误并提供重试选项
* Requirements: 8.1
*/
private async handleNetworkError(operation: () => Promise<any>, maxRetries: number = 3): Promise<any> {
let lastError: Error | null = null
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 在重试前检查网络连接
if (attempt > 1) {
console.log(`${attempt} 次尝试前检查网络连接...`)
const networkCheck = await this.checkNetworkConnection()
if (!networkCheck.isConnected) {
throw new GoManagerError(
GoErrorType.NETWORK_ERROR,
'网络连接不可用',
'请检查网络连接后重试。确保可以访问 golang.org',
true,
{ networkCheck }
)
}
}
return await operation()
} catch (error: any) {
lastError = error
console.error(`尝试 ${attempt}/${maxRetries} 失败:`, 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))
}
}
}
// 所有重试都失败了
throw new GoManagerError(
GoErrorType.NETWORK_ERROR,
`网络操作失败(已重试 ${maxRetries} 次): ${lastError?.message || '未知错误'}`,
'请检查网络连接,确保可以访问 golang.org。如果问题持续请尝试使用VPN或联系网络管理员',
true,
{ attempts: maxRetries, lastError: lastError?.message }
)
}
/**
* 验证系统环境并抛出具体错误
* Requirements: 8.2, 8.3
*/
private async validateSystemEnvironment(requiredSpaceBytes: number = 500 * 1024 * 1024): Promise<void> {
const resourceCheck = await this.checkSystemResources(requiredSpaceBytes)
if (!resourceCheck.hasDiskSpace) {
const availableMB = Math.round(resourceCheck.availableSpace / (1024 * 1024))
const requiredMB = Math.round(requiredSpaceBytes / (1024 * 1024))
throw new GoManagerError(
GoErrorType.DISK_SPACE_ERROR,
`磁盘空间不足,可用空间 ${availableMB}MB需要 ${requiredMB}MB`,
`请清理磁盘空间,至少需要 ${requiredMB}MB 可用空间。建议清理临时文件、回收站或卸载不需要的程序`,
false,
{ availableSpace: resourceCheck.availableSpace, requiredSpace: requiredSpaceBytes }
)
}
if (!resourceCheck.hasPermission) {
throw new GoManagerError(
GoErrorType.PERMISSION_ERROR,
'权限不足,无法写入目标目录',
'请以管理员身份运行程序,或确保当前用户对安装目录有写入权限。您也可以尝试更改安装目录到用户文件夹',
false,
{ permissionDetails: resourceCheck.permissionDetails }
)
}
}
/**
* 获取 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 版本列表
* 增强的错误处理,包含网络重试机制
* Requirements: 8.1
*/
async getAvailableVersions(): Promise<AvailableGoVersion[]> {
// 检查缓存
if (this.versionsCache.length > 0 && Date.now() - this.cacheTime < this.CACHE_DURATION) {
return this.versionsCache
}
try {
// 使用网络错误处理包装器
const versions = await this.handleNetworkError(async () => {
return await this.fetchGoVersions()
}, 3)
if (versions.length > 0) {
this.versionsCache = versions
this.cacheTime = Date.now()
return versions
}
} catch (error) {
console.error('获取 Go 版本列表失败:', error)
// 如果是网络错误,记录详细信息但不抛出异常
if (error instanceof GoManagerError && error.type === GoErrorType.NETWORK_ERROR) {
console.warn('使用备用版本列表,因为网络获取失败:', error.message)
}
}
// 返回硬编码的版本列表作为后备
console.log('使用备用版本列表')
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 版本
* 包含完整的安装流程:下载、解压、验证、配置
* 增强的错误处理和用户反馈
* Requirements: 8.1, 8.2, 8.3, 8.4, 8.5
*/
async install(version: string, downloadUrl: string, expectedSha256?: string): Promise<OperationResult> {
try {
console.log(`开始安装 Go ${version}...`)
// 1. 系统环境验证(磁盘空间和权限检查)
console.log('检查系统环境...')
await this.validateSystemEnvironment(500 * 1024 * 1024) // 500MB 最小空间要求
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 })
}
// 2. 重复安装检测和防护
if (await this.isVersionInstalled(version)) {
return {
success: false,
message: `Go ${version} 已安装`,
suggestion: '如需重新安装,请先卸载现有版本',
retryable: false
}
}
console.log(`系统环境检查通过,开始安装 Go ${version}`)
try {
// 3. 下载(使用增强的下载管理器,包含网络重试和 SHA256 验证)
console.log(`正在下载 Go ${version}...`)
await this.downloadFile(downloadUrl, zipPath, `go-${version}`, expectedSha256)
// 4. ZIP 文件解压到目标目录
console.log(`正在解压 Go ${version}...`)
await this.extractGoArchive(zipPath, goBasePath, version)
// 5. 安装后验证和完整性检查
console.log(`正在验证 Go ${version} 安装...`)
const validationResult = await this.performInstallationValidation(version)
if (!validationResult.isValid) {
// 如果验证失败,清理安装目录
await this.cleanupFailedInstallation(extractDir)
throw new GoManagerError(
GoErrorType.VALIDATION_ERROR,
`安装验证失败: ${validationResult.error}`,
'安装文件可能损坏,请重新下载安装。如果问题持续,请检查网络连接或尝试其他版本',
true,
{ validationError: validationResult.error }
)
}
// 6. 更新配置
await this.updateInstallationConfig(version)
// 7. 如果是第一个版本,设为默认
const installedVersions = await this.getInstalledVersions()
let activationMessage = ''
if (installedVersions.length === 1) {
const activationResult = await this.setActive(version)
if (activationResult.success) {
activationMessage = ',并已设为默认版本'
}
}
// 8. 返回详细的成功信息
const installPath = this.getGoPath(version)
return this.createSuccessResult(
`Go ${version} 安装成功${activationMessage}`,
{
version,
installPath,
isActive: installedVersions.length === 1,
totalVersions: installedVersions.length,
nextSteps: installedVersions.length === 1
? ['Go 已设为默认版本,可以在新的命令行窗口中使用 go 命令']
: [`使用 "设为默认" 按钮来激活 Go ${version}`, '或继续安装其他版本']
}
)
} finally {
// 清理临时文件(无论成功还是失败)
await this.cleanupTempFiles(zipPath)
}
} catch (error: any) {
console.error(`Go ${version} 安装失败:`, error)
return this.createErrorResult(error, `Go ${version} 安装`)
}
}
/**
* 卸载 Go 版本
* 包含完整的文件系统清理、环境变量清理和配置状态更新
* 增强的错误处理和用户反馈
* Requirements: 2.2, 2.3, 2.4, 2.5, 8.3, 8.4, 8.5
*/
async uninstall(version: string): Promise<OperationResult> {
try {
console.log(`开始卸载 Go ${version}...`)
// 1. 权限检查
await this.validateSystemEnvironment(0) // 卸载不需要磁盘空间,但需要权限
const goBasePath = this.getGoBasePath()
const versionDir = join(goBasePath, `go-${version}`)
// 2. 检查版本是否存在
if (!existsSync(versionDir)) {
return {
success: false,
message: `Go ${version} 未安装`,
suggestion: '请检查版本号是否正确,或刷新版本列表',
retryable: false
}
}
// 3. 获取当前活动版本
const activeVersion = this.configStore.get('activeGoVersion' as any)
const isActiveVersion = activeVersion === version
console.log(`版本目录: ${versionDir}`)
console.log(`是否为活动版本: ${isActiveVersion}`)
// 4. 如果是当前活动版本,执行环境变量清理
if (isActiveVersion) {
console.log('清理活动版本的环境变量...')
try {
await this.cleanupActiveVersionEnvironment(version)
} catch (error: any) {
throw new GoManagerError(
GoErrorType.ENVIRONMENT_ERROR,
`环境变量清理失败: ${error.message}`,
'请手动清理环境变量,或以管理员身份重试。您可以在系统设置中手动删除 GOROOT 和 GOPATH 环境变量',
true,
{ environmentError: error.message }
)
}
}
// 5. 执行文件系统清理和目录删除
console.log('执行文件系统清理...')
try {
await this.performFileSystemCleanup(versionDir, version)
} catch (error: any) {
throw new GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`文件系统清理失败: ${error.message}`,
'可能有文件正在使用中。请关闭所有相关程序(如 IDE、终端后重试或重启计算机后再次尝试',
true,
{ fileSystemError: error.message }
)
}
// 6. 更新配置状态
console.log('更新配置状态...')
try {
await this.updateUninstallConfiguration(version, isActiveVersion)
} catch (error: any) {
// 配置更新失败不应该阻止卸载成功
console.warn('配置状态更新失败,但文件已删除:', error.message)
}
// 7. 检查剩余版本并提供建议
const remainingVersions = await this.getInstalledVersions()
let nextSteps: string[] = []
if (remainingVersions.length === 0) {
nextSteps.push('所有 Go 版本已卸载,环境变量已清理')
} else if (isActiveVersion && remainingVersions.length > 0) {
nextSteps.push(`还有 ${remainingVersions.length} 个版本可用`)
nextSteps.push('请选择一个版本设为默认,或安装新版本')
}
console.log(`Go ${version} 卸载完成`)
return this.createSuccessResult(
`Go ${version} 已成功卸载`,
{
version,
wasActive: isActiveVersion,
remainingVersions: remainingVersions.length,
nextSteps
}
)
} catch (error: any) {
console.error(`Go ${version} 卸载失败:`, error)
return this.createErrorResult(error, `Go ${version} 卸载`)
}
}
/**
* 清理活动版本的环境变量
* 移除 GOROOT、GOPATH 和 PATH 中的相关配置
* 增强的错误处理
* Requirements: 2.3, 8.3, 8.5
*/
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}`)
// 如果是 GoManagerError直接重新抛出
if (error instanceof GoManagerError) {
throw error
}
// 否则包装为 GoManagerError
throw new GoManagerError(
GoErrorType.ENVIRONMENT_ERROR,
`环境变量清理失败: ${error.message}`,
'请以管理员身份运行程序,或在系统设置中手动清理 GOROOT、GOPATH 和 PATH 环境变量',
true,
{ version, originalError: error.message }
)
}
}
/**
* 执行文件系统清理和目录删除
* 完全删除版本目录及其所有内容
* 增强的错误处理
* Requirements: 2.2, 8.5
*/
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 GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`目录删除失败,目录仍然存在: ${versionDir}`,
'可能有文件正在使用中。请关闭所有相关程序(如 IDE、终端、Go 进程)后重试,或重启计算机后再次尝试',
true,
{ versionDir, dirStats }
)
}
console.log(`目录删除成功: ${versionDir}`)
} catch (error: any) {
console.error(`文件系统清理失败: ${error.message}`)
// 如果是 GoManagerError直接重新抛出
if (error instanceof GoManagerError) {
throw error
}
// 根据错误类型提供具体建议
let suggestion = '请关闭所有相关程序后重试,或重启计算机后再次尝试'
if (error.code === 'EBUSY' || error.code === 'ENOTEMPTY') {
suggestion = '文件正在使用中,请关闭所有 Go 相关程序IDE、终端、Go 进程)后重试'
} else if (error.code === 'EACCES' || error.code === 'EPERM') {
suggestion = '权限不足,请以管理员身份运行程序'
} else if (error.code === 'ENOENT') {
suggestion = '目录不存在或已被删除'
return // 目录不存在就不需要删除了
}
throw new GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`文件系统清理失败: ${error.message}`,
suggestion,
true,
{ versionDir, errorCode: error.code, originalError: error.message }
)
}
}
/**
* 更新卸载后的配置状态
* 从已安装版本列表中移除,清除活动版本配置
* 增强的错误处理
* Requirements: 2.4, 2.5, 8.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 GoManagerError(
GoErrorType.CONFIGURATION_ERROR,
`配置状态更新失败: ${error.message}`,
'配置文件可能损坏或权限不足。请检查应用程序配置目录权限,或重新启动应用程序',
true,
{ version, wasActiveVersion, originalError: 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: 8.3, 8.4, 8.5
*/
async setActive(version: string): Promise<OperationResult> {
try {
console.log(`设置 Go ${version} 为活动版本...`)
// 1. 权限检查
await this.validateSystemEnvironment(0) // 不需要磁盘空间,但需要权限
const goBasePath = this.getGoBasePath()
const versionDir = join(goBasePath, `go-${version}`)
// 2. 验证版本是否已安装
if (!existsSync(join(versionDir, 'bin', 'go.exe'))) {
return {
success: false,
message: `Go ${version} 未安装`,
suggestion: '请先安装此版本,然后再设为默认版本',
retryable: false
}
}
// 3. 更新环境变量
try {
await this.updateEnvironmentVariables(version)
} catch (error: any) {
throw new GoManagerError(
GoErrorType.ENVIRONMENT_ERROR,
`环境变量更新失败: ${error.message}`,
'请以管理员身份运行程序,或手动设置环境变量。您可以在系统设置中手动添加 GOROOT 和 PATH 配置',
true,
{ environmentError: error.message }
)
}
// 4. 更新配置
this.configStore.set('activeGoVersion' as any, version)
// 5. 验证设置是否生效
try {
const goInfo = await this.getGoInfo(version)
if (!goInfo) {
console.warn('无法验证 Go 版本设置,但配置已更新')
}
} catch (error) {
console.warn('Go 版本验证失败,但配置已更新:', error)
}
console.log(`Go ${version} 已设为活动版本`)
return this.createSuccessResult(
`已将 Go ${version} 设为默认版本`,
{
version,
goroot: this.getGoPath(version),
gopath: this.getDefaultGoPath(),
nextSteps: [
'请重新打开命令行窗口以使环境变量生效',
'使用 "go version" 命令验证版本是否正确'
]
}
)
} catch (error: any) {
console.error(`设置活动版本失败:`, error)
return this.createErrorResult(error, `设置 Go ${version} 为活动版本`)
}
}
/**
* 验证 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
}
}
// ==================== 私有方法 ====================
/**
* 检查版本是否已安装(重复安装检测)
*/
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 归档文件
* 增强的错误处理
* Requirements: 8.5
*/
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 GoManagerError(
GoErrorType.INSTALLATION_ERROR,
'解压后未找到 Go 目录',
'下载的文件可能损坏或格式不正确。请重新下载安装包',
true,
{ zipPath, destDir, expectedDir: extractedGoDir }
)
}
} catch (error: any) {
// 如果是 GoManagerError直接重新抛出
if (error instanceof GoManagerError) {
throw error
}
let suggestion = '请检查下载文件是否完整,或重新下载安装包'
if (error.code === 'EACCES' || error.code === 'EPERM') {
suggestion = '权限不足,请以管理员身份运行程序'
} else if (error.code === 'ENOSPC') {
suggestion = '磁盘空间不足,请清理磁盘空间后重试'
} else if (error.message.includes('invalid') || error.message.includes('corrupt')) {
suggestion = '文件损坏,请重新下载安装包'
}
throw new GoManagerError(
GoErrorType.INSTALLATION_ERROR,
`解压失败: ${error.message}`,
suggestion,
true,
{ zipPath, destDir, version, errorCode: error.code, originalError: 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}`)
// 不抛出错误,因为这不应该影响安装结果
}
}
/**
* 增强的文件下载管理器
* 支持 HTTPS 下载、进度跟踪、SHA256 校验、下载中断和重试
* Requirements: 8.1, 8.5
*/
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) {
// 如果下载结果包含建议,创建 GoManagerError
if (result.suggestion) {
throw new GoManagerError(
GoErrorType.DOWNLOAD_ERROR,
result.message,
result.suggestion,
result.retryable || false,
result.details
)
} else {
throw new Error(result.message)
}
}
}
/**
* 带重试机制的下载
* 增强的网络错误处理和用户反馈
* Requirements: 8.1
*/
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}`)
// 在重试前检查网络连接
if (attempt > 1) {
console.log(`${attempt} 次尝试前检查网络连接...`)
const networkCheck = await this.checkNetworkConnection()
if (!networkCheck.isConnected) {
throw new GoManagerError(
GoErrorType.NETWORK_ERROR,
'网络连接不可用',
'请检查网络连接后重试。确保可以访问 golang.org',
true,
{ networkCheck, attempt }
)
}
console.log(`网络连接正常,响应时间: ${networkCheck.responseTime}ms`)
}
// 检查是否存在部分下载的文件
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) {
console.log('验证文件完整性...')
const isValid = await this.verifySha256(options.dest, options.expectedSha256)
if (!isValid) {
// SHA256 验证失败,删除文件并重试
try {
unlinkSync(options.dest)
} catch {}
throw new GoManagerError(
GoErrorType.DOWNLOAD_ERROR,
'SHA256 校验失败,文件可能损坏',
'文件下载过程中可能出现错误,正在重新下载。如果问题持续,请检查网络连接',
true,
{ attempt, expectedSha256: options.expectedSha256 }
)
}
console.log('文件完整性验证通过')
}
// 清理部分下载文件
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))
}
}
}
// 所有重试都失败了
const errorMessage = lastError instanceof GoManagerError
? lastError.message
: `下载失败(已重试 ${maxRetries} 次): ${lastError?.message || '未知错误'}`
const suggestion = lastError instanceof GoManagerError
? lastError.suggestion
: '请检查网络连接,确保可以访问 golang.org。如果问题持续请尝试使用VPN或更换网络环境'
return {
success: false,
message: errorMessage,
suggestion,
retryable: true,
details: { attempts: maxRetries, lastError: lastError?.message }
}
}
/**
* 执行实际的下载操作
* 增强的错误处理和网络状态检测
* Requirements: 8.1
*/
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) {
const errorMsg = `下载失败: HTTP ${response.statusCode}`
let suggestion = '请检查网络连接和下载链接'
if (response.statusCode === 404) {
suggestion = '下载链接不存在,请尝试其他版本或联系支持'
} else if (response.statusCode === 403) {
suggestion = '访问被拒绝可能需要VPN或代理服务器'
} else if (response.statusCode === 500 || response.statusCode === 502 || response.statusCode === 503) {
suggestion = '服务器暂时不可用,请稍后重试'
}
reject(new GoManagerError(
GoErrorType.NETWORK_ERROR,
errorMsg,
suggestion,
true,
{ statusCode: response.statusCode, url: options.url }
))
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 GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`重命名文件失败: ${e}`,
'可能是权限问题或磁盘空间不足,请检查目标目录权限',
false,
{ renameError: e }
))
return
}
}
sendDownloadProgress('go', 100, totalSize, totalSize)
resolve()
})
file.on('error', (err) => {
try {
if (!isResume) {
unlinkSync(options.dest)
}
} catch {}
reject(new GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`文件写入失败: ${err.message}`,
'可能是磁盘空间不足或权限问题,请检查目标目录权限和可用空间',
false,
{ fileError: err.message }
))
})
})
request.on('error', (err) => {
reject(new GoManagerError(
GoErrorType.NETWORK_ERROR,
`网络错误: ${err.message}`,
'请检查网络连接,确保可以访问互联网。如果使用代理,请检查代理设置',
true,
{ networkError: err.message }
))
})
request.on('timeout', () => {
request.destroy()
reject(new GoManagerError(
GoErrorType.NETWORK_ERROR,
'下载超时',
'网络连接较慢或不稳定,请检查网络环境后重试',
true,
{ timeout: options.timeout }
))
})
})
}
/**
* 验证文件的 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, 8.3, 8.5
*/
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}`)
try {
// 确保 GOPATH 工作空间目录存在
await this.ensureGoPathStructure(goPath)
// 使用 PowerShell 脚本更新用户环境变量
const psScript = this.generateEnvironmentUpdateScript(goRoot, goPath, goBin)
await this.executePowerShellScript(psScript)
console.log(`环境变量更新成功: Go ${goVersion}`)
} catch (error: any) {
console.error(`环境变量更新失败: ${error.message}`)
// 根据错误类型提供具体建议
let suggestion = '请以管理员身份运行程序,或手动设置环境变量'
if (error.message.includes('ExecutionPolicy')) {
suggestion = '请以管理员身份运行 PowerShell 并执行: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser'
} else if (error.message.includes('Access') || error.message.includes('权限')) {
suggestion = '权限不足,请以管理员身份运行程序,或在系统设置中手动配置环境变量'
} else if (error.message.includes('timeout')) {
suggestion = '操作超时,请重试。如果问题持续,请手动配置环境变量'
}
throw new GoManagerError(
GoErrorType.ENVIRONMENT_ERROR,
`环境变量更新失败: ${error.message}`,
suggestion,
true,
{
goVersion,
goRoot,
goPath,
goBin,
originalError: error.message
}
)
}
}
/**
* 确保 GOPATH 工作空间目录结构存在
* 创建标准的 Go 工作空间结构src, pkg, bin
* 增强的错误处理
* Requirements: 8.2, 8.3
*/
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) {
let suggestion = '请检查目标目录权限,或选择其他位置作为 GOPATH'
if (error.code === 'EACCES' || error.code === 'EPERM') {
suggestion = '权限不足,请以管理员身份运行程序,或选择用户目录下的位置'
} else if (error.code === 'ENOSPC') {
suggestion = '磁盘空间不足,请清理磁盘空间或选择其他磁盘'
} else if (error.code === 'ENOTDIR') {
suggestion = '路径中存在同名文件,请删除该文件或选择其他路径'
}
throw new GoManagerError(
GoErrorType.FILE_SYSTEM_ERROR,
`创建 GOPATH 结构失败: ${error.message}`,
suggestion,
false,
{ goPath, errorCode: error.code, originalError: 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 中的相关配置
* 增强的错误处理
* Requirements: 8.3, 8.5
*/
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}`)
// 如果是 GoManagerError直接重新抛出
if (error instanceof GoManagerError) {
throw error
}
throw new GoManagerError(
GoErrorType.ENVIRONMENT_ERROR,
`环境变量清理失败: ${error.message}`,
'请以管理员身份运行程序,或在系统设置中手动清理环境变量',
true,
{ goBin, originalError: 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 $_
}
`
}
/**
* 执行 PowerShell 脚本
* 创建临时脚本文件并执行,完成后清理
* 增强的错误处理和权限检查
* Requirements: 8.3, 8.5
*/
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}"`
const { stdout, stderr } = await execAsync(command, { timeout: 30000 })
// 检查 PowerShell 脚本的输出
if (stderr && stderr.trim()) {
console.warn('PowerShell 脚本警告:', stderr)
// 某些警告不应该被视为错误
const ignorableWarnings = [
'WARNING:',
'Get-WmiObject',
'deprecated'
]
const isIgnorable = ignorableWarnings.some(warning =>
stderr.toLowerCase().includes(warning.toLowerCase())
)
if (!isIgnorable && stderr.includes('Error')) {
throw new Error(stderr)
}
}
if (stdout && stdout.trim()) {
console.log('PowerShell 脚本输出:', stdout)
}
} catch (error: any) {
console.error('PowerShell 脚本执行失败:', error)
let suggestion = '请以管理员身份运行程序,或手动配置环境变量'
let errorType = GoErrorType.ENVIRONMENT_ERROR
const errorMessage = error.message.toLowerCase()
if (errorMessage.includes('executionpolicy')) {
suggestion = '执行策略限制,请以管理员身份运行 PowerShell 并执行: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser'
errorType = GoErrorType.PERMISSION_ERROR
} else if (errorMessage.includes('access') || errorMessage.includes('denied') || errorMessage.includes('权限')) {
suggestion = '权限不足,请以管理员身份运行程序。您也可以在系统设置中手动配置环境变量'
errorType = GoErrorType.PERMISSION_ERROR
} else if (errorMessage.includes('timeout')) {
suggestion = '操作超时,可能是系统响应较慢。请重试,或手动配置环境变量'
} else if (errorMessage.includes('not found') || errorMessage.includes('找不到')) {
suggestion = 'PowerShell 不可用,请确保 Windows PowerShell 已正确安装'
}
throw new GoManagerError(
errorType,
`PowerShell 脚本执行失败: ${error.message}`,
suggestion,
true,
{
scriptPath: tempPs1,
command: 'powershell -ExecutionPolicy Bypass',
originalError: 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: '' }
]
}
}