@samitouri / QOSami-HFS / commits / 2e1f5056

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