@samitouri / QOSami-HFS / commits / 6921e587

fix: admin/plugins: start not always performed

Massimo Melina committed Jun 3, 2023 at 15:33 UTC 6921e5876fe05e57a05ac518ab8c4f1acc776ecb
5 files changed +84 -22
admin/src/InstalledPlugins.ts
+7 -6
@@ -57,9 +57,10 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
57 icon: StopCircle,
58 title: h(Box, {}, `Stop ${id}`, h('br'), `Started ` + new Date(row.started as string).toLocaleString()),
59 color: 'success',
60 - onClick: () =>
61 - apiCall('set_plugin', { id, enabled: false }).then(() =>
62 - toast("Plugin stopped", h(StopCircle, { color: 'warning' })))
60 + async onClick() {
61 + await apiCall('stop_plugin', { id })
62 + toast("Plugin stopped", h(StopCircle, { color: 'warning' }))
63 + }
64 } : {
65 icon: PlayCircle,
66 title: `Start ${id}`,
@@ -159,7 +160,7 @@ export function UpdateButton({ id, then }: { id: string, then: (id:string)=>void
160 })
161 }
162
162 -export function startPlugin(id: string) {
163 - return apiCall('set_plugin', { id, enabled: true }).then(() =>
164 - toast("Plugin started", h(PlayCircle, { color: 'success' })))
163 +export async function startPlugin(id: string) {
164 + await apiCall('start_plugin', { id })
165 + toast("Plugin started", h(PlayCircle, { color: 'success' }))
166 }
src/api.plugins.ts
+20 -3
@@ -14,6 +14,7 @@ import {
14 setPluginConfig,
15 findPluginByRepo,
16 isPluginEnabled,
17 + isPluginRunning, stopPlugin, startPlugin,
18 } from './plugins'
19 import _ from 'lodash'
20 import assert from 'assert'
@@ -64,12 +65,27 @@ const apis: ApiHandlers = {
65 return list
66 },
67
68 + async start_plugin({ id }) {
69 + if (isPluginRunning(id))
70 + return { msg: 'already running' }
71 + await stopPlugin(id)
72 + await startPlugin(id)
73 + return {}
74 + },
75 +
76 + async stop_plugin({ id }) {
77 + if (!isPluginRunning(id))
78 + return { msg: 'already stopped' }
79 + await stopPlugin(id)
80 + return {}
81 + },
82 +
83 async set_plugin({ id, enabled, config }) {
84 assert(id, 'id')
85 if (config)
70 - setPluginConfig(id, config) // since we may wait the plugin to start, we save other changes first
86 + setPluginConfig(id, config)
87 if (enabled !== undefined)
72 - await enablePlugin(id, enabled)
88 + enablePlugin(id, enabled)
89 return {}
90 },
91
@@ -143,7 +159,8 @@ const apis: ApiHandlers = {
159 const enabled = isPluginEnabled(found.id)
160 await enablePlugin(found.id, false)
161 await downloadPlugin(pl.id, pl.branch, true)
146 - enablePlugin(found.id, enabled).then() // don't wait, in case it fails to start
162 + if (enabled)
163 + startPlugin(found.id).then() // don't wait, in case it fails to start
164 return {}
165 },
166
src/commands.ts
+3 -6
@@ -8,7 +8,7 @@ import { openAdmin } from './listen'
8 import yaml from 'yaml'
9 import { argv, BUILD_TIMESTAMP, VERSION } from './const'
10 import { createInterface } from 'readline'
11 -import { enablePlugin } from './plugins'
11 +import { startPlugin, stopPlugin } from './plugins'
12
13 if (!argv.updating)
14 try {
@@ -125,13 +125,10 @@ const commands = {
125 },
126 'start-plugin': {
127 params: '<name>',
128 - async cb(name: string) {
129 - await enablePlugin(name, false)
130 - await enablePlugin(name)
131 - }
128 + cb: startPlugin,
129 },
130 'stop-plugin': {
131 params: '<name>',
135 - cb: (name: string) => enablePlugin(name, false),
132 + cb: stopPlugin,
133 },
134 }
src/misc.ts
+1 -1
@@ -133,7 +133,7 @@ export function onlyTruthy<T>(arr: T[]) {
133 return arr.filter(truthy)
134 }
135
136 -type PendingPromise<T> = Promise<T> & { resolve: (value: T) => void, reject: (reason?: any) => void }
136 +export type PendingPromise<T=unknown> = Promise<T> & { resolve: (value?: T) => void, reject: (reason?: any) => void }
137 export function pendingPromise<T>() {
138 let takeOut
139 const ret = new Promise<T>((resolve, reject) =>
src/plugins.ts
+53 -6
@@ -13,6 +13,7 @@ import {
13 Dict,
14 getOrSet,
15 onProcessExit,
16 + PendingPromise, pendingPromise,
17 same,
18 tryJson,
19 wait,
@@ -44,15 +45,34 @@ export function isPluginEnabled(id: string) {
45 return enablePlugins.get().includes(id)
46 }
47
47 -export async function enablePlugin(id: string, state=true) {
48 +export function enablePlugin(id: string, state=true) {
49 + if (state && !getPluginInfo(id))
50 + throw Error('miss')
51 console.log("switching plugin", id, state ? "on" : "off")
52 enablePlugins.set( arr =>
53 arr.includes(id) === state ? arr
54 : state ? [...arr, id]
55 : arr.filter((x: string) => x !== id)
56 )
54 - while (isPluginRunning(id) !== state)
57 +}
58 +
59 +export async function stopPlugin(id: string) {
60 + enablePlugin(id, false)
61 + await waitRunning(id, false)
62 +}
63 +
64 +export async function startPlugin(id: string) {
65 + enablePlugin(id)
66 + await waitRunning(id)
67 +}
68 +
69 +async function waitRunning(id: string, state=true) {
70 + while (isPluginRunning(id) !== state) {
71 await wait(500)
72 + const error = getError(id)
73 + if (error)
74 + throw Error(error)
75 + }
76 }
77
78 // nullish values are equivalent to defaultValues
@@ -196,6 +216,7 @@ export interface AvailablePlugin {
216 repo?: string
217 branch?: string
218 badApi?: string
219 + error?: string
220 }
221
222 let availablePlugins: Record<string, AvailablePlugin> = {}
@@ -243,6 +264,7 @@ export async function rescan() {
264
265 function watchPlugin(id: string, path: string) {
266 const module = resolve(path)
267 + let starting: PendingPromise | undefined
268 enablePlugins.sub(() => { // we take care of enabled-state after it was loaded
269 if (!getPluginInfo(id)) return // not loaded yet
270 const enabled = isPluginEnabled(id)
@@ -266,18 +288,27 @@ function watchPlugin(id: string, path: string) {
288 })
289 return unwatch
290
291 + async function markItAvailable() {
292 + delete plugins[id]
293 + const source = await readFile(module, 'utf8')
294 + availablePlugins[id] = parsePluginSource(id, source)
295 + }
296 +
297 async function stop() {
298 + await starting
299 const p = plugins[id]
300 if (!p) return
301 await p.unload()
273 - delete plugins[id]
274 - const source = await readFile(module, 'utf8')
275 - availablePlugins[id] = parsePluginSource(id, source)
302 + await markItAvailable()
303 events.emit('pluginStopped', p)
304 }
305
306 async function start() {
307 + if (starting) return
308 try {
309 + starting = pendingPromise()
310 + if (getPluginInfo(id))
311 + setError(id, '')
312 const alreadyRunning = plugins[id]
313 console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id)
314 const { init, ...data } = await import(module)
@@ -335,12 +366,28 @@ function watchPlugin(id: string, path: string) {
366 }
367
368 } catch (e: any) {
338 - console.log("plugin error:", e)
369 + await markItAvailable()
370 + e = e.message || String(e)
371 + console.log(`plugin error: ${id}:`, e)
372 + setError(id, e)
373 + }
374 + finally {
375 + starting?.resolve()
376 + starting = undefined
377 }
378
379 }
380 }
381
382 +function getError(id: string) {
383 + return getPluginInfo(id).error
384 +}
385 +
386 +function setError(id: string, error: string) {
387 + getPluginInfo(id).error = error
388 + events.emit('pluginUpdated', { id, error })
389 +}
390 +
391 function deleteModule(id: string) {
392 const { cache } = require
393 if (!cache) // bun 0.6.2 doesn't have it