From f2cc87d6600004896b7a33c3cb09ca8b61fb1f35 Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Fri, 6 Feb 2026 22:02:25 +0800 Subject: [PATCH] make daemon keep running in background --- packages/ui/src/main/features/run-common.ts | 33 ++++---- packages/ui/src/main/flow/LAUNCH_DAEMON.ts | 5 -- .../OPEN_SETTING_WINDOW/connect-to-daemon.ts | 82 ++++++++++--------- .../main/flow/OPEN_SETTING_WINDOW/index.ts | 3 - .../flow/OPEN_SETTING_WINDOW/ipc/index.ts | 2 +- .../flow/OPEN_SETTING_WINDOW/launch-daemon.ts | 80 +++++++++--------- packages/ui/src/main/index.ts | 13 ++- packages/ui/src/main/window/mainWindow.ts | 3 + 8 files changed, 108 insertions(+), 113 deletions(-) diff --git a/packages/ui/src/main/features/run-common.ts b/packages/ui/src/main/features/run-common.ts index d3696c5..8166992 100644 --- a/packages/ui/src/main/features/run-common.ts +++ b/packages/ui/src/main/features/run-common.ts @@ -1,8 +1,10 @@ -import { AUTO_CHAT_ERROR_EXIT_CODE } from "../../common/enums/auto-start-chat" -import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon" -import { saveAndGetCurrentRunRecord } from "../flow/OPEN_SETTING_WINDOW/utils/db" +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 { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db' -export async function runCommon ({ mode }) { +export async function runCommon({ mode }) { + app.dock?.hide() const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data const subProcessEnv = { ...process.env, @@ -12,14 +14,10 @@ export async function runCommon ({ mode }) { AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE ].join(',') } - const args = process.env.NODE_ENV === 'development' ? [ - process.argv[1], - `--mode=${mode}`, - `--run-record-id=${currentRunRecord?.id || 0}` - ] : [ - `--mode=${mode}`, - `--run-record-id=${currentRunRecord?.id || 0}` - ] + const args = + process.env.NODE_ENV === 'development' + ? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`] + : [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`] await sendToDaemon( { type: 'start-worker', @@ -32,8 +30,15 @@ export async function runCommon ({ mode }) { needCallback: true } ) - + ;['SIGINT', 'SIGTERM'].forEach((evName) => { + process.on(evName, () => { + sendToDaemon({ + type: 'stop-worker', + workerId: mode + }) + }) + }) return { runRecordId: currentRunRecord?.id } -} \ No newline at end of file +} diff --git a/packages/ui/src/main/flow/LAUNCH_DAEMON.ts b/packages/ui/src/main/flow/LAUNCH_DAEMON.ts index 9c1cc76..9bd457e 100644 --- a/packages/ui/src/main/flow/LAUNCH_DAEMON.ts +++ b/packages/ui/src/main/flow/LAUNCH_DAEMON.ts @@ -1,9 +1,4 @@ -import attachListenerForKillSelfOnParentExited from '../utils/attachListenerForKillSelfOnParentExited' ;(async () => { - process.once('disconnect', () => { - process.exit(0) - }) - attachListenerForKillSelfOnParentExited() await import('@geekgeekrun/pm/daemon.js') })() diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts index 95f93f6..7ba7880 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts @@ -1,40 +1,45 @@ -import { randomUUID } from "node:crypto"; -import { EventEmitter } from "node:events"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { randomUUID } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { tmpdir } from 'node:os' +import path from 'node:path' +import net from 'node:net' +import split2 from 'split2' -const net = require('net'); -const split2 = require('split2'); - -let daemonClient = null; +let daemonClient = null export const daemonEE = new EventEmitter() const waitForCallbackTaskMap = new Map() +export function getSocketPath(socketName) { + const ipcSocketPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\${socketName}` + : path.join(tmpdir(), `${socketName}.sock`) + return ipcSocketPath +} + // 连接到守护进程 export async function connectToDaemon() { - daemonClient = new net.Socket(); + daemonClient = new net.Socket() let isConnected = false await new Promise((resolve, reject) => { const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME - const ipcSocketPath = process.platform === 'win32' - ? `\\\\.\\pipe\\${ipcSocketName}` - : path.join(tmpdir(), `${ipcSocketName}.sock`) + const ipcSocketPath = getSocketPath(ipcSocketName) daemonClient.connect(ipcSocketPath, 'localhost', () => { isConnected = true - console.log('已连接到守护进程'); + console.log('已连接到守护进程') daemonEE.emit('connect') // 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON) - const splitStream = split2(); + const splitStream = split2() daemonClient.pipe(splitStream).on('data', (line) => { - const trimmedLine = line.toString().trim(); + const trimmedLine = line.toString().trim() if (!trimmedLine) { - return; // 跳过空行 + return // 跳过空行 } try { - const message = JSON.parse(trimmedLine); + const message = JSON.parse(trimmedLine) daemonEE.emit('message', message) // FIXME: - console.log('收到守护进程消息:', message); + // console.log('收到守护进程消息:', message) if (message._callbackUuid) { const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid) if (callbackInfo) { @@ -52,55 +57,54 @@ export async function connectToDaemon() { // mainWindow.webContents.send('daemon-message', message); // } } catch (parseError) { - console.error('解析守护进程消息失败:', parseError.message); - console.error('原始数据:', trimmedLine.substring(0, 100)); + console.error('解析守护进程消息失败:', parseError.message) + console.error('原始数据:', trimmedLine.substring(0, 100)) } - }); + }) splitStream.on('error', (err) => { - console.error('split2 流处理错误:', err); - }); + console.error('split2 流处理错误:', err) + }) daemonClient.on('close', () => { if (!isConnected) { return } - console.log('守护进程连接已关闭'); + console.log('守护进程连接已关闭') daemonEE.emit('close') - }); + }) resolve(true) // 通知渲染进程连接成功 // if (mainWindow) { // mainWindow.webContents.send('daemon-connected'); // } - }); + }) daemonClient.on('close', () => { if (isConnected) { return } reject(new Error('连接到守护进程超时')) - }); + }) daemonClient.on('error', (err) => { - console.error('守护进程连接错误:', err); - daemonEE.emit('error', err) + console.error('守护进程连接错误:', err) + // daemonEE.emit('error', err) reject(err) - }); + }) }) } // 向守护进程发送消息 -export function sendToDaemon(message, { - needCallback = false, - timeout = undefined -} = {}) { +export function sendToDaemon(message, { needCallback = false, timeout = undefined } = {}) { const _callbackUuid = randomUUID() if (daemonClient && !daemonClient.destroyed) { - daemonClient.write(JSON.stringify({ - ...message, - _callbackUuid - }) + '\n'); + daemonClient.write( + JSON.stringify({ + ...message, + _callbackUuid + }) + '\n' + ) if (needCallback) { let resolve, reject const promise = new Promise((_resolve, _reject) => { @@ -121,7 +125,7 @@ export function sendToDaemon(message, { return promise } } else { - console.error('守护进程未连接'); + console.error('守护进程未连接') } return undefined } 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 7a9c2f7..3ada941 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts @@ -71,9 +71,6 @@ export function openSettingWindow() { }) whenReadyPromise.then(async () => { - await launchDaemon() - await sleep(2000) - await connectToDaemon() await sendToDaemon( { type: 'ping' diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index d94bcdf..261da65 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -306,7 +306,7 @@ export default function initIpc() { ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => { mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping') - const p = new Promise(resolve => { + const p = new Promise((resolve) => { daemonEE.on('message', function handler (message) { if (message.workerId !== 'geekAutoStartWithBossMain') { return 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 950c33a..7f463c0 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 @@ -1,54 +1,47 @@ -const { app } = require('electron'); -const { spawn } = require('child_process'); +import { spawn } from 'child_process' +import { + ensureStorageFileExist, + writeStorageFile, + readStorageFile +} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' +import { randomUUID } from 'node:crypto' +import { connectToDaemon } from './connect-to-daemon' const isUiDev = process.env.NODE_ENV === 'development' +export async function ensureIpcPipeName() { + let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false }) + if (!ipcPipeName) { + ipcPipeName = `geekgeekrun-d_${randomUUID()}` + ensureStorageFileExist() + await writeStorageFile('ipc-pipe-name', ipcPipeName, { isJson: false }) + } + process.env.GEEKGEEKRUND_PIPE_NAME = ipcPipeName + return ipcPipeName +} -export function launchDaemon() { - let daemonProcess = null; - - // 所有窗口关闭时 - app.on('window-all-closed', () => { - // if (process.platform !== 'darwin') { - // 关闭守护进程 - if (daemonProcess) { - daemonProcess.kill(); - } - // app.quit(); - // } - }); - - // 应用退出前清理 - app.on('before-quit', () => { - if (daemonProcess) { - daemonProcess.kill(); - } - }); - - // 启动守护进程 +export async function launchDaemon() { async function startDaemon() { - console.log('启动守护进程...'); + console.log('启动守护进程...') // 添加参数使守护进程在后台运行,不显示 UI - daemonProcess = spawn( + const daemonProcess = spawn( process.argv[0], - isUiDev - ? [process.argv[1], `--mode=launchDaemon`] - : [`--mode=launchDaemon`], + isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`], { stdio: ['ignore', 'pipe', 'pipe', 'pipe'], - detached: false, + detached: true, env: { - ...process.env, + ...process.env } } ) daemonProcess.stdout.on('data', (data) => { - console.log(`守护进程输出: ${data}`); - }); + console.log(`守护进程输出: ${data}`) + }) daemonProcess.stderr.on('data', (data) => { - console.error(`守护进程错误: ${data}`); - }); + console.error(`守护进程错误: ${data}`) + }) return new Promise((resolve, reject) => { daemonProcess.stdio[3].on('data', (rawData) => { @@ -57,19 +50,20 @@ export function launchDaemon() { data = JSON.parse(rawData.toString()) if (data.type === 'DAEMON_READY') { resolve(true) - } - else if (data.type === 'DAEMON_FATAL') { + } else if (data.type === 'DAEMON_FATAL') { reject(new Error(data.error)) } - } - catch (err) { + } catch (err) { console.error('', err) } }) }) } - - // 应用准备就绪 - return app.whenReady().then(() => startDaemon()); + try { + await connectToDaemon() + } catch (err) { + // 启动守护进程 + await startDaemon() + await connectToDaemon() + } } - diff --git a/packages/ui/src/main/index.ts b/packages/ui/src/main/index.ts index 8a56525..d040b17 100644 --- a/packages/ui/src/main/index.ts +++ b/packages/ui/src/main/index.ts @@ -1,8 +1,6 @@ import minimist from 'minimist' import { runCommon } from './features/run-common'; -import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'; -import { connectToDaemon } from './flow/OPEN_SETTING_WINDOW/connect-to-daemon'; -import { randomUUID } from 'crypto'; +import { ensureIpcPipeName, launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'; // 捕获未处理的 EPIPE 错误 process.on('uncaughtException', (err) => { @@ -60,21 +58,20 @@ const runMode = commandlineArgs['mode']; // #region user entry case 'geekAutoStartWithBoss': { - process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` + await ensureIpcPipeName() await launchDaemon() - await connectToDaemon() await runCommon({ mode: 'geekAutoStartWithBossMain' }) break } case 'readNoReplyAutoReminder': { - process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` + await ensureIpcPipeName() await launchDaemon() - await connectToDaemon() await runCommon({ mode: 'readNoReplyAutoReminderMain' }) break } default: { - process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` + await ensureIpcPipeName() + await launchDaemon() const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index') openSettingWindow() break diff --git a/packages/ui/src/main/window/mainWindow.ts b/packages/ui/src/main/window/mainWindow.ts index 15d2c5c..850ab9a 100644 --- a/packages/ui/src/main/window/mainWindow.ts +++ b/packages/ui/src/main/window/mainWindow.ts @@ -64,5 +64,8 @@ export function createMainWindow(): BrowserWindow { mainWindow?.webContents?.send('worker-to-gui-message', message) } }) + daemonEE.on('error', (err) => { + console.log(err) + }) return mainWindow! }