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
|
### New Features
|
||||||
|
|
||||||
- 优化侧边栏卡片拖动体验
|
- 新增轻量模式,支持完全退出应用只保留内核后台运行
|
||||||
- 支持自定义侧边栏卡片大小
|
- 折叠不常用设置项
|
||||||
- 支持隐藏侧边栏卡片
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- 修复Ubuntu下每次开启Tun都需要密码的问题
|
|
||||||
- 修复Sub-Store无法读取剪切板的问题
|
|
||||||
|
|||||||
@ -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')) {
|
||||||
|
try {
|
||||||
mainWindow?.webContents.send('coreRestart')
|
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)
|
||||||
|
|||||||
@ -186,6 +186,7 @@ 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
|
||||||
|
try {
|
||||||
mainWindow?.webContents.send('mihomoTraffic', json)
|
mainWindow?.webContents.send('mihomoTraffic', json)
|
||||||
if (process.platform !== 'linux') {
|
if (process.platform !== 'linux') {
|
||||||
tray?.setToolTip(
|
tray?.setToolTip(
|
||||||
@ -195,6 +196,9 @@ const mihomoTraffic = async (): Promise<void> => {
|
|||||||
`${calcTraffic(json.down)}/s`.padStart(9)
|
`${calcTraffic(json.down)}/s`.padStart(9)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mihomoTrafficWs.onclose = (): void => {
|
mihomoTrafficWs.onclose = (): void => {
|
||||||
@ -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
|
||||||
|
try {
|
||||||
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
|
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
|
||||||
|
try {
|
||||||
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
|
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
|
||||||
|
try {
|
||||||
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mihomoConnectionsWs.onclose = (): void => {
|
mihomoConnectionsWs.onclose = (): void => {
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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: '重启应用',
|
||||||
|
|||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}>
|
||||||
退出应用
|
退出应用
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'))
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user