diff --git a/changelog.md b/changelog.md index 8576e60..e1af0b4 100644 --- a/changelog.md +++ b/changelog.md @@ -4,11 +4,5 @@ ### New Features -- 优化侧边栏卡片拖动体验 -- 支持自定义侧边栏卡片大小 -- 支持隐藏侧边栏卡片 - -### Bug Fixes - -- 修复Ubuntu下每次开启Tun都需要密码的问题 -- 修复Sub-Store无法读取剪切板的问题 +- 新增轻量模式,支持完全退出应用只保留内核后台运行 +- 折叠不常用设置项 diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 6b430a1..ede712b 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -1,5 +1,6 @@ import { ChildProcess, exec, execFile, spawn } from 'child_process' import { + dataDir, logPath, mihomoCoreDir, mihomoCorePath, @@ -14,7 +15,7 @@ import { patchAppConfig, patchControledMihomoConfig } from '../config' -import { dialog, ipcMain, safeStorage } from 'electron' +import { app, dialog, ipcMain, safeStorage } from 'electron' import { startMihomoTraffic, startMihomoConnections, @@ -26,10 +27,11 @@ import { stopMihomoMemory } from './mihomoApi' import chokidar from 'chokidar' -import { writeFile } from 'fs/promises' +import { readFile, rm, writeFile } from 'fs/promises' import { promisify } from 'util' import { mainWindow } from '..' import path from 'path' +import { existsSync } from 'fs' chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async () => { try { @@ -43,8 +45,27 @@ chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async let child: ChildProcess let retry = 10 -export async function startCore(): Promise[]> { - const { core = 'mihomo', autoSetDNS = true } = await getAppConfig() +export async function startCore(detached = false): Promise[]> { + const { core = 'mihomo', autoSetDNS = true, encryptedPassword } = await getAppConfig() + if (existsSync(path.join(dataDir(), 'core.pid'))) { + const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8')) + try { + process.kill(pid, 'SIGINT') + } catch { + if (process.platform !== 'win32' && encryptedPassword && isEncryptionAvailable()) { + const execPromise = promisify(exec) + const password = safeStorage.decryptString(Buffer.from(encryptedPassword)) + try { + await execPromise(`echo "${password}" | sudo -S kill ${pid}`) + } catch { + // ignore + } + } + } finally { + await rm(path.join(dataDir(), 'core.pid')) + } + } + const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) await autoGrantCorePermition(corePath) @@ -60,7 +81,9 @@ export async function startCore(): Promise[]> { }) } } - child = spawn(corePath, ['-d', mihomoWorkDir()]) + child = spawn(corePath, ['-d', mihomoWorkDir()], { + detached: detached + }) child.on('close', async (code, signal) => { await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, { flag: 'a' @@ -101,7 +124,11 @@ export async function startCore(): Promise[]> { new Promise((resolve) => { child.stdout?.on('data', async (data) => { if (data.toString().includes('Start initial Compatible provider default')) { - mainWindow?.webContents.send('coreRestart') + try { + mainWindow?.webContents.send('coreRestart') + } catch { + // ignore + } resolve() } }) @@ -146,6 +173,27 @@ export async function restartCore(): Promise { } } +export async function keepCoreAlive(): Promise { + try { + await startCore(true) + stopMihomoTraffic() + stopMihomoConnections() + stopMihomoLogs() + stopMihomoMemory() + if (child && child.pid) { + await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) + child.unref() + } + } catch (e) { + dialog.showErrorBox('内核启动出错', `${e}`) + } +} + +export async function quitWithoutCore(): Promise { + await keepCoreAlive() + app.exit() +} + async function checkProfile(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 0b85a1e..c3dc3a2 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -186,14 +186,18 @@ const mihomoTraffic = async (): Promise => { const data = e.data as string const json = JSON.parse(data) as IMihomoTrafficInfo trafficRetry = 10 - mainWindow?.webContents.send('mihomoTraffic', json) - if (process.platform !== 'linux') { - tray?.setToolTip( - '↑' + - `${calcTraffic(json.up)}/s`.padStart(9) + - '\n↓' + - `${calcTraffic(json.down)}/s`.padStart(9) - ) + try { + mainWindow?.webContents.send('mihomoTraffic', json) + if (process.platform !== 'linux') { + tray?.setToolTip( + '↑' + + `${calcTraffic(json.up)}/s`.padStart(9) + + '\n↓' + + `${calcTraffic(json.down)}/s`.padStart(9) + ) + } + } catch { + // ignore } } @@ -238,7 +242,11 @@ const mihomoMemory = async (): Promise => { mihomoMemoryWs.onmessage = (e): void => { const data = e.data as string memoryRetry = 10 - mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) + try { + mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) + } catch { + // ignore + } } mihomoMemoryWs.onclose = (): void => { @@ -284,7 +292,11 @@ const mihomoLogs = async (): Promise => { mihomoLogsWs.onmessage = (e): void => { const data = e.data as string logsRetry = 10 - mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) + try { + mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) + } catch { + // ignore + } } mihomoLogsWs.onclose = (): void => { @@ -330,7 +342,11 @@ const mihomoConnections = async (): Promise => { mihomoConnectionsWs.onmessage = (e): void => { const data = e.data as string connectionsRetry = 10 - mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) + try { + mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) + } catch { + // ignore + } } mihomoConnectionsWs.onclose = (): void => { diff --git a/src/main/resolve/shortcut.ts b/src/main/resolve/shortcut.ts index f1a1c53..c267cf3 100644 --- a/src/main/resolve/shortcut.ts +++ b/src/main/resolve/shortcut.ts @@ -8,6 +8,7 @@ import { } from '../config' import { triggerSysProxy } from '../sys/sysproxy' import { patchMihomoConfig } from '../core/mihomoApi' +import { quitWithoutCore } from '../core/manager' export async function registerShortcut( oldShortcut: string, @@ -80,6 +81,11 @@ export async function registerShortcut( ipcMain.emit('updateTrayMenu') }) } + case 'quitWithoutCoreShortcut': { + return globalShortcut.register(newShortcut, async () => { + await quitWithoutCore() + }) + } case 'restartAppShortcut': { return globalShortcut.register(newShortcut, () => { app.relaunch() @@ -98,6 +104,7 @@ export async function initShortcut(): Promise { ruleModeShortcut, globalModeShortcut, directModeShortcut, + quitWithoutCoreShortcut, restartAppShortcut } = await getAppConfig() if (showWindowShortcut) { @@ -142,6 +149,13 @@ export async function initShortcut(): Promise { // ignore } } + if (quitWithoutCoreShortcut) { + try { + await registerShortcut('', quitWithoutCoreShortcut, 'quitWithoutCoreShortcut') + } catch { + // ignore + } + } if (restartAppShortcut) { try { await registerShortcut('', restartAppShortcut, 'restartAppShortcut') diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index 5196512..566d15c 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -17,7 +17,7 @@ import { mainWindow, showMainWindow } from '..' import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { triggerSysProxy } from '../sys/sysproxy' -import { restartCore } from '../core/manager' +import { quitWithoutCore, restartCore } from '../core/manager' export let tray: Tray | null = null @@ -33,6 +33,7 @@ const buildContextMenu = async (): Promise => { ruleModeShortcut = '', globalModeShortcut = '', directModeShortcut = '', + quitWithoutCoreShortcut = '', restartAppShortcut = '' } = await getAppConfig() let groupsMenu: Electron.MenuItemConstructorOptions[] = [] @@ -195,6 +196,13 @@ const buildContextMenu = async (): Promise => { click: copyEnv }, { type: 'separator' }, + { + id: 'quitWithoutCore', + label: '轻量模式', + type: 'normal', + accelerator: quitWithoutCoreShortcut, + click: quitWithoutCore + }, { id: 'restart', label: '重启应用', diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 7899647..7ee3cd3 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -43,7 +43,12 @@ import { updateOverrideItem } from '../config' import { startSubStoreServer, subStoreFrontendPort, subStorePort } from '../resolve/server' -import { isEncryptionAvailable, manualGrantCorePermition, restartCore } from '../core/manager' +import { + isEncryptionAvailable, + manualGrantCorePermition, + quitWithoutCore, + restartCore +} from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { @@ -193,6 +198,7 @@ export function registerIpcMainHandlers(): void { app.relaunch() app.quit() }) + ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore)) ipcMain.handle('quitApp', () => app.quit()) } diff --git a/src/renderer/src/components/settings/actions.tsx b/src/renderer/src/components/settings/actions.tsx index f11c3fd..0b92f6f 100644 --- a/src/renderer/src/components/settings/actions.tsx +++ b/src/renderer/src/components/settings/actions.tsx @@ -1,10 +1,11 @@ -import { Button } from '@nextui-org/react' +import { Button, Tooltip } from '@nextui-org/react' import SettingCard from '../base/base-setting-card' import SettingItem from '../base/base-setting-item' -import { checkUpdate, quitApp } from '@renderer/utils/ipc' +import { checkUpdate, quitApp, quitWithoutCore } from '@renderer/utils/ipc' import { useState } from 'react' import UpdaterModal from '../updater/updater-modal' import { version } from '@renderer/utils/init' +import { IoIosHelpCircle } from 'react-icons/io' const Actions: React.FC = () => { const [newVersion, setNewVersion] = useState('') @@ -47,6 +48,21 @@ const Actions: React.FC = () => { 检查更新 + + + + } + divider + > + +