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