enhance daemon lifecycle logic like connect ensurance

This commit is contained in:
geekgeekrun
2026-01-17 11:28:47 +08:00
parent 199a472ec3
commit a05c73cbdf
9 changed files with 149 additions and 388 deletions

View File

@@ -5,8 +5,6 @@ import {
readConfigFile,
getPublicDbFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
// import { pipeWriteRegardlessError } from '../utils/pipe'
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
@@ -16,7 +14,7 @@ import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerF
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
import gtag from '../../utils/gtag'
import GtagPlugin from '../../utils/gtag/GtagPlugin'
import { connectToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
const { default: SqlitePlugin } = SqlitePluginModule
@@ -48,26 +46,8 @@ const runAutoChat = async () => {
app.exit()
})
app.dock?.hide()
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
// pipeWriteRegardlessError(
// pipe,
// JSON.stringify({
// type: 'INITIALIZE_PUPPETEER'
// }) + '\r\n'
// )
try {
await initPuppeteer()
// pipeWriteRegardlessError(
// pipe,
// JSON.stringify({
// type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
// }) + '\r\n'
// )
} catch (err) {
console.error(err)
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
@@ -101,20 +81,8 @@ const runAutoChat = async () => {
initPlugins(hooks)
gtag('run_auto_chat_with_boss_main_ready')
// pipeWriteRegardlessError(
// pipe,
// JSON.stringify({
// type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
// }) + '\r\n'
// )
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
// pipeWriteRegardlessError(
// pipe,
// JSON.stringify({
// type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
// }) + '\r\n'
// )
})
while (true) {
@@ -159,15 +127,16 @@ const runAutoChat = async () => {
}
}
export const waitForProcessHandShakeAndRunAutoChat = () => {
// let pipe: null | fs.WriteStream = null
// try {
// pipe = fs.createWriteStream(null, { fd: 3 })
// } catch {
// console.error('pipe is not available')
// app.exit(1)
// }
connectToDaemon()
export const waitForProcessHandShakeAndRunAutoChat = async () => {
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
runAutoChat()
}

View File

@@ -21,12 +21,6 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(1)

View File

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

View File

@@ -11,76 +11,79 @@ export const daemonEE = new EventEmitter()
const waitForCallbackTaskMap = new Map()
// 连接到守护进程
export function connectToDaemon() {
export async function connectToDaemon() {
daemonClient = new net.Socket();
daemonClient.connect(DAEMON_PORT, 'localhost', () => {
console.log('已连接到守护进程');
daemonEE.emit('connect')
// 通知渲染进程连接成功
// if (mainWindow) {
// mainWindow.webContents.send('daemon-connected');
// }
});
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
const splitStream = split2();
daemonClient.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim();
if (!trimmedLine) {
return; // 跳过空行
}
try {
const message = JSON.parse(trimmedLine);
daemonEE.emit('message', message)
// FIXME:
console.log('收到守护进程消息:', message);
if (
message._callbackUuid
) {
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
if (callbackInfo) {
const isError = message._isError
if (isError) {
callbackInfo.reject(message)
}
else {
callbackInfo.resolve(message)
}
waitForCallbackTaskMap.delete(message._callbackUuid)
let isConnected = false
await new Promise((resolve, reject) => {
daemonClient.connect(DAEMON_PORT, 'localhost', () => {
isConnected = true
console.log('已连接到守护进程');
daemonEE.emit('connect')
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON
const splitStream = split2();
daemonClient.pipe(splitStream).on('data', (line) => {
const trimmedLine = line.toString().trim();
if (!trimmedLine) {
return; // 跳过空行
}
}
// 转发消息到渲染进程
try {
const message = JSON.parse(trimmedLine);
daemonEE.emit('message', message)
// FIXME:
console.log('收到守护进程消息:', message);
if (message._callbackUuid) {
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
if (callbackInfo) {
const isError = message._isError
if (isError) {
callbackInfo.reject(message)
} else {
callbackInfo.resolve(message)
}
waitForCallbackTaskMap.delete(message._callbackUuid)
}
}
// 转发消息到渲染进程
// if (mainWindow) {
// mainWindow.webContents.send('daemon-message', message);
// }
} catch (parseError) {
console.error('解析守护进程消息失败:', parseError.message);
console.error('原始数据:', trimmedLine.substring(0, 100));
}
});
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err);
});
daemonClient.on('close', () => {
if (!isConnected) {
return
}
console.log('守护进程连接已关闭');
daemonEE.emit('close')
});
resolve(true)
// 通知渲染进程连接成功
// if (mainWindow) {
// mainWindow.webContents.send('daemon-message', message);
// mainWindow.webContents.send('daemon-connected');
// }
} catch (parseError) {
console.error('解析守护进程消息失败:', parseError.message);
console.error('原始数据:', trimmedLine.substring(0, 100));
}
});
});
splitStream.on('error', (err) => {
console.error('split2 流处理错误:', err);
});
daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err);
daemonEE.emit('error', err)
// 尝试重连
setTimeout(() => {
if (daemonClient.destroyed) {
connectToDaemon();
daemonClient.on('close', () => {
if (isConnected) {
return
}
}, 2000);
});
daemonClient.on('close', () => {
console.log('守护进程连接已关闭');
daemonEE.emit('close')
// 尝试重连
setTimeout(() => {
connectToDaemon();
}, 2000);
});
reject(new Error('连接到守护进程超时'))
});
daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err);
daemonEE.emit('error', err)
reject(err)
});
})
}
// 向守护进程发送消息

