add socketToWorkerIdSetMap to fix when user process exit, worker is still running; add isReset for ensureIpcPipeName to fix daemon may not be connected

This commit is contained in:
geekgeekrun
2026-02-07 04:43:19 +08:00
parent f2cc87d660
commit 8105f9933a
5 changed files with 104 additions and 31 deletions
+23 -7
View File
@@ -43,9 +43,10 @@ const ipcSocketPath = process.platform === 'win32'
: path.join(tmpdir(), `${ipcSocketName}.sock`) : path.join(tmpdir(), `${ipcSocketName}.sock`)
const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt } 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 stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件
const pidToProcessInfoMap = new Map() const pidToProcessInfoMap = new Map()
const socketToWorkerIdSetMap = new WeakMap()
// 创建TCP服务器 // 创建TCP服务器
const server = net.createServer((socket) => { const server = net.createServer((socket) => {
@@ -85,7 +86,7 @@ const server = net.createServer((socket) => {
socket.on('close', () => { socket.on('close', () => {
console.log('客户端已断开连接'); console.log('客户端已断开连接');
// 清理GUI客户端连接 // 清理GUI客户端连接
guiClients.delete(socket); userProcessClients.delete(socket);
// 清理工具进程连接 // 清理工具进程连接
for (const [workerId, workerInfo] of workers.entries()) { for (const [workerId, workerInfo] of workers.entries()) {
if (workerInfo.socket === socket) { if (workerInfo.socket === socket) {
@@ -93,6 +94,10 @@ const server = net.createServer((socket) => {
workerInfo.socket = null; workerInfo.socket = null;
} }
} }
const workerIdSet = socketToWorkerIdSetMap.get(socket) || new Set()
;[...workerIdSet].forEach(workerId => {
stopWorker(workerId);
})
}); });
}); });
@@ -107,11 +112,11 @@ function handleMessage(socket, message) {
}); });
return return
} }
if (message.type === 'gui-register') { if (message.type === 'user-process-register') {
// GUI客户端的控制消息 // GUI客户端的控制消息
// 标记为GUI客户端 // 标记为GUI客户端
if (!guiClients.has(socket)) { if (!userProcessClients.has(socket)) {
guiClients.add(socket); userProcessClients.add(socket);
} }
sendResponse(socket, _callbackUuid, { sendResponse(socket, _callbackUuid, {
success: true, success: true,
@@ -183,6 +188,17 @@ function handleMessage(socket, message) {
message: `工具进程 ${message.workerId} 已启动`, message: `工具进程 ${message.workerId} 已启动`,
workerId: 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 return
} }
@@ -398,13 +414,13 @@ function broadcastStatus() {
// 广播消息给所有GUI客户端 // 广播消息给所有GUI客户端
function broadcastToGUI(message) { function broadcastToGUI(message) {
guiClients.forEach(socket => { userProcessClients.forEach(socket => {
if (!socket.destroyed) { if (!socket.destroyed) {
try { try {
sendResponse(socket, null, message); sendResponse(socket, null, message);
} catch (e) { } catch (e) {
console.error('广播消息失败:', e); console.error('广播消息失败:', e);
guiClients.delete(socket); userProcessClients.delete(socket);
} }
} }
}); });
+40 -10
View File
@@ -1,10 +1,37 @@
import { app } from 'electron'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat' 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 { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
import minimist from 'minimist'
export async function runCommon({ mode }) { 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 currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
@@ -30,13 +57,16 @@ export async function runCommon({ mode }) {
needCallback: true needCallback: true
} }
) )
;['SIGINT', 'SIGTERM'].forEach((evName) => { daemonEE.on('message', (message) => {
process.on(evName, () => { if (message.type === 'worker-exited') {
sendToDaemon({ if (
type: 'stop-worker', message.workerId === mode &&
workerId: mode !message.restarting &&
}) globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui'
}) ) {
process.exit(0)
}
}
}) })
return { return {
runRecordId: currentRunRecord?.id runRecordId: currentRunRecord?.id
@@ -5,9 +5,7 @@ import './app-menu'
import initIpc from './ipc' import initIpc from './ipc'
import gtag from '../../utils/gtag' import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc' import initPublicIpc from '../../utils/initPublicIpc'
import { launchDaemon } from './launch-daemon' import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
import { connectToDaemon, sendToDaemon, closeDaemonClient } from './connect-to-daemon'
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
export function openSettingWindow() { export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments? // 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( await sendToDaemon(
{ {
type: 'gui-register' type: 'user-process-register'
}, },
{ {
needCallback: true needCallback: true
@@ -8,7 +8,10 @@ import { randomUUID } from 'node:crypto'
import { connectToDaemon } from './connect-to-daemon' import { connectToDaemon } from './connect-to-daemon'
const isUiDev = process.env.NODE_ENV === 'development' 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 }) let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false })
if (!ipcPipeName) { if (!ipcPipeName) {
ipcPipeName = `geekgeekrun-d_${randomUUID()}` ipcPipeName = `geekgeekrun-d_${randomUUID()}`
@@ -20,10 +23,11 @@ export async function ensureIpcPipeName() {
} }
export async function launchDaemon() { export async function launchDaemon() {
let daemonProcess
async function startDaemon() { async function startDaemon() {
console.log('启动守护进程...') console.log('启动守护进程...')
// 添加参数使守护进程在后台运行,不显示 UI // 添加参数使守护进程在后台运行,不显示 UI
const daemonProcess = spawn( daemonProcess = spawn(
process.argv[0], process.argv[0],
isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`], isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`],
{ {
@@ -59,11 +63,29 @@ export async function launchDaemon() {
}) })
}) })
} }
await ensureIpcPipeName()
try { try {
await connectToDaemon() await connectToDaemon()
} catch (err) { } catch (err) {
let isDaemonLaunched = false
console.log('cannot connect to daemon, try to launch it', err)
// 启动守护进程 // 启动守护进程
await startDaemon() try {
await connectToDaemon() 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()
}
} }
} }
+13 -6
View File
@@ -1,6 +1,7 @@
import minimist from 'minimist' import minimist from 'minimist'
import { runCommon } from './features/run-common'; 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 错误 // 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
@@ -58,19 +59,25 @@ const runMode = commandlineArgs['mode'];
// #region user entry // #region user entry
case 'geekAutoStartWithBoss': { case 'geekAutoStartWithBoss': {
await ensureIpcPipeName() app.dock?.hide()
await launchDaemon() await launchDaemon()
await runCommon({ mode: 'geekAutoStartWithBossMain' }) const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break break
} }
case 'readNoReplyAutoReminder': { case 'readNoReplyAutoReminder': {
await ensureIpcPipeName() app.dock?.hide()
await launchDaemon() await launchDaemon()
await runCommon({ mode: 'readNoReplyAutoReminderMain' }) const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break break
} }
default: { default: {
await ensureIpcPipeName() globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
await launchDaemon() await launchDaemon()
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index') const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
openSettingWindow() openSettingWindow()