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 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) {
appConfig = yaml.parse(fs.readFileSync(appConfigPath(), 'utf-8'))
const data = await readFile(appConfigPath(), 'utf-8')
appConfig = yaml.parse(data)
}
return appConfig
}
export function setAppConfig(patch: Partial<IAppConfig>): void {
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
if (patch.sysProxy) {
const oldSysProxy = appConfig.sysProxy || {}
const newSysProxy = Object.assign(oldSysProxy, patch.sysProxy)
patch.sysProxy = newSysProxy
}
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 { readFile, writeFile } from 'fs/promises'
import yaml from 'yaml'
import fs from 'fs'
import { getAxios, startMihomoMemory, startMihomoTraffic } from '../core/mihomoApi'
import { generateProfile } from '../resolve/factory'
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) {
controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath(), 'utf-8'))
const data = await readFile(controledMihomoConfigPath(), 'utf-8')
controledMihomoConfig = yaml.parse(data)
}
return controledMihomoConfig
}
export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void {
const { useNameserverPolicy } = getAppConfig()
export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
const { useNameserverPolicy } = await getAppConfig()
if (patch.tun) {
const oldTun = controledMihomoConfig.tun || {}
const newTun = Object.assign(oldTun, patch.tun)
@ -35,11 +36,12 @@ export function setControledMihomoConfig(patch: Partial<IMihomoConfig>): void {
patch.sniffer = newSniffer
}
controledMihomoConfig = Object.assign(controledMihomoConfig, patch)
if (patch['external-controller'] || patch.secret) {
getAxios(true)
startMihomoMemory()
startMihomoTraffic()
await getAxios(true)
await startMihomoMemory()
await startMihomoTraffic()
}
generateProfile()
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig))
await generateProfile()
await writeFile(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig), 'utf-8')
}

View File

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

View File

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

View File

