- 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
2048 lines
67 KiB
TypeScript
2048 lines
67 KiB
TypeScript
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: '' }
|
||
]
|
||
}
|
||
} |