phper/electron/services/GoManager.ts

458 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ConfigStore } from "./ConfigStore";
import { exec } from "child_process";
import { promisify } from "util";
import {
existsSync,
mkdirSync,
readdirSync,
rmSync,
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<GoVersion[]> {
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<AvailableGoVersion[]> {
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<AvailableGoVersion[]> {
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<void> {
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<void> {
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<void> {
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",
},
];
}
}