@ -1,87 +1,168 @@
import { getControledMihomoConfig } from './controledMihomo'
import { profileConfigPath, profilePath } from '../utils/dirs'
import { addProfileUpdater } from '../core/profileUpdater'
import { readFile, rm, writeFile } from 'fs/promises'
import { restartCore } from '../core/manager'
import { getAppConfig } from './app'
import { window } from '..'
import { mainWindow } from '..'
import { existsSync } from 'fs'
import axios from 'axios'
import yaml from 'yaml'
import fs from 'fs'
import { dialog } from 'electron'
import { addProfileUpdater } from '../core/profileUpdater'
import { defaultProfile } from '../utils/template'
let profileConfig: IProfileConfig // profile.yaml
export function getProfileConfig(force = false): IProfileConfig {
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
if (force || !profileConfig) {
profileConfig = yaml.parse(fs.readFileSync(profileConfigPath(), 'utf-8'))
const data = await readFile(profileConfigPath(), 'utf-8')
profileConfig = yaml.parse(data)
}
return profileConfig
}
export function setProfileConfig(config: IProfileConfig): void {
export async function setProfileConfig(config: IProfileConfig): Promise<void> {
profileConfig = config
window?.webContents.send('profileConfigUpdated')
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
mainWindow?.webContents.send('profileConfigUpdated')
await writeFile(profileConfigPath(), yaml.stringify(config), 'utf-8')
}
export function getProfileItem(id: string | undefined): IProfileItem {
const items = getProfileConfig().items
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' }
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
const { items } = await getProfileConfig()
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> {
const oldId = getProfileConfig().current
profileConfig.current = id
const config = await getProfileConfig()
const current = config.current
config.current = id
await setProfileConfig(config)
try {
await restartCore()
} catch (e) {
profileConfig.current = oldId
config.current = current
throw e
} finally {
window?.webContents.send('profileConfigUpdated')
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
await setProfileConfig(config)
}
}
export function updateProfileItem(item: IProfileItem): void {
const index = profileConfig.items.findIndex((i) => i.id === item.id)
profileConfig.items[index] = item
addProfileUpdater(item.id)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
export async function updateProfileItem(item: IProfileItem): Promise<void> {
const config = await getProfileConfig()
const index = config.items.findIndex((i) => i.id === item.id)
if (index === -1) {
throw new Error('Profile not found')
}
config.items[index] = item
await setProfileConfig(config)
await addProfileUpdater(item)
}
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item)
if (profileConfig.items.find((i) => i.id === newItem.id)) {
updateProfileItem(newItem)
const config = await getProfileConfig()
if (await getProfileItem(item.id)) {
await updateProfileItem(newItem)
} else {
profileConfig.items.push(newItem)
config.items.push(newItem)
}
await setProfileConfig(config)
if (!getProfileConfig().current) {
changeCurrentProfile(newItem.id)
if (!config.current) {
await changeCurrentProfile(newItem.id)
}
addProfileUpdater(newItem.id)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
await addProfileUpdater(newItem)
}
export function removeProfileItem(id: string): void {
profileConfig.items = profileConfig.items?.filter((item) => item.id !== id)
if (profileConfig.current === id) {
if (profileConfig.items.length > 0) {
profileConfig.current = profileConfig.items[0]?.id
export async function removeProfileItem(id: string): Promise<void> {
const config = await getProfileConfig()
config.items = config.items?.filter((item) => item.id !== id)
if (config.current === id) {
if (config.items.length > 0) {
config.current = config.items[0].id
} else {
profileConfig.current = undefined
config.current = undefined
}
}
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
fs.rmSync(profilePath(id))
window?.webContents.send('profileConfigUpdated')
await setProfileConfig(config)
if (existsSync(profilePath(id))) {
await rm(profilePath(id))
}
}
export function getCurrentProfileItem(): IProfileItem {
return getProfileItem(getProfileConfig().current)
export async function getCurrentProfileItem(): Promise<IProfileItem> {
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
@ -105,80 +186,3 @@ function parseSubinfo(str: string): ISubscriptionUserInfo {
})
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 {
logPath,
mihomoCorePath,
@ -7,29 +7,44 @@ import {
mihomoWorkDir
} from '../utils/dirs'
import { generateProfile } from '../resolve/factory'
import { getAppConfig, setAppConfig } from '../config'
import { getAppConfig, patchAppConfig } from '../config'
import { dialog, safeStorage } from 'electron'
import fs from 'fs'
import { pauseWebsockets } from './mihomoApi'
import { writeFile } from 'fs/promises'
import { promisify } from 'util'
let child: ChildProcess
let retry = 10
export async function startCore(): Promise<void> {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
grantCorePermition(corePath)
generateProfile()
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
await grantCorePermition(corePath)
await generateProfile()
await checkProfile()
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) => {
child = spawn(corePath, ['-d', mihomoWorkDir()])
child.stdout?.on('data', (data) => {
child.stdout?.on('data', async (data) => {
if (data.toString().includes('External controller listen error')) {
if (retry) {
retry--
resolve(startCore())
resolve(await startCore())
} else {
dialog.showErrorBox('External controller listen error', data.toString())
dialog.showErrorBox('内核连接失败', '请尝试更改外部控制端口后重启内核')
stopCore()
reject('External controller listen error')
}
}
@ -37,36 +52,7 @@ export async function startCore(): Promise<void> {
retry = 10
resolve()
}
fs.writeFileSync(
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()
}
await writeFile(logPath(), data, { flag: 'a' })
})
})
}
@ -84,44 +70,49 @@ export async function restartCore(): Promise<void> {
recover()
}
export function checkProfile(): Promise<void> {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
return new Promise((resolve, reject) => {
const child = execFile(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
child.stdout?.on('data', (data) => {
data
.toString()
async function checkProfile(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core)
const execFilePromise = promisify(execFile)
try {
await execFilePromise(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
} catch (error) {
if (error instanceof Error && 'stdout' in error) {
const { stdout } = error as { stdout: string }
const errorLines = stdout
.split('\n')
.forEach((line: string) => {
if (line.includes('level=error')) {
dialog.showErrorBox('Profile Check Failed', line.split('level=error')[1])
reject(line)
}
})
})
child.on('close', (code) => {
if (code === 0) {
resolve()
}
})
})
.filter((line) => line.includes('level=error'))
.map((line) => line.split('level=error')[1])
throw new Error(`Profile Check Failed:\n${errorLines.join('\n')}`)
} else {
throw error
}
}
}
export function grantCorePermition(corePath: string): void {
if (getAppConfig().encryptedPassword && isEncryptionAvailable()) {
const password = safeStorage.decryptString(Buffer.from(getAppConfig().encryptedPassword ?? []))
try {
if (process.platform === 'linux') {
execSync(
export async function grantCorePermition(corePath: string): Promise<void> {
const { encryptedPassword } = await getAppConfig()
const execPromise = promisify(exec)
if (encryptedPassword && isEncryptionAvailable()) {
const password = safeStorage.decryptString(Buffer.from(encryptedPassword))
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}`
)
} catch (error) {
patchAppConfig({ encryptedPassword: undefined })
throw error
}
if (process.platform === 'darwin') {
execSync(`echo "${password}" | sudo -S chown root:admin ${corePath}`)
execSync(`echo "${password}" | sudo -S chmod +sx ${corePath}`)
}
if (process.platform === 'darwin') {
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 { getAppConfig, getControledMihomoConfig } from '../config'
import { mainWindow } from '..'
import WebSocket from 'ws'
import { window } from '..'
let axiosIns: AxiosInstance = null!
let mihomoTrafficWs: WebSocket | null = null
@ -15,9 +15,9 @@ let connectionsRetry = 10
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
if (axiosIns && !force) return axiosIns
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
const controledMihomoConfig = await getControledMihomoConfig()
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
axiosIns = axios.create({
@ -26,138 +26,127 @@ export const getAxios = async (force: boolean = false): Promise<AxiosInstance> =
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
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
}
export async function mihomoVersion(): Promise<IMihomoVersion> {
const instance = await getAxios()
return (await instance.get('/version').catch(() => {
return { version: '-' }
})) as IMihomoVersion
try {
return await instance.get('/version')
} catch (error) {
return { version: '-', meta: true }
}
}
export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<void> => {
const instance = await getAxios()
return (await instance.patch('/configs', patch).catch((e) => {
return e.response.data
})) as Promise<void>
return await instance.patch('/configs', patch)
}
export const mihomoCloseConnection = async (id: string): Promise<void> => {
const instance = await getAxios()
return (await instance.delete(`/connections/${encodeURIComponent(id)}`).catch((e) => {
return e.response.data
})) as Promise<void>
return await instance.delete(`/connections/${encodeURIComponent(id)}`)
}
export const mihomoCloseAllConnections = async (): Promise<void> => {
const instance = await getAxios()
return (await instance.delete('/connections').catch((e) => {
return e.response.data
})) as Promise<void>
return await instance.delete('/connections')
}
export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
const instance = await getAxios()
return (await instance.get('/rules').catch(() => {
try {
return await instance.get('/rules')
} catch (e) {
return { rules: [] }
})) as IMihomoRulesInfo
}
}
export const mihomoProxies = async (): Promise<IMihomoProxies> => {
const instance = await getAxios()
return (await instance.get('/proxies').catch(() => {
try {
return await instance.get('/proxies')
} catch (e) {
return { proxies: {} }
})) as IMihomoProxies
}
}
export const mihomoProxyProviders = async (): Promise<IMihomoProxyProviders> => {
const instance = await getAxios()
return (await instance.get('/providers/proxies').catch(() => {
try {
return await instance.get('/providers/proxies')
} catch (e) {
return { providers: {} }
})) as IMihomoProxyProviders
}
}
export const mihomoUpdateProxyProviders = async (name: string): Promise<void> => {
const instance = await getAxios()
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`).catch((e) => {
return e.response.data
})
return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`)
}
export const mihomoRuleProviders = async (): Promise<IMihomoRuleProviders> => {
const instance = await getAxios()
return (await instance.get('/providers/rules').catch(() => {
try {
return await instance.get('/providers/rules')
} catch (e) {
return { providers: {} }
})) as IMihomoRuleProviders
}
}
export const mihomoUpdateRuleProviders = async (name: string): Promise<void> => {
const instance = await getAxios()
return instance.put(`/providers/rules/${encodeURIComponent(name)}`).catch((e) => {
return e.response.data
})
return await instance.put(`/providers/rules/${encodeURIComponent(name)}`)
}
export const mihomoChangeProxy = async (group: string, proxy: string): Promise<IMihomoProxy> => {
const instance = await getAxios()
return (await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }).catch(() => {
return {
alive: false,
extra: {},
history: [],
id: '',
name: '',
tfo: false,
type: 'Shadowsocks',
udp: false,
xudp: false
}
})) as IMihomoProxy
return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
}
export const mihomoUpgradeGeo = async (): Promise<void> => {
const instance = await getAxios()
return instance.post('/configs/geo').catch((e) => {
return e.response.data
})
return await instance.post('/configs/geo')
}
export const mihomoProxyDelay = async (proxy: string, url?: string): Promise<IMihomoDelay> => {
const appConfig = getAppConfig()
const appConfig = await getAppConfig()
const { delayTestUrl, delayTestTimeout } = appConfig
const instance = await getAxios()
return (await instance
.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
params: {
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
timeout: delayTestTimeout || 5000
}
})
.catch((e) => {
return e.response.data
})) as IMihomoDelay
return await instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, {
params: {
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
timeout: delayTestTimeout || 5000
}
})
}
export const mihomoGroupDelay = async (group: string, url?: string): Promise<IMihomoGroupDelay> => {
const appConfig = getAppConfig()
const appConfig = await getAppConfig()
const { delayTestUrl, delayTestTimeout } = appConfig
const instance = await getAxios()
return (await instance
.get(`/group/${encodeURIComponent(group)}/delay`, {
params: {
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
timeout: delayTestTimeout || 5000
}
})
.catch((e) => {
return e.response.data
})) as IMihomoGroupDelay
return await instance.get(`/group/${encodeURIComponent(group)}/delay`, {
params: {
url: url || delayTestUrl || 'https://www.gstatic.com/generate_204',
timeout: delayTestTimeout || 5000
}
})
}
export const startMihomoTraffic = (): void => {
mihomoTraffic()
export const startMihomoTraffic = async (): Promise<void> => {
await mihomoTraffic()
}
export const stopMihomoTraffic = (): void => {
@ -170,9 +159,10 @@ export const stopMihomoTraffic = (): void => {
}
}
const mihomoTraffic = (): void => {
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
const mihomoTraffic = async (): Promise<void> => {
const controledMihomoConfig = await getControledMihomoConfig()
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoTraffic()
@ -181,7 +171,7 @@ const mihomoTraffic = (): void => {
mihomoTrafficWs.onmessage = (e): void => {
const data = e.data as string
trafficRetry = 10
window?.webContents.send('mihomoTraffic', JSON.parse(data) as IMihomoTrafficInfo)
mainWindow?.webContents.send('mihomoTraffic', JSON.parse(data) as IMihomoTrafficInfo)
}
mihomoTrafficWs.onclose = (): void => {
@ -199,8 +189,8 @@ const mihomoTraffic = (): void => {
}
}
export const startMihomoMemory = (): void => {
mihomoMemory()
export const startMihomoMemory = async (): Promise<void> => {
await mihomoMemory()
}
export const stopMihomoMemory = (): void => {
@ -213,9 +203,10 @@ export const stopMihomoMemory = (): void => {
}
}
const mihomoMemory = (): void => {
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
const mihomoMemory = async (): Promise<void> => {
const controledMihomoConfig = await getControledMihomoConfig()
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoMemory()
@ -224,7 +215,7 @@ const mihomoMemory = (): void => {
mihomoMemoryWs.onmessage = (e): void => {
const data = e.data as string
memoryRetry = 10
window?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
mainWindow?.webContents.send('mihomoMemory', JSON.parse(data) as IMihomoMemoryInfo)
}
mihomoMemoryWs.onclose = (): void => {
@ -242,8 +233,8 @@ const mihomoMemory = (): void => {
}
}
export const startMihomoLogs = (): void => {
mihomoLogs()
export const startMihomoLogs = async (): Promise<void> => {
await mihomoLogs()
}
export const stopMihomoLogs = (): void => {
@ -256,9 +247,10 @@ export const stopMihomoLogs = (): void => {
}
}
const mihomoLogs = (): void => {
const { secret = '', 'log-level': level = 'info' } = getControledMihomoConfig()
let { 'external-controller': server } = getControledMihomoConfig()
const mihomoLogs = async (): Promise<void> => {
const controledMihomoConfig = await getControledMihomoConfig()
const { secret = '', 'log-level': level = 'info' } = controledMihomoConfig
let { 'external-controller': server } = controledMihomoConfig
if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoLogs()
@ -269,7 +261,7 @@ const mihomoLogs = (): void => {
mihomoLogsWs.onmessage = (e): void => {
const data = e.data as string
logsRetry = 10
window?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
mainWindow?.webContents.send('mihomoLogs', JSON.parse(data) as IMihomoLogInfo)
}
mihomoLogsWs.onclose = (): void => {
@ -287,8 +279,8 @@ const mihomoLogs = (): void => {
}
}
export const startMihomoConnections = (): void => {
mihomoConnections()
export const startMihomoConnections = async (): Promise<void> => {
await mihomoConnections()
}
export const stopMihomoConnections = (): void => {
@ -301,9 +293,10 @@ export const stopMihomoConnections = (): void => {
}
}
const mihomoConnections = (): void => {
let server = getControledMihomoConfig()['external-controller']
const secret = getControledMihomoConfig().secret ?? ''
const mihomoConnections = async (): Promise<void> => {
const controledMihomoConfig = await getControledMihomoConfig()
let server = controledMihomoConfig['external-controller']
const secret = controledMihomoConfig.secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}`
stopMihomoConnections()
@ -314,7 +307,7 @@ const mihomoConnections = (): void => {
mihomoConnectionsWs.onmessage = (e): void => {
const data = e.data as string
connectionsRetry = 10
window?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
mainWindow?.webContents.send('mihomoConnections', JSON.parse(data) as IMihomoConnectionsInfo)
}
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> = {}
export async function initProfileUpdater(): Promise<void> {
const { items, current } = getProfileConfig()
const currentItem = getCurrentProfileItem()
const { items, current } = await getProfileConfig()
const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.interval) {
await addProfileItem(item)
intervalPool[item.id] = setInterval(
intervalPool[item.id] = setTimeout(
async () => {
await addProfileItem(item)
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
},
item.interval * 60 * 1000
)
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
}
}
if (currentItem.type === 'remote' && currentItem.interval) {
await addProfileItem(currentItem)
intervalPool[currentItem.id] = setInterval(
if (currentItem?.type === 'remote' && currentItem.interval) {
intervalPool[currentItem.id] = setTimeout(
async () => {
await addProfileItem(currentItem)
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
},
currentItem.interval * 60 * 1000 + 10000 // +10s
)
try {
await addProfileItem(currentItem)
} catch (e) {
/* ignore */
}
}
}
export function addProfileUpdater(id: string): void {
const item = getProfileItem(id)
export async function addProfileUpdater(item: IProfileItem): Promise<void> {
if (item.type === 'remote' && item.interval) {
if (intervalPool[id]) {
clearInterval(intervalPool[id])
if (intervalPool[item.id]) {
clearTimeout(intervalPool[item.id])
}
intervalPool[id] = setInterval(
intervalPool[item.id] = setTimeout(
async () => {
await addProfileItem(item)
try {
await addProfileItem(item)
} catch (e) {
/* ignore */
}
},
item.interval * 60 * 1000
)

View File

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

View File

@ -1,7 +1,7 @@
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { registerIpcMainHandlers } from './utils/ipc'
import { app, shell, BrowserWindow, Menu } from 'electron'
import { stopCore, startCore } from './core/manager'
import { app, shell, BrowserWindow, Menu, dialog } from 'electron'
import { stopCore } from './core/manager'
import { triggerSysProxy } from './resolve/sysproxy'
import icon from '../../resources/icon.png?asset'
import { createTray } from './core/tray'
@ -14,76 +14,78 @@ import {
stopMihomoMemory,
stopMihomoTraffic
} from './core/mihomoApi'
import { initProfileUpdater } from './core/profileUpdater'
export let window: BrowserWindow | null = null
export let mainWindow: BrowserWindow | null = null
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
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')) {
url = url.replace('clash://install-config/?url=', '').replace('clash://install-config?url=', '')
addProfileItem({
await addProfileItem({
type: 'remote',
name: 'Remote File',
url
@ -93,7 +95,7 @@ function handleDeepLink(url: string): void {
url = url
.replace('mihomo://install-config/?url=', '')
.replace('mihomo://install-config?url=', '')
addProfileItem({
await addProfileItem({
type: 'remote',
name: 'Remote File',
url
@ -104,7 +106,7 @@ function handleDeepLink(url: string): void {
function createWindow(): void {
Menu.setApplicationMenu(null)
// Create the browser window.
window = new BrowserWindow({
mainWindow = new BrowserWindow({
minWidth: 800,
minHeight: 600,
width: 800,
@ -118,31 +120,32 @@ function createWindow(): void {
sandbox: false
}
})
window.on('ready-to-show', () => {
if (!getAppConfig().silentStart) {
window?.show()
window?.focusOnWebView()
mainWindow.on('ready-to-show', async () => {
const { silentStart } = await getAppConfig()
if (!silentStart) {
mainWindow?.show()
mainWindow?.focusOnWebView()
}
})
window.on('resize', () => {
window?.webContents.send('resize')
mainWindow.on('resize', () => {
mainWindow?.webContents.send('resize')
})
window.on('show', () => {
mainWindow.on('show', () => {
startMihomoTraffic()
startMihomoMemory()
})
window.on('close', (event) => {
mainWindow.on('close', (event) => {
stopMihomoTraffic()
stopMihomoMemory()
event.preventDefault()
window?.hide()
window?.webContents.reload()
mainWindow?.hide()
mainWindow?.webContents.reload()
})
window.webContents.setWindowOpenHandler((details) => {
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
@ -150,8 +153,8 @@ function createWindow(): void {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
window.loadURL(process.env['ELECTRON_RENDERER_URL'])
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} 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 { 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 fs from 'fs'
import { promisify } from 'util'
import path from 'path'
const appName = 'mihomo-party'
@ -51,12 +54,13 @@ const taskXml = `
export async function checkAutoRun(): Promise<boolean> {
if (process.platform === 'win32') {
const { stdout } = (await new Promise((resolve) => {
exec(`schtasks /query /tn "${appName}"`, (_err, stdout, stderr) => {
resolve({ stdout, stderr })
})
})) as { stdout: string; stderr: string }
return stdout.includes(appName)
const execPromise = promisify(exec)
try {
const { stdout } = await execPromise(`schtasks /query /tn "${appName}"`)
return stdout.includes(appName)
} catch (e) {
return false
}
}
if (process.platform === 'darwin') {
@ -64,16 +68,17 @@ export async function checkAutoRun(): Promise<boolean> {
}
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
}
export function enableAutoRun(): void {
export async function enableAutoRun(): Promise<void> {
if (process.platform === 'win32') {
const taskFilePath = `${app.getPath('userData')}\\${appName}.xml`
fs.writeFileSync(taskFilePath, taskXml)
exec(`schtasks /create /tn "${appName}" /xml "${taskFilePath}" /f`)
const execPromise = promisify(exec)
const taskFilePath = path.join(dataDir, `${appName}.xml`)
await writeFile(taskFilePath, taskXml)
await execPromise(`schtasks /create /tn "${appName}" /xml "${taskFilePath}" /f`)
}
if (process.platform === 'darwin') {
app.setLoginItemSettings({
@ -93,22 +98,23 @@ StartupWMClass=mihomo-party
Comment=Mihomo Party
Categories=Utility;
`
try {
if (fs.existsSync(`/usr/share/applications/${appName}.desktop`)) {
desktop = fs.readFileSync(`/usr/share/applications/${appName}.desktop`, 'utf8')
}
} catch (e) {
console.error(e)
if (existsSync(`/usr/share/applications/${appName}.desktop`)) {
desktop = await readFile(`/usr/share/applications/${appName}.desktop`, 'utf8')
}
fs.mkdirSync(`${app.getPath('home')}/.config/autostart`, { recursive: true })
const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop`
fs.writeFileSync(desktopFilePath, desktop)
const autostartDir = path.join(homeDir, '.config', 'autostart')
if (!existsSync(autostartDir)) {
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') {
exec(`schtasks /delete /tn "${appName}" /f`)
const execPromise = promisify(exec)
await execPromise(`schtasks /delete /tn "${appName}" /f`)
}
if (process.platform === 'darwin') {
app.setLoginItemSettings({
@ -116,11 +122,7 @@ export function disableAutoRun(): void {
})
}
if (process.platform === 'linux') {
const desktopFilePath = `${app.getPath('home')}/.config/autostart/${appName}.desktop`
try {
fs.rmSync(desktopFilePath)
} catch (e) {
console.error(e)
}
const desktopFilePath = path.join(homeDir, '.config', 'autostart', `${appName}.desktop`)
await rm(desktopFilePath)
}
}

View File

@ -4,26 +4,23 @@ import { app } from 'electron'
import { getControledMihomoConfig } from '../config'
export async function checkUpdate(): Promise<string | undefined> {
try {
const res = await axios.get(
'https://github.com/pompurin404/mihomo-party/releases/latest/download/latest.yml',
{
headers: { 'Content-Type': 'application/octet-stream' },
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: getControledMihomoConfig()['mixed-port'] || 7890
}
const res = await axios.get(
'https://github.com/pompurin404/mihomo-party/releases/latest/download/latest.yml',
{
headers: { 'Content-Type': 'application/octet-stream' },
proxy: {
protocol: 'http',
host: '127.0.0.1',
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 fs from 'fs'
export function generateProfile(): void {
const current = getProfileConfig().current
const currentProfile = overrideProfile(current, getProfile(current))
const controledMihomoConfig = getControledMihomoConfig()
export async function generateProfile(): Promise<void> {
const { current } = await getProfileConfig()
const currentProfile = await overrideProfile(current, await getProfile(current))
const controledMihomoConfig = await getControledMihomoConfig()
const { tun: profileTun = {} } = currentProfile
const { tun: controledTun } = controledMihomoConfig
const tun = Object.assign(profileTun, controledTun)
@ -26,13 +26,24 @@ export function generateProfile(): void {
profile.tun = tun
profile.dns = dns
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 {
const overrideScriptList = getProfileItem(current).override || []
for (const override of overrideScriptList) {
const script = getOverride(override)
async function overrideProfile(
current: string | undefined,
profile: IMihomoConfig
): Promise<IMihomoConfig> {
const { override = [] } = (await getProfileItem(current)) || {}
for (const ov of override) {
const script = await getOverride(ov)
profile = runOverrideScript(profile, script)
}
return profile
@ -42,9 +53,7 @@ function runOverrideScript(profile: IMihomoConfig, script: string): IMihomoConfi
try {
const func = eval(`${script} main`)
const newProfile = func(profile)
if (typeof newProfile !== 'object') {
throw new Error('Override script must return an object')
}
if (typeof newProfile !== 'object') return profile
return newProfile
} catch (e) {
return profile

View File

@ -20,83 +20,98 @@ import {
defaultProfileConfig
} from '../utils/template'
import yaml from 'yaml'
import fs from 'fs'
import { mkdir, writeFile, copyFile } from 'fs/promises'
import { existsSync } from 'fs'
import path from 'path'
import { startPacServer } from './server'
import { triggerSysProxy } from './sysproxy'
import { getAppConfig } from '../config'
import { app } from 'electron'
import { startCore } from '../core/manager'
import { initProfileUpdater } from '../core/profileUpdater'
function initDirs(): void {
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir)
async function initDirs(): Promise<void> {
if (!existsSync(dataDir)) {
await mkdir(dataDir)
}
if (!fs.existsSync(profilesDir())) {
fs.mkdirSync(profilesDir())
if (!existsSync(profilesDir())) {
await mkdir(profilesDir())
}
if (!fs.existsSync(overrideDir())) {
fs.mkdirSync(overrideDir())
if (!existsSync(overrideDir())) {
await mkdir(overrideDir())
}
if (!fs.existsSync(mihomoWorkDir())) {
fs.mkdirSync(mihomoWorkDir())
if (!existsSync(mihomoWorkDir())) {
await mkdir(mihomoWorkDir())
}
if (!fs.existsSync(logDir())) {
fs.mkdirSync(logDir())
if (!existsSync(logDir())) {
await mkdir(logDir())
}
if (!fs.existsSync(mihomoTestDir())) {
fs.mkdirSync(mihomoTestDir())
if (!existsSync(mihomoTestDir())) {
await mkdir(mihomoTestDir())
}
}
function initConfig(): void {
if (!fs.existsSync(appConfigPath())) {
fs.writeFileSync(appConfigPath(), yaml.stringify(defaultConfig))
async function initConfig(): Promise<void> {
if (!existsSync(appConfigPath())) {
await writeFile(appConfigPath(), yaml.stringify(defaultConfig))
}
if (!fs.existsSync(profileConfigPath())) {
fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig))
if (!existsSync(profileConfigPath())) {
await writeFile(profileConfigPath(), yaml.stringify(defaultProfileConfig))
}
if (!fs.existsSync(overrideConfigPath())) {
fs.writeFileSync(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
if (!existsSync(overrideConfigPath())) {
await writeFile(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
}
if (!fs.existsSync(profilePath('default'))) {
fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile))
if (!existsSync(profilePath('default'))) {
await writeFile(profilePath('default'), yaml.stringify(defaultProfile))
}
if (!fs.existsSync(controledMihomoConfigPath())) {
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
if (!existsSync(controledMihomoConfigPath())) {
await writeFile(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
}
}
function initFiles(): void {
const fileList = ['country.mmdb', 'geoip.dat', 'geosite.dat', 'ASN.mmdb']
for (const file of fileList) {
async function initFiles(): Promise<void> {
const copy = async (file: string): Promise<void> => {
const targetPath = path.join(mihomoWorkDir(), file)
const testTargrtPath = path.join(mihomoTestDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file)
if (!fs.existsSync(targetPath) && fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, targetPath)
if (!existsSync(targetPath) && existsSync(sourcePath)) {
await copyFile(sourcePath, targetPath)
}
if (!fs.existsSync(testTargrtPath) && fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, testTargrtPath)
if (!existsSync(testTargrtPath) && existsSync(sourcePath)) {
await copyFile(sourcePath, testTargrtPath)
}
}
await Promise.all([
copy('country.mmdb'),
copy('geoip.dat'),
copy('geosite.dat'),
copy('ASN.mmdb')
])
}
function initDeeplink(): void {
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('clash', process.execPath, [path.resolve(process.argv[1])])
app.setAsDefaultProtocolClient('mihomo', process.execPath, [path.resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient('clash')
app.setAsDefaultProtocolClient('mihomo')
}
}
export function init(): void {
initDirs()
initConfig()
initFiles()
initDeeplink()
startPacServer().then(() => {
triggerSysProxy(getAppConfig().sysProxy.enable)
export async function init(): Promise<void> {
await initDirs()
await initConfig()
await initFiles()
await startPacServer()
const { sysProxy } = await getAppConfig()
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> {
pacPort = await findAvailablePort(10000)
const server = http
.createServer((_req, res) => {
.createServer(async (_req, res) => {
const {
sysProxy: { pacScript }
} = getAppConfig()
const { 'mixed-port': port = 7890 } = getControledMihomoConfig()
} = await getAppConfig()
const { 'mixed-port': port = 7890 } = await getControledMihomoConfig()
let script = pacScript || defaultPacScript
script = script.replaceAll('%mixed-port%', port.toString())
res.writeHead(200, { 'Content-Type': 'application/x-ns-proxy-autoconfig' })

View File

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

View File

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

View File

@ -22,9 +22,9 @@ import {
import { checkAutoRun, disableAutoRun, enableAutoRun } from '../resolve/autoRun'
import {
getAppConfig,
setAppConfig,
patchAppConfig,
getControledMihomoConfig,
setControledMihomoConfig,
patchControledMihomoConfig,
getProfileConfig,
getCurrentProfileItem,
getProfileItem,
@ -48,68 +48,95 @@ import { isEncryptionAvailable, restartCore } from '../core/manager'
import { triggerSysProxy } from '../resolve/sysproxy'
import { checkUpdate } from '../resolve/autoUpdater'
import { exePath, mihomoCorePath, mihomoWorkConfigPath, resourcesDir } from './dirs'
import { execFile, execSync } from 'child_process'
import { exec, execFile } from 'child_process'
import yaml from 'yaml'
import fs from 'fs'
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 {
ipcMain.handle('mihomoVersion', mihomoVersion)
ipcMain.handle('mihomoCloseConnection', (_e, id) => mihomoCloseConnection(id))
ipcMain.handle('mihomoCloseAllConnections', mihomoCloseAllConnections)
ipcMain.handle('mihomoRules', mihomoRules)
ipcMain.handle('mihomoProxies', mihomoProxies)
ipcMain.handle('mihomoProxyProviders', () => mihomoProxyProviders())
ipcMain.handle('mihomoUpdateProxyProviders', (_e, name) => mihomoUpdateProxyProviders(name))
ipcMain.handle('mihomoRuleProviders', () => mihomoRuleProviders())
ipcMain.handle('mihomoUpdateRuleProviders', (_e, name) => mihomoUpdateRuleProviders(name))
ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) => mihomoChangeProxy(group, proxy))
ipcMain.handle('mihomoUpgradeGeo', mihomoUpgradeGeo)
ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) => mihomoProxyDelay(proxy, url))
ipcMain.handle('mihomoGroupDelay', (_e, group, url) => mihomoGroupDelay(group, url))
ipcMain.handle('startMihomoLogs', startMihomoLogs)
ipcMain.handle('mihomoVersion', ipcErrorWrapper(mihomoVersion))
ipcMain.handle('mihomoCloseConnection', (_e, id) => ipcErrorWrapper(mihomoCloseConnection)(id))
ipcMain.handle('mihomoCloseAllConnections', ipcErrorWrapper(mihomoCloseAllConnections))
ipcMain.handle('mihomoRules', ipcErrorWrapper(mihomoRules))
ipcMain.handle('mihomoProxies', ipcErrorWrapper(mihomoProxies))
ipcMain.handle('mihomoProxyProviders', ipcErrorWrapper(mihomoProxyProviders))
ipcMain.handle('mihomoUpdateProxyProviders', (_e, name) =>
ipcErrorWrapper(mihomoUpdateProxyProviders)(name)
)
ipcMain.handle('mihomoRuleProviders', ipcErrorWrapper(mihomoRuleProviders))
ipcMain.handle('mihomoUpdateRuleProviders', (_e, name) =>
ipcErrorWrapper(mihomoUpdateRuleProviders)(name)
)
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('startMihomoConnections', () => startMihomoConnections())
ipcMain.handle('stopMihomoConnections', () => stopMihomoConnections())
ipcMain.handle('patchMihomoConfig', (_e, patch) => patchMihomoConfig(patch))
ipcMain.handle('checkAutoRun', checkAutoRun)
ipcMain.handle('enableAutoRun', enableAutoRun)
ipcMain.handle('disableAutoRun', disableAutoRun)
ipcMain.handle('getAppConfig', (_e, force) => getAppConfig(force))
ipcMain.handle('setAppConfig', (_e, config) => setAppConfig(config))
ipcMain.handle('getControledMihomoConfig', (_e, force) => getControledMihomoConfig(force))
ipcMain.handle('setControledMihomoConfig', (_e, config) => setControledMihomoConfig(config))
ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force))
ipcMain.handle('setProfileConfig', (_e, config) => setProfileConfig(config))
ipcMain.handle('getCurrentProfileItem', getCurrentProfileItem)
ipcMain.handle('getProfileItem', (_e, id) => getProfileItem(id))
ipcMain.handle('getProfileStr', (_e, id) => getProfileStr(id))
ipcMain.handle('setProfileStr', (_e, id, str) => setProfileStr(id, str))
ipcMain.handle('updateProfileItem', (_e, item) => updateProfileItem(item))
ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id))
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item))
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id))
ipcMain.handle('getOverrideConfig', (_e, force) => getOverrideConfig(force))
ipcMain.handle('setOverrideConfig', (_e, config) => setOverrideConfig(config))
ipcMain.handle('getOverrideItem', (_e, id) => getOverrideItem(id))
ipcMain.handle('addOverrideItem', (_e, item) => addOverrideItem(item))
ipcMain.handle('removeOverrideItem', (_e, id) => removeOverrideItem(id))
ipcMain.handle('updateOverrideItem', (_e, item) => updateOverrideItem(item))
ipcMain.handle('getOverride', (_e, id) => getOverride(id))
ipcMain.handle('setOverride', (_e, id, str) => setOverride(id, str))
ipcMain.handle('restartCore', restartCore)
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable))
ipcMain.handle('startMihomoConnections', ipcErrorWrapper(startMihomoConnections))
ipcMain.handle('stopMihomoConnections', stopMihomoConnections)
ipcMain.handle('patchMihomoConfig', (_e, patch) => ipcErrorWrapper(patchMihomoConfig)(patch))
ipcMain.handle('checkAutoRun', ipcErrorWrapper(checkAutoRun))
ipcMain.handle('enableAutoRun', ipcErrorWrapper(enableAutoRun))
ipcMain.handle('disableAutoRun', ipcErrorWrapper(disableAutoRun))
ipcMain.handle('getAppConfig', (_e, force) => ipcErrorWrapper(getAppConfig)(force))
ipcMain.handle('patchAppConfig', (_e, config) => ipcErrorWrapper(patchAppConfig)(config))
ipcMain.handle('getControledMihomoConfig', (_e, force) =>
ipcErrorWrapper(getControledMihomoConfig)(force)
)
ipcMain.handle('patchControledMihomoConfig', (_e, config) =>
ipcErrorWrapper(patchControledMihomoConfig)(config)
)
ipcMain.handle('getProfileConfig', (_e, force) => ipcErrorWrapper(getProfileConfig)(force))
ipcMain.handle('setProfileConfig', (_e, config) => ipcErrorWrapper(setProfileConfig)(config))
ipcMain.handle('getCurrentProfileItem', ipcErrorWrapper(getCurrentProfileItem))
ipcMain.handle('getProfileItem', (_e, id) => ipcErrorWrapper(getProfileItem)(id))
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item))
ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id))
ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force))
ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config))
ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id))
ipcMain.handle('addOverrideItem', (_e, item) => ipcErrorWrapper(addOverrideItem)(item))
ipcMain.handle('removeOverrideItem', (_e, id) => ipcErrorWrapper(removeOverrideItem)(id))
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('encryptString', (_e, str) => safeStorage.encryptString(str))
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => readTextFile(filePath))
ipcMain.handle('getRuntimeConfigStr', getRuntimeConfigStr)
ipcMain.handle('getRuntimeConfig', getRuntimeConfig)
ipcMain.handle('checkUpdate', () => checkUpdate())
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
ipcMain.handle('getRuntimeConfig', ipcErrorWrapper(getRuntimeConfig))
ipcMain.handle('checkUpdate', ipcErrorWrapper(checkUpdate))
ipcMain.handle('getVersion', () => app.getVersion())
ipcMain.handle('platform', () => process.platform)
ipcMain.handle('openUWPTool', openUWPTool)
ipcMain.handle('setupFirewall', setupFirewall)
ipcMain.handle('openUWPTool', ipcErrorWrapper(openUWPTool))
ipcMain.handle('setupFirewall', ipcErrorWrapper(setupFirewall))
ipcMain.handle('quitApp', () => app.quit())
}
@ -121,51 +148,39 @@ function getFilePath(ext: string[]): string[] | undefined {
})
}
function readTextFile(filePath: string): string {
return fs.readFileSync(filePath, 'utf8')
async function readTextFile(filePath: string): Promise<string> {
return await readFile(filePath, 'utf8')
}
function getRuntimeConfigStr(): string {
return fs.readFileSync(mihomoWorkConfigPath(), 'utf8')
async function getRuntimeConfigStr(): Promise<string> {
return readFile(mihomoWorkConfigPath(), 'utf8')
}
function getRuntimeConfig(): IMihomoConfig {
return yaml.parse(getRuntimeConfigStr())
async function getRuntimeConfig(): Promise<IMihomoConfig> {
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 child = execFile(uwpToolPath)
child.unref()
await execFilePromise(uwpToolPath)
}
async function setupFirewall(): Promise<void> {
return new Promise((resolve, reject) => {
const removeCommand = `
const execPromise = promisify(exec)
const removeCommand = `
Remove-NetFirewallRule -DisplayName "mihomo" -ErrorAction SilentlyContinue
Remove-NetFirewallRule -DisplayName "mihomo-alpha" -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-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
`
if (process.platform === 'win32') {
try {
execSync(removeCommand, { 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()
})
if (process.platform === 'win32') {
await execPromise(removeCommand, { shell: 'powershell' })
await execPromise(createCommand, { shell: 'powershell' })
}
}

View File

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

View File

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

View File

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

View File

@ -165,7 +165,11 @@ const Profiles: React.FC = () => {
updateProfileItem={updateProfileItem}
info={item}
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> {
return await window.electron.ipcRenderer.invoke('mihomoVersion')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoVersion'))
}
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> {
return await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections'))
}
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> {
return await window.electron.ipcRenderer.invoke('mihomoProxies')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxies'))
}
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> {
return await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name)
return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name)
)
}
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> {
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> {
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> {
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> {
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> {
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> {
return await window.electron.ipcRenderer.invoke('startMihomoLogs')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoLogs'))
}
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> {
return await window.electron.ipcRenderer.invoke('startMihomoConnections')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoConnections'))
}
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> {
return await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch)
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch))
}
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> {
return await window.electron.ipcRenderer.invoke('enableAutoRun')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('enableAutoRun'))
}
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> {
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> {
return await window.electron.ipcRenderer.invoke('setAppConfig', patch)
export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchAppConfig', patch))
}
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> {
return await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch)
export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('patchControledMihomoConfig', patch)
)
}
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> {
return await window.electron.ipcRenderer.invoke('setProfileConfig', config)
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileConfig', config))
}
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
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> {
return await window.electron.ipcRenderer.invoke('restartCore')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartCore'))
}
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> {
return await window.electron.ipcRenderer.invoke('isEncryptionAvailable')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('isEncryptionAvailable'))
}
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> {
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> {
return await window.electron.ipcRenderer.invoke('readTextFile', filePath)
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('readTextFile', filePath))
}
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> {
return await window.electron.ipcRenderer.invoke('getRuntimeConfig')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuntimeConfig'))
}
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> {
return await window.electron.ipcRenderer.invoke('getVersion')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getVersion'))
}
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> {
return await window.electron.ipcRenderer.invoke('setupFirewall')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setupFirewall'))
}
export async function quitApp(): Promise<void> {
return await window.electron.ipcRenderer.invoke('quitApp')
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitApp'))
}