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 { 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 { const result: NetworkCheck = { isConnected: false, canReachGolang: false } try { // 检查基本网络连接 const startTime = Date.now() await new Promise((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, maxRetries: number = 3): Promise { 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 { 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 { 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 { // 检查缓存 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { try { if (existsSync(installDir)) { console.log(`清理失败的安装目录: ${installDir}`) rmSync(installDir, { recursive: true, force: true }) } } catch (error) { console.warn(`清理安装目录失败: ${error}`) } } /** * 更新安装配置 */ private async updateInstallationConfig(version: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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: '' } ] } }