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