mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
fix: handle failures in the PAC server, Gist sync and floating window (#2032)
- The PAC server called listen() with no 'error' listener. A host the user cannot bind to - or a port already in use - emitted an unhandled 'error' event, which in Node terminates the process, so a bad PAC host setting crashed the whole main process. Attach an error handler and surface the failure to the caller. - The Gist upload never checked the HTTP status. Once the token expired, GitHub answered 401 and the code still recorded the runtime config as successfully synced, so the user saw a healthy backup that did not exist. Check the status and report the failure. - On 'render-process-gone' the floating window handler only set the module reference to null without destroying the BrowserWindow. The dead window stayed on screen, always-on-top, with nothing left holding a reference to close it. Destroy it before clearing the reference.
This commit is contained in:
@@ -54,13 +54,26 @@ async function createFloatingWindow(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
floatingWindow = new BrowserWindow(windowOptions)
|
||||
const win = new BrowserWindow(windowOptions)
|
||||
floatingWindow = win
|
||||
floatingWindowState.manage(floatingWindow)
|
||||
|
||||
// 事件监听器
|
||||
floatingWindow.webContents.on('render-process-gone', (_, details) => {
|
||||
logError('Render process gone', details.reason)
|
||||
floatingWindow = null
|
||||
// 只丢引用不销毁的话,屏幕上会残留一个无边框、置顶、不在任务栏且 closable: false 的幽灵窗,
|
||||
// 用户既关不掉也找不到,只能重启应用
|
||||
if (!win.isDestroyed()) {
|
||||
win.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
// 窗口销毁后必须清掉引用,否则 isVisible() 之类的调用会作用在已销毁窗口上抛错;
|
||||
// 判断 win 是为了避免旧窗口的 closed 事件把新建窗口的引用清掉
|
||||
win.on('closed', () => {
|
||||
if (floatingWindow === win) {
|
||||
floatingWindow = null
|
||||
}
|
||||
})
|
||||
|
||||
floatingWindow.on('ready-to-show', () => {
|
||||
|
||||
@@ -30,6 +30,15 @@ function hashRuntimeConfig(runtimeConfig: string): string {
|
||||
return createHash('sha256').update(runtimeConfig).digest('hex')
|
||||
}
|
||||
|
||||
// chromeRequest 对任何状态码都会 resolve,不校验状态码的话 token 失效(401/403)会被当成成功:
|
||||
// 错误响应体被当作「远端没有这个 gist」,上传也「成功」,哈希被记为已同步后就再也不会重试
|
||||
function assertGithubOk(res: chromeRequest.Response, action: string): void {
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
const detail = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
|
||||
throw new Error(`GitHub API ${action} failed with status ${res.status}: ${detail}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function listGists(token: string): Promise<GistInfo[]> {
|
||||
const { 'mixed-port': port = DEFAULT_MIHOMO_PORTS.mixed } = await getControledMihomoConfig()
|
||||
const res = await chromeRequest.get('https://api.github.com/gists', {
|
||||
@@ -45,12 +54,13 @@ async function listGists(token: string): Promise<GistInfo[]> {
|
||||
},
|
||||
responseType: 'json'
|
||||
})
|
||||
assertGithubOk(res, 'list gists')
|
||||
return Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
|
||||
async function createGist(token: string, content: string): Promise<void> {
|
||||
const { 'mixed-port': port = DEFAULT_MIHOMO_PORTS.mixed } = await getControledMihomoConfig()
|
||||
await chromeRequest.post(
|
||||
const res = await chromeRequest.post(
|
||||
'https://api.github.com/gists',
|
||||
{
|
||||
description: 'Auto Synced Clash Party Runtime Config',
|
||||
@@ -70,11 +80,12 @@ async function createGist(token: string, content: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
)
|
||||
assertGithubOk(res, 'create gist')
|
||||
}
|
||||
|
||||
async function updateGist(token: string, id: string, content: string): Promise<void> {
|
||||
const { 'mixed-port': port = DEFAULT_MIHOMO_PORTS.mixed } = await getControledMihomoConfig()
|
||||
await chromeRequest.patch(
|
||||
const res = await chromeRequest.patch(
|
||||
`https://api.github.com/gists/${id}`,
|
||||
{
|
||||
description: 'Auto Synced Clash Party Runtime Config',
|
||||
@@ -93,6 +104,7 @@ async function updateGist(token: string, id: string, content: string): Promise<v
|
||||
}
|
||||
}
|
||||
)
|
||||
assertGithubOk(res, 'update gist')
|
||||
}
|
||||
|
||||
export async function getGistUrl(): Promise<string> {
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as chromeRequest from '../utils/chromeRequest'
|
||||
import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
|
||||
import { dataDir, mihomoWorkDir, subStoreDir, substoreLogPath } from '../utils/dirs'
|
||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||
import { systemLogger } from '../utils/logger'
|
||||
import { proxyLogger, systemLogger } from '../utils/logger'
|
||||
import { createCappedLogWritableStream } from '../utils/logFile'
|
||||
import { DEFAULT_MIHOMO_PORTS, DEFAULT_USE_SUB_STORE } from '../../shared/appConfig'
|
||||
|
||||
@@ -46,7 +46,7 @@ export function findAvailablePort(startPort: number): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
let pacServer: http.Server
|
||||
let pacServer: http.Server | undefined
|
||||
|
||||
export async function startPacServer(): Promise<void> {
|
||||
await stopPacServer()
|
||||
@@ -60,17 +60,37 @@ export async function startPacServer(): Promise<void> {
|
||||
const { 'mixed-port': port = DEFAULT_MIHOMO_PORTS.mixed } = await getControledMihomoConfig()
|
||||
script = script.replaceAll('%mixed-port%', port.toString())
|
||||
pacPort = await findAvailablePort(10000)
|
||||
pacServer = http
|
||||
.createServer(async (_req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })
|
||||
res.end(script)
|
||||
})
|
||||
.listen(pacPort, host)
|
||||
const server = http.createServer(async (_req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })
|
||||
res.end(script)
|
||||
})
|
||||
// host 由用户自由填写,findAvailablePort 只在 127.0.0.1 上探测过端口,这里绑定可能失败
|
||||
// (EADDRNOTAVAIL / ENOTFOUND)。listen 失败是异步事件,没有 'error' 监听器会直接变成主进程
|
||||
// 未捕获异常并弹错误框,所以把失败转成 reject 交给调用方处理
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (err: Error): void => {
|
||||
server.removeListener('listening', onListening)
|
||||
reject(err)
|
||||
}
|
||||
const onListening = (): void => {
|
||||
server.removeListener('error', onError)
|
||||
// 监听成功后仍保留 error 监听,防止运行期出错(如网络接口变化)再次崩溃主进程
|
||||
server.on('error', (err) => {
|
||||
proxyLogger.error('PAC server error', err).catch(() => {})
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
server.once('error', onError)
|
||||
server.once('listening', onListening)
|
||||
server.listen(pacPort, host)
|
||||
})
|
||||
pacServer = server
|
||||
}
|
||||
|
||||
export async function stopPacServer(): Promise<void> {
|
||||
if (pacServer) {
|
||||
pacServer.close()
|
||||
pacServer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user