fix: a plugin may not start at all if it has dependencies that didn't complete starting
Massimo Melina committed
Sep 27, 2023 at 14:20 UTC
2e1f5056ddc1ed5629f88eefc489434d39671583
3 files changed
+47
-41
src/api.plugins.ts
+8
-28
@@ -1,20 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import {
4
- AvailablePlugin,
5
- enablePlugins,
6
- getAvailablePlugins,
7
- getPluginConfigFields,
8
- mapPlugins,
9
- Plugin,
10
- pluginsConfig,
11
- PATH as PLUGINS_PATH,
12
- enablePlugin,
13
- getPluginInfo,
14
- setPluginConfig,
15
- findPluginByRepo,
16
- isPluginEnabled,
17
- isPluginRunning, stopPlugin, startPlugin, Repo,
4
+ AvailablePlugin, enablePlugins, getAvailablePlugins, getPluginConfigFields, mapPlugins, Plugin, pluginsConfig,
5
+ PATH as PLUGINS_PATH, enablePlugin, getPluginInfo, setPluginConfig, isPluginEnabled, isPluginRunning,
6
+ stopPlugin, startPlugin, CommonPluginInterface, getMissingDependencies,
7
} from './plugins'
8
import _ from 'lodash'
9
import assert from 'assert'
@@ -139,7 +128,7 @@ const apis: ApiHandlers = {
128
},
129
130
async download_plugin({ id, branch }) {
142
- await checkDependencies(id, branch)
131
+ await checkDependencies(await readOnlinePlugin(id, branch))
132
const res = await downloadPlugin(id, { branch })
133
if (typeof res !== 'string')
134
return res
@@ -151,7 +140,7 @@ const apis: ApiHandlers = {
140
const found = getPluginInfo(id)
141
if (!found)
142
return new ApiError(HTTP_NOT_FOUND)
154
- await checkDependencies(found.repo, )
143
+ await checkDependencies(found)
144
const enabled = isPluginEnabled(id)
145
await stopPlugin(id)
146
await downloadPlugin(found.repo, { overwrite: true })
@@ -178,17 +167,8 @@ function serialize(p: Readonly<Plugin> | AvailablePlugin) {
167
return _.defaults(o, { started: null, badApi: null }) // nulls should be used to be sure to overwrite previous values,
168
}
169
181
-async function checkDependencies(repo: Repo, branch?: string) {
182
- const rec = await readOnlinePlugin(repo, branch)
183
- const miss = rec?.depend?.map((dep: any) => {
184
- const res = findPluginByRepo(dep.repo)
185
- const error = !res ? 'missing'
186
- : (res.version || 0) < dep.version ? 'version'
187
- : !isPluginEnabled(res.id) ? 'disabled'
188
- : !isPluginRunning(res.id) ? 'stopped'
189
- : ''
190
- return error && { repo: dep.repo, error, id: res?.id }
191
- }).filter(Boolean)
192
- if (miss?.length)
170
+export async function checkDependencies(plugin: CommonPluginInterface) {
171
+ const miss = await getMissingDependencies(plugin)
172
+ if (miss.length)
173
throw new ApiError(HTTP_FAILED_DEPENDENCY, miss)
174
}
\ No newline at end of file
src/cross.ts
+2
-1
@@ -164,7 +164,8 @@ export function newObj<S extends (object | undefined | null),VR=any>(
164
return Object.fromEntries(onlyTruthy(pairs)) as S extends undefined | null ? S : { [K in keyof S]:VR }
165
}
166
167
-export async function waitFor<T>(cb: ()=> T, { interval=200, timeout=Infinity }={}) {
167
+// returns undefined if timeout is reached
168
+export async function waitFor<T>(cb: ()=> Promisable<T>, { interval=200, timeout=Infinity }={}) {
169
const started = Date.now()
170
while (1) {
171
const res = await cb()
src/plugins.ts
+37
-12
@@ -3,12 +3,12 @@
3
import glob from 'fast-glob'
4
import { watchLoad } from './watchLoad'
5
import _ from 'lodash'
6
-import { API_VERSION, APP_PATH, COMPATIBLE_API_VERSION, IS_WINDOWS, PLUGINS_PUB_URI } from './const'
6
+import { API_VERSION, APP_PATH, COMPATIBLE_API_VERSION, IS_WINDOWS, PLUGINS_PUB_URI, HTTP_FAILED_DEPENDENCY } from './const'
7
import * as Const from './const'
8
import Koa from 'koa'
9
import {
10
- adjustStaticPathForGlob, Callback, debounceAsync, Dict, getOrSet, onProcessExit,
11
- PendingPromise, pendingPromise, same, tryJson, wait, wantArray, watchDir
10
+ adjustStaticPathForGlob, Callback, debounceAsync, Dict, getOrSet, onlyTruthy, onProcessExit,
11
+ PendingPromise, pendingPromise, same, tryJson, wait, waitFor, wantArray, watchDir
12
} from './misc'
13
import { defineConfig, getConfig } from './config'
14
import { DirEntry } from './api.file_list'
@@ -151,7 +151,7 @@ function printError(id: string, e: any) {
151
interface OnDirEntryParams { entry:DirEntry, ctx:Koa.Context, node:VfsNode }
152
type OnDirEntry = (params:OnDirEntryParams) => void | false
153
154
-export class Plugin {
154
+export class Plugin implements CommonPluginInterface {
155
started: Date | null = new Date()
156
157
constructor(readonly id:string, readonly folder:string, private readonly data:any, private unwatch:()=>void){
@@ -170,9 +170,12 @@ export class Plugin {
170
}
171
plugins[id] = this
172
}
173
- get version(): undefined | number {
174
- return this.data?.version
175
- }
173
+ get version(): undefined | number { return this.data?.version }
174
+ get description(): undefined | string { return this.data?.description }
175
+ get apiRequired(): undefined | number | [number,number] { return this.data?.apiRequired }
176
+ get repo(): undefined | Repo { return this.data?.repo }
177
+ get depend(): undefined | Depend { return this.data?.depend }
178
+
179
get middleware(): undefined | PluginMiddleware {
180
return this.data?.middleware
181
}
@@ -238,13 +241,16 @@ type Stop = true
241
type CallMeAfter = ()=>any
242
243
export type Repo = string | { web?: string, main: string, zip?: string, zipRoot?: string }
241
-export interface AvailablePlugin {
244
+type Depend = { repo: string, version?: number }[]
245
+export interface CommonPluginInterface {
246
id: string
247
description?: string
248
version?: number
249
apiRequired?: number | [number,number]
250
repo?: Repo
247
- depend?: { repo: string, version?: number }[]
251
+ depend?: Depend
252
+}
253
+export interface AvailablePlugin extends CommonPluginInterface {
254
branch?: string
255
badApi?: string
256
error?: string
@@ -326,8 +332,11 @@ function watchPlugin(id: string, path: string) {
332
333
async function markItAvailable() {
334
delete plugins[id]
329
- const source = await readFile(module, 'utf8')
330
- availablePlugins[id] = parsePluginSource(id, source)
335
+ availablePlugins[id] = await parsePlugin()
336
+ }
337
+
338
+ async function parsePlugin() {
339
+ return parsePluginSource(id, await readFile(module, 'utf8'))
340
}
341
342
async function stop() {
@@ -343,6 +352,10 @@ function watchPlugin(id: string, path: string) {
352
if (starting) return
353
try {
354
starting = pendingPromise()
355
+ // if dependencies are not ready right now, we give some time. Not super-solid but good enough for now.
356
+ const info = await parsePlugin()
357
+ if (!await waitFor(async () => _.isEmpty(await getMissingDependencies(info)), { timeout: 5_000 }))
358
+ return console.debug("plugin missing dependencies", id)
359
if (getPluginInfo(id))
360
setError(id, '')
361
const alreadyRunning = plugins[id]
@@ -390,7 +403,7 @@ function watchPlugin(id: string, path: string) {
403
delete availablePlugins[id]
404
events.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin)
405
}
393
-
406
+ events.emit('pluginStarted:'+id)
407
} catch (e: any) {
408
await markItAvailable()
409
e = e.message || String(e)
@@ -465,3 +478,15 @@ function calculateBadApi(data: AvailablePlugin) {
478
: max! < COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS - check for updates"
479
: undefined
480
}
481
+
482
+export async function getMissingDependencies(plugin: CommonPluginInterface) {
483
+ return onlyTruthy((plugin?.depend || []).map((dep: any) => {
484
+ const res = findPluginByRepo(dep.repo)
485
+ const error = !res ? 'missing'
486
+ : (res.version || 0) < dep.version ? 'version'
487
+ : !isPluginEnabled(res.id) ? 'disabled'
488
+ : !isPluginRunning(res.id) ? 'stopped'
489
+ : ''
490
+ return error && { repo: dep.repo, error, id: res?.id }
491
+ }))
492
+}
\ No newline at end of file