View File

@@ -6,8 +6,9 @@ import initIpc from './ipc'
import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc'
import { launchDaemon } from './launch-daemon'
import { connectToDaemon } from './connect-to-daemon'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { connectToDaemon, sendToDaemon } from './connect-to-daemon'
import { sleep } from "@geekgeekrun/utils/sleep.mjs"
export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
if (!app.requestSingleInstanceLock()) {
@@ -71,8 +72,15 @@ export function openSettingWindow() {
whenReadyPromise.then(async () => {
await launchDaemon()
// FIXME:
await sleep(2000)
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
})
}

View File

@@ -1,5 +1,3 @@
import { connectToDaemon } from "./connect-to-daemon";
const { app } = require('electron');
const { spawn } = require('child_process');
@@ -27,7 +25,7 @@ export function launchDaemon() {
});
// 启动守护进程
function startDaemon() {
async function startDaemon() {
console.log('启动守护进程...');
// 使用 Electron 可执行程序路径,如果没有则回退到 node
const electronPath = process.execPath;
@@ -40,7 +38,7 @@ export function launchDaemon() {
? [process.argv[1], `--mode=launchDaemon`]
: [`--mode=launchDaemon`],
{
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: false,
env: {
...process.env,
@@ -69,26 +67,26 @@ export function launchDaemon() {
console.error(`守护进程错误: ${data}`);
});
daemonProcess.on('exit', (code) => {
console.log(`守护进程退出,代码: ${code}`);
// 如果守护进程意外退出,尝试重启
if (code !== 0) {
setTimeout(() => {
console.log('尝试重启守护进程...');
startDaemon();
}, 2000);
}
});
// 等待守护进程启动后连接
setTimeout(() => {
connectToDaemon();
}, 1000);
return new Promise((resolve, reject) => {
daemonProcess.stdio[3].on('data', (rawData) => {
let data
try {
data = JSON.parse(rawData.toString())
if (data.type === 'DAEMON_READY') {
resolve(true)
}
else if (data.type === 'DAEMON_FATAL') {
reject(new Error(data.error))
}
}
catch (err) {
console.error('', err)
}
})
})
}
// 应用准备就绪
return app.whenReady().then(() => {
startDaemon();
});
return app.whenReady().then(() => startDaemon());
}

View File

@@ -24,7 +24,7 @@ import gtag from '../../utils/gtag'
import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums';
import dayjs from 'dayjs'
import cheerio from 'cheerio'
import { connectToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
@@ -464,8 +464,13 @@ const rerunInterval = (() => {
})()
export async function runEntry() {
connectToDaemon()
app.dock?.hide()
await connectToDaemon()
await sendToDaemon({
type: 'ping'
}, {
needCallback: true
})
while (true) {
try {
await mainLoop()