fix: use atomic update in changeCurrentProfile

This commit is contained in:
xmk23333 2025-12-28 20:18:11 +08:00
parent fbde5c3f09
commit 3d6d545a93
2 changed files with 86 additions and 59 deletions

View File

@ -15,9 +15,8 @@ import { mihomoUpgradeConfig } from '../core/mihomoApi'
import i18next from 'i18next' import i18next from 'i18next'
let profileConfig: IProfileConfig // profile.yaml let profileConfig: IProfileConfig
let profileConfigWriteQueue: Promise<void> = Promise.resolve() let profileConfigWriteQueue: Promise<void> = Promise.resolve()
// 最终选中订阅ID
let targetProfileId: string | null = null let targetProfileId: string | null = null
export async function getProfileConfig(force = false): Promise<IProfileConfig> { export async function getProfileConfig(force = false): Promise<IProfileConfig> {
@ -26,7 +25,7 @@ export async function getProfileConfig(force = false): Promise<IProfileConfig> {
profileConfig = parse(data) || { items: [] } profileConfig = parse(data) || { items: [] }
} }
if (typeof profileConfig !== 'object') profileConfig = { items: [] } if (typeof profileConfig !== 'object') profileConfig = { items: [] }
return profileConfig return structuredClone(profileConfig)
} }
export async function setProfileConfig(config: IProfileConfig): Promise<void> { export async function setProfileConfig(config: IProfileConfig): Promise<void> {
@ -37,6 +36,22 @@ export async function setProfileConfig(config: IProfileConfig): Promise<void> {
await profileConfigWriteQueue await profileConfigWriteQueue
} }
export async function updateProfileConfig(
updater: (config: IProfileConfig) => IProfileConfig | Promise<IProfileConfig>
): Promise<IProfileConfig> {
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<IProfileItem | undefined> { export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {
const { items } = await getProfileConfig() const { items } = await getProfileConfig()
if (!id || id === 'default') if (!id || id === 'default')
@ -45,8 +60,7 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
} }
export async function changeCurrentProfile(id: string): Promise<void> { export async function changeCurrentProfile(id: string): Promise<void> {
const config = await getProfileConfig() const { current } = await getProfileConfig()
const current = config.current
if (current === id && targetProfileId !== id) { if (current === id && targetProfileId !== id) {
return return
@ -54,13 +68,12 @@ export async function changeCurrentProfile(id: string): Promise<void> {
targetProfileId = id targetProfileId = id
config.current = id
const configSavePromise = setProfileConfig(config)
try { try {
await configSavePromise await updateProfileConfig((config) => {
config.current = id
return config
})
// 检查订阅切换是否中断
if (targetProfileId !== id) { if (targetProfileId !== id) {
return return
} }
@ -70,8 +83,10 @@ export async function changeCurrentProfile(id: string): Promise<void> {
} }
} catch (e) { } catch (e) {
if (targetProfileId === id) { if (targetProfileId === id) {
config.current = current await updateProfileConfig((config) => {
await setProfileConfig(config) config.current = current
return config
})
targetProfileId = null targetProfileId = null
throw e throw e
} }
@ -79,47 +94,51 @@ export async function changeCurrentProfile(id: string): Promise<void> {
} }
export async function updateProfileItem(item: IProfileItem): Promise<void> { export async function updateProfileItem(item: IProfileItem): Promise<void> {
const config = await getProfileConfig() await updateProfileConfig((config) => {
const index = config.items.findIndex((i) => i.id === item.id) const index = config.items.findIndex((i) => i.id === item.id)
if (index === -1) { if (index === -1) {
throw new Error('Profile not found') throw new Error('Profile not found')
} }
config.items[index] = item config.items[index] = item
await setProfileConfig(config) return config
})
} }
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item) const newItem = await createProfile(item)
const config = await getProfileConfig() let shouldChangeCurrent = false
if (await getProfileItem(newItem.id)) { await updateProfileConfig((config) => {
await updateProfileItem(newItem) const existingIndex = config.items.findIndex((i) => i.id === newItem.id)
} else { if (existingIndex !== -1) {
config.items.push(newItem) config.items[existingIndex] = newItem
} } else {
await setProfileConfig(config) config.items.push(newItem)
}
if (!config.current) {
shouldChangeCurrent = true
}
return config
})
if (!config.current) { if (shouldChangeCurrent) {
await changeCurrentProfile(newItem.id) await changeCurrentProfile(newItem.id)
} }
await addProfileUpdater(newItem) await addProfileUpdater(newItem)
} }
export async function removeProfileItem(id: string): Promise<void> { export async function removeProfileItem(id: string): Promise<void> {
// 先清理自动更新定时器,防止已删除的订阅重新出现
await removeProfileUpdater(id) await removeProfileUpdater(id)
const config = await getProfileConfig()
config.items = config.items?.filter((item) => item.id !== id)
let shouldRestart = false let shouldRestart = false
if (config.current === id) { await updateProfileConfig((config) => {
shouldRestart = true config.items = config.items?.filter((item) => item.id !== id)
if (config.items.length > 0) { if (config.current === id) {
config.current = config.items[0].id shouldRestart = true
} else { config.current = config.items.length > 0 ? config.items[0].id : undefined
config.current = undefined
} }
} return config
await setProfileConfig(config) })
if (existsSync(profilePath(id))) { if (existsSync(profilePath(id))) {
await rm(profilePath(id)) await rm(profilePath(id))
} }

View File

@ -1,34 +1,40 @@
import { addProfileItem, getCurrentProfileItem, getProfileConfig } from '../config' import { addProfileItem, getCurrentProfileItem, getProfileConfig, getProfileItem } from '../config'
import { Cron } from 'croner' import { Cron } from 'croner'
import { logger } from '../utils/logger' import { logger } from '../utils/logger'
const intervalPool: Record<string, Cron | NodeJS.Timeout> = {} const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
async function updateProfile(id: string): Promise<void> {
const item = await getProfileItem(id)
if (item && item.type === 'remote') {
await addProfileItem(item)
}
}
export async function initProfileUpdater(): Promise<void> { export async function initProfileUpdater(): Promise<void> {
const { items, current } = await getProfileConfig() const { items, current } = await getProfileConfig()
const currentItem = await getCurrentProfileItem() const currentItem = await getCurrentProfileItem()
for (const item of items.filter((i) => i.id !== current)) { for (const item of items.filter((i) => i.id !== current)) {
if (item.type === 'remote' && item.autoUpdate && item.interval) { if (item.type === 'remote' && item.autoUpdate && item.interval) {
const itemId = item.id
if (typeof item.interval === 'number') { if (typeof item.interval === 'number') {
// 数字间隔使用 setInterval intervalPool[itemId] = setInterval(
intervalPool[item.id] = setInterval(
async () => { async () => {
try { try {
await addProfileItem(item) await updateProfile(itemId)
} catch (e) { } 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 item.interval * 60 * 1000
) )
} else if (typeof item.interval === 'string') { } else if (typeof item.interval === 'string') {
// 字符串间隔使用 Cron intervalPool[itemId] = new Cron(item.interval, async () => {
intervalPool[item.id] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await updateProfile(itemId)
} catch (e) { } 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<void> {
} }
if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) { if (currentItem?.type === 'remote' && currentItem.autoUpdate && currentItem.interval) {
const currentId = currentItem.id
if (typeof currentItem.interval === 'number') { if (typeof currentItem.interval === 'number') {
intervalPool[currentItem.id] = setInterval( intervalPool[currentId] = setInterval(
async () => { async () => {
try { try {
await addProfileItem(currentItem) await updateProfile(currentId)
} catch (e) { } catch (e) {
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
} }
@ -57,17 +64,17 @@ export async function initProfileUpdater(): Promise<void> {
setTimeout( setTimeout(
async () => { async () => {
try { try {
await addProfileItem(currentItem) await updateProfile(currentId)
} catch (e) { } catch (e) {
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, 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') { } else if (typeof currentItem.interval === 'string') {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => { intervalPool[currentId] = new Cron(currentItem.interval, async () => {
try { try {
await addProfileItem(currentItem) await updateProfile(currentId)
} catch (e) { } catch (e) {
await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e) await logger.warn(`[ProfileUpdater] Failed to update current profile:`, e)
} }
@ -92,23 +99,24 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
} }
} }
const itemId = item.id
if (typeof item.interval === 'number') { if (typeof item.interval === 'number') {
intervalPool[item.id] = setInterval( intervalPool[itemId] = setInterval(
async () => { async () => {
try { try {
await addProfileItem(item) await updateProfile(itemId)
} catch (e) { } 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 item.interval * 60 * 1000
) )
} else if (typeof item.interval === 'string') { } else if (typeof item.interval === 'string') {
intervalPool[item.id] = new Cron(item.interval, async () => { intervalPool[itemId] = new Cron(item.interval, async () => {
try { try {
await addProfileItem(item) await updateProfile(itemId)
} catch (e) { } catch (e) {
await logger.warn(`[ProfileUpdater] Failed to update profile ${item.name}:`, e) await logger.warn(`[ProfileUpdater] Failed to update profile ${itemId}:`, e)
} }
}) })
} }