From 3d6d545a9374238a13687f1050f0707e36840f2b Mon Sep 17 00:00:00 2001 From: xmk23333 Date: Sun, 28 Dec 2025 20:18:11 +0800 Subject: [PATCH] fix: use atomic update in changeCurrentProfile --- src/main/config/profile.ts | 95 ++++++++++++++++++++------------- src/main/core/profileUpdater.ts | 50 +++++++++-------- 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index f5b4eaa..0d6ffb5 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -15,9 +15,8 @@ import { mihomoUpgradeConfig } from '../core/mihomoApi' import i18next from 'i18next' -let profileConfig: IProfileConfig // profile.yaml +let profileConfig: IProfileConfig let profileConfigWriteQueue: Promise = Promise.resolve() -// 最终选中订阅ID let targetProfileId: string | null = null export async function getProfileConfig(force = false): Promise { @@ -26,7 +25,7 @@ export async function getProfileConfig(force = false): Promise { profileConfig = parse(data) || { items: [] } } if (typeof profileConfig !== 'object') profileConfig = { items: [] } - return profileConfig + return structuredClone(profileConfig) } export async function setProfileConfig(config: IProfileConfig): Promise { @@ -37,6 +36,22 @@ export async function setProfileConfig(config: IProfileConfig): Promise { await profileConfigWriteQueue } +export async function updateProfileConfig( + updater: (config: IProfileConfig) => IProfileConfig | Promise +): Promise { + let result: IProfileConfig + profileConfigWriteQueue = profileConfigWriteQueue.then(async () => { + const data = await readFile(profileConfigPath(), 'utf-8') + profileConfig = parse(data) || { items: [] } + if (typeof profileConfig !== 'object') profileConfig = { items: [] } + profileConfig = await updater(structuredClone(profileConfig)) + result = profileConfig + await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8') + }) + await profileConfigWriteQueue + return structuredClone(result!) +} + export async function getProfileItem(id: string | undefined): Promise { const { items } = await getProfileConfig() if (!id || id === 'default') @@ -45,8 +60,7 @@ export async function getProfileItem(id: string | undefined): Promise { - const config = await getProfileConfig() - const current = config.current + const { current } = await getProfileConfig() if (current === id && targetProfileId !== id) { return @@ -54,13 +68,12 @@ export async function changeCurrentProfile(id: string): Promise { targetProfileId = id - config.current = id - const configSavePromise = setProfileConfig(config) - try { - await configSavePromise + await updateProfileConfig((config) => { + config.current = id + return config + }) - // 检查订阅切换是否中断 if (targetProfileId !== id) { return } @@ -70,8 +83,10 @@ export async function changeCurrentProfile(id: string): Promise { } } catch (e) { if (targetProfileId === id) { - config.current = current - await setProfileConfig(config) + await updateProfileConfig((config) => { + config.current = current + return config + }) targetProfileId = null throw e } @@ -79,47 +94,51 @@ export async function changeCurrentProfile(id: string): Promise { } 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 updateProfileConfig((config) => { + const index = config.items.findIndex((i) => i.id === item.id) + if (index === -1) { + throw new Error('Profile not found') + } + config.items[index] = item + return config + }) } export async function addProfileItem(item: Partial): Promise { const newItem = await createProfile(item) - const config = await getProfileConfig() - if (await getProfileItem(newItem.id)) { - await updateProfileItem(newItem) - } else { - config.items.push(newItem) - } - await setProfileConfig(config) + let shouldChangeCurrent = false + await updateProfileConfig((config) => { + const existingIndex = config.items.findIndex((i) => i.id === newItem.id) + if (existingIndex !== -1) { + config.items[existingIndex] = newItem + } else { + config.items.push(newItem) + } + if (!config.current) { + shouldChangeCurrent = true + } + return config + }) - if (!config.current) { + if (shouldChangeCurrent) { await changeCurrentProfile(newItem.id) } await addProfileUpdater(newItem) } export async function removeProfileItem(id: string): Promise { - // 先清理自动更新定时器,防止已删除的订阅重新出现 await removeProfileUpdater(id) - const config = await getProfileConfig() - config.items = config.items?.filter((item) => item.id !== id) let shouldRestart = false - if (config.current === id) { - shouldRestart = true - if (config.items.length > 0) { - config.current = config.items[0].id - } else { - config.current = undefined + await updateProfileConfig((config) => { + config.items = config.items?.filter((item) => item.id !== id) + if (config.current === id) { + shouldRestart = true + config.current = config.items.length > 0 ? config.items[0].id : undefined } - } - await setProfileConfig(config) + return config + }) + if (existsSync(profilePath(id))) { await rm(profilePath(id)) } diff --git a/src/main/core/profileUpdater.ts b/src/main/core/profileUpdater.ts index 208cf76..f3f3aee 100644 --- a/src/main/core/profileUpdater.ts +++ b/src/main/core/profileUpdater.ts @@ -1,34 +1,40 @@ -import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' +import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config' import { Cron } from 'croner' import { logger } from '../utils/logger' const intervalPool: Record = {} +async function updateProfile(id: string): Promise { + const item = await getProfileItem(id) + if (item && item.type === 'remote') { + await addProfileItem(item) + } +} + export async function initProfileUpdater(): Promise { const { items, current } = await getProfileConfig() const currentItem = await getCurrentProfileItem() for (const item of items.filter((i) => i.id !== current)) { if (item.type === 'remote' && item.autoUpdate && item.interval) { + const itemId = item.id if (typeof item.interval === 'number') { - // 数字间隔使用 setInterval - intervalPool[item.id] = setInterval( + intervalPool[itemId] = setInterval( async () => { try { - await addProfileItem(item) + await updateProfile(itemId) } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) + await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) } }, item.interval * 60 * 1000 ) } else if (typeof item.interval === 'string') { - // 字符串间隔使用 Cron - intervalPool[item.id] = new Cron(item.interval, async () => { + intervalPool[itemId] = new Cron(item.interval, async () => { try { - await addProfileItem(item) + await updateProfile(itemId) } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) + await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) } }) } @@ -42,11 +48,12 @@ export async function initProfileUpdater(): Promise { } if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) { + const currentId = currentItem.id if (typeof currentItem.interval === 'number') { - intervalPool[currentItem.id] = setInterval( + intervalPool[currentId] = setInterval( async () => { try { - await addProfileItem(currentItem) + await updateProfile(currentId) } catch (e) { await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } @@ -57,17 +64,17 @@ export async function initProfileUpdater(): Promise { setTimeout( async () => { try { - await addProfileItem(currentItem) + await updateProfile(currentId) } catch (e) { await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } }, - currentItem.interval * 60 * 1000 + 10000 // +10s + currentItem.interval * 60 * 1000 + 10000 ) } else if (typeof currentItem.interval === 'string') { - intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => { + intervalPool[currentId] = new Cron(currentItem.interval, async () => { try { - await addProfileItem(currentItem) + await updateProfile(currentId) } catch (e) { await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) } @@ -92,23 +99,24 @@ export async function addProfileUpdater(item: IProfileItem): Promise { } } + const itemId = item.id if (typeof item.interval === 'number') { - intervalPool[item.id] = setInterval( + intervalPool[itemId] = setInterval( async () => { try { - await addProfileItem(item) + await updateProfile(itemId) } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) + await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) } }, item.interval * 60 * 1000 ) } else if (typeof item.interval === 'string') { - intervalPool[item.id] = new Cron(item.interval, async () => { + intervalPool[itemId] = new Cron(item.interval, async () => { try { - await addProfileItem(item) + await updateProfile(itemId) } catch (e) { - await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) + await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e) } }) }