use async and optimize error handling

This commit is contained in:
pompurin404 2024-08-13 15:39:11 +08:00
parent 2b62a2f1e1
commit 2c5aa1a482
No known key found for this signature in database
23 changed files with 825 additions and 746 deletions

View File

@ -1,22 +1,23 @@
import { readFile, writeFile } from 'fs/promises'
import { appConfigPath } from '../utils/dirs' import { appConfigPath } from '../utils/dirs'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs'
export let appConfig: IAppConfig // config.yaml let appConfig: IAppConfig // config.yaml
export function getAppConfig(force = false): IAppConfig { export async function getAppConfig(force = false): Promise<IAppConfig> {
if (force || !appConfig) { if (force || !appConfig) {
appConfig = yaml.parse(fs.readFileSync(appConfigPath(), 'utf-8')) const data = await readFile(appConfigPath(), 'utf-8')
appConfig = yaml.parse(data)
} }
return appConfig return appConfig
} }
export function setAppConfig(patch: Partial<IAppConfig>): void { export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
if (patch.sysProxy) { if (patch.sysProxy) {
const oldSysProxy = appConfig.sysProxy || {} const oldSysProxy = appConfig.sysProxy || {}
const newSysProxy = Object.assign(oldSysProxy, patch.sysProxy) const newSysProxy = Object.assign(oldSysProxy, patch.sysProxy)
patch.sysProxy = newSysProxy patch.sysProxy = newSysProxy
} }
appConfig = Object.assign(appConfig, patch) appConfig = Object.assign(appConfig, patch)
fs.writeFileSync(appConfigPath(), yaml.stringify(appConfig)) await writeFile(appConfigPath(), yaml.stringify(appConfig))
} }

View File

@ -1,21 +1,22 @@
import { controledMihomoConfigPath } from '../utils/dirs' import { controledMihomoConfigPath } from '../utils/dirs'
import { readFile, writeFile } from 'fs/promises'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs'
import { getAxios, startMihomoMemory, startMihomoTraffic } from '../core/mihomoApi' import { getAxios, startMihomoMemory, startMihomoTraffic } from '../core/mihomoApi'
import { generateProfile } from '../resolve/factory' import { generateProfile } from '../resolve/factory'
import { getAppConfig } from './app' import { getAppConfig } from './app'
export let controledMihomoConfig: Partial<IMihomoConfig> // mihomo.yaml let controledMihomoConfig: Partial<IMihomoConfig> // mihomo.yaml
export function getControledMihomoConfig(force = false): Partial<IMihomoConfig> { export async function getControledMihomoConfig(force = false): Promise<Partial<IMihomoConfig>> {
if (force || !controledMihomoConfig) { if (force || !controledMihomoConfig) {
controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath(), 'utf-8')) const data = await readFile(controledMihomoConfigPath(), 'utf-8')
controledMihomoConfig = yaml.parse(data)
} }
return controledMihomoConfig return controledMihomoConfig
} }
export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void { export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
const { useNameserverPolicy } = getAppConfig() const { useNameserverPolicy } = await getAppConfig()
if (patch.tun) { if (patch.tun) {
const oldTun = controledMihomoConfig.tun || {} const oldTun = controledMihomoConfig.tun || {}
const newTun = Object.assign(oldTun, patch.tun) const newTun = Object.assign(oldTun, patch.tun)
@ -35,11 +36,12 @@ export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void {
patch.sniffer = newSniffer patch.sniffer = newSniffer
} }
controledMihomoConfig = Object.assign(controledMihomoConfig, patch) controledMihomoConfig = Object.assign(controledMihomoConfig, patch)
if (patch['external-controller'] || patch.secret) { if (patch['external-controller'] || patch.secret) {
getAxios(true) await getAxios(true)
startMihomoMemory() await startMihomoMemory()
startMihomoTraffic() await startMihomoTraffic()
} }
generateProfile() await generateProfile()
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig)) await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
} }

View File

@ -1,5 +1,5 @@
export { getAppConfig, setAppConfig } from './app' export { getAppConfig, patchAppConfig } from './app'
export { getControledMihomoConfig, setControledMihomoConfig } from './controledMihomo' export { getControledMihomoConfig, patchControledMihomoConfig } from './controledMihomo'
export { export {
getProfile, getProfile,
getCurrentProfileItem, getCurrentProfileItem,

View File

@ -1,47 +1,56 @@
import { overrideConfigPath, overridePath } from '../utils/dirs' import { overrideConfigPath, overridePath } from '../utils/dirs'
import yaml from 'yaml'
import fs from 'fs'
import { dialog } from 'electron'
import axios from 'axios'
import { getControledMihomoConfig } from './controledMihomo' import { getControledMihomoConfig } from './controledMihomo'
import { readFile, writeFile, rm } from 'fs/promises'
import { existsSync } from 'fs'
import axios from 'axios'
import yaml from 'yaml'
let overrideConfig: IOverrideConfig // override.yaml let overrideConfig: IOverrideConfig // override.yaml
export function getOverrideConfig(force = false): IOverrideConfig { export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
if (force || !overrideConfig) { if (force || !overrideConfig) {
overrideConfig = yaml.parse(fs.readFileSync(overrideConfigPath(), 'utf-8')) const data = await readFile(overrideConfigPath(), 'utf-8')
overrideConfig = yaml.parse(data)
} }
return overrideConfig return overrideConfig
} }
export function setOverrideConfig(config: IOverrideConfig): void { export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
overrideConfig = config overrideConfig = config
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig)) await writeFile(overrideConfigPath(), yaml.stringify(overrideConfig), 'utf-8')
} }
export function getOverrideItem(id: string): IOverrideItem | undefined { export async function getOverrideItem(id: string | undefined): Promise<IOverrideItem | undefined> {
return overrideConfig.items.find((item) => item.id === id) const { items } = await getOverrideConfig()
return items.find((item) => item.id === id)
} }
export function updateOverrideItem(item: IOverrideItem): void {
const index = overrideConfig.items.findIndex((i) => i.id === item.id) export async function updateOverrideItem(item: IOverrideItem): Promise<void> {
overrideConfig.items[index] = item const config = await getOverrideConfig()
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig)) const index = config.items.findIndex((i) => i.id === item.id)
if (index === -1) {
throw new Error('Override not found')
}
config.items[index] = item
await setOverrideConfig(config)
} }
export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> { export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> {
const config = await getOverrideConfig()
const newItem = await createOverride(item) const newItem = await createOverride(item)
if (overrideConfig.items.find((i) => i.id === newItem.id)) { if (await getOverrideItem(item.id)) {
updateOverrideItem(newItem) updateOverrideItem(newItem)
} else { } else {
overrideConfig.items.push(newItem) config.items.push(newItem)
} }
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig)) await setOverrideConfig(config)
} }
export function removeOverrideItem(id: string): void { export async function removeOverrideItem(id: string): Promise<void> {
overrideConfig.items = overrideConfig.items?.filter((item) => item.id !== id) const config = await getOverrideConfig()
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig)) config.items = config.items?.filter((item) => item.id !== id)
fs.rmSync(overridePath(id)) await setOverrideConfig(config)
await rm(overridePath(id))
} }
export async function createOverride(item: Partial<IOverrideItem>): Promise<IOverrideItem> { export async function createOverride(item: Partial<IOverrideItem>): Promise<IOverrideItem> {
@ -55,31 +64,21 @@ export async function createOverride(item: Partial<IOverrideItem>): Promise<IOve
} as IOverrideItem } as IOverrideItem
switch (newItem.type) { switch (newItem.type) {
case 'remote': { case 'remote': {
if (!item.url) { const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
throw new Error('URL is required for remote script') if (!item.url) throw new Error('Empty URL')
} const res = await axios.get(item.url, {
try { proxy: {
const res = await axios.get(item.url, { protocol: 'http',
proxy: { host: '127.0.0.1',
protocol: 'http', port: mixedPort
host: '127.0.0.1', }
port: getControledMihomoConfig()['mixed-port'] || 7890 })
}, const data = res.data
responseType: 'text' await setOverride(id, data)
})
const data = res.data
setOverride(id, data)
} catch (e) {
dialog.showErrorBox('Failed to fetch remote script', `${e}\nurl: ${item.url}`)
throw new Error(`Failed to fetch remote script ${e}`)
}
break break
} }
case 'local': { case 'local': {
if (!item.file) { const data = item.file || ''
throw new Error('File is required for local script')
}
const data = item.file
setOverride(id, data) setOverride(id, data)
break break
} }
@ -88,13 +87,13 @@ export async function createOverride(item: Partial<IOverrideItem>): Promise<IOve
return newItem return newItem
} }
export function getOverride(id: string): string { export async function getOverride(id: string): Promise<string> {
if (!fs.existsSync(overridePath(id))) { if (!existsSync(overridePath(id))) {
return `function main(config){ return config }` return `function main(config){ return config }`
} }
return fs.readFileSync(overridePath(id), 'utf-8') return await readFile(overridePath(id), 'utf-8')
} }
export function setOverride(id: string, content: string): void { export async function setOverride(id: string, content: string): Promise<void> {
fs.writeFileSync(overridePath(id), content, 'utf-8') await writeFile(overridePath(id), content, 'utf-8')
} }

View File

