fix: issue with plugin folder not containing plugin.js
Massimo Melina committed
Jun 2, 2023 at 11:24 UTC
73989fe0b9582bf72a322592cf589c3db70c466a
3 files changed
+84
-65
src/api.plugins.ts
+7
-3
@@ -13,15 +13,16 @@ import {
13
getPluginInfo,
14
setPluginConfig,
15
findPluginByRepo,
16
- isPluginEnabled
16
+ isPluginEnabled,
17
} from './plugins'
18
import _ from 'lodash'
19
import assert from 'assert'
20
-import { Callback, newObj, onOff } from './misc'
20
+import { Callback, newObj, onOff, waitFor } from './misc'
21
import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
22
import events from './events'
23
import { rm } from 'fs/promises'
24
import { downloadPlugin, getFolder2repo, getRepoInfo, readOnlinePlugin, searchPlugins } from './github'
25
+import { HTTP_SERVER_ERROR } from './const'
26
27
const apis: ApiHandlers = {
28
@@ -129,7 +130,10 @@ const apis: ApiHandlers = {
130
131
async download_plugin(pl) {
132
const res = await downloadPlugin(pl.id, pl.branch)
132
- return typeof res === 'string' ? getPluginInfo(res) : res
133
+ if (typeof res !== 'string')
134
+ return res
135
+ return (await waitFor(() => getPluginInfo(res), { timeout: 5000 }))
136
+ || new ApiError(HTTP_SERVER_ERROR)
137
},
138
139
async update_plugin(pl) {
src/github.ts
+12
-8
@@ -3,17 +3,17 @@
3
import events from './events'
4
import { httpsString, httpsStream, unzip } from './misc'
5
import {
6
+ DISABLING_POSTFIX,
7
getAvailablePlugins,
8
mapPlugins,
9
parsePluginSource,
10
PATH as PLUGINS_PATH,
10
- pluginsWatcher,
11
- rescan,
11
} from './plugins'
12
import { ApiError } from './apiMiddleware'
13
import _ from 'lodash'
14
import { DAY, HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT } from './const'
16
-import { rm } from 'fs/promises'
15
+import { rename, rm } from 'fs/promises'
16
+import { join } from 'path'
17
18
const DIST_ROOT = 'dist'
19
@@ -31,6 +31,7 @@ function downloadProgress(id: string, status: DownloadStatus) {
31
export async function downloadPlugin(repo: string, branch='', overwrite?: boolean) {
32
if (downloading[repo])
33
return new ApiError(HTTP_CONFLICT, "already downloading")
34
+ console.log('downloading plugin', repo)
35
downloadProgress(repo, true)
36
try {
37
const rec = await getRepoInfo(repo)
@@ -51,17 +52,20 @@ export async function downloadPlugin(repo: string, branch='', overwrite?: boolea
52
rootWithinZip + '-' + process.platform,
53
rootWithinZip,
54
].map(x => x + '/')
54
- pluginsWatcher.pause()
55
// this zip doesn't have content-length, so we cannot produce progress event
56
const stream = await httpsStream(`https://github.com/${repo}/archive/refs/heads/${branch}.zip`)
57
+ const MAIN = 'plugin.js'
58
await unzip(stream, async path => {
59
const folder = foldersToCopy.find(x => path.startsWith(x))
60
if (!folder || path.endsWith('/')) return false
60
- const dest = installPath + '/' + path.slice(folder.length)
61
- return rm(dest).then(() => dest, () => false)
61
+ let dest = path.slice(folder.length)
62
+ if (dest === MAIN) // avoid being possibly loaded before the download is complete
63
+ dest += DISABLING_POSTFIX
64
+ dest = join(installPath, dest)
65
+ return rm(dest, { force: true }).then(() => dest, () => false)
66
})
63
- pluginsWatcher.unpause()
64
- await rescan() // workaround: for some reason, operations are not triggering the rescan of the watched folder. Let's invoke it.
67
+ const main = join(installPath, MAIN)
68
+ await rename(main + DISABLING_POSTFIX, main) // we are good now, restore name
69
return folder
70
}
71
finally {
src/plugins.ts
+65
-54
@@ -27,7 +27,7 @@ import events from './events'
27
import { mkdir, readFile } from 'fs/promises'
28
import { existsSync, mkdirSync } from 'fs'
29
import { getConnections } from './connections'
30
-import { dirname, resolve } from 'path'
30
+import { dirname, join, resolve } from 'path'
31
import { newCustomHtmlState, watchLoadCustomHtml } from './customHtml'
32
33
export const PATH = 'plugins'
@@ -122,7 +122,7 @@ export function pluginsMiddleware(): Koa.Middleware {
122
}
123
124
function printError(id: string, e: any) {
125
- console.log('error middleware plugin', id, String(e))
125
+ console.log(`error middleware plugin ${id}: ${e?.message || e}`)
126
console.debug(e)
127
}
128
}
@@ -132,13 +132,10 @@ interface OnDirEntryParams { entry:DirEntry, ctx:Koa.Context, node:VfsNode }
132
type OnDirEntry = (params:OnDirEntryParams) => void | false
133
134
export class Plugin {
135
- started = new Date()
135
+ started: Date | null = new Date()
136
137
constructor(readonly id:string, readonly folder:string, private readonly data:any, private unwatch:()=>void){
138
if (!data) throw 'invalid data'
139
- if (plugins[id])
140
- throw "unload first: " + id
141
- plugins[id] = this
139
140
this.data = data = { ...data } // clone to make object modifiable. Objects coming from import are not.
141
// some validation
@@ -170,6 +167,8 @@ export class Plugin {
167
}
168
169
async unload(reloading=false) {
170
+ if (!this.started) return
171
+ this.started = null
172
const { id } = this
173
try {
174
await this.data?.unload?.()
@@ -179,13 +178,9 @@ export class Plugin {
178
catch(e) {
179
console.log('error unloading plugin', id, String(e))
180
}
182
- delete plugins[id]
183
- if (reloading) return
181
+ if (this.data)
182
+ this.data.unload = undefined
183
this.unwatch()
185
- if (availablePlugins[id])
186
- events.emit('pluginStopped', availablePlugins[id])
187
- else
188
- events.emit('pluginUninstalled', id)
184
}
185
}
186
@@ -220,53 +215,68 @@ enablePlugins.sub(rescanAsap)
215
216
export const pluginsConfig = defineConfig('plugins_config', {} as Record<string,any>)
217
218
+const pluginWatchers = new Map<string, ReturnType<typeof watchPlugin>>()
219
+
220
export async function rescan() {
221
console.debug('scanning plugins')
225
- const found: string[] = []
226
- const foundDisabled: typeof availablePlugins = {}
227
- const MASK = PATH + '/*/plugin.js' // be sure to not use path.join as fast-glob doesn't work with \
228
- const pluginSources = [MASK]
222
+ const patterns = [PATH + '/*']
223
if (APP_PATH !== process.cwd())
230
- pluginSources.push(adjustStaticPathForGlob(APP_PATH) + '/' + MASK)
231
- for (const f of await glob(pluginSources)) {
232
- const id = f.split('/').slice(-2)[0]!
233
- if (id.endsWith(DISABLING_POSTFIX)) continue
234
- if (!enablePlugins.get().includes(id)) {
235
- try {
236
- const source = await readFile(f, 'utf8')
237
- foundDisabled[id] = parsePluginSource(id, source)
238
- }
239
- catch {}
240
- continue
241
- }
242
- if (found.includes(id)) // not twice
243
- continue
244
- found.push(id)
245
- if (!plugins[id]) // already loaded
246
- loadPlugin(id, f)
224
+ patterns.push(adjustStaticPathForGlob(APP_PATH) + '/' + patterns[0])
225
+ const met = []
226
+ for (const { path, dirent } of await glob(patterns, { onlyFiles: false, suppressErrors: true, objectMode: true })) {
227
+ if (!dirent.isDirectory() || path.endsWith(DISABLING_POSTFIX)) continue
228
+ const id = path.split('/').slice(-1)[0]!
229
+ met.push(id)
230
+ const w = pluginWatchers.get(id)
231
+ if (w) continue
232
+ console.debug('plugin watch', id)
233
+ pluginWatchers.set(id, watchPlugin(id, join(path, 'plugin.js')))
234
}
248
- for (const [id,p] of Object.entries(foundDisabled)) {
249
- const a = availablePlugins[id]
250
- if (same(a, p)) continue
251
- availablePlugins[id] = p
252
- if (a)
253
- events.emit('pluginUpdated', p)
254
- else if (!plugins[id])
255
- events.emit('pluginInstalled', p)
256
- }
257
- for (const id in availablePlugins)
258
- if (!foundDisabled[id] && !found.includes(id) && !plugins[id]) {
259
- delete availablePlugins[id]
260
- events.emit('pluginUninstalled', id)
235
+ for (const [id, cancelWatcher] of pluginWatchers.entries())
236
+ if (!met.includes(id)) {
237
+ enablePlugin(id, false)
238
+ console.debug('plugin unwatch', id)
239
+ cancelWatcher()
240
+ pluginWatchers.delete(id)
241
}
262
- for (const [id,p] of Object.entries(plugins))
263
- if (!found.includes(id))
264
- await p.unload()
242
}
243
267
-function loadPlugin(id: string, path: string) {
244
+function watchPlugin(id: string, path: string) {
245
const module = resolve(path)
269
- const { unwatch } = watchLoad(path, async () => {
246
+ enablePlugins.sub(() => { // we take care of enabled-state after it was loaded
247
+ if (!getPluginInfo(id)) return // not loaded yet
248
+ const enabled = isPluginEnabled(id)
249
+ if (enabled !== isPluginRunning(id))
250
+ return enabled ? start() : stop()
251
+ })
252
+ const { unwatch } = watchLoad(module, async (source) => {
253
+ const notRunning = availablePlugins[id]
254
+ if (!source) {
255
+ await stop()
256
+ delete availablePlugins[id]
257
+ events.emit('pluginUninstalled', id)
258
+ return
259
+ }
260
+ if (isPluginEnabled(id))
261
+ return start()
262
+ const p = parsePluginSource(id, source)
263
+ if (same(notRunning, p)) return
264
+ availablePlugins[id] = p
265
+ events.emit(notRunning ? 'pluginUpdated' : 'pluginInstalled', p)
266
+ })
267
+ return unwatch
268
+
269
+ async function stop() {
270
+ const p = plugins[id]
271
+ if (!p) return
272
+ await p.unload()
273
+ delete plugins[id]
274
+ const source = await readFile(module, 'utf8')
275
+ availablePlugins[id] = parsePluginSource(id, source)
276
+ events.emit('pluginStopped', p)
277
+ }
278
+
279
+ async function start() {
280
try {
281
const alreadyRunning = plugins[id]
282
console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id)
@@ -314,7 +324,7 @@ function loadPlugin(id: string, path: string) {
324
customHtml: newCustomHtmlState()
325
})
326
const customHtmlWatcher = watchLoadCustomHtml(data.customHtml, folder)
317
- const plugin = new Plugin(id, folder, data, _.flow(unwatch, customHtmlWatcher.unwatch))
327
+ const plugin = plugins[id] = new Plugin(id, folder, data, customHtmlWatcher.unwatch)
328
if (alreadyRunning)
329
events.emit('pluginUpdated', Object.assign(_.pick(plugin, 'started'), getPluginInfo(id)))
330
else {
@@ -324,10 +334,11 @@ function loadPlugin(id: string, path: string) {
334
events.emit(wasInstalled ? 'pluginStarted' : 'pluginInstalled', plugin)
335
}
336
327
- } catch (e) {
337
+ } catch (e: any) {
338
console.log("plugin error:", e)
339
}
330
- })
340
+
341
+ }
342
}
343
344
function deleteModule(id: string) {