Skip to content

Commit

Permalink
fix: race condition in minimal mode close mf-5994 (#11304)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed Jan 23, 2024
1 parent 70ccc3a commit 4a175d6
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 79 deletions.
Expand Up @@ -19,8 +19,8 @@ import Services from '#services'
import { useMaskSharedTrans } from '../../../shared-ui/index.js'

function useDisabledPlugins() {
const activated = new Set(useActivatedPluginsSiteAdaptor('any').map((x) => x.ID))
const minimalMode = new Set(useActivatedPluginsSiteAdaptor(true).map((x) => x.ID))
const activated = new Set<string>(useActivatedPluginsSiteAdaptor('any').map((x) => x.ID))
const minimalMode = new Set<string>(useActivatedPluginsSiteAdaptor(true).map((x) => x.ID))
const disabledPlugins = useSubscription(registeredPlugins)
.filter((plugin) => !activated.has(plugin[0]) || minimalMode.has(plugin[0]))
.map((x) => x[1])
Expand Down
57 changes: 25 additions & 32 deletions packages/plugin-infra/src/manager/manage.ts
@@ -1,7 +1,7 @@
import { noop } from 'lodash-es'
import { timeout } from '@masknet/kit'
import { Emitter } from '@servie/events'
import { BooleanPreference, ObservableSet, type PluginID } from '@masknet/shared-base'
import { BooleanPreference, ValueRefWithReady } from '@masknet/shared-base'
import { type Plugin } from '../types.js'
import { getPluginDefine, onNewPluginRegistered, registeredPlugins } from './store.js'

Expand All @@ -21,18 +21,10 @@ export function createManager<
instance: T
controller: AbortController
context: Context
minimalModeEnabled: ValueRefWithReady<boolean>
}
const resolved = new Map<PluginID, T>()
const activated = new Map<PluginID, ActivatedPluginInstance>()
const minimalModePluginIDs = (() => {
const value = new ObservableSet<string>()
value.event.on('add', (id) => id.forEach((id) => events.emit('minimalModeChanged', id, true)))
value.event.on('delete', (id) => events.emit('minimalModeChanged', id, false))
value.clear = () => {
throw new TypeError('[@masknet/plugin-infra] Cannot clear minimal mode plugin IDs')
}
return value
})()
const resolved = new Map<string, T>()
const activated = new Map<string, ActivatedPluginInstance>()
let _host: Plugin.__Host.Host<T, Omit<Context, ManagedContext>> = undefined!
const events = new Emitter<{
activateChanged: [id: string, enabled: boolean]
Expand All @@ -43,7 +35,6 @@ export function createManager<
configureHostHooks: (host: Plugin.__Host.Host<T, Omit<Context, ManagedContext>>) => (_host = host),
activatePlugin,
stopPlugin,
isMinimalMode,
isActivated,
startDaemon,
activated: {
Expand All @@ -54,14 +45,21 @@ export function createManager<
},
} as Iterable<T>,
},
minimalMode: {
[Symbol.iterator]: () => minimalModePluginIDs.values(),
} as Iterable<string>,
minimalMode: new Proxy(
{},
{
get(_, pluginID) {
if (typeof pluginID === 'symbol') return undefined
if (activated.has(pluginID)) return activated.get(pluginID)!.minimalModeEnabled
return undefined
},
},
) as Partial<Record<string, ValueRefWithReady<boolean>>>,
events,
}

async function updateCompositedMinimalMode(id: string) {
const definition = await __getDefinition(id as PluginID)
const definition = await __getDefinition(id)
if (!definition) return

const settings = await _host.minimalMode.isEnabled(id)
Expand All @@ -71,12 +69,13 @@ export function createManager<
// plugin default minimal mode is false
else result = !!definition.inMinimalModeByDefault

result ? minimalModePluginIDs.add(id) : minimalModePluginIDs.delete(id)
const instance = activated.get(id)
if (instance) instance.minimalModeEnabled.value = result
}

function startDaemon(
host: Plugin.__Host.Host<T, Omit<Context, ManagedContext>>,
extraCheck?: (id: PluginID) => boolean,
extraCheck?: (id: string) => boolean,
) {
_host = host
const { signal = new AbortController().signal, addI18NResource, minimalMode } = _host
Expand Down Expand Up @@ -110,7 +109,7 @@ export function createManager<
}
}

async function meetRequirement(id: PluginID) {
async function meetRequirement(id: string) {
const define = getPluginDefine(id)
if (!define) return false

Expand All @@ -126,7 +125,7 @@ export function createManager<
)
}

