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.
|
||||
*/
|
||||
|
||||
/* 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 fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
@@ -84,31 +84,6 @@ for (const required of [executable, script, config]) {
|
||||
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) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
|
||||
|
||||
+4
-2
@@ -1857,13 +1857,15 @@ app.on('before-quit', (event) => {
|
||||
|
||||
void (async () => {
|
||||
agentHubService.stop()
|
||||
personalWechatSendService.stop()
|
||||
flushBootstrapCacheWritesSync()
|
||||
const [, nativeCallsDrained] = await Promise.all([
|
||||
apiServer.stop().catch(() => undefined),
|
||||
chat.closeChatDbForQuit().catch(() => false),
|
||||
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) {
|
||||
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 => {
|
||||
const rawConversationId = String(item.conversationId || '').trim()
|
||||
const contact =
|
||||
contactsById.get(rawConversationId) || contactsById.get(rawConversationId.toLocaleLowerCase())
|
||||
contactsById.get(rawConversationId) ||
|
||||
contactsById.get(rawConversationId.toLocaleLowerCase())
|
||||
return {
|
||||
...item,
|
||||
conversationId: contact?.md5 || rawConversationId,
|
||||
|
||||
@@ -265,27 +265,6 @@ export class PersonalWechatRuntimeManager {
|
||||
addModifiedWorkNotice(script)
|
||||
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 [
|
||||
executable,
|
||||
script,
|
||||
|
||||
@@ -372,20 +372,29 @@ async function readOneBotProcessInfo(): Promise<OneBotProcessInfo | undefined> {
|
||||
|
||||
async function terminateOneBot(info: OneBotProcessInfo): Promise<void> {
|
||||
if (!/(^|\/)onebot(?:\s|$)/.test(info.command)) return
|
||||
await terminateProcess(info.pid)
|
||||
}
|
||||
|
||||
async function terminateProcess(pid: number): Promise<void> {
|
||||
try {
|
||||
process.kill(info.pid, 'SIGTERM')
|
||||
process.kill(pid, 'SIGTERM')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < STOP_TIMEOUT_MS) {
|
||||
try {
|
||||
process.kill(info.pid, 0)
|
||||
process.kill(pid, 0)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL')
|
||||
} catch {
|
||||
// The process exited between the last liveness check and the forced stop.
|
||||
}
|
||||
}
|
||||
|
||||
async function requestWithTimeout(
|
||||
@@ -605,18 +614,11 @@ export class PersonalWechatSendService {
|
||||
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> {
|
||||
const trackedPid = this.child?.pid
|
||||
const oneBot = await readOneBotProcessInfo()
|
||||
if (oneBot) await terminateOneBot(oneBot)
|
||||
if (trackedPid && trackedPid !== oneBot?.pid) await terminateProcess(trackedPid)
|
||||
this.child = null
|
||||
this.startPromise = null
|
||||
this.lastError = ''
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest'
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
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