fix: same-id plugin could be loading more than once at a time, causing "plugin error: unload first"

Massimo Melina committed Mar 27, 2023 at 10:28 UTC 544c93fa8172ee1743d7224f17d0ec8897a80cca
1 file changed +63 -59
src/plugins.ts
+63 -59
@@ -206,7 +206,7 @@ export const pluginsConfig = defineConfig('plugins_config', {} as Record<string,
206
207 export async function rescan() {
208 console.debug('scanning plugins')
209 - const found = []
209 + const found: string[] = []
210 const foundDisabled: typeof availablePlugins = {}
211 const MASK = PATH + '/*/plugin.js' // be sure to not use path.join as fast-glob doesn't work with \
212 const pluginSources = [MASK]
@@ -223,65 +223,10 @@ export async function rescan() {
223 catch {}
224 continue
225 }
226 - found.push(id)
227 - if (plugins[id]) // already loaded
226 + if (found.includes(id)) // not twice
227 continue
229 - const module = resolve(f)
230 - const { unwatch } = watchLoad(f, async () => {
231 - try {
232 - const alreadyRunning = plugins[id]
233 - console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id)
234 - const { init, ...data } = await import(module)
235 - delete data.default
236 - deleteModule(require.resolve(module)) // avoid caching at next import
237 - calculateBadApi(data)
238 - if (data.badApi)
239 - console.log("plugin", id, data.badApi)
240 -
241 - await alreadyRunning?.unload(true)
242 - console.debug("starting plugin", id)
243 - const res = await init?.call(null, {
244 - srcDir: __dirname,
245 - const: Const,
246 - require,
247 - getConnections,
248 - events,
249 - log(...args: any[]) {
250 - console.log('plugin', id, ':', ...args)
251 - },
252 - getConfig: (cfgKey: string) =>
253 - pluginsConfig.get()?.[id]?.[cfgKey] ?? data.config?.[cfgKey]?.defaultValue,
254 - setConfig: (cfgKey: string, value: any) =>
255 - setPluginConfig(id, { [cfgKey]: value }),
256 - subscribeConfig(cfgKey: string, cb: Callback<any>) {
257 - let last = this.getConfig(cfgKey)
258 - cb(last)
259 - return pluginsConfig.sub(() => {
260 - const now = this.getConfig(cfgKey)
261 - if (same(now, last)) return
262 - try { cb(last = now) }
263 - catch(e){
264 - console.log('plugin', id, String(e))
265 - }
266 - })
267 - },
268 - getHfsConfig: getConfig,
269 - })
270 - Object.assign(data, res)
271 - const plugin = new Plugin(id, dirname(module), data, unwatch)
272 - if (alreadyRunning)
273 - events.emit('pluginUpdated', Object.assign(_.pick(plugin, 'started'), getPluginInfo(id)))
274 - else {
275 - const wasInstalled = availablePlugins[id]
276 - if (wasInstalled)
277 - delete availablePlugins[id]
278 - events.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin)
279 - }
280 -
281 - } catch (e) {
282 - console.log("plugin error:", e)
283 - }
284 - })
228 + found.push(id)
229 + loadPlugin(id, f)
230 }
231 for (const [id,p] of Object.entries(foundDisabled)) {
232 const a = availablePlugins[id]
@@ -302,6 +247,65 @@ export async function rescan() {
247 await p.unload()
248 }
249
250 +function loadPlugin(id: string, path: string) {
251 + const module = resolve(path)
252 + const { unwatch } = watchLoad(path, async () => {
253 + try {
254 + const alreadyRunning = plugins[id]
255 + console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id)
256 + const { init, ...data } = await import(module)
257 + delete data.default
258 + deleteModule(require.resolve(module)) // avoid caching at next import
259 + calculateBadApi(data)
260 + if (data.badApi)
261 + console.log("plugin", id, data.badApi)
262 +
263 + await alreadyRunning?.unload(true)
264 + console.debug("starting plugin", id)
265 + const res = await init?.call(null, {
266 + srcDir: __dirname,
267 + const: Const,
268 + require,
269 + getConnections,
270 + events,
271 + log(...args: any[]) {
272 + console.log('plugin', id, ':', ...args)
273 + },
274 + getConfig: (cfgKey: string) =>
275 + pluginsConfig.get()?.[id]?.[cfgKey] ?? data.config?.[cfgKey]?.defaultValue,
276 + setConfig: (cfgKey: string, value: any) =>
277 + setPluginConfig(id, { [cfgKey]: value }),
278 + subscribeConfig(cfgKey: string, cb: Callback<any>) {
279 + let last = this.getConfig(cfgKey)
280 + cb(last)
281 + return pluginsConfig.sub(() => {
282 + const now = this.getConfig(cfgKey)
283 + if (same(now, last)) return
284 + try { cb(last = now) }
285 + catch(e){
286 + console.log('plugin', id, String(e))
287 + }
288 + })
289 + },
290 + getHfsConfig: getConfig,
291 + })
292 + Object.assign(data, res)
293 + const plugin = new Plugin(id, dirname(module), data, unwatch)
294 + if (alreadyRunning)
295 + events.emit('pluginUpdated', Object.assign(_.pick(plugin, 'started'), getPluginInfo(id)))
296 + else {
297 + const wasInstalled = availablePlugins[id]
298 + if (wasInstalled)
299 + delete availablePlugins[id]
300 + events.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin)
301 + }
302 +
303 + } catch (e) {
304 + console.log("plugin error:", e)
305 + }
306 + })
307 +}
308 +
309 function deleteModule(id: string) {
310 const { cache } = require
311 // build reversed map of dependencies