mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
fix: 加固退出清理
退出应用时终止后台 进程 移除运行时动态安装 Python 依赖
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
* onebot/script.js. See docs/third-party/wechat-chatter/NOTICE.md.
|
* onebot/script.js. See docs/third-party/wechat-chatter/NOTICE.md.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-require-imports */
|
||||||
const { execFileSync } = require('node:child_process')
|
const { execFileSync } = require('node:child_process')
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const os = require('node:os')
|
const os = require('node:os')
|
||||||
@@ -84,31 +84,6 @@ for (const required of [executable, script, config]) {
|
|||||||
if (!fs.existsSync(required)) throw new Error(`运行时文件缺失:${required}`)
|
if (!fs.existsSync(required)) throw new Error(`运行时文件缺失:${required}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pythonPackages = path.join(outputDir, 'python')
|
|
||||||
const pilkPackage = path.join(pythonPackages, 'pilk')
|
|
||||||
if (!fs.existsSync(pilkPackage)) {
|
|
||||||
console.log('[wechat-personal] 安装项目内语音编码器 pilk')
|
|
||||||
fs.mkdirSync(pythonPackages, { recursive: true })
|
|
||||||
try {
|
|
||||||
execFileSync(
|
|
||||||
'python3',
|
|
||||||
[
|
|
||||||
'-m',
|
|
||||||
'pip',
|
|
||||||
'install',
|
|
||||||
'--disable-pip-version-check',
|
|
||||||
'--no-compile',
|
|
||||||
'--target',
|
|
||||||
pythonPackages,
|
|
||||||
'pilk==0.2.4'
|
|
||||||
],
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
console.warn('[wechat-personal] pilk 安装失败,将使用 OneBot 内置的 Go SILK 编码器')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchPerSendPayload(scriptPath) {
|
function patchPerSendPayload(scriptPath) {
|
||||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||||
if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
|
if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
|
||||||
|
|||||||
+4
-2
@@ -1857,13 +1857,15 @@ app.on('before-quit', (event) => {
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
agentHubService.stop()
|
agentHubService.stop()
|
||||||
personalWechatSendService.stop()
|
|
||||||
flushBootstrapCacheWritesSync()
|
flushBootstrapCacheWritesSync()
|
||||||
const [, nativeCallsDrained] = await Promise.all([
|
const [, nativeCallsDrained] = await Promise.all([
|
||||||
apiServer.stop().catch(() => undefined),
|
apiServer.stop().catch(() => undefined),
|
||||||
chat.closeChatDbForQuit().catch(() => false),
|
chat.closeChatDbForQuit().catch(() => false),
|
||||||
voiceRecognition?.dispose().catch(() => undefined),
|
voiceRecognition?.dispose().catch(() => undefined),
|
||||||
knowledgeSearchService?.dispose().catch(() => undefined)
|
knowledgeSearchService?.dispose().catch(() => undefined),
|
||||||
|
personalWechatSendService.terminate().catch((error) => {
|
||||||
|
console.warn('[Shutdown] personal WeChat sender cleanup failed:', error)
|
||||||
|
})
|
||||||
])
|
])
|
||||||
if (!nativeCallsDrained) {
|
if (!nativeCallsDrained) {
|
||||||
console.warn('[Shutdown] WCDB async calls did not fully drain before quit')
|
console.warn('[Shutdown] WCDB async calls did not fully drain before quit')
|
||||||
|
|||||||
@@ -1057,7 +1057,8 @@ export class AiSearchPipelineService {
|
|||||||
return result.evidence.map((item): AiSearchPipelineEvidence => {
|
return result.evidence.map((item): AiSearchPipelineEvidence => {
|
||||||
const rawConversationId = String(item.conversationId || '').trim()
|
const rawConversationId = String(item.conversationId || '').trim()
|
||||||
const contact =
|
const contact =
|
||||||
contactsById.get(rawConversationId) || contactsById.get(rawConversationId.toLocaleLowerCase())
|
contactsById.get(rawConversationId) ||
|
||||||
|
contactsById.get(rawConversationId.toLocaleLowerCase())
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
conversationId: contact?.md5 || rawConversationId,
|
conversationId: contact?.md5 || rawConversationId,
|
||||||
|
|||||||
@@ -265,27 +265,6 @@ export class PersonalWechatRuntimeManager {
|
|||||||
addModifiedWorkNotice(script)
|
addModifiedWorkNotice(script)
|
||||||
await chmod(executable, 0o755)
|
await chmod(executable, 0o755)
|
||||||
|
|
||||||
const pythonPackages = join(stagedDirectory, 'python')
|
|
||||||
await mkdir(pythonPackages, { recursive: true })
|
|
||||||
try {
|
|
||||||
await execFileAsync('/usr/bin/env', [
|
|
||||||
'python3',
|
|
||||||
'-m',
|
|
||||||
'pip',
|
|
||||||
'install',
|
|
||||||
'--disable-pip-version-check',
|
|
||||||
'--no-compile',
|
|
||||||
'--target',
|
|
||||||
pythonPackages,
|
|
||||||
'pilk==0.2.4'
|
|
||||||
])
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
'[PersonalWechatRuntime] pilk 安装失败,将使用 OneBot 内置的 Go SILK 编码器:',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const required of [
|
for (const required of [
|
||||||
executable,
|
executable,
|
||||||
script,
|
script,
|
||||||
|
|||||||
@@ -372,20 +372,29 @@ async function readOneBotProcessInfo(): Promise<OneBotProcessInfo | undefined> {
|
|||||||
|
|
||||||
async function terminateOneBot(info: OneBotProcessInfo): Promise<void> {
|
async function terminateOneBot(info: OneBotProcessInfo): Promise<void> {
|
||||||
if (!/(^|\/)onebot(?:\s|$)/.test(info.command)) return
|
if (!/(^|\/)onebot(?:\s|$)/.test(info.command)) return
|
||||||
|
await terminateProcess(info.pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function terminateProcess(pid: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
process.kill(info.pid, 'SIGTERM')
|
process.kill(pid, 'SIGTERM')
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
while (Date.now() - startedAt < STOP_TIMEOUT_MS) {
|
while (Date.now() - startedAt < STOP_TIMEOUT_MS) {
|
||||||
try {
|
try {
|
||||||
process.kill(info.pid, 0)
|
process.kill(pid, 0)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL')
|
||||||
|
} catch {
|
||||||
|
// The process exited between the last liveness check and the forced stop.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestWithTimeout(
|
async function requestWithTimeout(
|
||||||
@@ -605,18 +614,11 @@ export class PersonalWechatSendService {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
|
||||||
const child = this.child
|
|
||||||
this.child = null
|
|
||||||
this.startPromise = null
|
|
||||||
// A second Attach to the same WeChat process is unstable. Leave OneBot alive;
|
|
||||||
// it monitors the WeChat PID and exits when that process ends.
|
|
||||||
if (child && child.exitCode === null) child.unref()
|
|
||||||
}
|
|
||||||
|
|
||||||
async terminate(): Promise<void> {
|
async terminate(): Promise<void> {
|
||||||
|
const trackedPid = this.child?.pid
|
||||||
const oneBot = await readOneBotProcessInfo()
|
const oneBot = await readOneBotProcessInfo()
|
||||||
if (oneBot) await terminateOneBot(oneBot)
|
if (oneBot) await terminateOneBot(oneBot)
|
||||||
|
if (trackedPid && trackedPid !== oneBot?.pid) await terminateProcess(trackedPid)
|
||||||
this.child = null
|
this.child = null
|
||||||
this.startPromise = null
|
this.startPromise = null
|
||||||
this.lastError = ''
|
this.lastError = ''
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mkdtempSync, readFileSync, rmSync } from 'fs'
|
import { mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
import { join } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||||
import { classifyStickerHttpFailure } from '../../src/shared/sticker'
|
import { classifyStickerHttpFailure } from '../../src/shared/sticker'
|
||||||
|
|
||||||
@@ -51,3 +51,42 @@ describe('sticker HTTP failures', () => {
|
|||||||
expect(classifyStickerHttpFailure(429, 'https://fixture.invalid/a').code).toBe('rate_limited')
|
expect(classifyStickerHttpFailure(429, 'https://fixture.invalid/a').code).toBe('rate_limited')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('personal WeChat runtime security invariants', () => {
|
||||||
|
it('waits for sender termination during application shutdown', () => {
|
||||||
|
const mainSource = readFileSync(resolve('src/main/index.ts'), 'utf8')
|
||||||
|
const shutdownStart = mainSource.indexOf("app.on('before-quit'")
|
||||||
|
const shutdownEnd = mainSource.indexOf('function showMainWindow', shutdownStart)
|
||||||
|
const shutdownSource = mainSource.slice(shutdownStart, shutdownEnd)
|
||||||
|
|
||||||
|
expect(shutdownStart).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(shutdownEnd).toBeGreaterThan(shutdownStart)
|
||||||
|
expect(shutdownSource).toContain('await Promise.all([')
|
||||||
|
expect(shutdownSource).toContain('personalWechatSendService.terminate()')
|
||||||
|
expect(shutdownSource).not.toContain('personalWechatSendService.stop()')
|
||||||
|
|
||||||
|
const senderSource = readFileSync(
|
||||||
|
resolve('src/main/services/personal-wechat-send-service.ts'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
expect(senderSource).toContain("process.kill(pid, 'SIGTERM')")
|
||||||
|
expect(senderSource).toContain("process.kill(pid, 'SIGKILL')")
|
||||||
|
expect(senderSource).toContain('const trackedPid = this.child?.pid')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not install Python packages while preparing the sender runtime', () => {
|
||||||
|
const runtimeManagerSource = readFileSync(
|
||||||
|
resolve('src/main/services/personal-wechat-runtime-manager.ts'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
const preparationScriptSource = readFileSync(
|
||||||
|
resolve('scripts/prepare-wechat-chatter-runtime.cjs'),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const source of [runtimeManagerSource, preparationScriptSource]) {
|
||||||
|
expect(source).not.toContain('pilk==')
|
||||||
|
expect(source).not.toMatch(/['"]pip['"]/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user