@ -1,87 +1,168 @@
import { getControledMihomoConfig } from './controledMihomo' import { getControledMihomoConfig } from './controledMihomo'
import { profileConfigPath, profilePath } from '../utils/dirs' import { profileConfigPath, profilePath } from '../utils/dirs'
import { addProfileUpdater } from '../core/profileUpdater'
import { readFile, rm, writeFile } from 'fs/promises'
import { restartCore } from '../core/manager' import { restartCore } from '../core/manager'
import { getAppConfig } from './app' import { getAppConfig } from './app'
import { window } from '..' import { mainWindow } from '..'
import { existsSync } from 'fs'
import axios from 'axios' import axios from 'axios'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import { defaultProfile } from '../utils/template'
import { dialog } from 'electron'
import { addProfileUpdater } from '../core/profileUpdater'
let profileConfig: IProfileConfig // profile.yaml let profileConfig: IProfileConfig // profile.yaml
export function getProfileConfig(force = false): IProfileConfig { export async function getProfileConfig(force = false): Promise<IProfileConfig> {
if (force || !profileConfig) { if (force || !profileConfig) {
profileConfig = yaml.parse(fs.readFileSync(profileConfigPath(), 'utf-8')) const data = await readFile(profileConfigPath(), 'utf-8')
profileConfig = yaml.parse(data)
} }
return profileConfig return profileConfig
} }
export function setProfileConfig(config: IProfileConfig): void { export async function setProfileConfig(config: IProfileConfig): Promise<void> {
profileConfig = config profileConfig = config
window?.webContents.send('profileConfigUpdated') mainWindow?.webContents.send('profileConfigUpdated')
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) await writeFile(profileConfigPath(), yaml.stringify(config), 'utf-8')
} }
export function getProfileItem(id: string | undefined): IProfileItem { export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
const items = getProfileConfig().items const { items } = await getProfileConfig()
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' } if (!id || id === 'default') return { id: 'default', type: 'local', name: '空白订阅' }
return items.find((item) => item.id === id)
} }
export async function changeCurrentProfile(id: string): Promise<void> { export async function changeCurrentProfile(id: string): Promise<void> {
const oldId = getProfileConfig().current const config = await getProfileConfig()
profileConfig.current = id const current = config.current
config.current = id
await setProfileConfig(config)
try { try {
await restartCore() await restartCore()
} catch (e) { } catch (e) {
profileConfig.current = oldId config.current = current
throw e
} finally { } finally {
window?.webContents.send('profileConfigUpdated') await setProfileConfig(config)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
} }
} }
export function updateProfileItem(item: IProfileItem): void { export async function updateProfileItem(item: IProfileItem): Promise<void> {
const index = profileConfig.items.findIndex((i) => i.id === item.id) const config = await getProfileConfig()
profileConfig.items[index] = item const index = config.items.findIndex((i) => i.id === item.id)
addProfileUpdater(item.id) if (index === -1) {
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) throw new Error('Profile not found')
window?.webContents.send('profileConfigUpdated') }
config.items[index] = item
await setProfileConfig(config)
await addProfileUpdater(item)
} }
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item) const newItem = await createProfile(item)
if (profileConfig.items.find((i) => i.id === newItem.id)) { const config = await getProfileConfig()
updateProfileItem(newItem) if (await getProfileItem(item.id)) {
await updateProfileItem(newItem)
} else { } else {
profileConfig.items.push(newItem) config.items.push(newItem)
} }
await setProfileConfig(config)
if (!getProfileConfig().current) { if (!config.current) {
changeCurrentProfile(newItem.id) await changeCurrentProfile(newItem.id)
} }
addProfileUpdater(newItem.id) await addProfileUpdater(newItem)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
} }
export function removeProfileItem(id: string): void { export async function removeProfileItem(id: string): Promise<void> {
profileConfig.items = profileConfig.items?.filter((item) => item.id !== id) const config = await getProfileConfig()
if (profileConfig.current === id) { config.items = config.items?.filter((item) => item.id !== id)
if (profileConfig.items.length > 0) { if (config.current === id) {
profileConfig.current = profileConfig.items[0]?.id if (config.items.length > 0) {
config.current = config.items[0].id
} else { } else {
profileConfig.current = undefined config.current = undefined
} }
} }
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) await setProfileConfig(config)
fs.rmSync(profilePath(id)) if (existsSync(profilePath(id))) {
window?.webContents.send('profileConfigUpdated') await rm(profilePath(id))
}
} }
export function getCurrentProfileItem(): IProfileItem { export async function getCurrentProfileItem(): Promise<IProfileItem> {
return getProfileItem(getProfileConfig().current) const { current } = await getProfileConfig()
return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' }
}
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
const id = item.id || new Date().getTime().toString(16)
const newItem = {
id,
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
type: item.type,
url: item.url,
interval: item.interval || 0,
updated: new Date().getTime()
} as IProfileItem
switch (newItem.type) {
case 'remote': {
const { userAgent = 'clash-meta' } = await getAppConfig()
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
if (!item.url) throw new Error('Empty URL')
const res = await axios.get(item.url, {
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
},
headers: {
'User-Agent': userAgent
}
})
const data = res.data
const headers = res.headers
if (headers['content-disposition'] && newItem.name === 'Remote File') {
newItem.name = parseFilename(headers['content-disposition'])
}
if (headers['profile-web-page-url']) {
newItem.home = headers['profile-web-page-url']
}
if (headers['profile-update-interval']) {
newItem.interval = parseInt(headers['profile-update-interval']) * 60
}
if (headers['subscription-userinfo']) {
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
}
await setProfileStr(id, data)
break
}
case 'local': {
const data = item.file || ''
await setProfileStr(id, data)
break
}
}
return newItem
}
export async function getProfileStr(id: string | undefined): Promise<string> {
if (existsSync(profilePath(id || 'default'))) {
return await readFile(profilePath(id || 'default'), 'utf-8')
} else {
return yaml.stringify(defaultProfile)
}
}
export async function setProfileStr(id: string, content: string): Promise<void> {
const { current } = await getProfileConfig()
await writeFile(profilePath(id), content, 'utf-8')
if (current === id) await restartCore()
}
export async function getProfile(id: string | undefined): Promise<IMihomoConfig> {
const profile = await getProfileStr(id)
return yaml.parse(profile)
} }
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx // attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
@ -105,80 +186,3 @@ function parseSubinfo(str: string): ISubscriptionUserInfo {
}) })
return obj return obj
} }
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
const id = item.id || new Date().getTime().toString(16)
const newItem = {
id,
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
type: item.type,
url: item.url,
interval: item.interval || 0,
updated: new Date().getTime()
} as IProfileItem
switch (newItem.type) {
case 'remote': {
if (!item.url) {
throw new Error('URL is required for remote profile')
}
try {
const ua = getAppConfig().userAgent || 'clash-meta'
const res = await axios.get(item.url, {
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: getControledMihomoConfig()['mixed-port'] || 7890
},
headers: {
'User-Agent': ua
},
responseType: 'text'
})
const data = res.data
const headers = res.headers
if (headers['content-disposition'] && newItem.name === 'Remote File') {
newItem.name = parseFilename(headers['content-disposition'])
}
if (headers['profile-web-page-url']) {
newItem.home = headers['profile-web-page-url']
}
if (headers['profile-update-interval']) {
newItem.interval = parseInt(headers['profile-update-interval']) * 60
}
if (headers['subscription-userinfo']) {
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
}
await setProfileStr(id, data)
} catch (e) {
dialog.showErrorBox('Failed to fetch remote profile', `${e}\nurl: ${item.url}`)
throw new Error(`Failed to fetch remote profile ${e}`)
}
break
}
case 'local': {
if (!item.file) {
throw new Error('File is required for local profile')
}
const data = item.file
await setProfileStr(id, data)
break
}
}
return newItem
}
export function getProfileStr(id: string): string {
return fs.readFileSync(profilePath(id), 'utf-8')
}
export async function setProfileStr(id: string, content: string): Promise<void> {
fs.writeFileSync(profilePath(id), content, 'utf-8')
if (id === getProfileConfig().current) {
await restartCore()
}
}
export function getProfile(id: string | undefined): IMihomoConfig {
return yaml.parse(getProfileStr(id || 'default'))
}

View File

@ -1,4 +1,4 @@
import { ChildProcess, execFile, execSync, spawn } from 'child_process' import { ChildProcess, exec, execFile, spawn } from 'child_process'
import { import {
logPath, logPath,
mihomoCorePath, mihomoCorePath,
@ -7,29 +7,44 @@ import {
mihomoWorkDir mihomoWorkDir
} from '../utils/dirs' } from '../utils/dirs'
import { generateProfile } from '../resolve/factory' import { generateProfile } from '../resolve/factory'
import { getAppConfig, setAppConfig } from '../config' import { getAppConfig, patchAppConfig } from '../config'
import { dialog, safeStorage } from 'electron' import { dialog, safeStorage } from 'electron'
import fs from 'fs'
import { pauseWebsockets } from './mihomoApi' import { pauseWebsockets } from './mihomoApi'
import { writeFile } from 'fs/promises'
import { promisify } from 'util'
let child: ChildProcess let child: ChildProcess
let retry = 10 let retry = 10
export async function startCore(): Promise<void> { export async function startCore(): Promise<void> {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo') const { core = 'mihomo' } = await getAppConfig()
grantCorePermition(corePath) const corePath = mihomoCorePath(core)
generateProfile() await grantCorePermition(corePath)
await generateProfile()
await checkProfile() await checkProfile()
stopCore() stopCore()
child = spawn(corePath, ['-d', mihomoWorkDir()])
child.on('close', async (code, signal) => {
await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
flag: 'a'
})
if (retry) {
await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' })
retry--
await restartCore()
} else {
stopCore()
}
})
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
child = spawn(corePath, ['-d', mihomoWorkDir()]) child.stdout?.on('data', async (data) => {
child.stdout?.on('data', (data) => {
if (data.toString().includes('External controller listen error')) { if (data.toString().includes('External controller listen error')) {
if (retry) { if (retry) {
retry-- retry--
resolve(startCore()) resolve(await startCore())
} else { } else {
dialog.showErrorBox('External controller listen error', data.toString()) dialog.showErrorBox('内核连接失败', '请尝试更改外部控制端口后重启内核')
stopCore()
reject('External controller listen error') reject('External controller listen error')
} }
} }
@ -37,36 +52,7 @@ export async function startCore(): Promise<void> {
retry = 10 retry = 10
resolve() resolve()
} }
fs.writeFileSync( await writeFile(logPath(), data, { flag: 'a' })
logPath(),
data
.toString()
.split('\n')
.map((line: string) => {
if (line) return `[Mihomo]: ${line}`
return ''
})
.filter(Boolean)
.join('\n'),
{
flag: 'a'
}
)
})
child.on('close', async (code, signal) => {
fs.writeFileSync(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
flag: 'a'
})
fs.writeFileSync(logPath(), `[Manager]: Restart Core\n`, {
flag: 'a'
})
if (retry) {
retry--
await restartCore()
} else {
dialog.showErrorBox('Mihomo Core Closed', `Core closed, code: ${code}, signal: ${signal}`)
stopCore()
}
}) })
}) })
} }
@ -84,44 +70,49 @@ export async function restartCore(): Promise<void> {
recover() recover()
} }
export function checkProfile(): Promise<void> { async function checkProfile(): Promise<void> {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo') const { core = 'mihomo' } = await getAppConfig()
return new Promise((resolve, reject) => { const corePath = mihomoCorePath(core)
const child = execFile(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()]) const execFilePromise = promisify(execFile)
child.stdout?.on('data', (data) => { try {
data await execFilePromise(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
.toString() } catch (error) {
if (error instanceof Error && 'stdout' in error) {
const { stdout } = error as { stdout: string }
const errorLines = stdout
.split('\n') .split('\n')
.forEach((line: string) => { .filter((line) => line.includes('level=error'))
if (line.includes('level=error')) { .map((line) => line.split('level=error')[1])
dialog.showErrorBox('Profile Check Failed', line.split('level=error')[1]) throw new Error(`Profile Check Failed:\n${errorLines.join('\n')}`)
reject(line) } else {
} throw error
}) }
}) }
child.on('close', (code) => {
if (code === 0) {
resolve()
}
})
})
} }
export function grantCorePermition(corePath: string): void { export async function grantCorePermition(corePath: string): Promise<void> {
if (getAppConfig().encryptedPassword && isEncryptionAvailable()) { const { encryptedPassword } = await getAppConfig()
const password = safeStorage.decryptString(Buffer.from(getAppConfig().encryptedPassword ?? [])) const execPromise = promisify(exec)
try { if (encryptedPassword && isEncryptionAvailable()) {
if (process.platform === 'linux') { const password = safeStorage.decryptString(Buffer.from(encryptedPassword))
execSync( if (process.platform === 'linux') {
try {
await execPromise(
`echo "${password}" | sudo -S setcap cap_net_bind_service,cap_net_admin,cap_sys_ptrace,cap_dac_read_search,cap_dac_override,cap_net_raw=+ep ${corePath}` `echo "${password}" | sudo -S setcap cap_net_bind_service,cap_net_admin,cap_sys_ptrace,cap_dac_read_search,cap_dac_override,cap_net_raw=+ep ${corePath}`
) )
} catch (error) {
patchAppConfig({ encryptedPassword: undefined })
throw error
} }
if (process.platform === 'darwin') { }
execSync(`echo "${password}" | sudo -S chown root:admin ${corePath}`) if (process.platform === 'darwin') {
execSync(`echo "${password}" | sudo -S chmod +sx ${corePath}`) try {
await execPromise(`echo "${password}" | sudo -S chown root:admin ${corePath}`)
await execPromise(`echo "${password}" | sudo -S chmod +sx ${corePath}`)
} catch (error) {
patchAppConfig({ encryptedPassword: undefined })
throw error
} }
} catch (e) {
setAppConfig({ encryptedPassword: undefined })
} }
} }
} }

