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") })