phper/electron/services/ServiceManager.ts

602 lines
20 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, spawn } from 'child_process'
import { promisify } from 'util'
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs'
import { join } from 'path'
const execAsync = promisify(exec)
interface ServiceStatus {
name: string
displayName: string
running: boolean
autoStart: boolean
}
export class ServiceManager {
private configStore: ConfigStore
private startupDir: string
constructor(configStore: ConfigStore) {
this.configStore = configStore
// Windows 启动目录
this.startupDir = join(process.env.APPDATA || '', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
}
/**
* 获取所有服务状态
*/
async getAllServices(): Promise<ServiceStatus[]> {
const services: ServiceStatus[] = []
// 检查 Nginx
const nginxPath = this.configStore.getNginxPath()
if (existsSync(join(nginxPath, 'nginx.exe'))) {
const running = await this.checkProcess('nginx.exe')
const autoStart = this.checkAutoStart('nginx')
services.push({
name: 'nginx',
displayName: 'Nginx',
running,
autoStart
})
}
// 检查 MySQL
const mysqlVersions = this.configStore.get('mysqlVersions')
for (const version of mysqlVersions) {
const mysqlPath = this.configStore.getMysqlPath(version)
if (existsSync(join(mysqlPath, 'bin', 'mysqld.exe'))) {
const running = await this.checkProcess('mysqld.exe')
const autoStart = this.checkAutoStart(`mysql-${version}`)
services.push({
name: `mysql-${version}`,
displayName: `MySQL ${version}`,
running,
autoStart
})
}
}
// 检查 Redis
const redisPath = this.configStore.getRedisPath()
if (existsSync(join(redisPath, 'redis-server.exe'))) {
const running = await this.checkProcess('redis-server.exe')
const autoStart = this.checkAutoStart('redis')
services.push({
name: 'redis',
displayName: 'Redis',
running,
autoStart
})
}
// 检查 PHP-CGI
const activePhp = this.configStore.get('activePhpVersion')
if (activePhp) {
const phpPath = this.configStore.getPhpPath(activePhp)
if (existsSync(join(phpPath, 'php-cgi.exe'))) {
const running = await this.checkProcess('php-cgi.exe')
const autoStart = this.checkAutoStart('php-cgi')
services.push({
name: 'php-cgi',
displayName: `PHP-CGI (${activePhp})`,
running,
autoStart
})
}
}
return services
}
/**
* 设置服务开机自启
*/
async setAutoStart(service: string, enabled: boolean): Promise<{ success: boolean; message: string }> {
try {
const batPath = join(this.startupDir, `phper-${service}.bat`)
if (enabled) {
// 创建启动脚本
let script = '@echo off\n'
script += `cd /d "${this.configStore.getBasePath()}"\n`
if (service === 'nginx') {
const nginxPath = this.configStore.getNginxPath()
script += `start "" /B "${join(nginxPath, 'nginx.exe')}"\n`
} else if (service.startsWith('mysql-')) {
const version = service.replace('mysql-', '')
const mysqlPath = this.configStore.getMysqlPath(version)
script += `start "" /B "${join(mysqlPath, 'bin', 'mysqld.exe')}" --defaults-file="${join(mysqlPath, 'my.ini')}"\n`
} else if (service === 'redis') {
const redisPath = this.configStore.getRedisPath()
script += `start "" /B "${join(redisPath, 'redis-server.exe')}" "${join(redisPath, 'redis.windows.conf')}"\n`
} else if (service === 'php-cgi') {
const activePhp = this.configStore.get('activePhpVersion')
if (activePhp) {
const phpPath = this.configStore.getPhpPath(activePhp)
script += `start "" /B "${join(phpPath, 'php-cgi.exe')}" -b 127.0.0.1:9000\n`
}
}
writeFileSync(batPath, script)
// 更新配置
const autoStart = this.configStore.get('autoStart')
if (service === 'nginx') autoStart.nginx = true
else if (service.startsWith('mysql')) autoStart.mysql = true
else if (service === 'redis') autoStart.redis = true
this.configStore.set('autoStart', autoStart)
return { success: true, message: `${service} 开机自启已启用` }
} else {
// 删除启动脚本
if (existsSync(batPath)) {
const { unlinkSync } = await import('fs')
unlinkSync(batPath)
}
// 更新配置
const autoStart = this.configStore.get('autoStart')
if (service === 'nginx') autoStart.nginx = false
else if (service.startsWith('mysql')) autoStart.mysql = false
else if (service === 'redis') autoStart.redis = false
this.configStore.set('autoStart', autoStart)
return { success: true, message: `${service} 开机自启已禁用` }
}
} catch (error: any) {
return { success: false, message: `设置失败: ${error.message}` }
}
}
/**
* 检查服务是否开机自启
*/
getAutoStart(service: string): boolean {
return this.checkAutoStart(service)
}
/**
* 根据配置启动设置为自启动的服务
*/
async startAutoStartServices(): Promise<{ success: boolean; message: string; details: string[] }> {
const details: string[] = []
const autoStart = this.configStore.get('autoStart')
try {
// 检查 Nginx 自启动
if (autoStart.nginx) {
const nginxPath = this.configStore.getNginxPath()
if (existsSync(join(nginxPath, 'nginx.exe'))) {
if (!(await this.checkProcess('nginx.exe'))) {
await this.startProcess(join(nginxPath, 'nginx.exe'), [], nginxPath)
details.push('Nginx 已自动启动')
}
}
}
// 检查 MySQL 自启动
if (autoStart.mysql) {
const mysqlVersions = this.configStore.get('mysqlVersions')
if (mysqlVersions.length > 0) {
if (!(await this.checkProcess('mysqld.exe'))) {
for (const version of mysqlVersions) {
const mysqlPath = this.configStore.getMysqlPath(version)
const mysqld = join(mysqlPath, 'bin', 'mysqld.exe')
if (existsSync(mysqld)) {
// 使用 VBScript 隐藏窗口启动
const vbsPath = join(mysqlPath, 'start_mysql.vbs')
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${mysqld}"" --defaults-file=""${join(mysqlPath, 'my.ini')}""", 0, False`
writeFileSync(vbsPath, vbsContent)
await execAsync(`cscript //nologo "${vbsPath}"`, { cwd: mysqlPath })
details.push(`MySQL ${version} 已自动启动`)
break // 只启动第一个版本
}
}
}
}
}
// 检查 Redis 自启动
if (autoStart.redis) {
const redisPath = this.configStore.getRedisPath()
const redisServer = join(redisPath, 'redis-server.exe')
if (existsSync(redisServer)) {
if (!(await this.checkProcess('redis-server.exe'))) {
const configFile = join(redisPath, 'redis.windows.conf')
const args = existsSync(configFile) ? ['redis.windows.conf'] : []
await this.startProcess(redisServer, args, redisPath)
details.push('Redis 已自动启动')
}
}
}
// 如果 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 }
}
return { success: true, message: '自动启动服务完成', details }
} catch (error: any) {
return { success: false, message: `自动启动失败: ${error.message}`, details }
}
}
/**
* 启动所有已安装的服务
*/
async startAll(): Promise<{ success: boolean; message: string; details: string[] }> {
const details: string[] = []
try {
// 启动 Nginx
const nginxPath = this.configStore.getNginxPath()
if (existsSync(join(nginxPath, 'nginx.exe'))) {
if (!(await this.checkProcess('nginx.exe'))) {
await this.startProcess(join(nginxPath, 'nginx.exe'), [], nginxPath)
details.push('Nginx 已启动')
} else {
details.push('Nginx 已在运行')
}
}
// 启动 MySQL (启动第一个已安装的版本)
const mysqlVersions = this.configStore.get('mysqlVersions')
if (mysqlVersions.length > 0) {
if (!(await this.checkProcess('mysqld.exe'))) {
for (const version of mysqlVersions) {
const mysqlPath = this.configStore.getMysqlPath(version)
const mysqld = join(mysqlPath, 'bin', 'mysqld.exe')
if (existsSync(mysqld)) {
// 使用 VBScript 隐藏窗口启动
const vbsPath = join(mysqlPath, 'start_mysql.vbs')
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${mysqld}"" --defaults-file=""${join(mysqlPath, 'my.ini')}""", 0, False`
writeFileSync(vbsPath, vbsContent)
await execAsync(`cscript //nologo "${vbsPath}"`, { cwd: mysqlPath })
details.push(`MySQL ${version} 已启动`)
break // 只启动第一个版本
}
}
} else {
details.push('MySQL 已在运行')
}
}
// 启动 Redis
const redisPath = this.configStore.getRedisPath()
const redisServer = join(redisPath, 'redis-server.exe')
if (existsSync(redisServer)) {
if (!(await this.checkProcess('redis-server.exe'))) {
const configFile = join(redisPath, 'redis.windows.conf')
const args = existsSync(configFile) ? ['redis.windows.conf'] : []
await this.startProcess(redisServer, args, redisPath)
details.push('Redis 已启动')
} else {
details.push('Redis 已在运行')
}
}
// 启动所有已安装 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 }
}
return { success: true, message: '服务启动完成', details }
} catch (error: any) {
return { success: false, message: `启动失败: ${error.message}`, details }
}
}
/**
* 停止所有服务
*/
async stopAll(): Promise<{ success: boolean; message: string; details: string[] }> {
const details: string[] = []
try {
// 停止 PHP-CGI
if (await this.checkProcess('php-cgi.exe')) {
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => { })
details.push('PHP-CGI 已停止')
}
// 停止 Nginx
if (await this.checkProcess('nginx.exe')) {
const nginxPath = this.configStore.getNginxPath()
try {
await execAsync(`"${join(nginxPath, 'nginx.exe')}" -s stop`, { cwd: nginxPath, timeout: 5000 })
} catch (e) {
await execAsync('taskkill /F /IM nginx.exe', { timeout: 5000 }).catch(() => { })
}
details.push('Nginx 已停止')
}
// 停止 MySQL
if (await this.checkProcess('mysqld.exe')) {
await execAsync('taskkill /F /IM mysqld.exe', { timeout: 5000 }).catch(() => { })
details.push('MySQL 已停止')
}
// 停止 Redis
if (await this.checkProcess('redis-server.exe')) {
const redisPath = this.configStore.getRedisPath()
const redisCli = join(redisPath, 'redis-cli.exe')
if (existsSync(redisCli)) {
try {
await execAsync(`"${redisCli}" shutdown`, { timeout: 5000 })
} catch (e) {
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => { })
}
} else {
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => { })
}
details.push('Redis 已停止')
}
return { success: true, message: '所有服务已停止', details }
} catch (error: any) {
return { success: false, message: `停止失败: ${error.message}`, details }
}
}
/**
* 根据 PHP 版本获取 FastCGI 端口
* PHP 8.0.x -> 9080, PHP 8.1.x -> 9081, etc.
*/
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
}
/**
* 启动指定版本的 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 ${version} 的 php-cgi.exe 不存在` }
}
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:${port}`], phpPath)
// 等待启动
await new Promise(resolve => setTimeout(resolve, 1000))
const started = await this.checkPort(port)
if (started) {
return { success: true, message: `PHP-CGI ${version} 启动成功 (端口 ${port})` }
} else {
return { success: false, message: `PHP-CGI ${version} 启动失败` }
}
} catch (error: any) {
return { success: false, message: `启动失败: ${error.message}` }
}
}
/**
* 停止指定版本的 PHP-CGI 进程
*/
async stopPhpCgiVersion(version: string): Promise<{ success: boolean; message: string }> {
try {
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<boolean> {
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 状态
* 只返回实际安装的 PHP 版本php-cgi.exe 存在的版本)
*/
async getPhpCgiStatus(): Promise<{ version: string; port: number; running: boolean }[]> {
const status: { version: string; port: number; running: boolean }[] = []
const phpDir = join(this.configStore.getBasePath(), 'php')
// 检查 PHP 目录是否存在
if (!existsSync(phpDir)) {
return status
}
// 扫描实际安装的 PHP 版本
const dirs = readdirSync(phpDir, { withFileTypes: true })
for (const dir of dirs) {
if (dir.isDirectory() && dir.name.startsWith('php-')) {
const version = dir.name.replace('php-', '')
const phpPath = join(phpDir, dir.name)
const phpCgiExe = join(phpPath, 'php-cgi.exe')
// 只有当 php-cgi.exe 存在时才添加到列表
if (existsSync(phpCgiExe)) {
const port = this.getPhpCgiPort(version)
const running = await this.checkPort(port)
status.push({ version, port, running })
}
}
}
// 按版本号排序(降序)
status.sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))
return status
}
// ==================== 私有方法 ====================
private async checkProcess(name: string): Promise<boolean> {
try {
const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${name}" /FO CSV /NH`)
return stdout.includes(name)
} catch (e) {
return false
}
}
private checkAutoStart(service: string): boolean {
const batPath = join(this.startupDir, `phper-${service}.bat`)
return existsSync(batPath)
}
private async startProcess(exe: string, args: string[], cwd: string): Promise<void> {
// 使用 VBScript 来完全隐藏窗口启动进程
const argsStr = args.map(a => `"${a}"`).join(' ')
const command = args.length > 0 ? `"${exe}" ${argsStr}` : `"${exe}"`
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run ${JSON.stringify(command)}, 0, False`
const vbsPath = join(cwd, `start_${Date.now()}.vbs`)
try {
writeFileSync(vbsPath, vbsContent)
await execAsync(`cscript //nologo "${vbsPath}"`, {
cwd,
windowsHide: true,
timeout: 10000
})
// 延迟删除 VBS 文件
setTimeout(() => {
try {
if (existsSync(vbsPath)) {
unlinkSync(vbsPath)
}
} catch (e) {
// 忽略删除失败
}
}, 2000)
} catch (error) {
// 如果 VBS 方式失败,回退到 spawn
const child = spawn(exe, args, {
cwd,
detached: true,
stdio: 'ignore',
windowsHide: true
})
child.unref()
}
}
}