From a91146c4e9cec191c7959fa61e7a6d4c65bf7349 Mon Sep 17 00:00:00 2001 From: ethanfly Date: Fri, 26 Dec 2025 08:59:59 +0800 Subject: [PATCH] Implement Composer management features in Electron app, including installation, uninstallation, status retrieval, and mirror configuration. Enhance PHP-CGI service management with start/stop capabilities for multiple versions and update UI components to reflect these changes. --- .gitignore | 2 + electron/main.ts | 22 ++ electron/preload.ts | 20 +- electron/services/ConfigStore.ts | 4 + electron/services/MysqlManager.ts | 6 +- electron/services/NginxManager.ts | 26 +- electron/services/PhpManager.ts | 459 ++++++++++++++++++++++++++++ electron/services/ServiceManager.ts | 178 +++++++++-- src/App.vue | 40 +-- src/stores/serviceStore.ts | 241 +++++++++++++++ src/views/Dashboard.vue | 348 +++++++++++++++++---- src/views/MysqlManager.vue | 5 + src/views/NginxManager.vue | 5 + src/views/PhpManager.vue | 257 ++++++++++++++++ src/views/RedisManager.vue | 5 + src/views/SitesManager.vue | 254 ++++++++++++++- 16 files changed, 1755 insertions(+), 117 deletions(-) create mode 100644 src/stores/serviceStore.ts diff --git a/.gitignore b/.gitignore index c0beecb..73b8e09 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ Thumbs.db # Electron *.asar + +data/ \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts index 76ad0fe..4febe1d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -362,6 +362,19 @@ ipcMain.handle("php:saveConfig", (_, version: string, config: string) => phpManager.saveConfig(version, config) ); +// ==================== Composer 管理 ==================== +ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus()); +ipcMain.handle("composer:install", () => phpManager.installComposer()); +ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer()); +ipcMain.handle("composer:setMirror", (_, mirror: string) => + phpManager.setComposerMirror(mirror) +); +ipcMain.handle( + "composer:createLaravelProject", + (_, projectName: string, targetDir: string) => + phpManager.createLaravelProject(projectName, targetDir) +); + // ==================== MySQL 管理 ==================== ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions()); ipcMain.handle("mysql:getAvailableVersions", () => @@ -489,6 +502,15 @@ ipcMain.handle("service:getAutoStart", (_, service: string) => ); ipcMain.handle("service:startAll", () => serviceManager.startAll()); ipcMain.handle("service:stopAll", () => serviceManager.stopAll()); +// PHP-CGI 管理 - 支持多版本 +ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus()); +ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi()); +ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi()); +ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi()); +ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi()); +ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version)); +ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version)); +ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(version)); // ==================== Hosts 管理 ==================== ipcMain.handle("hosts:get", () => hostsManager.getHosts()); diff --git a/electron/preload.ts b/electron/preload.ts index 6cfdeba..089e522 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -39,6 +39,15 @@ contextBridge.exposeInMainWorld('electronAPI', { saveConfig: (version: string, config: string) => ipcRenderer.invoke('php:saveConfig', version, config) }, + // Composer 管理 + composer: { + getStatus: () => ipcRenderer.invoke('composer:getStatus'), + install: () => ipcRenderer.invoke('composer:install'), + uninstall: () => ipcRenderer.invoke('composer:uninstall'), + setMirror: (mirror: string) => ipcRenderer.invoke('composer:setMirror', mirror), + createLaravelProject: (projectName: string, targetDir: string) => ipcRenderer.invoke('composer:createLaravelProject', projectName, targetDir) + }, + // MySQL 管理 mysql: { getVersions: () => ipcRenderer.invoke('mysql:getVersions'), @@ -107,7 +116,16 @@ contextBridge.exposeInMainWorld('electronAPI', { setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled), getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service), startAll: () => ipcRenderer.invoke('service:startAll'), - stopAll: () => ipcRenderer.invoke('service:stopAll') + stopAll: () => ipcRenderer.invoke('service:stopAll'), + // PHP-CGI 多版本管理 + getPhpCgiStatus: () => ipcRenderer.invoke('service:getPhpCgiStatus'), + startPhpCgi: () => ipcRenderer.invoke('service:startPhpCgi'), + stopPhpCgi: () => ipcRenderer.invoke('service:stopPhpCgi'), + startAllPhpCgi: () => ipcRenderer.invoke('service:startAllPhpCgi'), + stopAllPhpCgi: () => ipcRenderer.invoke('service:stopAllPhpCgi'), + startPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:startPhpCgiVersion', version), + stopPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:stopPhpCgiVersion', version), + getPhpCgiPort: (version: string) => ipcRenderer.invoke('service:getPhpCgiPort', version) }, // Hosts 管理 diff --git a/electron/services/ConfigStore.ts b/electron/services/ConfigStore.ts index e5d9366..b8e4747 100644 --- a/electron/services/ConfigStore.ts +++ b/electron/services/ConfigStore.ts @@ -19,6 +19,8 @@ interface ConfigSchema { // 应用设置 autoLaunch: boolean; startMinimized: boolean; + // Composer 设置 + composerMirror: string; } export interface SiteConfig { @@ -66,6 +68,8 @@ export class ConfigStore { // 应用设置默认值 autoLaunch: false, startMinimized: false, + // Composer 镜像(空为官方源) + composerMirror: "", }, }); diff --git a/electron/services/MysqlManager.ts b/electron/services/MysqlManager.ts index 255b68c..3797c44 100644 --- a/electron/services/MysqlManager.ts +++ b/electron/services/MysqlManager.ts @@ -341,12 +341,16 @@ export class MysqlManager { } catch (pwdError: any) { console.log('设置默认密码失败,root密码为空:', pwdError.message) } + + // 设置密码后停止 MySQL,让用户手动启动 + console.log('停止 MySQL 服务...') + await this.stop(version) } console.log(`MySQL ${version} 安装完成`) return { success: true, - message: `MySQL ${version} 安装成功!\n\n连接信息:\n• 主机:localhost\n• 端口:3306\n• 用户:root\n• 密码:123456` + message: `MySQL ${version} 安装成功!\n\n连接信息:\n• 主机:localhost\n• 端口:3306\n• 用户:root\n• 密码:123456\n\n注意:MySQL 已停止,请手动启动服务。` } } catch (error: any) { console.error('MySQL 安装失败:', error) diff --git a/electron/services/NginxManager.ts b/electron/services/NginxManager.ts index ba448b6..d968f06 100644 --- a/electron/services/NginxManager.ts +++ b/electron/services/NginxManager.ts @@ -33,6 +33,20 @@ export class NginxManager { this.configStore = configStore } + /** + * 根据 PHP 版本获取 FastCGI 端口 + * PHP 8.0.x -> 9080, PHP 8.1.x -> 9081, etc. + */ + private getPhpCgiPort(version: string): number { + const match = version.match(/^(\d+)\.(\d+)/) + if (match) { + const major = parseInt(match[1]) + const minor = parseInt(match[2]) + return 9000 + major * 10 + minor // 8.5 -> 9085, 8.4 -> 9084, 8.3 -> 9083 + } + return 9000 + } + /** * 获取已安装的 Nginx 版本列表 */ @@ -612,8 +626,10 @@ http { private generateSiteConfig(site: SiteConfig): string { const phpPath = this.configStore.getPhpPath(site.phpVersion) const logsPath = this.configStore.getLogsPath() + const phpCgiPort = this.getPhpCgiPort(site.phpVersion) let config = ` +# PHP Version: ${site.phpVersion} -> Port ${phpCgiPort} server { listen 80; server_name ${site.domain}; @@ -628,7 +644,7 @@ server { } location ~ \\.php$ { - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass 127.0.0.1:${phpCgiPort}; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; @@ -662,7 +678,7 @@ server { } location ~ \\.php$ { - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass 127.0.0.1:${phpCgiPort}; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; @@ -682,8 +698,10 @@ server { const logsPath = this.configStore.getLogsPath() // Laravel 项目 public 目录 const publicPath = join(site.rootPath, 'public').replace(/\\/g, '/') + const phpCgiPort = this.getPhpCgiPort(site.phpVersion) let config = ` +# Laravel Project - PHP Version: ${site.phpVersion} -> Port ${phpCgiPort} server { listen 80; server_name ${site.domain}; @@ -708,7 +726,7 @@ server { error_page 404 /index.php; location ~ \\.php$ { - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass 127.0.0.1:${phpCgiPort}; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } @@ -752,7 +770,7 @@ server { error_page 404 /index.php; location ~ \\.php$ { - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass 127.0.0.1:${phpCgiPort}; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; } diff --git a/electron/services/PhpManager.ts b/electron/services/PhpManager.ts index cc76d4a..3d78050 100644 --- a/electron/services/PhpManager.ts +++ b/electron/services/PhpManager.ts @@ -2060,4 +2060,463 @@ if ($verifyPath -and $verifyPath.Contains($NewPhpPath)) { rmdirSync(dir); } } + + // ==================== Composer 管理 ==================== + + /** + * 获取 Composer 状态 + */ + async getComposerStatus(): Promise<{ + installed: boolean; + version?: string; + path?: string; + mirror?: string; + }> { + const composerPath = this.getComposerPath(); + const composerBatPath = join(this.configStore.getBasePath(), "tools", "composer.bat"); + const mirror = this.configStore.get("composerMirror") || ""; + + console.log("检查 Composer 路径:", composerPath); + + if (!existsSync(composerPath)) { + console.log("Composer 未安装"); + return { installed: false, mirror }; + } + + let version: string | undefined; + + // 方法1: 尝试直接使用 composer.bat(如果在 PATH 中) + try { + console.log("尝试使用 composer --version..."); + const { stdout } = await execAsync("composer --version", { + windowsHide: true, + timeout: 15000, + encoding: "utf8", + }); + console.log("Composer 输出:", stdout); + + // 解析版本号 - 支持多种格式 + // "Composer version 2.9-dev+9497eca6e15b115d25833c68b7c5c76589953b65 (2.9-dev)" + // "Composer version 2.7.1 2024-01-01" + const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/); + if (versionMatch) { + version = versionMatch[1]; + console.log("解析到版本:", version); + return { installed: true, version, path: composerPath, mirror }; + } + } catch (e: any) { + console.log("composer 命令不可用,尝试其他方式:", e.message); + } + + // 方法2: 使用 composer.bat + if (existsSync(composerBatPath)) { + try { + console.log("尝试使用 composer.bat..."); + const { stdout } = await execAsync(`"${composerBatPath}" --version`, { + windowsHide: true, + timeout: 15000, + encoding: "utf8", + }); + const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/); + if (versionMatch) { + version = versionMatch[1]; + console.log("解析到版本:", version); + return { installed: true, version, path: composerPath, mirror }; + } + } catch (e: any) { + console.log("composer.bat 执行失败:", e.message); + } + } + + // 方法3: 使用 PHP 运行 composer.phar + try { + const activePhp = this.configStore.get("activePhpVersion"); + if (activePhp) { + const phpPath = this.configStore.getPhpPath(activePhp); + const phpExe = join(phpPath, "php.exe"); + + if (existsSync(phpExe)) { + console.log("尝试使用 PHP 运行 composer.phar..."); + const { stdout } = await execAsync(`"${phpExe}" "${composerPath}" --version`, { + windowsHide: true, + timeout: 15000, + encoding: "utf8", + }); + const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/); + if (versionMatch) { + version = versionMatch[1]; + console.log("解析到版本:", version); + } + } + } + } catch (e: any) { + console.log("PHP 运行 composer.phar 失败:", e.message); + } + + return { installed: true, version, path: composerPath, mirror }; + } + + /** + * 获取 Composer 路径 + */ + private getComposerPath(): string { + return join(this.configStore.getBasePath(), "tools", "composer.phar"); + } + + /** + * 安装 Composer + */ + async installComposer(): Promise<{ success: boolean; message: string }> { + try { + const toolsDir = join(this.configStore.getBasePath(), "tools"); + const composerPath = join(toolsDir, "composer.phar"); + const composerBatPath = join(toolsDir, "composer.bat"); + + // 确保目录存在 + if (!existsSync(toolsDir)) { + mkdirSync(toolsDir, { recursive: true }); + } + + // 下载 Composer + console.log("正在下载 Composer..."); + + // 尝试多个下载源 + const urls = [ + "https://getcomposer.org/composer.phar", + "https://mirrors.aliyun.com/composer/composer.phar", + ]; + + let downloaded = false; + let lastError: Error | null = null; + + for (const url of urls) { + try { + console.log(`尝试从 ${url} 下载...`); + await this.downloadFile(url, composerPath); + downloaded = true; + break; + } catch (e: any) { + console.error(`从 ${url} 下载失败:`, e.message); + lastError = e; + } + } + + if (!downloaded) { + throw lastError || new Error("所有下载源均失败"); + } + + // 验证文件是否下载成功 + if (!existsSync(composerPath)) { + return { success: false, message: "Composer 下载失败,文件不存在" }; + } + + // 创建 composer.bat 批处理文件 + const batContent = `@echo off\r\nphp "%~dp0composer.phar" %*`; + writeFileSync(composerBatPath, batContent); + console.log("创建 composer.bat:", composerBatPath); + + // 添加到环境变量 + await this.addComposerToPath(toolsDir); + + // 验证安装 + const activePhp = this.configStore.get("activePhpVersion"); + if (activePhp) { + const phpPath = this.configStore.getPhpPath(activePhp); + const phpExe = join(phpPath, "php.exe"); + + if (existsSync(phpExe)) { + try { + const { stdout } = await execAsync(`"${phpExe}" "${composerPath}" --version`, { + windowsHide: true, + timeout: 10000, + }); + console.log("Composer 安装成功:", stdout); + } catch (e) { + console.log("Composer 已下载,但验证失败(可能是 PHP 问题)"); + } + } + } + + return { success: true, message: "Composer 安装成功,已添加到系统环境变量" }; + } catch (error: any) { + console.error("Composer 安装失败:", error); + return { success: false, message: `安装失败: ${error.message}` }; + } + } + + /** + * 卸载 Composer + */ + async uninstallComposer(): Promise<{ success: boolean; message: string }> { + try { + const toolsDir = join(this.configStore.getBasePath(), "tools"); + const composerPath = join(toolsDir, "composer.phar"); + const composerBatPath = join(toolsDir, "composer.bat"); + + // 删除文件 + if (existsSync(composerPath)) { + unlinkSync(composerPath); + console.log("已删除:", composerPath); + } + + if (existsSync(composerBatPath)) { + unlinkSync(composerBatPath); + console.log("已删除:", composerBatPath); + } + + // 从环境变量移除 + await this.removeComposerFromPath(toolsDir); + + return { success: true, message: "Composer 已卸载" }; + } catch (error: any) { + console.error("Composer 卸载失败:", error); + return { success: false, message: `卸载失败: ${error.message}` }; + } + } + + /** + * 添加 Composer 到环境变量 + */ + private async addComposerToPath(toolsDir: string): Promise { + try { + const tempScriptPath = join(this.configStore.getTempPath(), "add_composer_path.ps1"); + mkdirSync(this.configStore.getTempPath(), { recursive: true }); + + const psScript = ` +param([string]$ComposerPath) + +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath -eq $null) { $userPath = '' } + +# Check if already exists +if ($userPath.ToLower().Contains($ComposerPath.ToLower())) { + Write-Host "Composer path already in PATH" + exit 0 +} + +# Add to PATH +$newPath = $ComposerPath + ";" + $userPath +[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User') + +Write-Host "SUCCESS: Composer path added to user PATH" +`; + + writeFileSync(tempScriptPath, psScript, "utf-8"); + + const { stdout } = await execAsync( + `powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -ComposerPath "${toolsDir}"`, + { windowsHide: true, timeout: 30000 } + ); + + console.log("Composer PATH 更新:", stdout); + + if (existsSync(tempScriptPath)) { + unlinkSync(tempScriptPath); + } + } catch (error: any) { + console.error("添加 Composer 到 PATH 失败:", error); + } + } + + /** + * 从环境变量移除 Composer + */ + private async removeComposerFromPath(toolsDir: string): Promise { + try { + const tempScriptPath = join(this.configStore.getTempPath(), "remove_composer_path.ps1"); + mkdirSync(this.configStore.getTempPath(), { recursive: true }); + + const psScript = ` +param([string]$ComposerPath) + +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath -eq $null) { exit 0 } + +$paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_.ToLower() -ne $ComposerPath.ToLower() } +$newPath = $paths -join ';' + +[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User') + +Write-Host "SUCCESS: Composer path removed from user PATH" +`; + + writeFileSync(tempScriptPath, psScript, "utf-8"); + + const { stdout } = await execAsync( + `powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -ComposerPath "${toolsDir}"`, + { windowsHide: true, timeout: 30000 } + ); + + console.log("Composer PATH 移除:", stdout); + + if (existsSync(tempScriptPath)) { + unlinkSync(tempScriptPath); + } + } catch (error: any) { + console.error("从 PATH 移除 Composer 失败:", error); + } + } + + /** + * 设置 Composer 镜像 + */ + async setComposerMirror( + mirror: string + ): Promise<{ success: boolean; message: string }> { + try { + const activePhp = this.configStore.get("activePhpVersion"); + if (!activePhp) { + return { success: false, message: "请先设置默认 PHP 版本" }; + } + + const composerPath = this.getComposerPath(); + if (!existsSync(composerPath)) { + return { success: false, message: "Composer 未安装" }; + } + + const phpPath = this.configStore.getPhpPath(activePhp); + const phpExe = join(phpPath, "php.exe"); + + // 检查 PHP 是否存在 + if (!existsSync(phpExe)) { + return { success: false, message: `PHP 未找到: ${phpExe}` }; + } + + // 先移除旧的镜像配置 + try { + await execAsync( + `"${phpExe}" "${composerPath}" config -g --unset repos.packagist`, + { windowsHide: true, timeout: 30000 } + ); + } catch (e) { + // 忽略移除失败 + } + + if (mirror) { + // 设置新镜像 + await execAsync( + `"${phpExe}" "${composerPath}" config -g repos.packagist composer ${mirror}`, + { windowsHide: true, timeout: 30000 } + ); + } + + // 保存到配置 + this.configStore.set("composerMirror", mirror); + + const mirrorName = this.getMirrorName(mirror); + return { + success: true, + message: mirror ? `已设置镜像为: ${mirrorName}` : "已恢复为官方源", + }; + } catch (error: any) { + console.error("设置镜像失败:", error); + // 即使命令执行失败,也保存配置(因为镜像配置主要在创建项目时使用) + this.configStore.set("composerMirror", mirror); + const mirrorName = this.getMirrorName(mirror); + return { + success: true, + message: mirror ? `镜像配置已保存: ${mirrorName}` : "已恢复为官方源", + }; + } + } + + /** + * 获取镜像名称 + */ + private getMirrorName(mirror: string): string { + const mirrors: Record = { + "https://mirrors.aliyun.com/composer/": "阿里云镜像", + "https://mirrors.cloud.tencent.com/composer/": "腾讯云镜像", + "https://mirrors.huaweicloud.com/repository/php/": "华为云镜像", + "https://packagist.phpcomposer.com": "中国全量镜像", + }; + return mirrors[mirror] || mirror; + } + + /** + * 创建 Laravel 项目 + */ + async createLaravelProject( + projectName: string, + targetDir: string + ): Promise<{ success: boolean; message: string; projectPath?: string }> { + try { + const activePhp = this.configStore.get("activePhpVersion"); + if (!activePhp) { + return { success: false, message: "请先设置默认 PHP 版本" }; + } + + const composerPath = this.getComposerPath(); + if (!existsSync(composerPath)) { + return { success: false, message: "Composer 未安装,请先安装 Composer" }; + } + + const phpPath = this.configStore.getPhpPath(activePhp); + const phpExe = join(phpPath, "php.exe"); + + // 检查 PHP 是否存在 + if (!existsSync(phpExe)) { + return { success: false, message: `PHP 未找到: ${phpExe}` }; + } + + // 确保目标目录存在 + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } + + const projectPath = join(targetDir, projectName); + + // 检查项目目录是否已存在 + if (existsSync(projectPath)) { + return { success: false, message: `项目目录已存在: ${projectPath}` }; + } + + console.log(`创建 Laravel 项目: ${projectName} 在 ${targetDir}`); + + // 获取镜像配置 + const mirror = this.configStore.get("composerMirror"); + let repoOption = ""; + if (mirror) { + // 使用环境变量设置镜像 + repoOption = `--repository=${mirror}`; + } + + // 使用 composer create-project 创建 Laravel 项目 + const cmd = `"${phpExe}" "${composerPath}" create-project --prefer-dist ${repoOption} laravel/laravel "${projectName}"`; + + console.log("执行命令:", cmd); + + const { stdout, stderr } = await execAsync(cmd, { + cwd: targetDir, + windowsHide: true, + timeout: 600000, // 10 分钟超时 + env: { + ...process.env, + COMPOSER_PROCESS_TIMEOUT: "600", + COMPOSER_HOME: join(this.configStore.getBasePath(), "tools", "composer"), + }, + }); + + console.log("Composer 输出:", stdout); + if (stderr) console.log("Composer stderr:", stderr); + + // 验证项目创建成功 + if (existsSync(join(projectPath, "artisan"))) { + return { + success: true, + message: `Laravel 项目 "${projectName}" 创建成功`, + projectPath, + }; + } else { + return { success: false, message: "项目创建失败,请查看日志" }; + } + } catch (error: any) { + console.error("创建 Laravel 项目失败:", error); + let errorMsg = error.message; + if (error.stderr) { + errorMsg = error.stderr; + } + return { success: false, message: `创建失败: ${errorMsg}` }; + } + } } diff --git a/electron/services/ServiceManager.ts b/electron/services/ServiceManager.ts index b83d344..f85b31b 100644 --- a/electron/services/ServiceManager.ts +++ b/electron/services/ServiceManager.ts @@ -213,6 +213,23 @@ export class ServiceManager { } } + // 如果 Nginx 启动了,自动启动所有 PHP-CGI + if (autoStart.nginx) { + const phpVersions = this.configStore.get('phpVersions') + for (const version of phpVersions) { + const phpPath = this.configStore.getPhpPath(version) + const phpCgi = join(phpPath, 'php-cgi.exe') + if (existsSync(phpCgi)) { + const port = this.getPhpCgiPort(version) + const isRunning = await this.checkPort(port) + if (!isRunning) { + await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath) + details.push(`PHP-CGI ${version} 已自动启动 (端口 ${port})`) + } + } + } + } + if (details.length === 0) { return { success: true, message: '没有需要自动启动的服务', details } } @@ -277,6 +294,23 @@ export class ServiceManager { } } + // 启动所有已安装 PHP 版本的 CGI 进程 + const phpVersions = this.configStore.get('phpVersions') + for (const version of phpVersions) { + const phpPath = this.configStore.getPhpPath(version) + const phpCgi = join(phpPath, 'php-cgi.exe') + if (existsSync(phpCgi)) { + const port = this.getPhpCgiPort(version) + const isRunning = await this.checkPort(port) + if (!isRunning) { + await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath) + details.push(`PHP-CGI ${version} 已启动 (端口 ${port})`) + } else { + details.push(`PHP-CGI ${version} 已在运行 (端口 ${port})`) + } + } + } + if (details.length === 0) { return { success: true, message: '没有已安装的服务', details } } @@ -340,37 +374,51 @@ export class ServiceManager { } /** - * 启动 PHP-CGI 进程(FastCGI) + * 根据 PHP 版本获取 FastCGI 端口 + * PHP 8.0.x -> 9080, PHP 8.1.x -> 9081, etc. */ - async startPhpCgi(): Promise<{ success: boolean; message: string }> { - try { - const activePhp = this.configStore.get('activePhpVersion') - if (!activePhp) { - return { success: false, message: '未设置活动的 PHP 版本' } - } + getPhpCgiPort(version: string): number { + // 提取主版本号,如 "8.5.1" -> "8.5" -> 85 + const match = version.match(/^(\d+)\.(\d+)/) + if (match) { + const major = parseInt(match[1]) + const minor = parseInt(match[2]) + return 9000 + major * 10 + minor // 8.5 -> 9085, 8.4 -> 9084, 8.3 -> 9083 + } + return 9000 + } - const phpPath = this.configStore.getPhpPath(activePhp) + /** + * 启动指定版本的 PHP-CGI 进程 + */ + async startPhpCgiVersion(version: string): Promise<{ success: boolean; message: string }> { + try { + const phpPath = this.configStore.getPhpPath(version) const phpCgi = join(phpPath, 'php-cgi.exe') if (!existsSync(phpCgi)) { - return { success: false, message: 'php-cgi.exe 不存在' } + return { success: false, message: `PHP ${version} 的 php-cgi.exe 不存在` } } - // 检查是否已在运行 - if (await this.checkProcess('php-cgi.exe')) { - return { success: true, message: 'PHP-CGI 已经在运行' } + const port = this.getPhpCgiPort(version) + + // 检查端口是否已被占用 + const isPortInUse = await this.checkPort(port) + if (isPortInUse) { + return { success: true, message: `PHP-CGI ${version} 已在端口 ${port} 运行` } } // 启动 PHP-CGI - await this.startProcess(phpCgi, ['-b', '127.0.0.1:9000'], phpPath) + await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath) // 等待启动 await new Promise(resolve => setTimeout(resolve, 1000)) - if (await this.checkProcess('php-cgi.exe')) { - return { success: true, message: 'PHP-CGI 启动成功' } + const started = await this.checkPort(port) + if (started) { + return { success: true, message: `PHP-CGI ${version} 启动成功 (端口 ${port})` } } else { - return { success: false, message: 'PHP-CGI 启动失败' } + return { success: false, message: `PHP-CGI ${version} 启动失败` } } } catch (error: any) { return { success: false, message: `启动失败: ${error.message}` } @@ -378,20 +426,104 @@ export class ServiceManager { } /** - * 停止 PHP-CGI 进程 + * 停止指定版本的 PHP-CGI 进程 */ - async stopPhpCgi(): Promise<{ success: boolean; message: string }> { + async stopPhpCgiVersion(version: string): Promise<{ success: boolean; message: string }> { try { - await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }) - return { success: true, message: 'PHP-CGI 已停止' } - } catch (error: any) { - if (error.message.includes('not found')) { - return { success: true, message: 'PHP-CGI 未运行' } + const port = this.getPhpCgiPort(version) + // 查找并结束监听该端口的进程 + try { + const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`, { windowsHide: true }) + const lines = stdout.split('\n').filter(line => line.includes('LISTENING')) + for (const line of lines) { + const parts = line.trim().split(/\s+/) + const pid = parts[parts.length - 1] + if (pid && /^\d+$/.test(pid)) { + await execAsync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }).catch(() => {}) + } + } + } catch (e) { + // 端口可能未被使用 } + return { success: true, message: `PHP-CGI ${version} 已停止` } + } catch (error: any) { return { success: false, message: `停止失败: ${error.message}` } } } + /** + * 启动所有已安装 PHP 版本的 CGI 进程 + */ + async startAllPhpCgi(): Promise<{ success: boolean; message: string; details: string[] }> { + const details: string[] = [] + const phpVersions = this.configStore.get('phpVersions') + + for (const version of phpVersions) { + const result = await this.startPhpCgiVersion(version) + details.push(`PHP ${version}: ${result.message}`) + } + + return { success: true, message: '所有 PHP-CGI 启动完成', details } + } + + /** + * 停止所有 PHP-CGI 进程 + */ + async stopAllPhpCgi(): Promise<{ success: boolean; message: string }> { + try { + await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => {}) + return { success: true, message: '所有 PHP-CGI 已停止' } + } catch (error: any) { + return { success: true, message: 'PHP-CGI 未运行' } + } + } + + /** + * 检查端口是否被占用 + */ + private async checkPort(port: number): Promise { + try { + const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`, { windowsHide: true }) + return stdout.includes('LISTENING') + } catch (e) { + return false + } + } + + /** + * 启动 PHP-CGI 进程(FastCGI)- 兼容旧接口,启动默认版本 + */ + async startPhpCgi(): Promise<{ success: boolean; message: string }> { + const activePhp = this.configStore.get('activePhpVersion') + if (!activePhp) { + return { success: false, message: '未设置活动的 PHP 版本' } + } + return this.startPhpCgiVersion(activePhp) + } + + /** + * 停止 PHP-CGI 进程 - 兼容旧接口,停止所有 + */ + async stopPhpCgi(): Promise<{ success: boolean; message: string }> { + return this.stopAllPhpCgi() + } + + /** + * 获取所有 PHP-CGI 状态 + */ + async getPhpCgiStatus(): Promise<{ version: string; port: number; running: boolean }[]> { + const phpVersions = this.configStore.get('phpVersions') + const status: { version: string; port: number; running: boolean }[] = [] + + for (const version of phpVersions) { + const port = this.getPhpCgiPort(version) + const running = await this.checkPort(port) + status.push({ version, port, running }) + } + + return status + } + // ==================== 私有方法 ==================== private async checkProcess(name: string): Promise { diff --git a/src/App.vue b/src/App.vue index de1142f..cd706c3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -73,19 +73,22 @@ @@ -593,5 +780,58 @@ onMounted(() => { color: var(--accent-color); } } + +.no-php-hint { + font-size: 13px; + color: var(--text-muted); + font-style: italic; +} + +.php-cgi-section { + margin-bottom: 24px; + + .section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + } + + .section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + } + + .section-actions { + display: flex; + gap: 8px; + } +} + +.php-cgi-card { + .port-info { + font-size: 12px; + color: var(--text-muted); + font-family: 'Fira Code', monospace; + margin-bottom: 4px; + } +} + +.php-cgi-empty { + margin-bottom: 24px; + + a { + color: var(--accent-color); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/src/views/MysqlManager.vue b/src/views/MysqlManager.vue index ad91f7d..c94c197 100644 --- a/src/views/MysqlManager.vue +++ b/src/views/MysqlManager.vue @@ -220,6 +220,9 @@