diff --git a/src/main/config/app.ts b/src/main/config/app.ts index c6d9c67..14955ac 100644 --- a/src/main/config/app.ts +++ b/src/main/config/app.ts @@ -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 { 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): void { +export async function patchAppConfig(patch: Partial): Promise { 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)) } diff --git a/src/main/config/controledMihomo.ts b/src/main/config/controledMihomo.ts index 9af2ae5..dc3d1eb 100644 --- a/src/main/config/controledMihomo.ts +++ b/src/main/config/controledMihomo.ts @@ -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 // mihomo.yaml +let controledMihomoConfig: Partial // mihomo.yaml -export function getControledMihomoConfig(force = false): Partial { +export async function getControledMihomoConfig(force = false): Promise> { 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): void { - const { useNameserverPolicy } = getAppConfig() +export async function patchControledMihomoConfig(patch: Partial): Promise { + 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): 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') } diff --git a/src/main/config/index.ts b/src/main/config/index.ts index cd68d37..ac62fd5 100644 --- a/src/main/config/index.ts +++ b/src/main/config/index.ts @@ -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, diff --git a/src/main/config/override.ts b/src/main/config/override.ts index b3df855..536d1c4 100644 --- a/src/main/config/override.ts +++ b/src/main/config/override.ts @@ -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 { 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 { 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 { + 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 { + 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): Promise { + 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 { + 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): Promise { @@ -55,31 +64,21 @@ export async function createOverride(item: Partial): Promise): Promise { + 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 { + await writeFile(overridePath(id), content, 'utf-8') } diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index ffe0a34..d53d27f 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -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 { 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 { 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 { + 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 { - 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 { + 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): Promise { 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 { + 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 { + const { current } = await getProfileConfig() + return (await getProfileItem(current)) || { id: 'default', type: 'local', name: '空白订阅' } +} + +export async function createProfile(item: Partial): Promise { + 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 { + 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 { + const { current } = await getProfileConfig() + await writeFile(profilePath(id), content, 'utf-8') + if (current === id) await restartCore() +} + +export async function getProfile(id: string | undefined): Promise { + 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): Promise { - 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 { - 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')) -} diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 5c6d68a..81a36ec 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -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 { - 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 { 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 { recover() } -export function checkProfile(): Promise { - 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 { + 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 { + 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 }) } } } diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 452b1c1..197d157 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -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 => { 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 = 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 { 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): Promise => { const instance = await getAxios() - return (await instance.patch('/configs', patch).catch((e) => { - return e.response.data - })) as Promise + return await instance.patch('/configs', patch) } export const mihomoCloseConnection = async (id: string): Promise => { const instance = await getAxios() - return (await instance.delete(`/connections/${encodeURIComponent(id)}`).catch((e) => { - return e.response.data - })) as Promise + return await instance.delete(`/connections/${encodeURIComponent(id)}`) } export const mihomoCloseAllConnections = async (): Promise => { const instance = await getAxios() - return (await instance.delete('/connections').catch((e) => { - return e.response.data - })) as Promise + return await instance.delete('/connections') } export const mihomoRules = async (): Promise => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { - 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 => { - 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index 6490166..dae6493 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -1,42 +1,60 @@ -import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config' +import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' const intervalPool: Record = {} export async function initProfileUpdater(): Promise { - 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 { 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 ) diff --git a/src/main/core/tray.ts b/src/main/core/tray.ts index 82fa102..9824dd3 100644 --- a/src/main/core/tray.ts +++ b/src/main/core/tray.ts @@ -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 => { + 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 => { + 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 => { + 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 => { + 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 => { 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 => { 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 { 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 { + const menu = await buildContextMenu() tray?.setContextMenu(menu) // 更新菜单 } diff --git a/src/main/index.ts b/src/main/index.ts index f6e324e..a3f1ea2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 { 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')) } } diff --git a/src/main/resolve/autoRun.ts b/src/main/resolve/autoRun.ts index 56e09d2..0259361 100644 --- a/src/main/resolve/autoRun.ts +++ b/src/main/resolve/autoRun.ts @@ -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 { 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 { } 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 { 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 { 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) } } diff --git a/src/main/resolve/autoUpdater.ts b/src/main/resolve/autoUpdater.ts index 8c8225b..e48c17d 100644 --- a/src/main/resolve/autoUpdater.ts +++ b/src/main/resolve/autoUpdater.ts @@ -4,26 +4,23 @@ import { app } from 'electron' import { getControledMihomoConfig } from '../config' export async function checkUpdate(): Promise { - 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 } diff --git a/src/main/resolve/factory.ts b/src/main/resolve/factory.ts index a2af2fc..2f74f20 100644 --- a/src/main/resolve/factory.ts +++ b/src/main/resolve/factory.ts @@ -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 { + 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 { + 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 diff --git a/src/main/resolve/init.ts b/src/main/resolve/init.ts index dd82e85..598eae0 100644 --- a/src/main/resolve/init.ts +++ b/src/main/resolve/init.ts @@ -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 { + 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 { + 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 { + const copy = async (file: string): Promise => { 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 { + await initDirs() + await initConfig() + await initFiles() + await startPacServer() + const { sysProxy } = await getAppConfig() + await triggerSysProxy(sysProxy.enable) + startCore().then(() => { + setTimeout(async () => { + await initProfileUpdater() + }, 60000) }) + initDeeplink() } diff --git a/src/main/resolve/server.ts b/src/main/resolve/server.ts index 8cc958a..85c4209 100644 --- a/src/main/resolve/server.ts +++ b/src/main/resolve/server.ts @@ -34,11 +34,11 @@ function findAvailablePort(startPort: number): Promise { export async function startPacServer(): Promise { 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' }) diff --git a/src/main/resolve/sysproxy.ts b/src/main/resolve/sysproxy.ts index 05e3fbd..3ba1de6 100644 --- a/src/main/resolve/sysproxy.ts +++ b/src/main/resolve/sysproxy.ts @@ -42,19 +42,19 @@ if (process.platform === 'win32') '' ] -export function triggerSysProxy(enable: boolean): void { +export async function triggerSysProxy(enable: boolean): Promise { if (enable) { disableSysProxy() - enableSysProxy() + await enableSysProxy() } else { disableSysProxy() } } -export function enableSysProxy(): void { - const { sysProxy } = getAppConfig() +export async function enableSysProxy(): Promise { + 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': { diff --git a/src/main/utils/dirs.ts b/src/main/utils/dirs.ts index 9e51e32..ea930cc 100644 --- a/src/main/utils/dirs.ts +++ b/src/main/utils/dirs.ts @@ -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') diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 33f513b..a26b954 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -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( // eslint-disable-next-line @typescript-eslint/no-explicit-any + fn: (...args: any[]) => Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (...args: any[]) => Promise { + // 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 { + return await readFile(filePath, 'utf8') } -function getRuntimeConfigStr(): string { - return fs.readFileSync(mihomoWorkConfigPath(), 'utf8') +async function getRuntimeConfigStr(): Promise { + return readFile(mihomoWorkConfigPath(), 'utf8') } -function getRuntimeConfig(): IMihomoConfig { - return yaml.parse(getRuntimeConfigStr()) +async function getRuntimeConfig(): Promise { + return yaml.parse(await getRuntimeConfigStr()) } -function openUWPTool(): void { +async function openUWPTool(): Promise { + 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 { - 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' }) + } } diff --git a/src/renderer/src/components/profiles/profile-item.tsx b/src/renderer/src/components/profiles/profile-item.tsx index da8f6d5..7d2e8b6 100644 --- a/src/renderer/src/components/profiles/profile-item.tsx +++ b/src/renderer/src/components/profiles/profile-item.tsx @@ -179,9 +179,13 @@ const ProfileItem: React.FC = (props) => { disabled={updating} onPress={() => { setUpdating(true) - addProfileItem(info).finally(() => { - setUpdating(false) - }) + addProfileItem(info) + .catch((e) => { + alert(e) + }) + .finally(() => { + setUpdating(false) + }) }} > { const { data: appConfig, mutate: mutateAppConfig } = useSWR('getConfig', () => getAppConfig()) const patchAppConfig = async (value: Partial): Promise => { - await setAppConfig(value) + await patch(value) mutateAppConfig() window.electron.ipcRenderer.send('appConfigUpdated') } diff --git a/src/renderer/src/hooks/use-controled-mihomo-config.tsx b/src/renderer/src/hooks/use-controled-mihomo-config.tsx index 90d5396..64715d1 100644 --- a/src/renderer/src/hooks/use-controled-mihomo-config.tsx +++ b/src/renderer/src/hooks/use-controled-mihomo-config.tsx @@ -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): Promise => { - await setControledMihomoConfig(value) + await patch(value) mutateControledMihomoConfig() window.electron.ipcRenderer.send('controledMihomoConfigUpdated') } diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index 29b92e1..1fc5a24 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -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) + } }} /> ))} diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 5641c0e..20c9898 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -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 { - return await window.electron.ipcRenderer.invoke('mihomoVersion') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoVersion')) } export async function mihomoCloseConnection(id: string): Promise { - return await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoCloseConnection', id)) } export async function mihomoCloseAllConnections(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoCloseAllConnections')) } export async function mihomoRules(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoRules') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRules')) } export async function mihomoProxies(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoProxies') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxies')) } export async function mihomoProxyProviders(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoProxyProviders') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoProxyProviders')) } export async function mihomoUpdateProxyProviders(name: string): Promise { - return await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name) + return ipcErrorWrapper( + await window.electron.ipcRenderer.invoke('mihomoUpdateProxyProviders', name) + ) } export async function mihomoRuleProviders(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoRuleProviders') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRuleProviders')) } export async function mihomoUpdateRuleProviders(name: string): Promise { - 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 { - return await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy) + return ipcErrorWrapper( + await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy) + ) } export async function mihomoUpgradeGeo(): Promise { - return await window.electron.ipcRenderer.invoke('mihomoUpgradeGeo') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoUpgradeGeo')) } export async function mihomoProxyDelay(proxy: string, url?: string): Promise { - 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 { - return await window.electron.ipcRenderer.invoke('mihomoGroupDelay', group, url) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoGroupDelay', group, url)) } export async function startMihomoLogs(): Promise { - return await window.electron.ipcRenderer.invoke('startMihomoLogs') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoLogs')) } export async function stopMihomoLogs(): Promise { - return await window.electron.ipcRenderer.invoke('stopMihomoLogs') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopMihomoLogs')) } export async function startMihomoConnections(): Promise { - return await window.electron.ipcRenderer.invoke('startMihomoConnections') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('startMihomoConnections')) } export async function stopMihomoConnections(): Promise { - return await window.electron.ipcRenderer.invoke('stopMihomoConnections') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopMihomoConnections')) } export async function patchMihomoConfig(patch: Partial): Promise { - return await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchMihomoConfig', patch)) } export async function checkAutoRun(): Promise { - return await window.electron.ipcRenderer.invoke('checkAutoRun') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkAutoRun')) } export async function enableAutoRun(): Promise { - return await window.electron.ipcRenderer.invoke('enableAutoRun') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('enableAutoRun')) } export async function disableAutoRun(): Promise { - return await window.electron.ipcRenderer.invoke('disableAutoRun') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('disableAutoRun')) } export async function getAppConfig(force = false): Promise { - return await window.electron.ipcRenderer.invoke('getAppConfig', force) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getAppConfig', force)) } -export async function setAppConfig(patch: Partial): Promise { - return await window.electron.ipcRenderer.invoke('setAppConfig', patch) +export async function patchAppConfig(patch: Partial): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('patchAppConfig', patch)) } export async function getControledMihomoConfig(force = false): Promise> { - return await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force) + return ipcErrorWrapper( + await window.electron.ipcRenderer.invoke('getControledMihomoConfig', force) + ) } -export async function setControledMihomoConfig(patch: Partial): Promise { - return await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) +export async function patchControledMihomoConfig(patch: Partial): Promise { + return ipcErrorWrapper( + await window.electron.ipcRenderer.invoke('patchControledMihomoConfig', patch) + ) } export async function getProfileConfig(force = false): Promise { - return await window.electron.ipcRenderer.invoke('getProfileConfig', force) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileConfig', force)) } export async function setProfileConfig(config: IProfileConfig): Promise { - return await window.electron.ipcRenderer.invoke('setProfileConfig', config) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileConfig', config)) } export async function getCurrentProfileItem(): Promise { - return await window.electron.ipcRenderer.invoke('getCurrentProfileItem') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getCurrentProfileItem')) } export async function getProfileItem(id: string | undefined): Promise { - return await window.electron.ipcRenderer.invoke('getProfileItem', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileItem', id)) } export async function changeCurrentProfile(id: string): Promise { - return await window.electron.ipcRenderer.invoke('changeCurrentProfile', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('changeCurrentProfile', id)) } export async function addProfileItem(item: Partial): Promise { - return await window.electron.ipcRenderer.invoke('addProfileItem', item) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('addProfileItem', item)) } export async function removeProfileItem(id: string): Promise { - return await window.electron.ipcRenderer.invoke('removeProfileItem', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('removeProfileItem', id)) } export async function updateProfileItem(item: IProfileItem): Promise { - return await window.electron.ipcRenderer.invoke('updateProfileItem', item) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateProfileItem', item)) } export async function getProfileStr(id: string): Promise { - 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 { - 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 { - return await window.electron.ipcRenderer.invoke('getOverrideConfig', force) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideConfig', force)) } export async function setOverrideConfig(config: IOverrideConfig): Promise { - return await window.electron.ipcRenderer.invoke('setOverrideConfig', config) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setOverrideConfig', config)) } export async function getOverrideItem(id: string): Promise { - return await window.electron.ipcRenderer.invoke('getOverrideItem', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideItem', id)) } export async function addOverrideItem(item: Partial): Promise { - return await window.electron.ipcRenderer.invoke('addOverrideItem', item) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('addOverrideItem', item)) } export async function removeOverrideItem(id: string): Promise { - return await window.electron.ipcRenderer.invoke('removeOverrideItem', id) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('removeOverrideItem', id)) } export async function updateOverrideItem(item: IOverrideItem): Promise { - return await window.electron.ipcRenderer.invoke('updateOverrideItem', item) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateOverrideItem', item)) } export async function getOverride(id: string): Promise { - 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 { - return await window.electron.ipcRenderer.invoke('setOverride', id, str) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setOverride', id, str)) } export async function restartCore(): Promise { - return await window.electron.ipcRenderer.invoke('restartCore') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartCore')) } export async function triggerSysProxy(enable: boolean): Promise { - return await window.electron.ipcRenderer.invoke('triggerSysProxy', enable) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) } export async function isEncryptionAvailable(): Promise { - return await window.electron.ipcRenderer.invoke('isEncryptionAvailable') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('isEncryptionAvailable')) } export async function encryptString(str: string): Promise { - return await window.electron.ipcRenderer.invoke('encryptString', str) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('encryptString', str)) } export async function getFilePath(ext: string[]): Promise { - return await window.electron.ipcRenderer.invoke('getFilePath', ext) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) } export async function readTextFile(filePath: string): Promise { - return await window.electron.ipcRenderer.invoke('readTextFile', filePath) + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('readTextFile', filePath)) } export async function getRuntimeConfigStr(): Promise { - return await window.electron.ipcRenderer.invoke('getRuntimeConfigStr') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuntimeConfigStr')) } export async function getRuntimeConfig(): Promise { - return await window.electron.ipcRenderer.invoke('getRuntimeConfig') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getRuntimeConfig')) } export async function checkUpdate(): Promise { - return await window.electron.ipcRenderer.invoke('checkUpdate') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('checkUpdate')) } export async function getVersion(): Promise { - return await window.electron.ipcRenderer.invoke('getVersion') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getVersion')) } export async function getPlatform(): Promise { - return await window.electron.ipcRenderer.invoke('platform') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('platform')) } export async function setupFirewall(): Promise { - return await window.electron.ipcRenderer.invoke('setupFirewall') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setupFirewall')) } export async function quitApp(): Promise { - return await window.electron.ipcRenderer.invoke('quitApp') + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('quitApp')) }