diff --git a/src/main/resolve/floatingWindow.ts b/src/main/resolve/floatingWindow.ts index 49d18523..fde66ab8 100644 --- a/src/main/resolve/floatingWindow.ts +++ b/src/main/resolve/floatingWindow.ts @@ -54,13 +54,26 @@ async function createFloatingWindow(): Promise { } } - 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', () => { diff --git a/src/main/resolve/gistApi.ts b/src/main/resolve/gistApi.ts index e7d190a6..2390a230 100644 --- a/src/main/resolve/gistApi.ts +++ b/src/main/resolve/gistApi.ts @@ -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 { 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 { }, responseType: 'json' }) + assertGithubOk(res, 'list gists') return Array.isArray(res.data) ? res.data : [] } async function createGist(token: string, content: string): Promise { 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 { } } ) + assertGithubOk(res, 'create gist') } async function updateGist(token: string, id: string, content: string): Promise { 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 { diff --git a/src/main/resolve/server.ts b/src/main/resolve/server.ts index 96dbb8c2..431614c5 100644 --- a/src/main/resolve/server.ts +++ b/src/main/resolve/server.ts @@ -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 { }) } -let pacServer: http.Server +let pacServer: http.Server | undefined export async function startPacServer(): Promise { await stopPacServer() @@ -60,17 +60,37 @@ export async function startPacServer(): Promise { 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((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 { if (pacServer) { pacServer.close() + pacServer = undefined } }