Files
ZenEcho 489f3d4f1f feat: 新增游戏平台扫描、主题模式与重置功能
- 新增 BattleNet、WeGame、GOG 等游戏平台扫描支持
- 添加系统代理绕过列表和 Bootstrap DNS 配置
- 实现应用重置功能,支持清理数据并重启
- 增加主题模式选择(亮色/暗色/自动)
- 优化本地游戏扫描性能,支持并行扫描
- 改进代理监控,支持链式代理子进程检测
- 修复日志发送时的窗口销毁检查
- 更新图标和侧边栏布局
- 移除 prepare-singbox-core 脚本,简化构建流程
2026-02-22 03:57:55 +08:00

262 lines
6.6 KiB
TypeScript

import { exec, execFile } from 'child_process'
import { ipcMain } from 'electron'
import { promisify } from 'util'
const execAsync = promisify(exec)
const PROCESS_EXEC_MAX_BUFFER = 20 * 1024 * 1024
/**
* 进程节点结构
*/
export interface ProcessNode {
path: string
name: string
ppid: number
pid: number
children: ProcessNode[]
}
type FlatProcessNode = {
path: string
name: string
ppid: number
pid: number
}
function toInt(value: unknown): number {
const n = Number.parseInt(String(value ?? '').trim(), 10)
return Number.isFinite(n) ? n : 0
}
function unquoteCsvCell(value: string): string {
const raw = String(value || '').trim()
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
return raw.slice(1, -1).replace(/""/g, '"')
}
return raw
}
/**
* 将扁平进程列表构建为树结构
*/
export function buildProcessTree(flatNodes: FlatProcessNode[]): ProcessNode[] {
const normalized: ProcessNode[] = flatNodes
.map((row) => ({
path: String(row.path || ''),
name: String(row.name || '').trim(),
ppid: toInt(row.ppid),
pid: toInt(row.pid),
children: []
}))
.filter((row) => row.pid > 0 && !!row.name)
const processMap = new Map<number, ProcessNode>()
for (const row of normalized) {
if (!processMap.has(row.pid)) {
processMap.set(row.pid, row)
}
}
const roots: ProcessNode[] = []
for (const row of processMap.values()) {
if (row.ppid > 0 && row.ppid !== row.pid && processMap.has(row.ppid)) {
processMap.get(row.ppid)!.children.push(row)
} else {
roots.push(row)
}
}
return roots
}
/**
* 解析 PowerShell/CIM 的 JSON 输出
*/
export function parsePowerShellProcessJson(stdout: string): ProcessNode[] {
const raw = String(stdout || '').trim()
if (!raw) return []
let parsed: any
try {
parsed = JSON.parse(raw)
} catch {
return []
}
const rows = Array.isArray(parsed) ? parsed : [parsed]
const flat: FlatProcessNode[] = rows.map((row: any) => ({
path: String(row?.ExecutablePath || ''),
name: String(row?.Name || ''),
ppid: toInt(row?.ParentProcessId),
pid: toInt(row?.ProcessId)
}))
return buildProcessTree(flat)
}
/**
* 解析 WMIC CSV 输出
* 支持 ExecutablePath 中包含逗号的情况。
*/
export function parseWmicProcessCsv(stdout: string): ProcessNode[] {
const lines = String(stdout || '')
.split(/\r?\n/g)
.map((line) => line.trim())
.filter(Boolean)
if (lines.length <= 1) return []
const rows = lines.slice(1)
const flat: FlatProcessNode[] = []
for (const line of rows) {
const rawParts = line.split(',')
if (rawParts.length < 5) continue
const parts = rawParts.map(unquoteCsvCell)
const pid = toInt(parts[parts.length - 1])
const ppid = toInt(parts[parts.length - 2])
const name = String(parts[parts.length - 3] || '').trim()
const exePath = parts.slice(1, parts.length - 3).join(',').trim()
if (!pid || !name) continue
flat.push({
path: exePath,
name,
ppid,
pid
})
}
return buildProcessTree(flat)
}
function execFileText(command: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile(
command,
args,
{ windowsHide: true, maxBuffer: PROCESS_EXEC_MAX_BUFFER },
(error, stdout) => {
if (error) {
reject(error)
return
}
resolve(String(stdout || ''))
}
)
})
}
/**
* 进程管理服务
*
* 提供跨平台的进程扫描和进程树获取功能。
* 主要用于识别游戏进程及其父子关系。
*/
export class ProcessService {
constructor() {
this.setupIPC()
}
/**
* 注册 IPC 监听器
*/
private setupIPC() {
ipcMain.handle('process-scan', async () => {
return await this.scanProcesses()
})
ipcMain.handle('process-tree', async () => {
return await this.getProcessTree()
})
}
/**
* 扫描当前运行的所有进程
*
* @returns Promise<string[]> - 返回进程名列表 (如 ['chrome.exe', 'svchost.exe'])
*/
async scanProcesses(): Promise<string[]> {
try {
let command = ''
if (process.platform === 'win32') {
command = 'tasklist /FO CSV /NH'
} else if (process.platform === 'darwin' || process.platform === 'linux') {
command = 'ps -e -o comm='
}
if (!command) return []
const { stdout } = await execAsync(command)
if (process.platform === 'win32') {
// Parse CSV format from tasklist
// "Image Name","PID","Session Name","Session#","Mem Usage"
return stdout
.split('\r\n')
.map(line => {
const match = line.match(/^"([^"]+)"/)
return match ? match[1] : ''
})
.filter(name => name)
} else {
// Parse simple list for unix
return stdout
.split('\n')
.map(line => line.trim())
.filter(name => name)
.map(path => {
// Extract filename from path
const parts = path.split('/')
return parts[parts.length - 1]
})
}
} catch (error) {
console.error('扫描进程失败:', error)
return []
}
}
/**
* 获取系统进程树
*
* 仅支持 Windows 平台。
* 优先使用 PowerShell/CIM,在老系统或受限环境下回退到 WMIC。
* 返回包含 PID, PPID, Name, ExecutablePath 的树状结构或扁平列表。
*
* @returns Promise<any[]> - 进程树根节点列表
*/
async getProcessTree(): Promise<ProcessNode[]> {
if (process.platform !== 'win32') return []
try {
const psScript = "$ErrorActionPreference='Stop'; Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,ExecutablePath | ConvertTo-Json -Compress"
const psOutput = await execFileText('powershell', [
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
psScript
])
const parsed = parsePowerShellProcessJson(psOutput)
if (parsed.length > 0) {
return parsed
}
console.warn('[进程服务] PowerShell/CIM 进程树为空,尝试 WMIC 回退')
} catch (error) {
console.warn('[进程服务] PowerShell/CIM 获取进程树失败,尝试 WMIC 回退:', error)
}
try {
const { stdout } = await execAsync(
'wmic process get ProcessId,ParentProcessId,Name,ExecutablePath /FORMAT:CSV',
{ maxBuffer: PROCESS_EXEC_MAX_BUFFER }
)
return parseWmicProcessCsv(String(stdout || ''))
} catch (error) {
console.error('获取进程树失败:', error)
return []
}
}
}