make daemon keep running in background

This commit is contained in:
geekgeekrun
2026-02-06 22:02:25 +08:00
parent dcf4706eb6
commit f2cc87d660
8 changed files with 108 additions and 113 deletions
+19 -14
View File
@@ -1,8 +1,10 @@
import { AUTO_CHAT_ERROR_EXIT_CODE } from "../../common/enums/auto-start-chat" import { app } from 'electron'
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon" import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
import { saveAndGetCurrentRunRecord } from "../flow/OPEN_SETTING_WINDOW/utils/db" 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 currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
const subProcessEnv = { const subProcessEnv = {
...process.env, ...process.env,
@@ -12,14 +14,10 @@ export async function runCommon ({ mode }) {
AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE
].join(',') ].join(',')
} }
const args = process.env.NODE_ENV === 'development' ? [ const args =
process.argv[1], process.env.NODE_ENV === 'development'
`--mode=${mode}`, ? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
`--run-record-id=${currentRunRecord?.id || 0}` : [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
] : [
`--mode=${mode}`,
`--run-record-id=${currentRunRecord?.id || 0}`
]
await sendToDaemon( await sendToDaemon(
{ {
type: 'start-worker', type: 'start-worker',
@@ -32,8 +30,15 @@ export async function runCommon ({ mode }) {
needCallback: true needCallback: true
} }
) )
;['SIGINT', 'SIGTERM'].forEach((evName) => {
process.on(evName, () => {
sendToDaemon({
type: 'stop-worker',
workerId: mode
})
})
})
return { return {
runRecordId: currentRunRecord?.id runRecordId: currentRunRecord?.id
} }
} }
@@ -1,9 +1,4 @@
import attachListenerForKillSelfOnParentExited from '../utils/attachListenerForKillSelfOnParentExited'
;(async () => { ;(async () => {
process.once('disconnect', () => {
process.exit(0)
})
attachListenerForKillSelfOnParentExited()
await import('@geekgeekrun/pm/daemon.js') await import('@geekgeekrun/pm/daemon.js')
})() })()
@@ -1,40 +1,45 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from 'node:crypto'
import { EventEmitter } from "node:events"; import { EventEmitter } from 'node:events'
import { tmpdir } from "node:os"; import { tmpdir } from 'node:os'
import path from "node:path"; import path from 'node:path'
import net from 'node:net'
import split2 from 'split2'
const net = require('net'); let daemonClient = null
const split2 = require('split2');
let daemonClient = null;
export const daemonEE = new EventEmitter() export const daemonEE = new EventEmitter()
const waitForCallbackTaskMap = new Map() 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() { export async function connectToDaemon() {
daemonClient = new net.Socket(); daemonClient = new net.Socket()
let isConnected = false let isConnected = false
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
const ipcSocketPath = process.platform === 'win32' const ipcSocketPath = getSocketPath(ipcSocketName)
? `\\\\.\\pipe\\${ipcSocketName}`
: path.join(tmpdir(), `${ipcSocketName}.sock`)
daemonClient.connect(ipcSocketPath, 'localhost', () => { daemonClient.connect(ipcSocketPath, 'localhost', () => {
isConnected = true isConnected = true
console.log('已连接到守护进程'); console.log('已连接到守护进程')
daemonEE.emit('connect') daemonEE.emit('connect')
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON // 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
const splitStream = split2(); const splitStream = split2()
daemonClient.pipe(splitStream).on('data', (line) => { daemonClient.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim(); const trimmedLine = line.toString().trim()
if (!trimmedLine) { if (!trimmedLine) {
return; // 跳过空行 return // 跳过空行
} }
try { try {
const message = JSON.parse(trimmedLine); const message = JSON.parse(trimmedLine)
daemonEE.emit('message', message) daemonEE.emit('message', message)
// FIXME: // FIXME:
console.log('收到守护进程消息:', message); // console.log('收到守护进程消息:', message)
if (message._callbackUuid) { if (message._callbackUuid) {
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid) const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
if (callbackInfo) { if (callbackInfo) {
@@ -52,55 +57,54 @@ export async function connectToDaemon() {
// mainWindow.webContents.send('daemon-message', message); // mainWindow.webContents.send('daemon-message', message);
// } // }
} catch (parseError) { } catch (parseError) {
console.error('解析守护进程消息失败:', parseError.message); console.error('解析守护进程消息失败:', parseError.message)
console.error('原始数据:', trimmedLine.substring(0, 100)); console.error('原始数据:', trimmedLine.substring(0, 100))
} }
}); })
splitStream.on('error', (err) => { splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err); console.error('split2 流处理错误:', err)
}); })
daemonClient.on('close', () => { daemonClient.on('close', () => {
if (!isConnected) { if (!isConnected) {
return return
} }
console.log('守护进程连接已关闭'); console.log('守护进程连接已关闭')
daemonEE.emit('close') daemonEE.emit('close')
}); })
resolve(true) resolve(true)
// 通知渲染进程连接成功 // 通知渲染进程连接成功
// if (mainWindow) { // if (mainWindow) {
// mainWindow.webContents.send('daemon-connected'); // mainWindow.webContents.send('daemon-connected');
// } // }
}); })
daemonClient.on('close', () => { daemonClient.on('close', () => {
if (isConnected) { if (isConnected) {
return return
} }
reject(new Error('连接到守护进程超时')) reject(new Error('连接到守护进程超时'))
}); })
daemonClient.on('error', (err) => { daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err); console.error('守护进程连接错误:', err)
daemonEE.emit('error', err) // daemonEE.emit('error', err)
reject(err) reject(err)
}); })
}) })
} }
// 向守护进程发送消息 // 向守护进程发送消息
export function sendToDaemon(message, { export function sendToDaemon(message, { needCallback = false, timeout = undefined } = {}) {
needCallback = false,
timeout = undefined
} = {}) {
const _callbackUuid = randomUUID() const _callbackUuid = randomUUID()
if (daemonClient && !daemonClient.destroyed) { if (daemonClient && !daemonClient.destroyed) {
daemonClient.write(JSON.stringify({ daemonClient.write(
...message, JSON.stringify({
_callbackUuid ...message,
}) + '\n'); _callbackUuid
}) + '\n'
)
if (needCallback) { if (needCallback) {
let resolve, reject let resolve, reject
const promise = new Promise((_resolve, _reject) => { const promise = new Promise((_resolve, _reject) => {
@@ -121,7 +125,7 @@ export function sendToDaemon(message, {
return promise return promise
} }
} else { } else {
console.error('守护进程未连接'); console.error('守护进程未连接')
} }
return undefined return undefined
} }
@@ -71,9 +71,6 @@ export function openSettingWindow() {
}) })
whenReadyPromise.then(async () => { whenReadyPromise.then(async () => {
await launchDaemon()
await sleep(2000)
await connectToDaemon()
await sendToDaemon( await sendToDaemon(
{ {
type: 'ping' type: 'ping'
@@ -306,7 +306,7 @@ export default function initIpc() {
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => { ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping') 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) { daemonEE.on('message', function handler (message) {
if (message.workerId !== 'geekAutoStartWithBossMain') { if (message.workerId !== 'geekAutoStartWithBossMain') {
return return
@@ -1,54 +1,47 @@
const { app } = require('electron'); import { spawn } from 'child_process'
const { spawn } = require('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' 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() { export async 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();
}
});
// 启动守护进程
async function startDaemon() { async function startDaemon() {
console.log('启动守护进程...'); console.log('启动守护进程...')
// 添加参数使守护进程在后台运行,不显示 UI // 添加参数使守护进程在后台运行,不显示 UI
daemonProcess = spawn( const daemonProcess = spawn(
process.argv[0], process.argv[0],
isUiDev isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`],
? [process.argv[1], `--mode=launchDaemon`]
: [`--mode=launchDaemon`],
{ {
stdio: ['ignore', 'pipe', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: false, detached: true,
env: { env: {
...process.env, ...process.env
} }
} }
) )
daemonProcess.stdout.on('data', (data) => { daemonProcess.stdout.on('data', (data) => {
console.log(`守护进程输出: ${data}`); console.log(`守护进程输出: ${data}`)
}); })
daemonProcess.stderr.on('data', (data) => { daemonProcess.stderr.on('data', (data) => {
console.error(`守护进程错误: ${data}`); console.error(`守护进程错误: ${data}`)
}); })
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
daemonProcess.stdio[3].on('data', (rawData) => { daemonProcess.stdio[3].on('data', (rawData) => {
@@ -57,19 +50,20 @@ export function launchDaemon() {
data = JSON.parse(rawData.toString()) data = JSON.parse(rawData.toString())
if (data.type === 'DAEMON_READY') { if (data.type === 'DAEMON_READY') {
resolve(true) resolve(true)
} } else if (data.type === 'DAEMON_FATAL') {
else if (data.type === 'DAEMON_FATAL') {
reject(new Error(data.error)) reject(new Error(data.error))
} }
} } catch (err) {
catch (err) {
console.error('', err) console.error('', err)
} }
}) })
}) })
} }
try {
// 应用准备就绪 await connectToDaemon()
return app.whenReady().then(() => startDaemon()); } catch (err) {
// 启动守护进程
await startDaemon()
await connectToDaemon()
}
} }
+5 -8
View File
@@ -1,8 +1,6 @@
import minimist from 'minimist' import minimist from 'minimist'
import { runCommon } from './features/run-common'; import { runCommon } from './features/run-common';
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'; import { ensureIpcPipeName, launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon';
import { connectToDaemon } from './flow/OPEN_SETTING_WINDOW/connect-to-daemon';
import { randomUUID } from 'crypto';
// 捕获未处理的 EPIPE 错误 // 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
@@ -60,21 +58,20 @@ const runMode = commandlineArgs['mode'];
// #region user entry // #region user entry
case 'geekAutoStartWithBoss': { case 'geekAutoStartWithBoss': {
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` await ensureIpcPipeName()
await launchDaemon() await launchDaemon()
await connectToDaemon()
await runCommon({ mode: 'geekAutoStartWithBossMain' }) await runCommon({ mode: 'geekAutoStartWithBossMain' })
break break
} }
case 'readNoReplyAutoReminder': { case 'readNoReplyAutoReminder': {
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` await ensureIpcPipeName()
await launchDaemon() await launchDaemon()
await connectToDaemon()
await runCommon({ mode: 'readNoReplyAutoReminderMain' }) await runCommon({ mode: 'readNoReplyAutoReminderMain' })
break break
} }
default: { default: {
process.env.GEEKGEEKRUND_PIPE_NAME = `geekgeekrun-d_${randomUUID()}` await ensureIpcPipeName()
await launchDaemon()
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index') const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
openSettingWindow() openSettingWindow()
break break
@@ -64,5 +64,8 @@ export function createMainWindow(): BrowserWindow {
mainWindow?.webContents?.send('worker-to-gui-message', message) mainWindow?.webContents?.send('worker-to-gui-message', message)
} }
}) })
daemonEE.on('error', (err) => {
console.log(err)
})
return mainWindow! return mainWindow!
} }