async function activatePlugin(id: PluginID) {
async function activatePlugin(id: string) {
if (activated.has(id)) return
const definition = await __getDefinition(id)
if (!definition) return
Expand All @@ -144,13 +143,11 @@ export function createManager<
const activatedPlugin: ActivatedPluginInstance = {
instance: definition,
controller: abort,
// Type 'Pick<Context, ManagedContext> & Omit<Context, ManagedContext>' is not assignable to type 'Context'. but it does.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
context: {
...getManagedContext(id, abort.signal),
..._host.createContext(id, definition, abort.signal),
},
} as any,
minimalModeEnabled: new ValueRefWithReady(),
}
activated.set(id, activatedPlugin)
if (definition.init) {
Expand All @@ -163,23 +160,19 @@ export function createManager<
events.emit('activateChanged', id, true)
}

function stopPlugin(id: PluginID) {
function stopPlugin(id: string) {
const instance = activated.get(id)
if (!instance) return
instance.controller.abort()
activated.delete(id)
events.emit('activateChanged', id, false)
}

function isActivated(id: PluginID) {
function isActivated(id: string) {
return activated.has(id)
}

function isMinimalMode(id: PluginID) {
return minimalModePluginIDs.has(id)
}

async function __getDefinition(id: PluginID) {
async function __getDefinition(id: string) {
if (resolved.has(id)) return resolved.get(id)!

const deferredDefinition = getPluginDefine(id)
Expand Down
97 changes: 57 additions & 40 deletions packages/plugin-infra/src/manager/site-adaptor.ts
@@ -1,47 +1,66 @@
import { useMemo } from 'react'
import { isEqual } from 'lodash-es'
import { unreachable } from '@masknet/kit'
import { useValueRef } from '@masknet/shared-base-ui'
import { type EnhanceableSite, ValueRefWithReady } from '@masknet/shared-base'
import { type EnhanceableSite, ValueRef, ValueRefWithReady } from '@masknet/shared-base'
import { createManager } from './manage.js'
import { getPluginDefine } from './store.js'
import type { Plugin } from '../types.js'

const { events, activated, startDaemon, minimalMode } = createManager(
(def) => def.SiteAdaptor,
createManager.NoManagedContext,
)
const activatedSub = new ValueRefWithReady<Plugin.SiteAdaptor.Definition[]>([], isEqual)
events.on('activateChanged', () => (activatedSub.value = [...activated.plugins]))
const {
events,
activated,
startDaemon,
minimalMode: minimalModeSub,
} = createManager((def) => def.SiteAdaptor, createManager.NoManagedContext)

const minimalModeSub = new ValueRefWithReady<string[]>([], isEqual)
events.on('minimalModeChanged', () => (minimalModeSub.value = [...minimalMode]))
const ActivatedPluginsSiteAdaptorAny = new ValueRefWithReady<Plugin.SiteAdaptor.Definition[]>([])
const ActivatedPluginsSiteAdaptorTrue = new ValueRefWithReady<Plugin.SiteAdaptor.Definition[]>([])
const ActivatedPluginsSiteAdaptorFalse = new ValueRefWithReady<Plugin.SiteAdaptor.Definition[]>([])

{
const update = () => {
ActivatedPluginsSiteAdaptorTrue.value = query(true)
ActivatedPluginsSiteAdaptorFalse.value = query(false)
}
events.on('activateChanged', () => {
ActivatedPluginsSiteAdaptorAny.value = [...activated.plugins]
})
events.on('activateChanged', update)
events.on('minimalModeChanged', update)

function query(minimalModeEqualsTo: boolean): Plugin.SiteAdaptor.Definition[] {
const result = [...activated.plugins]
if (minimalModeEqualsTo === true) return result.filter((x) => minimalModeSub[x.ID]?.value)
else if (minimalModeEqualsTo === false) return result.filter((x) => !minimalModeSub[x.ID]?.value)
return result
}
}

export function useActivatedPluginsSiteAdaptor(minimalModeEqualsTo: 'any' | boolean) {
const minimalMode = useValueRef(minimalModeSub)
const result = useValueRef(activatedSub)
return useMemo(() => {
if (minimalModeEqualsTo === 'any') return result
else if (minimalModeEqualsTo === true) return result.filter((x) => minimalMode.includes(x.ID))
else if (minimalModeEqualsTo === false) return result.filter((x) => !minimalMode.includes(x.ID))
unreachable(minimalModeEqualsTo)
}, [result, minimalMode, minimalModeEqualsTo])
return useValueRef(
minimalModeEqualsTo === 'any' ? ActivatedPluginsSiteAdaptorAny
: minimalModeEqualsTo === true ? ActivatedPluginsSiteAdaptorTrue
: minimalModeEqualsTo === false ? ActivatedPluginsSiteAdaptorFalse
: unreachable(minimalModeEqualsTo),
)
}
useActivatedPluginsSiteAdaptor.visibility = {
useMinimalMode: useActivatedPluginsSiteAdaptor.bind(null, true),
useNotMinimalMode: useActivatedPluginsSiteAdaptor.bind(null, false),
useAnyMode: useActivatedPluginsSiteAdaptor.bind(null, 'any'),
useMinimalMode: () => useValueRef(ActivatedPluginsSiteAdaptorTrue),
useNotMinimalMode: () => useValueRef(ActivatedPluginsSiteAdaptorFalse),
useAnyMode: () => useValueRef(ActivatedPluginsSiteAdaptorAny),
}

// this should never be used for a normal plugin
const TRUE = new ValueRef(true)
export function useIsMinimalMode(pluginID: string) {
return useValueRef(minimalModeSub).includes(pluginID)
return useValueRef(minimalModeSub[pluginID] || TRUE)
}

export async function checkIsMinimalMode(pluginID: string) {
await minimalModeSub.readyPromise
return minimalModeSub.value.includes(pluginID)
const sub = minimalModeSub[pluginID]
if (!sub) return true
await sub.readyPromise
return sub.value
}

/**
*
* @param pluginID Get the plugin ID
Expand All @@ -50,21 +69,19 @@ export async function checkIsMinimalMode(pluginID: string) {
*/
export function useActivatedPluginSiteAdaptor(pluginID: string, minimalModeEqualsTo: 'any' | boolean) {
const plugins = useActivatedPluginsSiteAdaptor(minimalModeEqualsTo)
const minimalMode = useValueRef(minimalModeSub)
const minimalMode = useIsMinimalMode(pluginID)

return useMemo(() => {
const result = plugins.find((x) => x.ID === pluginID)
if (!result) return result
if (minimalModeEqualsTo === 'any') return result
else if (minimalModeEqualsTo === true) {
if (minimalMode.includes(result.ID)) return result
return undefined
} else if (minimalModeEqualsTo === false) {
if (minimalMode.includes(result.ID)) return undefined
return result
}
unreachable(minimalModeEqualsTo)
}, [pluginID, plugins, minimalMode, minimalModeEqualsTo])
const result = plugins.find((x) => x.ID === pluginID)
if (!result) return undefined
if (minimalModeEqualsTo === 'any') return result
else if (minimalModeEqualsTo === true) {
if (minimalMode) return result
return undefined
} else if (minimalModeEqualsTo === false) {
if (minimalMode) return undefined
return result
}
unreachable(minimalModeEqualsTo)
}
useActivatedPluginSiteAdaptor.visibility = {
useMinimalMode: (pluginID: string) => useActivatedPluginSiteAdaptor(pluginID, true),
Expand Down
9 changes: 4 additions & 5 deletions packages/plugin-infra/src/manager/store.ts
@@ -1,10 +1,9 @@
// DO NOT import React in this file. This file is also used by worker.
import type { Subscription } from 'use-subscription'
import { env, type BuildInfoFile } from '@masknet/flags'
import type { PluginID, NetworkPluginID } from '@masknet/shared-base'
import type { Plugin } from '../types.js'

const __registered = new Map<PluginID, Plugin.DeferredDefinition>()
const __registered = new Map<string, Plugin.DeferredDefinition>()
const listeners = new Set<onNewPluginRegisteredListener>()

type onNewPluginRegisteredListener = (id: string, def: Plugin.DeferredDefinition) => void
Expand All @@ -13,7 +12,7 @@ export function onNewPluginRegistered(f: onNewPluginRegisteredListener) {
return () => listeners.delete(f)
}

export const registeredPlugins: Subscription<Array<[PluginID, Plugin.DeferredDefinition]>> = (() => {
export const registeredPlugins: Subscription<Array<[string, Plugin.DeferredDefinition]>> = (() => {
let value: any[] | undefined
onNewPluginRegistered(() => (value = undefined))
return {
Expand All @@ -26,8 +25,8 @@ export const registeredPlugins: Subscription<Array<[PluginID, Plugin.DeferredDef
}
})()

export function getPluginDefine(id: PluginID | NetworkPluginID) {
return __registered.get(id as unknown as PluginID)
export function getPluginDefine(id: string) {
return __registered.get(id)
}

export function registerPlugin(def: Plugin.DeferredDefinition) {
Expand Down

0 comments on commit 4a175d6

Please sign in to comment.