From 8105f9933a86f4848285b8437867c0d318c935dd Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Sat, 7 Feb 2026 04:43:19 +0800 Subject: [PATCH] add socketToWorkerIdSetMap to fix when user process exit, worker is still running; add isReset for ensureIpcPipeName to fix daemon may not be connected --- packages/pm/daemon.js | 30 ++++++++--- packages/ui/src/main/features/run-common.ts | 50 +++++++++++++++---- .../main/flow/OPEN_SETTING_WINDOW/index.ts | 6 +-- .../flow/OPEN_SETTING_WINDOW/launch-daemon.ts | 30 +++++++++-- packages/ui/src/main/index.ts | 19 ++++--- 5 files changed, 104 insertions(+), 31 deletions(-) diff --git a/packages/pm/daemon.js b/packages/pm/daemon.js index 254f4f1..8076929 100644 --- a/packages/pm/daemon.js +++ b/packages/pm/daemon.js @@ -43,9 +43,10 @@ const ipcSocketPath = process.platform === 'win32' : path.join(tmpdir(), `${ipcSocketName}.sock`) const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt } -const guiClients = new Set(); // GUI客户端连接集合 +const userProcessClients = new Set(); // GUI客户端连接集合 const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件 const pidToProcessInfoMap = new Map() +const socketToWorkerIdSetMap = new WeakMap() // 创建TCP服务器 const server = net.createServer((socket) => { @@ -85,7 +86,7 @@ const server = net.createServer((socket) => { socket.on('close', () => { console.log('客户端已断开连接'); // 清理GUI客户端连接 - guiClients.delete(socket); + userProcessClients.delete(socket); // 清理工具进程连接 for (const [workerId, workerInfo] of workers.entries()) { if (workerInfo.socket === socket) { @@ -93,6 +94,10 @@ const server = net.createServer((socket) => { workerInfo.socket = null; } } + const workerIdSet = socketToWorkerIdSetMap.get(socket) || new Set() + ;[...workerIdSet].forEach(workerId => { + stopWorker(workerId); + }) }); }); @@ -107,11 +112,11 @@ function handleMessage(socket, message) { }); return } - if (message.type === 'gui-register') { + if (message.type === 'user-process-register') { // GUI客户端的控制消息 // 标记为GUI客户端 - if (!guiClients.has(socket)) { - guiClients.add(socket); + if (!userProcessClients.has(socket)) { + userProcessClients.add(socket); } sendResponse(socket, _callbackUuid, { success: true, @@ -183,6 +188,17 @@ function handleMessage(socket, message) { message: `工具进程 ${message.workerId} 已启动`, workerId: message.workerId }); + let socketToWorkerIdSet = socketToWorkerIdSetMap.get(socket) + if ( + !(socketToWorkerIdSet instanceof Set) + ) { + socketToWorkerIdSet = new Set() + socketToWorkerIdSetMap.set( + socket, + socketToWorkerIdSet + ) + } + socketToWorkerIdSet.add(workerId) return } @@ -398,13 +414,13 @@ function broadcastStatus() { // 广播消息给所有GUI客户端 function broadcastToGUI(message) { - guiClients.forEach(socket => { + userProcessClients.forEach(socket => { if (!socket.destroyed) { try { sendResponse(socket, null, message); } catch (e) { console.error('广播消息失败:', e); - guiClients.delete(socket); + userProcessClients.delete(socket); } } }); diff --git a/packages/ui/src/main/features/run-common.ts b/packages/ui/src/main/features/run-common.ts index 8166992..5b0e19c 100644 --- a/packages/ui/src/main/features/run-common.ts +++ b/packages/ui/src/main/features/run-common.ts @@ -1,10 +1,37 @@ -import { app } from 'electron' import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat' -import { sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon' +import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon' import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db' +import minimist from 'minimist' export async function runCommon({ mode }) { - app.dock?.hide() + await sendToDaemon( + { + type: 'user-process-register' + }, + { + needCallback: true + } + ) + const taskList = ( + await sendToDaemon( + { + type: 'get-status' + }, + { + needCallback: true + } + ) + )?.workers + const runningTask = taskList?.find((it) => it.workerId === mode) + if (runningTask) { + const commandlineArgs = minimist(runningTask.args ?? []) + const runRecordId = Number(commandlineArgs['run-record-id']) + console.log('任务已在运行中') + return { + runRecordId, + isAlreadyRunning: true + } + } const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data const subProcessEnv = { ...process.env, @@ -30,13 +57,16 @@ export async function runCommon({ mode }) { needCallback: true } ) - ;['SIGINT', 'SIGTERM'].forEach((evName) => { - process.on(evName, () => { - sendToDaemon({ - type: 'stop-worker', - workerId: mode - }) - }) + daemonEE.on('message', (message) => { + if (message.type === 'worker-exited') { + if ( + message.workerId === mode && + !message.restarting && + globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui' + ) { + process.exit(0) + } + } }) return { runRecordId: currentRunRecord?.id diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts index 3ada941..b3eef82 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts @@ -5,9 +5,7 @@ import './app-menu' import initIpc from './ipc' import gtag from '../../utils/gtag' import initPublicIpc from '../../utils/initPublicIpc' -import { launchDaemon } from './launch-daemon' -import { connectToDaemon, sendToDaemon, closeDaemonClient } from './connect-to-daemon' -import { sleep } from "@geekgeekrun/utils/sleep.mjs" +import { sendToDaemon, closeDaemonClient } from './connect-to-daemon' export function openSettingWindow() { // TODO: singleton lock; how can we check if there is another process should run as singleton with arguments? @@ -81,7 +79,7 @@ export function openSettingWindow() { ) await sendToDaemon( { - type: 'gui-register' + type: 'user-process-register' }, { needCallback: true diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts index 7f463c0..0b6a543 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts @@ -8,7 +8,10 @@ import { randomUUID } from 'node:crypto' import { connectToDaemon } from './connect-to-daemon' const isUiDev = process.env.NODE_ENV === 'development' -export async function ensureIpcPipeName() { +export async function ensureIpcPipeName({ isReset } = {}) { + if (isReset) { + await writeStorageFile('ipc-pipe-name', '', { isJson: false }) + } let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false }) if (!ipcPipeName) { ipcPipeName = `geekgeekrun-d_${randomUUID()}` @@ -20,10 +23,11 @@ export async function ensureIpcPipeName() { } export async function launchDaemon() { + let daemonProcess async function startDaemon() { console.log('启动守护进程...') // 添加参数使守护进程在后台运行,不显示 UI - const daemonProcess = spawn( + daemonProcess = spawn( process.argv[0], isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`], { @@ -59,11 +63,29 @@ export async function launchDaemon() { }) }) } + await ensureIpcPipeName() try { await connectToDaemon() } catch (err) { + let isDaemonLaunched = false + console.log('cannot connect to daemon, try to launch it', err) // 启动守护进程 - await startDaemon() - await connectToDaemon() + try { + await startDaemon() + isDaemonLaunched = true + } catch (err) { + console.log('cannot launch to daemon, try to change port', err) + daemonProcess?.kill('SIGKILL') + await ensureIpcPipeName({ isReset: true }) + try { + await startDaemon() + isDaemonLaunched = true + } catch (err) { + console.log('cannot launch to daemon, try to change port failed', err) + } + } + if (isDaemonLaunched) { + await connectToDaemon() + } } } diff --git a/packages/ui/src/main/index.ts b/packages/ui/src/main/index.ts index d040b17..fd787bd 100644 --- a/packages/ui/src/main/index.ts +++ b/packages/ui/src/main/index.ts @@ -1,6 +1,7 @@ import minimist from 'minimist' import { runCommon } from './features/run-common'; -import { ensureIpcPipeName, launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'; +import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'; +import { app } from 'electron'; // 捕获未处理的 EPIPE 错误 process.on('uncaughtException', (err) => { @@ -58,19 +59,25 @@ const runMode = commandlineArgs['mode']; // #region user entry case 'geekAutoStartWithBoss': { - await ensureIpcPipeName() + app.dock?.hide() await launchDaemon() - await runCommon({ mode: 'geekAutoStartWithBossMain' }) + const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' }) + if (isAlreadyRunning) { + process.exit(0) + } break } case 'readNoReplyAutoReminder': { - await ensureIpcPipeName() + app.dock?.hide() await launchDaemon() - await runCommon({ mode: 'readNoReplyAutoReminderMain' }) + const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' }) + if (isAlreadyRunning) { + process.exit(0) + } break } default: { - await ensureIpcPipeName() + globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui' await launchDaemon() const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index') openSettingWindow()