support quit without core

This commit is contained in:
pompurin404 2024-09-07 20:08:12 +08:00
parent 130150325a
commit 5e9c06105f
No known key found for this signature in database
10 changed files with 146 additions and 30 deletions

View File

@ -4,11 +4,5 @@
### New Features ### New Features
- 优化侧边栏卡片拖动体验 - 新增轻量模式,支持完全退出应用只保留内核后台运行
- 支持自定义侧边栏卡片大小 - 折叠不常用设置项
- 支持隐藏侧边栏卡片
### Bug Fixes
- 修复Ubuntu下每次开启Tun都需要密码的问题
- 修复Sub-Store无法读取剪切板的问题

View File

@ -1,5 +1,6 @@
import { ChildProcess, exec, execFile, spawn } from 'child_process' import { ChildProcess, exec, execFile, spawn } from 'child_process'
import { import {
dataDir,
logPath, logPath,
mihomoCoreDir, mihomoCoreDir,
mihomoCorePath, mihomoCorePath,
@ -14,7 +15,7 @@ import {
patchAppConfig, patchAppConfig,
patchControledMihomoConfig patchControledMihomoConfig
} from '../config' } from '../config'
import { dialog, ipcMain, safeStorage } from 'electron' import { app, dialog, ipcMain, safeStorage } from 'electron'
import { import {
startMihomoTraffic, startMihomoTraffic,
startMihomoConnections, startMihomoConnections,
@ -26,10 +27,11 @@ import {
stopMihomoMemory stopMihomoMemory
} from './mihomoApi' } from './mihomoApi'
import chokidar from 'chokidar' import chokidar from 'chokidar'
import { writeFile } from 'fs/promises' import { readFile, rm, writeFile } from 'fs/promises'
import { promisify } from 'util' import { promisify } from 'util'
import { mainWindow } from '..' import { mainWindow } from '..'
import path from 'path' import path from 'path'
import { existsSync } from 'fs'
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async () => { chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async () => {
try { try {
@ -43,8 +45,27 @@ chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async
let child: ChildProcess let child: ChildProcess
let retry = 10 let retry = 10
export async function startCore(): Promise<Promise<void>[]> { export async function startCore(detached = false): Promise<Promise<void>[]> {
const { core = 'mihomo', autoSetDNS = true } = await getAppConfig() 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 { tun } = await getControledMihomoConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
await autoGrantCorePermition(corePath) await autoGrantCorePermition(corePath)
@ -60,7 +81,9 @@ export async function startCore(): Promise<Promise<void>[]> {
}) })
} }
} }
child = spawn(corePath, ['-d', mihomoWorkDir()]) child = spawn(corePath, ['-d', mihomoWorkDir()], {
detached: detached
})
child.on('close', async (code, signal) => { child.on('close', async (code, signal) => {
await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, { await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
flag: 'a' flag: 'a'
@ -101,7 +124,11 @@ export async function startCore(): Promise<Promise<void>[]> {
new Promise((resolve) => { new Promise((resolve) => {
child.stdout?.on('data', async (data) => { child.stdout?.on('data', async (data) => {
if (data.toString().includes('Start initial Compatible provider default')) { if (data.toString().includes('Start initial Compatible provider default')) {
mainWindow?.webContents.send('coreRestart') try {
mainWindow?.webContents.send('coreRestart')
} catch {
// ignore
}
resolve() resolve()
} }
}) })
@ -146,6 +173,27 @@ export async function restartCore(): Promise<void> {
} }
} }
export async function keepCoreAlive(): Promise<void> {
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<void> {
await keepCoreAlive()
app.exit()
}
async function checkProfile(): Promise<void> { async function checkProfile(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)

View File

@ -186,14 +186,18 @@ const mihomoTraffic = async (): Promise<void> => {
const data = e.data as string const data = e.data as string
const json = JSON.parse(data) as IMihomoTrafficInfo const json = JSON.parse(data) as IMihomoTrafficInfo
trafficRetry = 10 trafficRetry = 10
mainWindow?.webContents.send('mihomoTraffic', json) try {
if (process.platform !== 'linux') { mainWindow?.webContents.send('mihomoTraffic', json)
tray?.setToolTip( if (process.platform !== 'linux') {
'↑' + tray?.setToolTip(
`${calcTraffic(json.up)}/s`.padStart(9) + '↑' +
'\n↓' + `${calcTraffic(json.up)}/s`.padStart(9) +
`${calcTraffic(json.down)}/s`.padStart(9) '\n↓' +
) `${calcTraffic(json.down)}/s`.padStart(9)
)
}
} catch {
// ignore
} }
} }
@ -238,7 +242,11 @@ const mihomoMemory = async (): Promise<void> => {
mihomoMemoryWs.onmessage = (e): void => { mihomoMemoryWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
memoryRetry = 10 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 => { mihomoMemoryWs.onclose = (): void => {
@ -284,7 +292,11 @@ const mihomoLogs = async (): Promise<void> => {
mihomoLogsWs.onmessage = (e): void => { mihomoLogsWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
logsRetry = 10 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 => { mihomoLogsWs.onclose = (): void => {
@ -330,7 +342,11 @@ const mihomoConnections = async (): Promise<void> => {
mihomoConnectionsWs.onmessage = (e): void => { mihomoConnectionsWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
connectionsRetry = 10 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 => { mihomoConnectionsWs.onclose = (): void => {

View File

@ -8,6 +8,7 @@ import {
} from '../config' } from '../config'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { patchMihomoConfig } from '../core/mihomoApi' import { patchMihomoConfig } from '../core/mihomoApi'
import { quitWithoutCore } from '../core/manager'
export async function registerShortcut( export async function registerShortcut(
oldShortcut: string, oldShortcut: string,
@ -80,6 +81,11 @@ export async function registerShortcut(
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
}) })
} }
case 'quitWithoutCoreShortcut': {
return globalShortcut.register(newShortcut, async () => {
await quitWithoutCore()
})
}
case 'restartAppShortcut': { case 'restartAppShortcut': {
return globalShortcut.register(newShortcut, () => { return globalShortcut.register(newShortcut, () => {
app.relaunch() app.relaunch()
@ -98,6 +104,7 @@ export async function initShortcut(): Promise<void> {
ruleModeShortcut, ruleModeShortcut,
globalModeShortcut, globalModeShortcut,
directModeShortcut, directModeShortcut,
quitWithoutCoreShortcut,
restartAppShortcut restartAppShortcut
} = await getAppConfig() } = await getAppConfig()
if (showWindowShortcut) { if (showWindowShortcut) {
@ -142,6 +149,13 @@ export async function initShortcut(): Promise<void> {
// ignore // ignore
} }
} }
if (quitWithoutCoreShortcut) {
try {
await registerShortcut('', quitWithoutCoreShortcut, 'quitWithoutCoreShortcut')
} catch {
// ignore
}
}
if (restartAppShortcut) { if (restartAppShortcut) {
try { try {
await registerShortcut('', restartAppShortcut, 'restartAppShortcut') await registerShortcut('', restartAppShortcut, 'restartAppShortcut')

View File

@ -17,7 +17,7 @@ import { mainWindow, showMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron' import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { restartCore } from '../core/manager' import { quitWithoutCore, restartCore } from '../core/manager'
export let tray: Tray | null = null export let tray: Tray | null = null
@ -33,6 +33,7 @@ const buildContextMenu = async (): Promise<Menu> => {
ruleModeShortcut = '', ruleModeShortcut = '',
globalModeShortcut = '', globalModeShortcut = '',
directModeShortcut = '', directModeShortcut = '',
quitWithoutCoreShortcut = '',
restartAppShortcut = '' restartAppShortcut = ''
} = await getAppConfig() } = await getAppConfig()
let groupsMenu: Electron.MenuItemConstructorOptions[] = [] let groupsMenu: Electron.MenuItemConstructorOptions[] = []
@ -195,6 +196,13 @@ const buildContextMenu = async (): Promise<Menu> => {
click: copyEnv click: copyEnv
}, },
{ type: 'separator' }, { type: 'separator' },
{
id: 'quitWithoutCore',
label: '轻量模式',
type: 'normal',
accelerator: quitWithoutCoreShortcut,
click: quitWithoutCore
},
{ {
id: 'restart', id: 'restart',
label: '重启应用', label: '重启应用',

View File

@ -43,7 +43,12 @@ import {
updateOverrideItem updateOverrideItem
} from '../config' } from '../config'
import { startSubStoreServer, subStoreFrontendPort, subStorePort } from '../resolve/server' 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 { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
import { import {
@ -193,6 +198,7 @@ export function registerIpcMainHandlers(): void {
app.relaunch() app.relaunch()
app.quit() app.quit()
}) })
ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore))
ipcMain.handle('quitApp', () => app.quit()) ipcMain.handle('quitApp', () => app.quit())
} }

View File

@ -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 SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item' 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 { useState } from 'react'
import UpdaterModal from '../updater/updater-modal' import UpdaterModal from '../updater/updater-modal'
import { version } from '@renderer/utils/init' import { version } from '@renderer/utils/init'
import { IoIosHelpCircle } from 'react-icons/io'
const Actions: React.FC = () => { const Actions: React.FC = () => {
const [newVersion, setNewVersion] = useState('') const [newVersion, setNewVersion] = useState('')
@ -47,6 +48,21 @@ const Actions: React.FC = () => {
</Button> </Button>
</SettingItem> </SettingItem>
<SettingItem
title="轻量模式"
actions={
<Tooltip content="完全退出软件,只保留内核进程">
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
</Tooltip>
}
divider
>
<Button size="sm" onPress={quitWithoutCore}>
</Button>
</SettingItem>
<SettingItem title="退出应用" divider> <SettingItem title="退出应用" divider>
<Button size="sm" onPress={quitApp}> <Button size="sm" onPress={quitApp}>
退 退

View File

@ -48,6 +48,7 @@ const ShortcutConfig: React.FC = () => {
ruleModeShortcut = '', ruleModeShortcut = '',
globalModeShortcut = '', globalModeShortcut = '',
directModeShortcut = '', directModeShortcut = '',
quitWithoutCoreShortcut = '',
restartAppShortcut = '' restartAppShortcut = ''
} = appConfig || {} } = appConfig || {}
@ -107,6 +108,15 @@ const ShortcutConfig: React.FC = () => {
/> />
</div> </div>
</SettingItem> </SettingItem>
<SettingItem title="轻量模式">
<div className="flex justify-end w-[60%]">
<ShortcutInput
value={quitWithoutCoreShortcut}
patchAppConfig={patchAppConfig}
action="quitWithoutCoreShortcut"
/>
</div>
</SettingItem>
<SettingItem title="重启应用"> <SettingItem title="重启应用">
<div className="flex justify-end w-[60%]"> <div className="flex justify-end w-[60%]">
<ShortcutInput <ShortcutInput

View File

@ -283,6 +283,10 @@ export async function relaunchApp(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('relaunchApp')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('relaunchApp'))
} }
export async function quitWithoutCore(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitWithoutCore'))
}
export async function quitApp(): Promise<void> { export async function quitApp(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitApp')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitApp'))
} }

View File

@ -267,7 +267,7 @@ interface IAppConfig {
globalModeShortcut?: string globalModeShortcut?: string
directModeShortcut?: string directModeShortcut?: string
restartAppShortcut?: string restartAppShortcut?: string
quitAppShortcut?: string quitWithoutCoreShortcut?: string
} }
interface IMihomoTunConfig { interface IMihomoTunConfig {