mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
support quit without core
This commit is contained in:
parent
130150325a
commit
5e9c06105f
10
changelog.md
10
changelog.md
@ -4,11 +4,5 @@
|
||||
|
||||
### New Features
|
||||
|
||||
- 优化侧边栏卡片拖动体验
|
||||
- 支持自定义侧边栏卡片大小
|
||||
- 支持隐藏侧边栏卡片
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复Ubuntu下每次开启Tun都需要密码的问题
|
||||
- 修复Sub-Store无法读取剪切板的问题
|
||||
- 新增轻量模式,支持完全退出应用只保留内核后台运行
|
||||
- 折叠不常用设置项
|
||||
|
||||
@ -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')) {
|
||||
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)
|
||||
|
||||
@ -186,6 +186,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
const data = e.data as string
|
||||
const json = JSON.parse(data) as IMihomoTrafficInfo
|
||||
trafficRetry = 10
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoTraffic', json)
|
||||
if (process.platform !== 'linux') {
|
||||
tray?.setToolTip(
|
||||
@ -195,6 +196,9 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
`${calcTraffic(json.down)}/s`.padStart(9)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
mihomoTrafficWs.onclose = (): void => {
|
||||
@ -238,7 +242,11 @@ const mihomoMemory = async (): Promise<void> => {
|
||||
mihomoMemoryWs.onmessage = (e): void => {
|
||||
const data = e.data as string
|
||||
memoryRetry = 10
|
||||
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
|
||||
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
|
||||
try {
|
||||
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
mihomoConnectionsWs.onclose = (): void => {
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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: '重启应用',
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -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}>
|
||||
退出应用
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'))
|
||||
}
|
||||
|
||||
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
@ -267,7 +267,7 @@ interface IAppConfig {
|
||||
globalModeShortcut?: string
|
||||
directModeShortcut?: string
|
||||
restartAppShortcut?: string
|
||||
quitAppShortcut?: string
|
||||
quitWithoutCoreShortcut?: string
|
||||
}
|
||||
|
||||
interface IMihomoTunConfig {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user