@samitouri / QOSami-HFS / commits / a67ef784

try to wait a bit on EPERM problems while updating plugins, especially on Windows

Massimo Melina committed Oct 23, 2025 at 13:43 UTC a67ef784592ff548415983d29e266ac0bd5a94b5
2 files changed +23 -11
src/cross.ts
+10
@@ -537,6 +537,16 @@ export function patchKey(o: any, k: string, replacer: (was: unknown) => unknown)
537 return o
538 }
539
540 +// consider the callback successful if it returns a truthy value
541 +export async function retry(cb: () => Promise<any>, delay=1000) {
542 + let retry = 3
543 + while (true) {
544 + if (await cb()) break
545 + if (! retry--) break
546 + await wait(delay)
547 + }
548 +}
549 +
550 export type Mutable<T> = { -readonly [K in keyof T]: T[K] }
551 export function toMutable<T>(value: readonly T[]): T[]
552 export function toMutable<T extends object>(value: T): Mutable<T>
src/github.ts
+13 -11
@@ -2,7 +2,7 @@
2
3 import events from './events'
4 import {
5 - httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray, wait, popKey, onlyTruthy, HOUR, DAY
5 + httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray, retry, popKey, onlyTruthy, HOUR, DAY
6 } from './misc'
7 import {
8 DISABLING_SUFFIX, enablePlugin, findPluginByRepo, getInactivePlugins, getPluginInfo, isPluginRunning, mapPlugins,
@@ -83,7 +83,7 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
83
84 async function go(url: string, folder: string, zipRoot: string) {
85 const installPath = PLUGINS_PATH + '/' + folder
86 - await access(installPath, fs.constants.W_OK) // early check for permission: access if it exists, mkdir if it doesn't
86 + await access(installPath, fs.constants.W_OK) // early check for permission: access if it exists, mkdir+rmdir if it doesn't
87 .catch(() => mkdir(installPath, { recursive: true }).then(() => rmdir(installPath)))
88 const tempInstallPath = installPath + '-installing' + DISABLING_SUFFIX
89 const foldersToCopy = [ // from longer to shorter, so we first test the longer
@@ -120,16 +120,18 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
120 const wasRunning = isPluginRunning(folder)
121 if (wasRunning)
122 await stopPlugin(folder) // stop old
123 - let retry = 3
124 - while (retry--) { // move data, and consider late release of the resource, up to a few seconds
125 - const res = rename(join(installPath, STORAGE_FOLDER), join(tempInstallPath, STORAGE_FOLDER))
126 - if (await res.then(() => true, e => e.code === 'ENOENT')) break
127 - await wait(1000)
128 - }
129 - // delete old folder, but it may fail in the presence of .node files, so we rename it first as a precaution (clearing require.cache doesn't help)
123 + // move data, and consider late release of the resource, up to a few seconds
124 + await retry(() => rename(join(installPath, STORAGE_FOLDER), join(tempInstallPath, STORAGE_FOLDER))
125 + .then(() => 1, e => e.code === 'ENOENT'))
126 + // delete old folder (if any), but it may fail in the presence of .node files, so we rename it first as a precaution (clearing require.cache doesn't help). Especially on Windows, it may be impossible to delete dll files until our process is terminated (in which case, retrying is useless).
127 const deleteMe = installPath + DELETE_ME_SUFFIX
131 - await rename(installPath, deleteMe).catch(() => {})
132 - await rm(deleteMe, { recursive: true, force: true }).catch(e => console.warn(String(e)))
128 + await retry(() => rename(installPath, deleteMe).then(() => 1, (e: any) => {
129 + if (e.code === 'ENOENT') return 1 // nothing to do
130 + console.warn("error renaming old plugin folder:", String(e))
131 + }))
132 + await retry(() => rm(deleteMe, { recursive: true, force: true /*ignore ENOENT*/ }).then(() => 1, e => {
133 + console.warn("error deleting old plugin folder:", String(e))
134 + }))
135 // final replace
136 await rename(tempInstallPath, installPath)
137 .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_NOT_ACCEPTABLE, "missing main file") })