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

View File

@@ -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
}
}
}

View File

@@ -1,9 +1,4 @@
import attachListenerForKillSelfOnParentExited from '../utils/attachListenerForKillSelfOnParentExited'
;(async () => {
process.once('disconnect', () => {
process.exit(0)
})
attachListenerForKillSelfOnParentExited()
await import('@geekgeekrun/pm/daemon.js')
})()

View File

@@ -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
}

View File

@@ -71,9 +71,6 @@ export function openSettingWindow() {
})
whenReadyPromise.then(async () => {
await launchDaemon()
await sleep(2000)
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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!
}