import { ConfigStore } from "./ConfigStore"; import { exec } from "child_process"; import { promisify } from "util"; import { existsSync, mkdirSync, readdirSync, rmSync, unlinkSync, renameSync, } from "fs"; import { join } from "path"; import https from "https"; import http from "http"; import { createWriteStream } from "fs"; import unzipper from "unzipper"; import { sendDownloadProgress } from "../main"; const execAsync = promisify(exec); interface GoVersion { version: string; path: string; isActive: boolean; } interface AvailableGoVersion { version: string; stable: boolean; downloadUrl: string; filename: 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 版本 */ async getInstalledVersions(): Promise { const versions: GoVersion[] = []; const goPath = this.configStore.getGoPath(); const activeVersion = this.configStore.get("activeGoVersion") || ""; if (!existsSync(goPath)) { return versions; } const dirs = readdirSync(goPath, { withFileTypes: true }); for (const dir of dirs) { if (dir.isDirectory() && dir.name.startsWith("go-")) { const versionDir = join(goPath, dir.name); const goExe = join(versionDir, "bin", "go.exe"); if (existsSync(goExe)) { const version = dir.name.replace("go-", ""); versions.push({ version, path: versionDir, isActive: version === activeVersion, }); } } } 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.dev 获取可用版本列表 */ async getAvailableVersions(): Promise { if ( this.versionsCache.length > 0 && Date.now() - this.cacheTime < this.CACHE_DURATION ) { return this.versionsCache; } try { const versions = await this.fetchVersionsFromGoDev(); if (versions.length > 0) { this.versionsCache = versions; this.cacheTime = Date.now(); return versions; } } catch (error) { console.error("[Go] Failed to fetch versions:", error); } return this.getFallbackVersions(); } /** * 从 go.dev/dl API 获取版本列表 */ private async fetchVersionsFromGoDev(): Promise { return new Promise((resolve, reject) => { const url = "https://go.dev/dl/?mode=json"; https .get( url, { headers: { "User-Agent": "PHPer-Dev-Manager/1.0" }, timeout: 30000, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { const releases = JSON.parse(data); const versions: AvailableGoVersion[] = []; for (const rel of releases) { const winZip = rel.files?.find( (f: any) => f.os === "windows" && f.arch === "amd64" && f.filename?.endsWith(".zip"), ); if (winZip) { versions.push({ version: rel.version, stable: rel.stable ?? true, downloadUrl: `https://go.dev/dl/${winZip.filename}`, filename: winZip.filename, }); } } resolve(versions.slice(0, 30)); } catch (e) { reject(e); } }); }, ) .on("error", reject) .on("timeout", () => reject(new Error("Request timeout"))); }); } /** * 安装 Go 版本 */ async install( version: string, downloadUrl: string, ): Promise<{ success: boolean; message: string }> { try { const goPath = this.configStore.getGoPath(); const tempPath = this.configStore.getTempPath(); const zipPath = join(tempPath, `go-${version}.zip`); const extractDir = join(tempPath, `go-extract-${version}`); const versionDir = join(goPath, `go-${version}`); if (!existsSync(goPath)) mkdirSync(goPath, { recursive: true }); if (!existsSync(tempPath)) mkdirSync(tempPath, { recursive: true }); if ( existsSync(versionDir) && existsSync(join(versionDir, "bin", "go.exe")) ) { return { success: false, message: `Go ${version} 已安装` }; } console.log(`[Go] Downloading ${version}...`); await this.downloadFile(downloadUrl, zipPath, `go-${version}`); console.log(`[Go] Extracting ${version}...`); await this.extractZip(zipPath, extractDir); // Go zip 解压后根目录是 "go" 文件夹,需要重命名为 go-version const innerGoDir = join(extractDir, "go"); if (existsSync(innerGoDir)) { renameSync(innerGoDir, versionDir); } else { return { success: false, message: "解压失败:未找到 go 目录" }; } try { unlinkSync(zipPath); rmSync(extractDir, { recursive: true, force: true }); } catch (e) { // 忽略清理错误 } if (!existsSync(join(versionDir, "bin", "go.exe"))) { return { success: false, message: "安装失败:go.exe 不存在" }; } const goVersions = this.configStore.get("goVersions") || []; if (!goVersions.includes(version)) { goVersions.push(version); this.configStore.set("goVersions", goVersions); } if (goVersions.length === 1) { await this.setActive(version); } return { success: true, message: `Go ${version} 安装成功` }; } catch (error: any) { return { success: false, message: `安装失败: ${error.message}` }; } } /** * 卸载 Go 版本 */ async uninstall( version: string, ): Promise<{ success: boolean; message: string }> { try { const goPath = this.configStore.getGoPath(); const versionDir = join(goPath, `go-${version}`); if (!existsSync(versionDir)) { return { success: false, message: `Go ${version} 未安装` }; } const activeVersion = this.configStore.get("activeGoVersion"); if (activeVersion === version) { this.configStore.set("activeGoVersion", ""); } rmSync(versionDir, { recursive: true, force: true }); const goVersions = this.configStore.get("goVersions") || []; const index = goVersions.indexOf(version); if (index > -1) { goVersions.splice(index, 1); this.configStore.set("goVersions", goVersions); } return { success: true, message: `Go ${version} 已卸载` }; } catch (error: any) { return { success: false, message: `卸载失败: ${error.message}` }; } } /** * 设置活动的 Go 版本 */ async setActive( version: string, ): Promise<{ success: boolean; message: string }> { try { const goPath = this.configStore.getGoPath(); const versionDir = join(goPath, `go-${version}`); if (!existsSync(join(versionDir, "bin", "go.exe"))) { return { success: false, message: `Go ${version} 未安装` }; } await this.addToPath(versionDir); this.configStore.set("activeGoVersion", version); return { success: true, message: `已将 Go ${version} 设为默认版本` }; } catch (error: any) { return { success: false, message: `设置失败: ${error.message}` }; } } /** * 获取 Go 信息 */ async getGoInfo( version: string, ): Promise<{ goVersion: string; path: string } | null> { const goPath = this.configStore.getGoPath(); const versionDir = join(goPath, `go-${version}`); const goExe = join(versionDir, "bin", "go.exe"); if (!existsSync(goExe)) return null; try { const { stdout } = await execAsync(`"${goExe}" version`, { timeout: 5000, }); return { goVersion: stdout.trim(), path: versionDir, }; } catch { return null; } } private async downloadFile( url: string, dest: string, name: string, ): Promise { return new Promise((resolve, reject) => { const protocol = url.startsWith("https") ? https : http; const request = protocol.get( url, { headers: { "User-Agent": "PHPer-Dev-Manager/1.0" }, timeout: 600000, }, (response) => { if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; if (redirectUrl) { this.downloadFile(redirectUrl, dest, name) .then(resolve) .catch(reject); return; } } if (response.statusCode !== 200) { reject(new Error(`Download failed: HTTP ${response.statusCode}`)); return; } const totalSize = parseInt( response.headers["content-length"] || "0", 10, ); let downloadedSize = 0; let lastProgressTime = 0; const file = createWriteStream(dest); response.on("data", (chunk) => { downloadedSize += chunk.length; const now = Date.now(); if (now - lastProgressTime > 500) { const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; sendDownloadProgress("go", progress, downloadedSize, totalSize); lastProgressTime = now; } }); response.pipe(file); file.on("finish", () => { file.close(); sendDownloadProgress("go", 100, totalSize, totalSize); resolve(); }); file.on("error", (err) => { unlinkSync(dest); reject(err); }); }, ); request.on("error", reject); request.on("timeout", () => { request.destroy(); reject(new Error("Download timeout")); }); }); } private async extractZip(zipPath: string, destDir: string): Promise { return new Promise((resolve, reject) => { const { createReadStream } = require("fs"); createReadStream(zipPath) .pipe(unzipper.Extract({ path: destDir })) .on("close", resolve) .on("error", reject); }); } private async addToPath(goPath: string): Promise { const binPath = join(goPath, "bin"); const psScript = ` $ErrorActionPreference = 'Stop' $newPath = '${binPath.replace(/\\/g, "\\\\")}' $currentPath = [Environment]::GetEnvironmentVariable('Path', 'User') $pathArray = $currentPath -split ';' | Where-Object { $_ -ne '' } $filteredPaths = $pathArray | Where-Object { $p = $_.ToLower() -not ($p -like '*\\\\go-*\\\\bin*' -or $p -like '*\\\\go\\\\bin*' -or $p -like '*phper*go*') } $newPathArray = @($newPath) + $filteredPaths $finalPath = ($newPathArray | Select-Object -Unique) -join ';' [Environment]::SetEnvironmentVariable('Path', $finalPath, 'User') Write-Output "PATH updated" `; const tempPs1 = join(this.configStore.getTempPath(), "update_go_path.ps1"); const { writeFileSync } = require("fs"); writeFileSync(tempPs1, psScript, "utf-8"); try { await execAsync(`powershell -ExecutionPolicy Bypass -File "${tempPs1}"`, { timeout: 30000, }); } finally { try { unlinkSync(tempPs1); } catch (e) {} } } private getFallbackVersions(): AvailableGoVersion[] { return [ { version: "go1.25.7", stable: true, downloadUrl: "https://go.dev/dl/go1.25.7.windows-amd64.zip", filename: "go1.25.7.windows-amd64.zip", }, { version: "go1.24.13", stable: true, downloadUrl: "https://go.dev/dl/go1.24.13.windows-amd64.zip", filename: "go1.24.13.windows-amd64.zip", }, { version: "go1.23.5", stable: true, downloadUrl: "https://go.dev/dl/go1.23.5.windows-amd64.zip", filename: "go1.23.5.windows-amd64.zip", }, { version: "go1.22.14", stable: true, downloadUrl: "https://go.dev/dl/go1.22.14.windows-amd64.zip", filename: "go1.22.14.windows-amd64.zip", }, ]; } }