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

View File

@ -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<Promise<void>[]> {
const { core = 'mihomo', autoSetDNS = true } = await getAppConfig()
export async function startCore(detached = false): Promise<Promise<void>[]> {
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<Promise<void>[]> {
})
}
}
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<Promise<void>[]> {
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<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> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)

View File

@ -186,14 +186,18 @@ const mihomoTraffic = async (): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
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 => {

View File

@ -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<void> {
ruleModeShortcut,
globalModeShortcut,
directModeShortcut,
quitWithoutCoreShortcut,
restartAppShortcut
} = await getAppConfig()
if (showWindowShortcut) {
@ -142,6 +149,13 @@ export async function initShortcut(): Promise<void> {
// ignore
}
}
if (quitWithoutCoreShortcut) {
try {
await registerShortcut('', quitWithoutCoreShortcut, 'quitWithoutCoreShortcut')
} catch {
// ignore
}
}
if (restartAppShortcut) {
try {
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 { 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<Menu> => {
ruleModeShortcut = '',
globalModeShortcut = '',
directModeShortcut = '',
quitWithoutCoreShortcut = '',
restartAppShortcut = ''
} = await getAppConfig()
let groupsMenu: Electron.MenuItemConstructorOptions[] = []
@ -195,6 +196,13 @@ const buildContextMenu = async (): Promise<Menu> => {
click: copyEnv
},
{ type: 'separator' },
{
id: 'quitWithoutCore',
label: '轻量模式',
type: 'normal',
accelerator: quitWithoutCoreShortcut,
click: quitWithoutCore
},
{
id: 'restart',
label: '重启应用',

View File

@ -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())
}

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 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 = () => {
</Button>
</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>
<Button size="sm" onPress={quitApp}>
退

View File

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

View File

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

View File

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