Skip to content

Commit

Permalink
idk
Browse files Browse the repository at this point in the history
  • Loading branch information
lukekarrys committed Oct 11, 2024
1 parent b9d1509 commit 26eebea
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 100 deletions.
23 changes: 9 additions & 14 deletions src/retry-busy.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
// note: max backoff is the maximum that any *single* backoff will do

import { setTimeout } from 'timers/promises'
import { RimrafAsyncOptions, RimrafOptions } from './index.js'
import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js'
import { isFsError } from './error.js'

export const MAXBACKOFF = 200
export const RATE = 1.2
export const MAXRETRIES = 10
export const codes = new Set(['EMFILE', 'ENFILE', 'EBUSY'])

export const retryBusy = <T>(
fn: (path: string) => Promise<T>,
export const retryBusy = <T, U extends RimrafAsyncOptions>(
fn: (path: string, opt: U) => Promise<T>,
extraCodes?: Set<string>,
) => {
const method = async (
path: string,
opt: RimrafAsyncOptions,
backoff = 1,
total = 0,
) => {
const method = async (path: string, opt: U, backoff = 1, total = 0) => {
const mbo = opt.maxBackoff || MAXBACKOFF
const rate = opt.backoff || RATE
const max = opt.maxRetries || MAXRETRIES
let retries = 0
while (true) {
try {
return await fn(path)
return await fn(path, opt)
} catch (er) {
if (
isFsError(er) &&
Expand Down Expand Up @@ -66,16 +61,16 @@ export const retryBusy = <T>(
}

// just retries, no async so no backoff
export const retryBusySync = <T>(
fn: (path: string) => T,
export const retryBusySync = <T, U extends RimrafSyncOptions>(
fn: (path: string, opt: U) => T,
extraCodes?: Set<string>,
) => {
const method = (path: string, opt: RimrafOptions) => {
const method = (path: string, opt: U) => {
const max = opt.maxRetries || MAXRETRIES
let retries = 0
while (true) {
try {
return fn(path)
return fn(path, opt)
} catch (er) {
if (
isFsError(er) &&
Expand Down
142 changes: 72 additions & 70 deletions src/rimraf-move-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,57 @@ import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js'
import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js'
import { fixEPERM, fixEPERMSync } from './fix-eperm.js'
import { errorCode } from './error.js'
import { retryBusy, retryBusySync } from './retry-busy.js'
import * as retry from './retry-busy.js'
const { lstat, rename, unlink, rmdir } = promises

type RimrafAsyncOptionsTmp = RimrafAsyncOptions & { tmp: string }
type RimrafSyncOptionsTmp = RimrafSyncOptions & { tmp: string }

// crypto.randomBytes is much slower, and Math.random() is enough here
const uniqueFilename = (path: string) => `.${basename(path)}.${Math.random()}`

const retryCodes = new Set(['EPERM'])
const unlinkFixEPERM = retryBusy(fixEPERM(unlink), retryCodes)
const unlinkFixEPERMSync = retryBusySync(fixEPERMSync(unlinkSync), retryCodes)
const rmdirFixEPERM = retryBusy(fixEPERM(rmdir), retryCodes)
const rmdirFixEPERMSync = retryBusySync(fixEPERMSync(rmdirSync), retryCodes)

// always retry EPERM errors on windows
/* c8 ignore next */
const retryCodes = process.platform === 'win32' ? new Set(['EPERM']) : undefined
const retryBusy = <T, U extends RimrafAsyncOptions>(
fn: (path: string, opt: U) => Promise<T>,
) => retry.retryBusy(fn, retryCodes)
const retryBusySync = <T, U extends RimrafSyncOptions>(
fn: (path: string, opt: U) => T,
) => retry.retryBusySync(fn, retryCodes)

const unlinkFixEPERM = retryBusy(fixEPERM(unlink))
const unlinkFixEPERMSync = retryBusySync(fixEPERMSync(unlinkSync))
const rmdirFixEPERM = retryBusy(fixEPERM(rmdir))
const rmdirFixEPERMSync = retryBusySync(fixEPERMSync(rmdirSync))
const retryReaddirOrError = retryBusy(readdirOrError)
const retryReaddirOrErrorSync = retryBusySync(readdirOrErrorSync)
const retryRename = retryBusy(async (path, opt: RimrafAsyncOptionsTmp) => {
const tmpFile = resolve(opt.tmp, uniqueFilename(path))
await rename(path, tmpFile)
return tmpFile
})
const retryRenameSync = retryBusySync((path, opt: RimrafSyncOptionsTmp) => {
const tmpFile = resolve(opt.tmp, uniqueFilename(path))
renameSync(path, tmpFile)
return tmpFile
})

const tmpUnlink = async (
path: string,
opt: RimrafAsyncOptionsTmp,
rm: (p: string, opt: RimrafAsyncOptionsTmp) => Promise<void>,
) => {
await rm(await retryRename(path, opt), opt)
}

type RimrafAsyncOptionsNoTmp = Omit<RimrafAsyncOptions, 'tmp'>
type RimrafSyncOptionsNoTmp = Omit<RimrafSyncOptions, 'tmp'>
const tmpUnlinkSync = (
path: string,
opt: RimrafSyncOptionsTmp,
rmSync: (p: string, opt: RimrafSyncOptionsTmp) => void,
) => {
rmSync(retryRenameSync(path, opt), opt)
}

export const rimrafMoveRemove = async (
path: string,
Expand All @@ -51,15 +85,34 @@ export const rimrafMoveRemove = async (

return (
(await ignoreENOENT(
lstat(path).then(stat => rimrafMoveRemoveDir(path, tmp, opt, stat)),
lstat(path).then(stat =>
rimrafMoveRemoveDir(path, { ...opt, tmp }, stat),
),
)) ?? true
)
}

export const rimrafMoveRemoveSync = (
path: string,
{ tmp, ...opt }: RimrafSyncOptions,
) => {
opt?.signal?.throwIfAborted()

tmp ??= defaultTmpSync(path)
if (path === tmp && parse(path).root !== path) {
throw new Error('cannot delete temp directory used for deletion')
}

return (
ignoreENOENTSync(() =>
rimrafMoveRemoveDirSync(path, { ...opt, tmp }, lstatSync(path)),
) ?? true
)
}

const rimrafMoveRemoveDir = async (
path: string,
tmp: string,
opt: RimrafAsyncOptionsNoTmp,
opt: RimrafAsyncOptionsTmp,
ent: Dirent | Stats,
): Promise<boolean> => {
opt?.signal?.throwIfAborted()
Expand All @@ -74,11 +127,6 @@ const rimrafMoveRemoveDir = async (
if (errorCode(entries) === 'ENOENT') {
return true
}
if (errorCode(entries) === 'EPERM') {
// TODO: what to do here??
console.trace('EPERM', entries)
throw entries
}
if (errorCode(entries) !== 'ENOTDIR') {
throw entries
}
Expand All @@ -87,14 +135,14 @@ const rimrafMoveRemoveDir = async (
if (opt.filter && !(await opt.filter(path, ent))) {
return false
}
await ignoreENOENT(tmpUnlink(path, tmp, opt, unlinkFixEPERM))
await ignoreENOENT(tmpUnlink(path, opt, unlinkFixEPERM))
return true
}

const removedAll = (
await Promise.all(
entries.map(ent =>
rimrafMoveRemoveDir(resolve(path, ent.name), tmp, opt, ent),
rimrafMoveRemoveDir(resolve(path, ent.name), opt, ent),
),
)
).every(v => v === true)
Expand All @@ -111,43 +159,13 @@ const rimrafMoveRemoveDir = async (
if (opt.filter && !(await opt.filter(path, ent))) {
return false
}
await ignoreENOENT(tmpUnlink(path, tmp, opt, rmdirFixEPERM))
await ignoreENOENT(tmpUnlink(path, opt, rmdirFixEPERM))
return true
}

const tmpUnlink = async (
path: string,
tmp: string,
opt: RimrafAsyncOptionsNoTmp,
rm: (p: string, opt: RimrafAsyncOptionsNoTmp) => Promise<void>,
) => {
const tmpFile = resolve(tmp, uniqueFilename(path))
await rename(path, tmpFile)
return await rm(tmpFile, opt)
}

export const rimrafMoveRemoveSync = (
path: string,
{ tmp, ...opt }: RimrafSyncOptions,
) => {
opt?.signal?.throwIfAborted()

tmp ??= defaultTmpSync(path)
if (path === tmp && parse(path).root !== path) {
throw new Error('cannot delete temp directory used for deletion')
}

return (
ignoreENOENTSync(() =>
rimrafMoveRemoveDirSync(path, tmp, opt, lstatSync(path)),
) ?? true
)
}

const rimrafMoveRemoveDirSync = (
path: string,
tmp: string,
opt: RimrafSyncOptionsNoTmp,
opt: RimrafSyncOptionsTmp,
ent: Dirent | Stats,
): boolean => {
opt?.signal?.throwIfAborted()
Expand All @@ -161,11 +179,6 @@ const rimrafMoveRemoveDirSync = (
if (errorCode(entries) === 'ENOENT') {
return true
}
if (errorCode(entries) === 'EPERM') {
// TODO: what to do here??
console.trace('EPERM', entries)
throw entries
}
if (errorCode(entries) !== 'ENOTDIR') {
throw entries
}
Expand All @@ -174,14 +187,14 @@ const rimrafMoveRemoveDirSync = (
if (opt.filter && !opt.filter(path, ent)) {
return false
}
ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, opt, unlinkFixEPERMSync))
ignoreENOENTSync(() => tmpUnlinkSync(path, opt, unlinkFixEPERMSync))
return true
}

let removedAll = true
for (const ent of entries) {
const p = resolve(path, ent.name)
removedAll = rimrafMoveRemoveDirSync(p, tmp, opt, ent) && removedAll
removedAll = rimrafMoveRemoveDirSync(p, opt, ent) && removedAll
}
if (!removedAll) {
return false
Expand All @@ -192,17 +205,6 @@ const rimrafMoveRemoveDirSync = (
if (opt.filter && !opt.filter(path, ent)) {
return false
}
ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, opt, rmdirFixEPERMSync))
ignoreENOENTSync(() => tmpUnlinkSync(path, opt, rmdirFixEPERMSync))
return true
}

const tmpUnlinkSync = (
path: string,
tmp: string,
opt: RimrafSyncOptionsNoTmp,
rmSync: (p: string, opt: RimrafSyncOptionsNoTmp) => void,
) => {
const tmpFile = resolve(tmp, uniqueFilename(path))
renameSync(path, tmpFile)
return rmSync(tmpFile, opt)
}
21 changes: 19 additions & 2 deletions src/rimraf-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@ const createFallback =
errorCode(er) === 'ENOTEMPTY' ||
(errorCode(er) === 'EPERM' && !errorCode(errorCause(er)))
) {
/* c8 ignore start */
if (errorCode(er) === 'EPERM') {
console.trace('EPERM', er)
}
/* c8 ignore stop */
// already filtered, remove from options so we don't call unnecessarily
return rimrafMoveRemove(path, { ...opt, filter: undefined })
try {
return rimrafMoveRemove(path, { ...opt, filter: undefined })
/* c8 ignore start */
} catch (e2) {
console.trace(e2)
throw e2
}
/* c8 ignore stop */
}
throw er
}
Expand All @@ -52,11 +61,19 @@ const createFallbackSync =
errorCode(er) === 'ENOTEMPTY' ||
(errorCode(er) === 'EPERM' && !errorCode(errorCause(er)))
) {
/* c8 ignore start */
if (errorCode(er) === 'EPERM') {
console.trace('EPERM', er)
}
/* c8 ignore stop */
// already filtered, remove from options so we don't call unnecessarily
return rimrafMoveRemoveSync(path, { ...opt, filter: undefined })
try {
return rimrafMoveRemoveSync(path, { ...opt, filter: undefined })
} catch (e2) {
console.trace(e2)
throw e2
}
/* c8 ignore stop */
}
throw er
}
Expand Down
4 changes: 2 additions & 2 deletions src/use-native.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RimrafAsyncOptions, RimrafOptions } from './index.js'
import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js'

/* c8 ignore next */
const [major = 0, minor = 0] = process.version
Expand All @@ -13,7 +13,7 @@ export const useNative: (opt?: RimrafAsyncOptions) => boolean =
!hasNative || process.platform === 'win32' ?
() => false
: opt => !opt?.signal && !opt?.filter
export const useNativeSync: (opt?: RimrafOptions) => boolean =
export const useNativeSync: (opt?: RimrafSyncOptions) => boolean =
!hasNative || process.platform === 'win32' ?
() => false
: opt => !opt?.signal && !opt?.filter
Loading

0 comments on commit 26eebea

Please sign in to comment.