mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-06 16:07:17 +08:00
make daemon keep running in background
This commit is contained in:
@@ -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,7 +30,14 @@ 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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!
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user