View File

@ -1,7 +1,7 @@
import axios, { AxiosInstance } from 'axios' import axios, { AxiosInstance } from 'axios'
import { getAppConfig, getControledMihomoConfig } from '../config' import { getAppConfig, getControledMihomoConfig } from '../config'
import { mainWindow } from '..'
import WebSocket from 'ws' import WebSocket from 'ws'
import { window } from '..'
let axiosIns: AxiosInstance = null! let axiosIns: AxiosInstance = null!
let mihomoTrafficWs: WebSocket | null = null let mihomoTrafficWs: WebSocket | null = null
@ -15,9 +15,9 @@ let connectionsRetry = 10
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => { export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
if (axiosIns && !force) return axiosIns if (axiosIns && !force) return axiosIns
const controledMihomoConfig = await getControledMihomoConfig()
let server = getControledMihomoConfig()['external-controller'] let server = controledMihomoConfig['external-controller']
const secret = getControledMihomoConfig().secret ?? '' const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
axiosIns = axios.create({ axiosIns = axios.create({
@ -26,138 +26,127 @@ export const getAxios = async (force: boolean = false): Promise<AxiosInstance> =
headers: secret ? { Authorization: `Bearer ${secret}` } : {}, headers: secret ? { Authorization: `Bearer ${secret}` } : {},
timeout: 15000 timeout: 15000
}) })
axiosIns.interceptors.response.use((r) => r.data)
axiosIns.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data)
}
return Promise.reject(error)
}
)
return axiosIns return axiosIns
} }
export async function mihomoVersion(): Promise<IMihomoVersion> { export async function mihomoVersion(): Promise<IMihomoVersion> {
const instance = await getAxios() const instance = await getAxios()
return (await instance.get('/version').catch(() => { try {
return { version: '-' } return await instance.get('/version')
})) as IMihomoVersion } catch (error) {
return { version: '-', meta: true }
}
} }
export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<void> => { export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.patch('/configs', patch).catch((e) => { return await instance.patch('/configs', patch)
return e.response.data
})) as Promise<void>
} }
export const mihomoCloseConnection = async (id: string): Promise<void> => { export const mihomoCloseConnection = async (id: string): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.delete(`/connections/${encodeURIComponent(id)}`).catch((e) => { return await instance.delete(`/connections/${encodeURIComponent(id)}`)
return e.response.data
})) as Promise<void>
} }
export const mihomoCloseAllConnections = async (): Promise<void> => { export const mihomoCloseAllConnections = async (): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.delete('/connections').catch((e) => { return await instance.delete('/connections')
return e.response.data
})) as Promise<void>
} }
export const mihomoRules = async (): Promise<IMihomoRulesInfo> => { export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.get('/rules').catch(() => { try {
return await instance.get('/rules')
} catch (e) {
return { rules: [] } return { rules: [] }
})) as IMihomoRulesInfo }
} }
export const mihomoProxies = async (): Promise<IMihomoProxies> => { export const mihomoProxies = async (): Promise<IMihomoProxies> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.get('/proxies').catch(() => { try {
return await instance.get('/proxies')
} catch (e) {
return { proxies: {} } return { proxies: {} }
})) as IMihomoProxies }
} }
export const mihomoProxyProviders = async (): Promise<IMihomoProxyProviders> => { export const mihomoProxyProviders = async (): Promise<IMihomoProxyProviders> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.get('/providers/proxies').catch(() => { try {
return await instance.get('/providers/proxies')
} catch (e) {
return { providers: {} } return { providers: {} }
})) as IMihomoProxyProviders }
} }
export const mihomoUpdateProxyProviders = async (name: string): Promise<void> => { export const mihomoUpdateProxyProviders = async (name: string): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`).catch((e) => { return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`)
return e.response.data
})
} }
export const mihomoRuleProviders = async (): Promise<IMihomoRuleProviders> => { export const mihomoRuleProviders = async (): Promise<IMihomoRuleProviders> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.get('/providers/rules').catch(() => { try {
return await instance.get('/providers/rules')
} catch (e) {
return { providers: {} } return { providers: {} }
})) as IMihomoRuleProviders }
} }
export const mihomoUpdateRuleProviders = async (name: string): Promise<void> => { export const mihomoUpdateRuleProviders = async (name: string): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return instance.put(`/providers/rules/${encodeURIComponent(name)}`).catch((e) => { return await instance.put(`/providers/rules/${encodeURIComponent(name)}`)
return e.response.data
})
} }
export const mihomoChangeProxy = async (group: string, proxy: string): Promise<IMihomoProxy> => { export const mihomoChangeProxy = async (group: string, proxy: string): Promise<IMihomoProxy> => {
const instance = await getAxios() const instance = await getAxios()
return (await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }).catch(() => { return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
return {
alive: false,
extra: {},
history: [],
id: '',
name: '',
tfo: false,
type: 'Shadowsocks',
udp: false,
xudp: false
}
})) as IMihomoProxy
} }
export const mihomoUpgradeGeo = async (): Promise<void> => { export const mihomoUpgradeGeo = async (): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return instance.post('/configs/geo').catch((e) => { return await instance.post('/configs/geo')
return e.response.data
})
} }
export const mihomoProxyDelay = async (proxy: string, url?: string): Promise<IMihomoDelay> => { export const mihomoProxyDelay = async (proxy: string, url?: string): Promise<IMihomoDelay> => {
const appConfig = getAppConfig() const appConfig = await getAppConfig()
const { delayTestUrl, delayTestTimeout } = appConfig const { delayTestUrl, delayTestTimeout } = appConfig
const instance = await getAxios() const instance = await getAxios()
return (await instance return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
.get(`/proxies/${encodeURIComponent(proxy)}/delay`, { params: {
params: { url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204', timeout: delayTestTimeout || 5000
timeout: delayTestTimeout || 5000 }
} })
})
.catch((e) => {
return e.response.data
})) as IMihomoDelay
} }
export const mihomoGroupDelay = async (group: string, url?: string): Promise<IMihomoGroupDelay> => { export const mihomoGroupDelay = async (group: string, url?: string): Promise<IMihomoGroupDelay> => {
const appConfig = getAppConfig() const appConfig = await getAppConfig()
const { delayTestUrl, delayTestTimeout } = appConfig const { delayTestUrl, delayTestTimeout } = appConfig
const instance = await getAxios() const instance = await getAxios()
return (await instance return await instance.get(`/group/${encodeURIComponent(group)}/delay`, {
.get(`/group/${encodeURIComponent(group)}/delay`, { params: {
params: { url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204', timeout: delayTestTimeout || 5000
timeout: delayTestTimeout || 5000 }
} })
})
.catch((e) => {
return e.response.data
})) as IMihomoGroupDelay
} }
export const startMihomoTraffic = (): void => { export const startMihomoTraffic = async (): Promise<void> => {
mihomoTraffic() await mihomoTraffic()
} }
export const stopMihomoTraffic = (): void => { export const stopMihomoTraffic = (): void => {
@ -170,9 +159,10 @@ export const stopMihomoTraffic = (): void => {
} }
} }
const mihomoTraffic = (): void => { const mihomoTraffic = async (): Promise<void> => {
let server = getControledMihomoConfig()['external-controller'] const controledMihomoConfig = await getControledMihomoConfig()
const secret = getControledMihomoConfig().secret ?? '' let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoTraffic() stopMihomoTraffic()
@ -181,7 +171,7 @@ const mihomoTraffic = (): void => {
mihomoTrafficWs.onmessage = (e): void => { mihomoTrafficWs.onmessage = (e): void => {
const data = e.data as string const data = e.data as string
trafficRetry = 10 trafficRetry = 10
window?.webContents.send('mihomoTraffic', JSON.parse(data) as IMihomoTrafficInfo) mainWindow?.webContents.send('mihomoTraffic', JSON.parse(data) as IMihomoTrafficInfo)
} }
mihomoTrafficWs.onclose = (): void => { mihomoTrafficWs.onclose = (): void => {
@ -199,8 +189,8 @@ const mihomoTraffic = (): void => {
} }
} }
export const startMihomoMemory = (): void => { export const startMihomoMemory = async (): Promise<void> => {
mihomoMemory() await mihomoMemory()
} }
export const stopMihomoMemory = (): void => { export const stopMihomoMemory = (): void => {
@ -213,9 +203,10 @@ export const stopMihomoMemory = (): void => {
} }
} }
const mihomoMemory = (): void => { const mihomoMemory = async (): Promise<void> => {
let server = getControledMihomoConfig()['external-controller'] const controledMihomoConfig = await getControledMihomoConfig()
const secret = getControledMihomoConfig().secret ?? '' let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoMemory() stopMihomoMemory()
@ -224,7 +215,7 @@ const mihomoMemory = (): 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
window?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo) mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
} }
mihomoMemoryWs.onclose = (): void => { mihomoMemoryWs.onclose = (): void => {
@ -242,8 +233,8 @@ const mihomoMemory = (): void => {
} }
} }
export const startMihomoLogs = (): void => { export const startMihomoLogs = async (): Promise<void> => {
mihomoLogs() await mihomoLogs()
} }
export const stopMihomoLogs = (): void => { export const stopMihomoLogs = (): void => {
@ -256,9 +247,10 @@ export const stopMihomoLogs = (): void => {
} }
} }
const mihomoLogs = (): void => { const mihomoLogs = async (): Promise<void> => {
const { secret = '', 'log-level': level = 'info' } = getControledMihomoConfig() const controledMihomoConfig = await getControledMihomoConfig()
let { 'external-controller': server } = getControledMihomoConfig() const { secret = '', 'log-level': level = 'info' } = controledMihomoConfig
let { 'external-controller': server } = controledMihomoConfig
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoLogs() stopMihomoLogs()
@ -269,7 +261,7 @@ const mihomoLogs = (): 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
window?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo) mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
} }
mihomoLogsWs.onclose = (): void => { mihomoLogsWs.onclose = (): void => {
@ -287,8 +279,8 @@ const mihomoLogs = (): void => {
} }
} }
export const startMihomoConnections = (): void => { export const startMihomoConnections = async (): Promise<void> => {
mihomoConnections() await mihomoConnections()
} }
export const stopMihomoConnections = (): void => { export const stopMihomoConnections = (): void => {
@ -301,9 +293,10 @@ export const stopMihomoConnections = (): void => {
} }
} }
const mihomoConnections = (): void => { const mihomoConnections = async (): Promise<void> => {
let server = getControledMihomoConfig()['external-controller'] const controledMihomoConfig = await getControledMihomoConfig()
const secret = getControledMihomoConfig().secret ?? '' let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoConnections() stopMihomoConnections()
@ -314,7 +307,7 @@ const mihomoConnections = (): 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
window?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo) mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
} }
mihomoConnectionsWs.onclose = (): void => { mihomoConnectionsWs.onclose = (): void => {

View File

@ -1,42 +1,60 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config' import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config'
const intervalPool: Record<string, NodeJS.Timeout> = {} const intervalPool: Record<string, NodeJS.Timeout> = {}
export async function initProfileUpdater(): Promise<void> { export async function initProfileUpdater(): Promise<void> {
const { items, current } = getProfileConfig() const { items, current } = await getProfileConfig()
const currentItem = getCurrentProfileItem() const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) { for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.interval) { if (item.type === 'remote' && item.interval) {
await addProfileItem(item) intervalPool[item.id] = setTimeout(
intervalPool[item.id] = setInterval(
async () => { async () => {
await addProfileItem(item) try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
}, },
item.interval * 60 * 1000 item.interval * 60 * 1000
) )
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
} }
} }
if (currentItem.type === 'remote' && currentItem.interval) { if (currentItem?.type === 'remote' && currentItem.interval) {
await addProfileItem(currentItem) intervalPool[currentItem.id] = setTimeout(
intervalPool[currentItem.id] = setInterval(
async () => { async () => {
await addProfileItem(currentItem) try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
}, },
currentItem.interval * 60 * 1000 + 10000 // +10s currentItem.interval * 60 * 1000 + 10000 // +10s
) )
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
} }
} }
export function addProfileUpdater(id: string): void { export async function addProfileUpdater(item: IProfileItem): Promise<void> {
const item = getProfileItem(id)
if (item.type === 'remote' && item.interval) { if (item.type === 'remote' && item.interval) {
if (intervalPool[id]) { if (intervalPool[item.id]) {
clearInterval(intervalPool[id]) clearTimeout(intervalPool[item.id])
} }
intervalPool[id] = setInterval( intervalPool[item.id] = setTimeout(
async () => { async () => {
await addProfileItem(item) try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
}, },
item.interval * 60 * 1000 item.interval * 60 * 1000
) )

View File

@ -1,98 +1,100 @@
import { import {
getAppConfig, getAppConfig,
getControledMihomoConfig, getControledMihomoConfig,
setAppConfig, patchAppConfig,
setControledMihomoConfig patchControledMihomoConfig
} from '../config' } from '../config'
import icoIcon from '../../../resources/icon.ico?asset' import icoIcon from '../../../resources/icon.ico?asset'
import pngIcon from '../../../resources/icon.png?asset' import pngIcon from '../../../resources/icon.png?asset'
import { patchMihomoConfig } from './mihomoApi' import { patchMihomoConfig } from './mihomoApi'
import { window } from '..' import { mainWindow } from '..'
import { app, ipcMain, Menu, shell, Tray } from 'electron' import { app, ipcMain, Menu, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs' import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../resolve/sysproxy' import { triggerSysProxy } from '../resolve/sysproxy'
let tray: Tray | null = null let tray: Tray | null = null
const buildContextMenu = (): Menu => { const buildContextMenu = async (): Promise<Menu> => {
const { mode, tun } = await getControledMihomoConfig()
const { sysProxy } = await getAppConfig()
const contextMenu = [ const contextMenu = [
{ {
id: 'show', id: 'show',
label: '显示窗口', label: '显示窗口',
type: 'normal', type: 'normal',
click: (): void => { click: (): void => {
window?.show() mainWindow?.show()
window?.focusOnWebView() mainWindow?.focusOnWebView()
} }
}, },
{ {
id: 'rule', id: 'rule',
label: '规则模式', label: '规则模式',
type: 'radio', type: 'radio',
checked: getControledMihomoConfig().mode === 'rule', checked: mode === 'rule',
click: (): void => { click: async (): Promise<void> => {
setControledMihomoConfig({ mode: 'rule' }) await patchControledMihomoConfig({ mode: 'rule' })
patchMihomoConfig({ mode: 'rule' }) await patchMihomoConfig({ mode: 'rule' })
window?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
updateTrayMenu() await updateTrayMenu()
} }
}, },
{ {
id: 'global', id: 'global',
label: '全局模式', label: '全局模式',
type: 'radio', type: 'radio',
checked: getControledMihomoConfig().mode === 'global', checked: mode === 'global',
click: (): void => { click: async (): Promise<void> => {
setControledMihomoConfig({ mode: 'global' }) await patchControledMihomoConfig({ mode: 'global' })
patchMihomoConfig({ mode: 'global' }) await patchMihomoConfig({ mode: 'global' })
window?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
updateTrayMenu() await updateTrayMenu()
} }
}, },
{ {
id: 'direct', id: 'direct',
label: '直连模式', label: '直连模式',
type: 'radio', type: 'radio',
checked: getControledMihomoConfig().mode === 'direct', checked: mode === 'direct',
click: (): void => { click: async (): Promise<void> => {
setControledMihomoConfig({ mode: 'direct' }) await patchControledMihomoConfig({ mode: 'direct' })
patchMihomoConfig({ mode: 'direct' }) await patchMihomoConfig({ mode: 'direct' })
window?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
updateTrayMenu() await updateTrayMenu()
} }
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
type: 'checkbox', type: 'checkbox',
label: '系统代理', label: '系统代理',
checked: getAppConfig().sysProxy?.enable ?? false, checked: sysProxy.enable,
click: (item): void => { click: async (item): Promise<void> => {
const enable = item.checked const enable = item.checked
try { try {
await patchAppConfig({ sysProxy: { enable } })
triggerSysProxy(enable) triggerSysProxy(enable)
setAppConfig({ sysProxy: { enable } })
window?.webContents.send('appConfigUpdated')
} catch (e) { } catch (e) {
setAppConfig({ sysProxy: { enable: !enable } }) await patchAppConfig({ sysProxy: { enable: !enable } })
} finally { } finally {
updateTrayMenu() mainWindow?.webContents.send('appConfigUpdated')
await updateTrayMenu()
} }
} }
}, },
{ {
type: 'checkbox', type: 'checkbox',
label: '虚拟网卡', label: '虚拟网卡',
checked: getControledMihomoConfig().tun?.enable ?? false, checked: tun?.enable ?? false,
click: (item): void => { click: async (item): Promise<void> => {
const enable = item.checked const enable = item.checked
if (enable) { if (enable) {
setControledMihomoConfig({ tun: { enable }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
} else { } else {
setControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })
} }
patchMihomoConfig({ tun: { enable } }) await patchMihomoConfig({ tun: { enable } })
window?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
updateTrayMenu() await updateTrayMenu()
} }
}, },
{ type: 'separator' }, { type: 'separator' },
@ -137,19 +139,19 @@ const buildContextMenu = (): Menu => {
return Menu.buildFromTemplate(contextMenu) return Menu.buildFromTemplate(contextMenu)
} }
export function createTray(): void { export async function createTray(): Promise<void> {
if (process.platform === 'linux') { if (process.platform === 'linux') {
tray = new Tray(pngIcon) tray = new Tray(pngIcon)
} else { } else {
tray = new Tray(icoIcon) tray = new Tray(icoIcon)
} }
const menu = buildContextMenu() const menu = await buildContextMenu()
ipcMain.on('controledMihomoConfigUpdated', () => { ipcMain.on('controledMihomoConfigUpdated', async () => {
updateTrayMenu() await updateTrayMenu()
}) })
ipcMain.on('appConfigUpdated', () => { ipcMain.on('appConfigUpdated', async () => {
updateTrayMenu() await updateTrayMenu()
}) })
tray.setContextMenu(menu) tray.setContextMenu(menu)
@ -157,11 +159,11 @@ export function createTray(): void {
tray.setToolTip('Another Mihomo GUI.') tray.setToolTip('Another Mihomo GUI.')
tray.setTitle('Mihomo Party') tray.setTitle('Mihomo Party')
tray.addListener('click', () => { tray.addListener('click', () => {
window?.isVisible() ? window?.hide() : window?.show() mainWindow?.isVisible() ? mainWindow?.hide() : mainWindow?.show()
}) })
} }
function updateTrayMenu(): void { async function updateTrayMenu(): Promise<void> {
const menu = buildContextMenu() const menu = await buildContextMenu()
tray?.setContextMenu(menu) // 更新菜单 tray?.setContextMenu(menu) // 更新菜单
} }

View File

@ -1,7 +1,7 @@
import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { registerIpcMainHandlers } from './utils/ipc' import { registerIpcMainHandlers } from './utils/ipc'
import { app, shell, BrowserWindow, Menu } from 'electron' import { app, shell, BrowserWindow, Menu, dialog } from 'electron'
import { stopCore, startCore } from './core/manager' import { stopCore } from './core/manager'
import { triggerSysProxy } from './resolve/sysproxy' import { triggerSysProxy } from './resolve/sysproxy'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { createTray } from './core/tray' import { createTray } from './core/tray'
@ -14,76 +14,78 @@ import {
stopMihomoMemory, stopMihomoMemory,
stopMihomoTraffic stopMihomoTraffic
} from './core/mihomoApi' } from './core/mihomoApi'
import { initProfileUpdater } from './core/profileUpdater'
export let window: BrowserWindow | null = null export let mainWindow: BrowserWindow | null = null
const gotTheLock = app.requestSingleInstanceLock() const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) { if (!gotTheLock) {
app.quit() app.quit()
} else {
init()
app.on('second-instance', (_event, commandline) => {
window?.show()
window?.focusOnWebView()
const url = commandline.pop()
if (url) {
handleDeepLink(url)
}
})
app.on('open-url', (_event, url) => {
window?.show()
window?.focusOnWebView()
handleDeepLink(url)
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
stopCore()
triggerSysProxy(false)
app.exit()
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('party.mihomo.app')
startCore().then(() => {
setTimeout(async () => {
await initProfileUpdater()
}, 60000)
})
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
registerIpcMainHandlers()
createWindow()
createTray()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
} }
const initPromise = init()
function handleDeepLink(url: string): void { app.on('second-instance', async (_event, commandline) => {
mainWindow?.show()
mainWindow?.focusOnWebView()
const url = commandline.pop()
if (url) {
await handleDeepLink(url)
}
})
app.on('open-url', async (_event, url) => {
mainWindow?.show()
mainWindow?.focusOnWebView()
await handleDeepLink(url)
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
stopCore()
triggerSysProxy(false)
app.exit()
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('party.mihomo.app')
try {
await initPromise
} catch (e) {
dialog.showErrorBox('应用初始化失败', `${e}`)
app.quit()
}
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
registerIpcMainHandlers()
createWindow()
await createTray()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
async function handleDeepLink(url: string): Promise<void> {
if (url.startsWith('clash://install-config')) { if (url.startsWith('clash://install-config')) {
url = url.replace('clash://install-config/?url=', '').replace('clash://install-config?url=', '') url = url.replace('clash://install-config/?url=', '').replace('clash://install-config?url=', '')
addProfileItem({ await addProfileItem({
type: 'remote', type: 'remote',
name: 'Remote File', name: 'Remote File',
url url
@ -93,7 +95,7 @@ function handleDeepLink(url: string): void {
url = url url = url
.replace('mihomo://install-config/?url=', '') .replace('mihomo://install-config/?url=', '')
.replace('mihomo://install-config?url=', '') .replace('mihomo://install-config?url=', '')
addProfileItem({ await addProfileItem({
type: 'remote', type: 'remote',
name: 'Remote File', name: 'Remote File',
url url
@ -104,7 +106,7 @@ function handleDeepLink(url: string): void {
function createWindow(): void { function createWindow(): void {
Menu.setApplicationMenu(null) Menu.setApplicationMenu(null)
// Create the browser window. // Create the browser window.
window = new BrowserWindow({ mainWindow = new BrowserWindow({
minWidth: 800, minWidth: 800,
minHeight: 600, minHeight: 600,
width: 800, width: 800,
@ -118,31 +120,32 @@ function createWindow(): void {
sandbox: false sandbox: false
} }
}) })
window.on('ready-to-show', () => { mainWindow.on('ready-to-show', async () => {
if (!getAppConfig().silentStart) { const { silentStart } = await getAppConfig()
window?.show() if (!silentStart) {
window?.focusOnWebView() mainWindow?.show()
mainWindow?.focusOnWebView()
} }
}) })
window.on('resize', () => { mainWindow.on('resize', () => {
window?.webContents.send('resize') mainWindow?.webContents.send('resize')
}) })
window.on('show', () => { mainWindow.on('show', () => {
startMihomoTraffic() startMihomoTraffic()
startMihomoMemory() startMihomoMemory()
}) })
window.on('close', (event) => { mainWindow.on('close', (event) => {
stopMihomoTraffic() stopMihomoTraffic()
stopMihomoMemory() stopMihomoMemory()
event.preventDefault() event.preventDefault()
window?.hide() mainWindow?.hide()
window?.webContents.reload() mainWindow?.webContents.reload()
}) })
window.webContents.setWindowOpenHandler((details) => { mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url) shell.openExternal(details.url)
return { action: 'deny' } return { action: 'deny' }
}) })
@ -150,8 +153,8 @@ function createWindow(): void {
// HMR for renderer base on electron-vite cli. // HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production. // Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) { if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
window.loadURL(process.env['ELECTRON_RENDERER_URL']) mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else { } else {
window.loadFile(join(__dirname, '../renderer/index.html')) mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
} }
} }

View File

@ -1,7 +1,10 @@
import { exec } from 'child_process' import { exec } from 'child_process'
import { exePath } from '../utils/dirs' import { dataDir, exePath, homeDir } from '../utils/dirs'
import { mkdir, readFile, rm, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { app } from 'electron' import { app } from 'electron'
import fs from 'fs' import { promisify } from 'util'
import path from 'path'
const appName = 'mihomo-party' const appName = 'mihomo-party'
@ -51,12 +54,13 @@ const taskXml = `
export async function checkAutoRun(): Promise<boolean> { export async function checkAutoRun(): Promise<boolean> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const { stdout } = (await new Promise((resolve) => { const execPromise = promisify(exec)
exec(`schtasks /query /tn "${appName}"`, (_err, stdout, stderr) => { try {
resolve({ stdout, stderr }) const { stdout } = await execPromise(`schtasks /query /tn "${appName}"`)
}) return stdout.includes(appName)
})) as { stdout: string; stderr: string } } catch (e) {
return stdout.includes(appName) return false
}
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@ -64,16 +68,17 @@ export async function checkAutoRun(): Promise<boolean> {
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
return fs.existsSync(`${app.getPath('home')}/.config/autostart/${appName}.desktop`) return existsSync(path.join(homeDir, '.config', 'autostart', `${appName}.desktop`))
} }
return false return false
} }
export function enableAutoRun(): void { export async function enableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const taskFilePath = `${app.getPath('userData')}\\${appName}.xml` const execPromise = promisify(exec)
fs.writeFileSync(taskFilePath, taskXml) const taskFilePath = path.join(dataDir, `${appName}.xml`)
exec(`schtasks /create /tn "${appName}" /xml "${taskFilePath}" /f`) await writeFile(taskFilePath, taskXml)
await execPromise(`schtasks /create /tn "${appName}" /xml "${taskFilePath}" /f`)
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.setLoginItemSettings({ app.setLoginItemSettings({
@ -93,22 +98,23 @@ StartupWMClass=mihomo-party
Comment=Mihomo Party Comment=Mihomo Party
Categories=Utility; Categories=Utility;
` `
try {
if (fs.existsSync(`/usr/share/applications/${appName}.desktop`)) { if (existsSync(`/usr/share/applications/${appName}.desktop`)) {
desktop = fs.readFileSync(`/usr/share/applications/${appName}.desktop`, 'utf8') desktop = await readFile(`/usr/share/applications/${appName}.desktop`, 'utf8')
}
} catch (e) {
console.error(e)
} }
fs.mkdirSync(`${app.getPath('home')}/.config/autostart`, { recursive: true }) const autostartDir = path.join(homeDir, '.config', 'autostart')
const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop` if (!existsSync(autostartDir)) {
fs.writeFileSync(desktopFilePath, desktop) await mkdir(autostartDir, { recursive: true })
}
const desktopFilePath = path.join(autostartDir, `${appName}.desktop`)
await writeFile(desktopFilePath, desktop)
} }
} }
export function disableAutoRun(): void { export async function disableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
exec(`schtasks /delete /tn "${appName}" /f`) const execPromise = promisify(exec)
await execPromise(`schtasks /delete /tn "${appName}" /f`)
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
app.setLoginItemSettings({ app.setLoginItemSettings({
@ -116,11 +122,7 @@ export function disableAutoRun(): void {
}) })
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop` const desktopFilePath = path.join(homeDir, '.config', 'autostart', `${appName}.desktop`)
try { await rm(desktopFilePath)
fs.rmSync(desktopFilePath)
} catch (e) {
console.error(e)
}
} }
} }

View File

@ -4,26 +4,23 @@ import { app } from 'electron'
import { getControledMihomoConfig } from '../config' import { getControledMihomoConfig } from '../config'
export async function checkUpdate(): Promise<string | undefined> { export async function checkUpdate(): Promise<string | undefined> {
try { const res = await axios.get(
const res = await axios.get( 'https://github.com/pompurin404/mihomo-party/releases/latest/download/latest.yml',
'https://github.com/pompurin404/mihomo-party/releases/latest/download/latest.yml', {
{ headers: { 'Content-Type': 'application/octet-stream' },
headers: { 'Content-Type': 'application/octet-stream' }, proxy: {
proxy: { protocol: 'http',
protocol: 'http', host: '127.0.0.1',
host: '127.0.0.1', port: getControledMihomoConfig()['mixed-port'] || 7890
port: getControledMihomoConfig()['mixed-port'] || 7890
}
} }
)
const latest = yaml.parse(res.data)
const remoteVersion = latest.version
const currentVersion = app.getVersion()
if (remoteVersion !== currentVersion) {
return remoteVersion
} }
} catch (e) { )
console.error(e) const latest = yaml.parse(res.data) as { version: string }
const remoteVersion = latest.version
const currentVersion = app.getVersion()
if (remoteVersion !== currentVersion) {
return remoteVersion
} else {
return undefined
} }
return undefined
} }

View File

@ -9,10 +9,10 @@ import { mihomoWorkConfigPath } from '../utils/dirs'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import fs from 'fs'
export function generateProfile(): void { export async function generateProfile(): Promise<void> {
const current = getProfileConfig().current const { current } = await getProfileConfig()
const currentProfile = overrideProfile(current, getProfile(current)) const currentProfile = await overrideProfile(current, await getProfile(current))
const controledMihomoConfig = getControledMihomoConfig() const controledMihomoConfig = await getControledMihomoConfig()
const { tun: profileTun = {} } = currentProfile const { tun: profileTun = {} } = currentProfile
const { tun: controledTun } = controledMihomoConfig const { tun: controledTun } = controledMihomoConfig
const tun = Object.assign(profileTun, controledTun) const tun = Object.assign(profileTun, controledTun)
@ -26,13 +26,24 @@ export function generateProfile(): void {
profile.tun = tun profile.tun = tun
profile.dns = dns profile.dns = dns
profile.sniffer = sniffer profile.sniffer = sniffer
fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile)) return new Promise((resolve, reject) => {
fs.writeFile(mihomoWorkConfigPath(), yaml.stringify(profile), (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
} }
function overrideProfile(current: string | undefined, profile: IMihomoConfig): IMihomoConfig { async function overrideProfile(
const overrideScriptList = getProfileItem(current).override || [] current: string | undefined,
for (const override of overrideScriptList) { profile: IMihomoConfig
const script = getOverride(override) ): Promise<IMihomoConfig> {
const { override = [] } = (await getProfileItem(current)) || {}
for (const ov of override) {
const script = await getOverride(ov)
profile = runOverrideScript(profile, script) profile = runOverrideScript(profile, script)
} }
return profile return profile
@ -42,9 +53,7 @@ function runOverrideScript(profile: IMihomoConfig, script: string): IMihomoConfi
try { try {
const func = eval(`${script} main`) const func = eval(`${script} main`)
const newProfile = func(profile) const newProfile = func(profile)
if (typeof newProfile !== 'object') { if (typeof newProfile !== 'object') return profile
throw new Error('Override script must return an object')
}
return newProfile return newProfile
} catch (e) { } catch (e) {
return profile return profile

View File

@ -20,83 +20,98 @@ import {
defaultProfileConfig defaultProfileConfig
} from '../utils/template' } from '../utils/template'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import { mkdir, writeFile, copyFile } from 'fs/promises'
import { existsSync } from 'fs'
import path from 'path' import path from 'path'
import { startPacServer } from './server' import { startPacServer } from './server'
import { triggerSysProxy } from './sysproxy' import { triggerSysProxy } from './sysproxy'
import { getAppConfig } from '../config' import { getAppConfig } from '../config'
import { app } from 'electron' import { app } from 'electron'
import { startCore } from '../core/manager'
import { initProfileUpdater } from '../core/profileUpdater'
function initDirs(): void { async function initDirs(): Promise<void> {
if (!fs.existsSync(dataDir)) { if (!existsSync(dataDir)) {
fs.mkdirSync(dataDir) await mkdir(dataDir)
} }
if (!fs.existsSync(profilesDir())) { if (!existsSync(profilesDir())) {
fs.mkdirSync(profilesDir()) await mkdir(profilesDir())
} }
if (!fs.existsSync(overrideDir())) { if (!existsSync(overrideDir())) {
fs.mkdirSync(overrideDir()) await mkdir(overrideDir())
} }
if (!fs.existsSync(mihomoWorkDir())) { if (!existsSync(mihomoWorkDir())) {
fs.mkdirSync(mihomoWorkDir()) await mkdir(mihomoWorkDir())
} }
if (!fs.existsSync(logDir())) { if (!existsSync(logDir())) {
fs.mkdirSync(logDir()) await mkdir(logDir())
} }
if (!fs.existsSync(mihomoTestDir())) { if (!existsSync(mihomoTestDir())) {
fs.mkdirSync(mihomoTestDir()) await mkdir(mihomoTestDir())
} }
} }
function initConfig(): void { async function initConfig(): Promise<void> {
if (!fs.existsSync(appConfigPath())) { if (!existsSync(appConfigPath())) {
fs.writeFileSync(appConfigPath(), yaml.stringify(defaultConfig)) await writeFile(appConfigPath(), yaml.stringify(defaultConfig))
} }
if (!fs.existsSync(profileConfigPath())) { if (!existsSync(profileConfigPath())) {
fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig)) await writeFile(profileConfigPath(), yaml.stringify(defaultProfileConfig))
} }
if (!fs.existsSync(overrideConfigPath())) { if (!existsSync(overrideConfigPath())) {
fs.writeFileSync(overrideConfigPath(), yaml.stringify(defaultOverrideConfig)) await writeFile(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
} }
if (!fs.existsSync(profilePath('default'))) { if (!existsSync(profilePath('default'))) {
fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile)) await writeFile(profilePath('default'), yaml.stringify(defaultProfile))
} }
if (!fs.existsSync(controledMihomoConfigPath())) { if (!existsSync(controledMihomoConfigPath())) {
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig)) await writeFile(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
} }
} }
function initFiles(): void { async function initFiles(): Promise<void> {
const fileList = ['country.mmdb', 'geoip.dat', 'geosite.dat', 'ASN.mmdb'] const copy = async (file: string): Promise<void> => {
for (const file of fileList) {
const targetPath = path.join(mihomoWorkDir(), file) const targetPath = path.join(mihomoWorkDir(), file)
const testTargrtPath = path.join(mihomoTestDir(), file) const testTargrtPath = path.join(mihomoTestDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file) const sourcePath = path.join(resourcesFilesDir(), file)
if (!fs.existsSync(targetPath) && fs.existsSync(sourcePath)) { if (!existsSync(targetPath) && existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, targetPath) await copyFile(sourcePath, targetPath)
} }
if (!fs.existsSync(testTargrtPath) && fs.existsSync(sourcePath)) { if (!existsSync(testTargrtPath) && existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, testTargrtPath) await copyFile(sourcePath, testTargrtPath)
} }
} }
await Promise.all([
copy('country.mmdb'),
copy('geoip.dat'),
copy('geosite.dat'),
copy('ASN.mmdb')
])
} }
function initDeeplink(): void { function initDeeplink(): void {
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('clash', process.execPath, [path.resolve(process.argv[1])]) app.setAsDefaultProtocolClient('clash', process.execPath, [path.resolve(process.argv[1])])
app.setAsDefaultProtocolClient('mihomo', process.execPath, [path.resolve(process.argv[1])])
} }
} else { } else {
app.setAsDefaultProtocolClient('clash') app.setAsDefaultProtocolClient('clash')
app.setAsDefaultProtocolClient('mihomo')
} }
} }
export function init(): void { export async function init(): Promise<void> {
initDirs() await initDirs()
initConfig() await initConfig()
initFiles() await initFiles()
initDeeplink() await startPacServer()
startPacServer().then(() => { const { sysProxy } = await getAppConfig()
triggerSysProxy(getAppConfig().sysProxy.enable) await triggerSysProxy(sysProxy.enable)
startCore().then(() => {
setTimeout(async () => {
await initProfileUpdater()
}, 60000)
}) })
initDeeplink()
} }

View File

@ -34,11 +34,11 @@ function findAvailablePort(startPort: number): Promise<number> {
export async function startPacServer(): Promise<void> { export async function startPacServer(): Promise<void> {
pacPort = await findAvailablePort(10000) pacPort = await findAvailablePort(10000)
const server = http const server = http
.createServer((_req, res) => { .createServer(async (_req, res) => {
const { const {
sysProxy: { pacScript } sysProxy: { pacScript }
} = getAppConfig() } = await getAppConfig()
const { 'mixed-port': port = 7890 } = getControledMihomoConfig() const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
let script = pacScript || defaultPacScript let script = pacScript || defaultPacScript
script = script.replaceAll('%mixed-port%', port.toString()) script = script.replaceAll('%mixed-port%', port.toString())
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' }) res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })

View File

@ -42,19 +42,19 @@ if (process.platform === 'win32')
'<local>' '<local>'
] ]
export function triggerSysProxy(enable: boolean): void { export async function triggerSysProxy(enable: boolean): Promise<void> {
if (enable) { if (enable) {
disableSysProxy() disableSysProxy()
enableSysProxy() await enableSysProxy()
} else { } else {
disableSysProxy() disableSysProxy()
} }
} }
export function enableSysProxy(): void { export async function enableSysProxy(): Promise<void> {
const { sysProxy } = getAppConfig() const { sysProxy } = await getAppConfig()
const { mode, host, bypass = defaultBypass } = sysProxy const { mode, host, bypass = defaultBypass } = sysProxy
const { 'mixed-port': port = 7890 } = getControledMihomoConfig() const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
switch (mode || 'manual') { switch (mode || 'manual') {
case 'auto': { case 'auto': {

View File

@ -3,6 +3,7 @@ import { app } from 'electron'
import path from 'path' import path from 'path'
export const dataDir = app.getPath('userData') export const dataDir = app.getPath('userData')
export const homeDir = app.getPath('home')
export function exePath(): string { export function exePath(): string {
return app.getPath('exe') return app.getPath('exe')

View File

@ -22,9 +22,9 @@ import {
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../resolve/autoRun' import { checkAutoRun, disableAutoRun, enableAutoRun } from '../resolve/autoRun'
import { import {
getAppConfig, getAppConfig,
setAppConfig, patchAppConfig,
getControledMihomoConfig, getControledMihomoConfig,
setControledMihomoConfig, patchControledMihomoConfig,
getProfileConfig, getProfileConfig,
getCurrentProfileItem, getCurrentProfileItem,
getProfileItem, getProfileItem,
@ -48,68 +48,95 @@ import { isEncryptionAvailable, restartCore } from '../core/manager'
import { triggerSysProxy } from '../resolve/sysproxy' import { triggerSysProxy } from '../resolve/sysproxy'
import { checkUpdate } from '../resolve/autoUpdater' import { checkUpdate } from '../resolve/autoUpdater'
import { exePath, mihomoCorePath, mihomoWorkConfigPath, resourcesDir } from './dirs' import { exePath, mihomoCorePath, mihomoWorkConfigPath, resourcesDir } from './dirs'
import { execFile, execSync } from 'child_process' import { exec, execFile } from 'child_process'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs'
import path from 'path' import path from 'path'
import { promisify } from 'util'
import { readFile } from 'fs/promises'
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
): (...args: any[]) => Promise<T | { invokeError: unknown }> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return async (...args: any[]) => {
try {
return await fn(...args)
} catch (e) {
return { invokeError: `${e}` }
}
}
}
export function registerIpcMainHandlers(): void { export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoVersion', ipcErrorWrapper(mihomoVersion))
ipcMain.handle('mihomoCloseConnection', (_e, id) => mihomoCloseConnection(id)) ipcMain.handle('mihomoCloseConnection', (_e, id) => ipcErrorWrapper(mihomoCloseConnection)(id))
ipcMain.handle('mihomoCloseAllConnections', mihomoCloseAllConnections) ipcMain.handle('mihomoCloseAllConnections', ipcErrorWrapper(mihomoCloseAllConnections))
ipcMain.handle('mihomoRules', mihomoRules) ipcMain.handle('mihomoRules', ipcErrorWrapper(mihomoRules))
ipcMain.handle('mihomoProxies', mihomoProxies) ipcMain.handle('mihomoProxies', ipcErrorWrapper(mihomoProxies))
ipcMain.handle('mihomoProxyProviders', () => mihomoProxyProviders()) ipcMain.handle('mihomoProxyProviders', ipcErrorWrapper(mihomoProxyProviders))
ipcMain.handle('mihomoUpdateProxyProviders', (_e, name) => mihomoUpdateProxyProviders(name)) ipcMain.handle('mihomoUpdateProxyProviders', (_e, name) =>
ipcMain.handle('mihomoRuleProviders', () => mihomoRuleProviders()) ipcErrorWrapper(mihomoUpdateProxyProviders)(name)
ipcMain.handle('mihomoUpdateRuleProviders', (_e, name) => mihomoUpdateRuleProviders(name)) )
ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) => mihomoChangeProxy(group, proxy)) ipcMain.handle('mihomoRuleProviders', ipcErrorWrapper(mihomoRuleProviders))
ipcMain.handle('mihomoUpgradeGeo', mihomoUpgradeGeo) ipcMain.handle('mihomoUpdateRuleProviders', (_e, name) =>
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) => mihomoProxyDelay(proxy, url)) ipcErrorWrapper(mihomoUpdateRuleProviders)(name)
ipcMain.handle('mihomoGroupDelay', (_e, group, url) => mihomoGroupDelay(group, url)) )
ipcMain.handle('startMihomoLogs', startMihomoLogs) ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) =>
ipcErrorWrapper(mihomoChangeProxy)(group, proxy)
)
ipcMain.handle('mihomoUpgradeGeo', ipcErrorWrapper(mihomoUpgradeGeo))
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) =>
ipcErrorWrapper(mihomoProxyDelay)(proxy, url)
)
ipcMain.handle('mihomoGroupDelay', (_e, group, url) =>
ipcErrorWrapper(mihomoGroupDelay)(group, url)
)
ipcMain.handle('startMihomoLogs', ipcErrorWrapper(startMihomoLogs))
ipcMain.handle('stopMihomoLogs', stopMihomoLogs) ipcMain.handle('stopMihomoLogs', stopMihomoLogs)
ipcMain.handle('startMihomoConnections', () => startMihomoConnections()) ipcMain.handle('startMihomoConnections', ipcErrorWrapper(startMihomoConnections))
ipcMain.handle('stopMihomoConnections', () => stopMihomoConnections()) ipcMain.handle('stopMihomoConnections', stopMihomoConnections)
ipcMain.handle('patchMihomoConfig', (_e, patch) => patchMihomoConfig(patch)) ipcMain.handle('patchMihomoConfig', (_e, patch) => ipcErrorWrapper(patchMihomoConfig)(patch))
ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('checkAutoRun', ipcErrorWrapper(checkAutoRun))
ipcMain.handle('enableAutoRun', enableAutoRun) ipcMain.handle('enableAutoRun', ipcErrorWrapper(enableAutoRun))
ipcMain.handle('disableAutoRun', disableAutoRun) ipcMain.handle('disableAutoRun', ipcErrorWrapper(disableAutoRun))
ipcMain.handle('getAppConfig', (_e, force) => getAppConfig(force)) ipcMain.handle('getAppConfig', (_e, force) => ipcErrorWrapper(getAppConfig)(force))
ipcMain.handle('setAppConfig', (_e, config) => setAppConfig(config)) ipcMain.handle('patchAppConfig', (_e, config) => ipcErrorWrapper(patchAppConfig)(config))
ipcMain.handle('getControledMihomoConfig', (_e, force) => getControledMihomoConfig(force)) ipcMain.handle('getControledMihomoConfig', (_e, force) =>
ipcMain.handle('setControledMihomoConfig', (_e, config) => setControledMihomoConfig(config)) ipcErrorWrapper(getControledMihomoConfig)(force)
ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force)) )
ipcMain.handle('setProfileConfig', (_e, config) => setProfileConfig(config)) ipcMain.handle('patchControledMihomoConfig', (_e, config) =>
ipcMain.handle('getCurrentProfileItem', getCurrentProfileItem) ipcErrorWrapper(patchControledMihomoConfig)(config)
ipcMain.handle('getProfileItem', (_e, id) => getProfileItem(id)) )
ipcMain.handle('getProfileStr', (_e, id) => getProfileStr(id)) ipcMain.handle('getProfileConfig', (_e, force) => ipcErrorWrapper(getProfileConfig)(force))
ipcMain.handle('setProfileStr', (_e, id, str) => setProfileStr(id, str)) ipcMain.handle('setProfileConfig', (_e, config) => ipcErrorWrapper(setProfileConfig)(config))
ipcMain.handle('updateProfileItem', (_e, item) => updateProfileItem(item)) ipcMain.handle('getCurrentProfileItem', ipcErrorWrapper(getCurrentProfileItem))
ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id)) ipcMain.handle('getProfileItem', (_e, id) => ipcErrorWrapper(getProfileItem)(id))
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item)) ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id)) ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
ipcMain.handle('getOverrideConfig', (_e, force) => getOverrideConfig(force)) ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
ipcMain.handle('setOverrideConfig', (_e, config) => setOverrideConfig(config)) ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
ipcMain.handle('getOverrideItem', (_e, id) => getOverrideItem(id)) ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item))
ipcMain.handle('addOverrideItem', (_e, item) => addOverrideItem(item)) ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id))
ipcMain.handle('removeOverrideItem', (_e, id) => removeOverrideItem(id)) ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force))
ipcMain.handle('updateOverrideItem', (_e, item) => updateOverrideItem(item)) ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config))
ipcMain.handle('getOverride', (_e, id) => getOverride(id)) ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id))
ipcMain.handle('setOverride', (_e, id, str) => setOverride(id, str)) ipcMain.handle('addOverrideItem', (_e, item) => ipcErrorWrapper(addOverrideItem)(item))
ipcMain.handle('restartCore', restartCore) ipcMain.handle('removeOverrideItem', (_e, id) => ipcErrorWrapper(removeOverrideItem)(id))
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable)) ipcMain.handle('updateOverrideItem', (_e, item) => ipcErrorWrapper(updateOverrideItem)(item))
ipcMain.handle('getOverride', (_e, id) => ipcErrorWrapper(getOverride)(id))
ipcMain.handle('setOverride', (_e, id, str) => ipcErrorWrapper(setOverride)(id, str))
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable) ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str)) ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str))
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => readTextFile(filePath)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', getRuntimeConfigStr) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
ipcMain.handle('getRuntimeConfig', getRuntimeConfig) ipcMain.handle('getRuntimeConfig', ipcErrorWrapper(getRuntimeConfig))
ipcMain.handle('checkUpdate', () => checkUpdate()) ipcMain.handle('checkUpdate', ipcErrorWrapper(checkUpdate))
ipcMain.handle('getVersion', () => app.getVersion()) ipcMain.handle('getVersion', () => app.getVersion())
ipcMain.handle('platform', () => process.platform) ipcMain.handle('platform', () => process.platform)
ipcMain.handle('openUWPTool', openUWPTool) ipcMain.handle('openUWPTool', ipcErrorWrapper(openUWPTool))
ipcMain.handle('setupFirewall', setupFirewall) ipcMain.handle('setupFirewall', ipcErrorWrapper(setupFirewall))
ipcMain.handle('quitApp', () => app.quit()) ipcMain.handle('quitApp', () => app.quit())
} }
@ -121,51 +148,39 @@ function getFilePath(ext: string[]): string[] | undefined {
}) })
} }
function readTextFile(filePath: string): string { async function readTextFile(filePath: string): Promise<string> {
return fs.readFileSync(filePath, 'utf8') return await readFile(filePath, 'utf8')
} }
function getRuntimeConfigStr(): string { async function getRuntimeConfigStr(): Promise<string> {
return fs.readFileSync(mihomoWorkConfigPath(), 'utf8') return readFile(mihomoWorkConfigPath(), 'utf8')
} }
function getRuntimeConfig(): IMihomoConfig { async function getRuntimeConfig(): Promise<IMihomoConfig> {
return yaml.parse(getRuntimeConfigStr()) return yaml.parse(await getRuntimeConfigStr())
} }
function openUWPTool(): void { async function openUWPTool(): Promise<void> {
const execFilePromise = promisify(execFile)
const uwpToolPath = path.join(resourcesDir(), 'files', 'enableLoopback.exe') const uwpToolPath = path.join(resourcesDir(), 'files', 'enableLoopback.exe')
const child = execFile(uwpToolPath) await execFilePromise(uwpToolPath)
child.unref()
} }
async function setupFirewall(): Promise<void> { async function setupFirewall(): Promise<void> {
return new Promise((resolve, reject) => { const execPromise = promisify(exec)
const removeCommand = ` const removeCommand = `
Remove-NetFirewallRule -DisplayName "mihomo" -ErrorAction SilentlyContinue Remove-NetFirewallRule -DisplayName "mihomo" -ErrorAction SilentlyContinue
Remove-NetFirewallRule -DisplayName "mihomo-alpha" -ErrorAction SilentlyContinue Remove-NetFirewallRule -DisplayName "mihomo-alpha" -ErrorAction SilentlyContinue
Remove-NetFirewallRule -DisplayName "Mihomo Party" -ErrorAction SilentlyContinue Remove-NetFirewallRule -DisplayName "Mihomo Party" -ErrorAction SilentlyContinue
` `
const createCommand = ` const createCommand = `
New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
New-NetFirewallRule -DisplayName "mihomo-alpha" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo-alpha')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "mihomo-alpha" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo-alpha')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
New-NetFirewallRule -DisplayName "Mihomo Party" -Direction Inbound -Action Allow -Program "${exePath()}" -Enabled True -Profile Any -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "Mihomo Party" -Direction Inbound -Action Allow -Program "${exePath()}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
` `
if (process.platform === 'win32') { if (process.platform === 'win32') {
try { await execPromise(removeCommand, { shell: 'powershell' })
execSync(removeCommand, { shell: 'powershell' }) await execPromise(createCommand, { shell: 'powershell' })
} catch { }
console.error('Remove-NetFirewallRule Failed')
}
try {
execSync(createCommand, { shell: 'powershell' })
} catch (e) {
dialog.showErrorBox('防火墙设置失败', `${e}`)
reject(e)
console.error('New-NetFirewallRule Failed')
}
}
resolve()
})
} }

View File

@ -179,9 +179,13 @@ const ProfileItem: React.FC<Props> = (props) => {
disabled={updating} disabled={updating}
onPress={() => { onPress={() => {
setUpdating(true) setUpdating(true)
addProfileItem(info).finally(() => { addProfileItem(info)
setUpdating(false) .catch((e) => {
}) alert(e)
})
.finally(() => {
setUpdating(false)
})
}} }}
> >
<IoMdRefresh <IoMdRefresh

View File

@ -1,5 +1,5 @@
import useSWR from 'swr' import useSWR from 'swr'
import { getAppConfig, setAppConfig } from '@renderer/utils/ipc' import { getAppConfig, patchAppConfig as patch } from '@renderer/utils/ipc'
import { useEffect } from 'react' import { useEffect } from 'react'
interface RetuenType { interface RetuenType {
@ -12,7 +12,7 @@ export const useAppConfig = (listenUpdate = false): RetuenType => {
const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig()) const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig())
const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => { const patchAppConfig = async (value: Partial<IAppConfig>): Promise<void> => {
await setAppConfig(value) await patch(value)
mutateAppConfig() mutateAppConfig()
window.electron.ipcRenderer.send('appConfigUpdated') window.electron.ipcRenderer.send('appConfigUpdated')
} }

View File

@ -1,5 +1,5 @@
import useSWR from 'swr' import useSWR from 'swr'
import { getControledMihomoConfig, setControledMihomoConfig } from '@renderer/utils/ipc' import { getControledMihomoConfig, patchControledMihomoConfig as patch } from '@renderer/utils/ipc'
import { useEffect } from 'react' import { useEffect } from 'react'
interface RetuenType { interface RetuenType {
@ -15,7 +15,7 @@ export const useControledMihomoConfig = (listenUpdate = false): RetuenType => {
) )
const patchControledMihomoConfig = async (value: Partial<IMihomoConfig>): Promise<void> => { const patchControledMihomoConfig = async (value: Partial<IMihomoConfig>): Promise<void> => {
await setControledMihomoConfig(value) await patch(value)
mutateControledMihomoConfig() mutateControledMihomoConfig()
window.electron.ipcRenderer.send('controledMihomoConfigUpdated') window.electron.ipcRenderer.send('controledMihomoConfigUpdated')
} }

View File

@ -165,7 +165,11 @@ const Profiles: React.FC = () => {
updateProfileItem={updateProfileItem} updateProfileItem={updateProfileItem}
info={item} info={item}
onClick={async () => { onClick={async () => {
await changeCurrentProfile(item.id) try {
await changeCurrentProfile(item.id)
} catch (e) {
alert(e)
}
}} }}
/> />
))} ))}

View File

@ -1,223 +1,242 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ipcErrorWrapper(response: any): any {
if (typeof response === 'object' && 'invokeError' in response) {
throw response.invokeError
} else {
return response
}
}
export async function mihomoVersion(): Promise<IMihomoVersion> { export async function mihomoVersion(): Promise<IMihomoVersion> {
return await window.electron.ipcRenderer.invoke('mihomoVersion') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoVersion'))
} }
export async function mihomoCloseConnection(id: string): Promise<void> { export async function mihomoCloseConnection(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id))
} }
export async function mihomoCloseAllConnections(): Promise<void> { export async function mihomoCloseAllConnections(): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections'))
} }
export async function mihomoRules(): Promise<IMihomoRulesInfo> { export async function mihomoRules(): Promise<IMihomoRulesInfo> {
return await window.electron.ipcRenderer.invoke('mihomoRules') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRules'))
} }
export async function mihomoProxies(): Promise<IMihomoProxies> { export async function mihomoProxies(): Promise<IMihomoProxies> {
return await window.electron.ipcRenderer.invoke('mihomoProxies') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxies'))
} }
export async function mihomoProxyProviders(): Promise<IMihomoProxyProviders> { export async function mihomoProxyProviders(): Promise<IMihomoProxyProviders> {
return await window.electron.ipcRenderer.invoke('mihomoProxyProviders') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxyProviders'))
} }
export async function mihomoUpdateProxyProviders(name: string): Promise<void> { export async function mihomoUpdateProxyProviders(name: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name)
)
} }
export async function mihomoRuleProviders(): Promise<IMihomoRuleProviders> { export async function mihomoRuleProviders(): Promise<IMihomoRuleProviders> {
return await window.electron.ipcRenderer.invoke('mihomoRuleProviders') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRuleProviders'))
} }
export async function mihomoUpdateRuleProviders(name: string): Promise<void> { export async function mihomoUpdateRuleProviders(name: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoUpdateRuleProviders', name) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('mihomoUpdateRuleProviders', name)
)
} }
export async function mihomoChangeProxy(group: string, proxy: string): Promise<IMihomoProxy> { export async function mihomoChangeProxy(group: string, proxy: string): Promise<IMihomoProxy> {
return await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy)
)
} }
export async function mihomoUpgradeGeo(): Promise<void> { export async function mihomoUpgradeGeo(): Promise<void> {
return await window.electron.ipcRenderer.invoke('mihomoUpgradeGeo') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoUpgradeGeo'))
} }
export async function mihomoProxyDelay(proxy: string, url?: string): Promise<IMihomoDelay> { export async function mihomoProxyDelay(proxy: string, url?: string): Promise<IMihomoDelay> {
return await window.electron.ipcRenderer.invoke('mihomoProxyDelay', proxy, url) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxyDelay', proxy, url))
} }
export async function mihomoGroupDelay(group: string, url?: string): Promise<IMihomoGroupDelay> { export async function mihomoGroupDelay(group: string, url?: string): Promise<IMihomoGroupDelay> {
return await window.electron.ipcRenderer.invoke('mihomoGroupDelay', group, url) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoGroupDelay', group, url))
} }
export async function startMihomoLogs(): Promise<void> { export async function startMihomoLogs(): Promise<void> {
return await window.electron.ipcRenderer.invoke('startMihomoLogs') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoLogs'))
} }
export async function stopMihomoLogs(): Promise<void> { export async function stopMihomoLogs(): Promise<void> {
return await window.electron.ipcRenderer.invoke('stopMihomoLogs') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopMihomoLogs'))
} }
export async function startMihomoConnections(): Promise<void> { export async function startMihomoConnections(): Promise<void> {
return await window.electron.ipcRenderer.invoke('startMihomoConnections') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoConnections'))
} }
export async function stopMihomoConnections(): Promise<void> { export async function stopMihomoConnections(): Promise<void> {
return await window.electron.ipcRenderer.invoke('stopMihomoConnections') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopMihomoConnections'))
} }
export async function patchMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> { export async function patchMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch))
} }
export async function checkAutoRun(): Promise<boolean> { export async function checkAutoRun(): Promise<boolean> {
return await window.electron.ipcRenderer.invoke('checkAutoRun') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkAutoRun'))
} }
export async function enableAutoRun(): Promise<void> { export async function enableAutoRun(): Promise<void> {
return await window.electron.ipcRenderer.invoke('enableAutoRun') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('enableAutoRun'))
} }
export async function disableAutoRun(): Promise<void> { export async function disableAutoRun(): Promise<void> {
return await window.electron.ipcRenderer.invoke('disableAutoRun') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('disableAutoRun'))
} }
export async function getAppConfig(force = false): Promise<IAppConfig> { export async function getAppConfig(force = false): Promise<IAppConfig> {
return await window.electron.ipcRenderer.invoke('getAppConfig', force) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getAppConfig', force))
} }
export async function setAppConfig(patch: Partial<IAppConfig>): Promise<void> { export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
return await window.electron.ipcRenderer.invoke('setAppConfig', patch) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchAppConfig', patch))
} }
export async function getControledMihomoConfig(force = false): Promise<Partial<IMihomoConfig>> { export async function getControledMihomoConfig(force = false): Promise<Partial<IMihomoConfig>> {
return await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force)
)
} }
export async function setControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> { export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('patchControledMihomoConfig', patch)
)
} }
export async function getProfileConfig(force = false): Promise<IProfileConfig> { export async function getProfileConfig(force = false): Promise<IProfileConfig> {
return await window.electron.ipcRenderer.invoke('getProfileConfig', force) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileConfig', force))
} }
export async function setProfileConfig(config: IProfileConfig): Promise<void> { export async function setProfileConfig(config: IProfileConfig): Promise<void> {
return await window.electron.ipcRenderer.invoke('setProfileConfig', config) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileConfig', config))
} }
export async function getCurrentProfileItem(): Promise<IProfileItem> { export async function getCurrentProfileItem(): Promise<IProfileItem> {
return await window.electron.ipcRenderer.invoke('getCurrentProfileItem') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getCurrentProfileItem'))
} }
export async function getProfileItem(id: string | undefined): Promise<IProfileItem> { export async function getProfileItem(id: string | undefined): Promise<IProfileItem> {
return await window.electron.ipcRenderer.invoke('getProfileItem', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileItem', id))
} }
export async function changeCurrentProfile(id: string): Promise<void> { export async function changeCurrentProfile(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('changeCurrentProfile', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('changeCurrentProfile', id))
} }
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
return await window.electron.ipcRenderer.invoke('addProfileItem', item) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('addProfileItem', item))
} }
export async function removeProfileItem(id: string): Promise<void> { export async function removeProfileItem(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('removeProfileItem', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('removeProfileItem', id))
} }
export async function updateProfileItem(item: IProfileItem): Promise<void> { export async function updateProfileItem(item: IProfileItem): Promise<void> {
return await window.electron.ipcRenderer.invoke('updateProfileItem', item) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateProfileItem', item))
} }
export async function getProfileStr(id: string): Promise<string> { export async function getProfileStr(id: string): Promise<string> {
return await window.electron.ipcRenderer.invoke('getProfileStr', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileStr', id))
} }
export async function setProfileStr(id: string, str: string): Promise<void> { export async function setProfileStr(id: string, str: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('setProfileStr', id, str) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str))
} }
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> { export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
return await window.electron.ipcRenderer.invoke('getOverrideConfig', force) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideConfig', force))
} }
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> { export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
return await window.electron.ipcRenderer.invoke('setOverrideConfig', config) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setOverrideConfig', config))
} }
export async function getOverrideItem(id: string): Promise<IOverrideItem | undefined> { export async function getOverrideItem(id: string): Promise<IOverrideItem | undefined> {
return await window.electron.ipcRenderer.invoke('getOverrideItem', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideItem', id))
} }
export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> { export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> {
return await window.electron.ipcRenderer.invoke('addOverrideItem', item) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('addOverrideItem', item))
} }
export async function removeOverrideItem(id: string): Promise<void> { export async function removeOverrideItem(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('removeOverrideItem', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('removeOverrideItem', id))
} }
export async function updateOverrideItem(item: IOverrideItem): Promise<void> { export async function updateOverrideItem(item: IOverrideItem): Promise<void> {
return await window.electron.ipcRenderer.invoke('updateOverrideItem', item) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateOverrideItem', item))
} }
export async function getOverride(id: string): Promise<string> { export async function getOverride(id: string): Promise<string> {
return await window.electron.ipcRenderer.invoke('getOverride', id) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverride', id))
} }
export async function setOverride(id: string, str: string): Promise<void> { export async function setOverride(id: string, str: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('setOverride', id, str) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setOverride', id, str))
} }
export async function restartCore(): Promise<void> { export async function restartCore(): Promise<void> {
return await window.electron.ipcRenderer.invoke('restartCore') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartCore'))
} }
export async function triggerSysProxy(enable: boolean): Promise<void> { export async function triggerSysProxy(enable: boolean): Promise<void> {
return await window.electron.ipcRenderer.invoke('triggerSysProxy', enable) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
} }
export async function isEncryptionAvailable(): Promise<boolean> { export async function isEncryptionAvailable(): Promise<boolean> {
return await window.electron.ipcRenderer.invoke('isEncryptionAvailable') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('isEncryptionAvailable'))
} }
export async function encryptString(str: string): Promise<Buffer> { export async function encryptString(str: string): Promise<Buffer> {
return await window.electron.ipcRenderer.invoke('encryptString', str) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('encryptString', str))
} }
export async function getFilePath(ext: string[]): Promise<string[] | undefined> { export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
return await window.electron.ipcRenderer.invoke('getFilePath', ext) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext))
} }
export async function readTextFile(filePath: string): Promise<string> { export async function readTextFile(filePath: string): Promise<string> {
return await window.electron.ipcRenderer.invoke('readTextFile', filePath) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('readTextFile', filePath))
} }
export async function getRuntimeConfigStr(): Promise<string> { export async function getRuntimeConfigStr(): Promise<string> {
return await window.electron.ipcRenderer.invoke('getRuntimeConfigStr') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuntimeConfigStr'))
} }
export async function getRuntimeConfig(): Promise<IMihomoConfig> { export async function getRuntimeConfig(): Promise<IMihomoConfig> {
return await window.electron.ipcRenderer.invoke('getRuntimeConfig') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuntimeConfig'))
} }
export async function checkUpdate(): Promise<string | undefined> { export async function checkUpdate(): Promise<string | undefined> {
return await window.electron.ipcRenderer.invoke('checkUpdate') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkUpdate'))
} }
export async function getVersion(): Promise<string> { export async function getVersion(): Promise<string> {
return await window.electron.ipcRenderer.invoke('getVersion') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getVersion'))
} }
export async function getPlatform(): Promise<NodeJS.Platform> { export async function getPlatform(): Promise<NodeJS.Platform> {
return await window.electron.ipcRenderer.invoke('platform') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('platform'))
} }
export async function setupFirewall(): Promise<void> { export async function setupFirewall(): Promise<void> {
return await window.electron.ipcRenderer.invoke('setupFirewall') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setupFirewall'))
} }
export async function quitApp(): Promise<void> { export async function quitApp(): Promise<void> {
return await window.electron.ipcRenderer.invoke('quitApp') return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitApp'))
} }