Skip to content

Commit

Permalink
eperm stuff
Browse files Browse the repository at this point in the history
eperm stuff
  • Loading branch information
lukekarrys committed Oct 11, 2024
1 parent d7b3cd4 commit ab88b5e
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 47 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,46 @@ name: CI
on: [push, pull_request]

jobs:
eperm:
runs-on: windows-latest
defaults:
run:
shell: bash

steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Use Nodejs 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x

- name: Install dependencies
run: npm install

- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async
- run: npm test -- -t0 test/integration/eperm.ts --disable-coverage --grep=. --grep=async

build:
strategy:
matrix:
Expand Down
32 changes: 32 additions & 0 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,35 @@ export const promises = {
lstat,
unlink,
}

// import fs, { Dirent } from 'fs'
// import fsPromises from 'fs/promises'

// export {
// chmodSync,
// mkdirSync,
// renameSync,
// rmdirSync,
// rmSync,
// statSync,
// lstatSync,
// unlinkSync,
// } from 'fs'

// export const readdirSync = (path: fs.PathLike): Dirent[] =>
// fs.readdirSync(path, { withFileTypes: true })

// const readdir = (path: fs.PathLike): Promise<Dirent[]> =>
// fsPromises.readdir(path, { withFileTypes: true })

// export const promises = {
// chmod: fsPromises.chmod,
// mkdir: fsPromises.mkdir,
// readdir,
// rename: fsPromises.rename,
// rm: fsPromises.rm,
// rmdir: fsPromises.rmdir,
// stat: fsPromises.stat,
// lstat: fsPromises.lstat,
// unlink: fsPromises.unlink,
// }
27 changes: 23 additions & 4 deletions src/retry-busy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ 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>(
fn: (path: string) => Promise<T>,
extraCodes?: Set<string>,
) => {
const method = async (
path: string,
opt: RimrafAsyncOptions,
Expand All @@ -24,9 +27,22 @@ export const retryBusy = <T>(fn: (path: string) => Promise<T>) => {
try {
return await fn(path)
} catch (er) {
if (isFsError(er) && er.path === path && codes.has(er.code)) {
if (
isFsError(er) &&
er.path === path &&
(codes.has(er.code) || extraCodes?.has(er.code))
) {
backoff = Math.ceil(backoff * rate)
total = backoff + total
if (er.code === 'EPERM') {
console.trace('EPERM retry busy', {
error: er,
total,
mbo,
retries,
backoff,
})
}
if (total < mbo) {
await setTimeout(backoff)
return method(path, opt, backoff, total)
Expand All @@ -45,7 +61,10 @@ export const retryBusy = <T>(fn: (path: string) => Promise<T>) => {
}

// just retries, no async so no backoff
export const retryBusySync = <T>(fn: (path: string) => T) => {
export const retryBusySync = <T>(
fn: (path: string) => T,
extraCodes?: Set<string>,
) => {
const method = (path: string, opt: RimrafOptions) => {
const max = opt.maxRetries || MAXRETRIES
let retries = 0
Expand All @@ -56,7 +75,7 @@ export const retryBusySync = <T>(fn: (path: string) => T) => {
if (
isFsError(er) &&
er.path === path &&
codes.has(er.code) &&
(codes.has(er.code) || extraCodes?.has(er.code)) &&
retries < max
) {
retries++
Expand Down
97 changes: 56 additions & 41 deletions src/rimraf-move-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,43 @@ 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'
const { lstat, rename, unlink, rmdir } = promises

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

const unlinkFixEPERM = fixEPERM(unlink)
const unlinkFixEPERMSync = fixEPERMSync(unlinkSync)
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)

export const rimrafMoveRemove = async (
path: string,
opt: RimrafAsyncOptions,
{ tmp, ...opt }: RimrafAsyncOptions,
) => {
opt?.signal?.throwIfAborted()

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

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

const rimrafMoveRemoveDir = async (
path: string,
opt: RimrafAsyncOptions,
tmp: string,
opt: Omit<RimrafAsyncOptions, 'tmp'>,
ent: Dirent | Stats,
): Promise<boolean> => {
opt?.signal?.throwIfAborted()
if (!opt.tmp) {
return rimrafMoveRemoveDir(
path,
{ ...opt, tmp: await defaultTmp(path) },
ent,
)
}
if (path === opt.tmp && parse(path).root !== path) {
throw new Error('cannot delete temp directory used for deletion')
}

const entries = ent.isDirectory() ? await readdirOrError(path) : null
if (!Array.isArray(entries)) {
Expand All @@ -66,6 +67,13 @@ const rimrafMoveRemoveDir = async (
if (errorCode(entries) === 'ENOENT') {
return true
}
if (errorCode(entries) === 'EPERM') {
// TODO: what to do here??
console.trace('EPERM reading dir uring move remove', {
error: entries,
})
throw entries
}
if (errorCode(entries) !== 'ENOTDIR') {
throw entries
}
Expand All @@ -74,14 +82,14 @@ const rimrafMoveRemoveDir = async (
if (opt.filter && !(await opt.filter(path, ent))) {
return false
}
await ignoreENOENT(tmpUnlink(path, opt.tmp, unlinkFixEPERM))
await ignoreENOENT(tmpUnlink(path, tmp, opt, unlinkFixEPERM))
return true
}

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

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

export const rimrafMoveRemoveSync = (path: string, opt: RimrafSyncOptions) => {
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, lstatSync(path)),
rimrafMoveRemoveDirSync(path, tmp, opt, lstatSync(path)),
) ?? true
)
}

const rimrafMoveRemoveDirSync = (
path: string,
opt: RimrafSyncOptions,
tmp: string,
opt: Omit<RimrafSyncOptions, 'tmp'>,
ent: Dirent | Stats,
): boolean => {
opt?.signal?.throwIfAborted()
if (!opt.tmp) {
return rimrafMoveRemoveDirSync(
path,
{ ...opt, tmp: defaultTmpSync(path) },
ent,
)
}
const tmp: string = opt.tmp

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

const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null
if (!Array.isArray(entries)) {
Expand All @@ -149,6 +156,13 @@ const rimrafMoveRemoveDirSync = (
if (errorCode(entries) === 'ENOENT') {
return true
}
if (errorCode(entries) === 'EPERM') {
// TODO: what to do here??
console.trace('EPERM reading dir uring move remove', {
error: entries,
})
throw entries
}
if (errorCode(entries) !== 'ENOTDIR') {
throw entries
}
Expand All @@ -157,14 +171,14 @@ const rimrafMoveRemoveDirSync = (
if (opt.filter && !opt.filter(path, ent)) {
return false
}
ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, unlinkFixEPERMSync))
ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, opt, unlinkFixEPERMSync))
return true
}

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

const tmpUnlinkSync = (
path: string,
tmp: string,
rmSync: (p: string) => void,
opt: RimrafSyncOptions,
rmSync: (p: string, opt: RimrafSyncOptions) => unknown,
) => {
const tmpFile = resolve(tmp, uniqueFilename(path))
renameSync(path, tmpFile)
return rmSync(tmpFile)
rmSync(tmpFile, opt)
}
4 changes: 2 additions & 2 deletions src/rimraf-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const rimrafWindowsDirMoveRemoveFallback = async (
await rimrafWindowsDirRetry(path, opt)
return true
} catch (er) {
if (errorCode(er) === 'ENOTEMPTY') {
if (errorCode(er) === 'ENOTEMPTY' || errorCode(er) === 'EPERM') {
return rimrafMoveRemove(path, opt)
}
throw er
Expand All @@ -55,7 +55,7 @@ const rimrafWindowsDirMoveRemoveFallbackSync = (
rimrafWindowsDirRetrySync(path, opt)
return true
} catch (er) {
if (errorCode(er) === 'ENOTEMPTY') {
if (errorCode(er) === 'ENOTEMPTY' || errorCode(er) === 'EPERM') {
return rimrafMoveRemoveSync(path, opt)
}
throw er
Expand Down
Loading

0 comments on commit ab88b5e

Please sign in to comment.