Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: move rmSync implementation to c++ #53617

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ let promises = null;
let ReadStream;
let WriteStream;
let rimraf;
let rimrafSync;
let kResistStopPropagation;

// These have to be separate because of how graceful-fs happens to do it's
Expand Down Expand Up @@ -1124,7 +1123,7 @@ function lazyLoadCp() {

function lazyLoadRimraf() {
if (rimraf === undefined)
({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
({ rimraf } = require('internal/fs/rimraf'));
}

/**
Expand Down Expand Up @@ -1192,8 +1191,7 @@ function rmdirSync(path, options) {
emitRecursiveRmdirWarning();
options = validateRmOptionsSync(path, { ...options, force: false }, true);
if (options !== false) {
lazyLoadRimraf();
return rimrafSync(path, options);
return binding.rmSync(path, options.maxRetries, options.recursive, options.retryDelay);
}
} else {
validateRmdirOptions(options);
Expand Down Expand Up @@ -1244,11 +1242,8 @@ function rm(path, options, callback) {
* @returns {void}
*/
function rmSync(path, options) {
lazyLoadRimraf();
return rimrafSync(
getValidatedPath(path),
validateRmOptionsSync(path, options, false),
);
const opts = validateRmOptionsSync(path, options, false);
return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay);
}

/**
Expand Down
145 changes: 2 additions & 143 deletions lib/internal/fs/rimraf.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,19 @@ const { Buffer } = require('buffer');
const fs = require('fs');
const {
chmod,
chmodSync,
lstat,
lstatSync,
readdir,
readdirSync,
rmdir,
rmdirSync,
stat,
statSync,
unlink,
unlinkSync,
} = fs;
const { sep } = require('path');
const { setTimeout } = require('timers');
const { sleep, isWindows } = require('internal/util');
const { isWindows } = require('internal/util');
const notEmptyErrorCodes = new SafeSet(['ENOTEMPTY', 'EEXIST', 'EPERM']);
const retryErrorCodes = new SafeSet(
['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
const epermHandler = isWindows ? fixWinEPERM : _rmdir;
const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
const readdirEncoding = 'buffer';
const separator = Buffer.from(sep);

Expand Down Expand Up @@ -172,138 +165,4 @@ function rimrafPromises(path, options) {
}


function rimrafSync(path, options) {
let stats;

try {
stats = lstatSync(path);
} catch (err) {
if (err.code === 'ENOENT')
return;

// Windows can EPERM on stat.
if (isWindows && err.code === 'EPERM')
fixWinEPERMSync(path, options, err);
}

try {
// SunOS lets the root user unlink directories.
if (stats?.isDirectory())
_rmdirSync(path, options, null);
else
_unlinkSync(path, options);
} catch (err) {
if (err.code === 'ENOENT')
return;
if (err.code === 'EPERM')
return epermHandlerSync(path, options, err);
if (err.code !== 'EISDIR')
throw err;

_rmdirSync(path, options, err);
}
}


function _unlinkSync(path, options) {
const tries = options.maxRetries + 1;

for (let i = 1; i <= tries; i++) {
try {
return unlinkSync(path);
} catch (err) {
// Only sleep if this is not the last try, and the delay is greater
// than zero, and an error was encountered that warrants a retry.
if (retryErrorCodes.has(err.code) &&
i < tries &&
options.retryDelay > 0) {
sleep(i * options.retryDelay);
} else if (err.code === 'ENOENT') {
// The file is already gone.
return;
} else if (i === tries) {
throw err;
}
}
}
}


function _rmdirSync(path, options, originalErr) {
try {
rmdirSync(path);
} catch (err) {
if (err.code === 'ENOENT')
return;
if (err.code === 'ENOTDIR') {
throw originalErr || err;
}

if (notEmptyErrorCodes.has(err.code)) {
// Removing failed. Try removing all children and then retrying the
// original removal. Windows has a habit of not closing handles promptly
// when files are deleted, resulting in spurious ENOTEMPTY failures. Work
// around that issue by retrying on Windows.
const pathBuf = Buffer.from(path);

ArrayPrototypeForEach(readdirSync(pathBuf, readdirEncoding), (child) => {
const childPath = Buffer.concat([pathBuf, separator, child]);

rimrafSync(childPath, options);
});

const tries = options.maxRetries + 1;

for (let i = 1; i <= tries; i++) {
try {
return fs.rmdirSync(path);
} catch (err) {
// Only sleep if this is not the last try, and the delay is greater
// than zero, and an error was encountered that warrants a retry.
if (retryErrorCodes.has(err.code) &&
i < tries &&
options.retryDelay > 0) {
sleep(i * options.retryDelay);
} else if (err.code === 'ENOENT') {
// The file is already gone.
return;
} else if (i === tries) {
throw err;
}
}
}
}

throw originalErr || err;
}
}


function fixWinEPERMSync(path, options, originalErr) {
try {
chmodSync(path, 0o666);
} catch (err) {
if (err.code === 'ENOENT')
return;

throw originalErr;
}

let stats;

try {
stats = statSync(path, { throwIfNoEntry: false });
} catch {
throw originalErr;
}

if (stats === undefined) return;

if (stats.isDirectory())
_rmdirSync(path, options, originalErr);
else
_unlinkSync(path, options);
}


module.exports = { rimraf, rimrafPromises, rimrafSync };
module.exports = { rimraf, rimrafPromises };
1 change: 1 addition & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_DLOPEN_FAILED, Error) \
V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
V(ERR_FS_EISDIR, Error) \
V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
V(ERR_INVALID_ADDRESS, Error) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
Expand Down
104 changes: 104 additions & 0 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
# include <io.h>
#endif

#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif

namespace node {

namespace fs {
Expand Down Expand Up @@ -1624,6 +1630,102 @@ static void RMDir(const FunctionCallbackInfo<Value>& args) {
}
}

static void RmSync(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

CHECK_EQ(args.Length(), 4); // path, maxRetries, recursive, retryDelay

BufferValue path(isolate, args[0]);
CHECK_NOT_NULL(*path);
ToNamespacedPath(env, &path);
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemWrite, path.ToStringView());
auto file_path = std::filesystem::path(path.ToStringView());
std::error_code error;
auto file_status = std::filesystem::status(file_path, error);

if (file_status.type() == std::filesystem::file_type::not_found) {
return;
}

int maxRetries = args[1].As<Int32>()->Value();
int recursive = args[2]->IsTrue();
int retryDelay = args[3].As<Int32>()->Value();

// File is a directory and recursive is false
if (file_status.type() == std::filesystem::file_type::directory &&
!recursive) {
return THROW_ERR_FS_EISDIR(
isolate, "Path is a directory: %s", file_path.c_str());
}

// Allowed errors are:
// - EBUSY: std::errc::device_or_resource_busy
// - EMFILE: std::errc::too_many_files_open
// - ENFILE: std::errc::too_many_files_open_in_system
// - ENOTEMPTY: std::errc::directory_not_empty
// - EPERM: std::errc::operation_not_permitted
auto can_omit_error = [](std::error_code error) -> bool {
return (error == std::errc::device_or_resource_busy ||
error == std::errc::too_many_files_open ||
error == std::errc::too_many_files_open_in_system ||
error == std::errc::directory_not_empty ||
error == std::errc::operation_not_permitted);
};

while (maxRetries >= 0) {
if (recursive) {
std::filesystem::remove_all(file_path, error);
} else {
std::filesystem::remove(file_path, error);
}

if (!error || error == std::errc::no_such_file_or_directory) {
return;
} else if (!can_omit_error(error)) {
break;
}

if (retryDelay != 0) {
#ifdef _WIN32
Sleep(retryDelay / 1000);
#else
sleep(retryDelay / 1000);
#endif
}
maxRetries--;
}

// This is required since std::filesystem::path::c_str() returns different
// values in Windows and Unix.
#ifdef _WIN32
auto file_ = file_path.string().c_str();
int permission_denied_error = EPERM;
#else
auto file_ = file_path.c_str();
int permission_denied_error = EACCES;
#endif // !_WIN32

if (error == std::errc::operation_not_permitted) {
std::string message = "Operation not permitted: " + file_path.string();
return env->ThrowErrnoException(EPERM, "rm", message.c_str(), file_);
} else if (error == std::errc::directory_not_empty) {
std::string message = "Directory not empty: " + file_path.string();
return env->ThrowErrnoException(EACCES, "rm", message.c_str(), file_);
} else if (error == std::errc::not_a_directory) {
std::string message = "Not a directory: " + file_path.string();
return env->ThrowErrnoException(ENOTDIR, "rm", message.c_str(), file_);
} else if (error == std::errc::permission_denied) {
std::string message = "Permission denied: " + file_path.string();
return env->ThrowErrnoException(
permission_denied_error, "rm", message.c_str(), file_);
}

std::string message = "Unknown error: " + error.message();
return env->ThrowErrnoException(UV_UNKNOWN, "rm", message.c_str(), file_);
}

int MKDirpSync(uv_loop_t* loop,
uv_fs_t* req,
const std::string& path,
Expand Down Expand Up @@ -3342,6 +3444,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethod(isolate, target, "rename", Rename);
SetMethod(isolate, target, "ftruncate", FTruncate);
SetMethod(isolate, target, "rmdir", RMDir);
SetMethod(isolate, target, "rmSync", RmSync);
SetMethod(isolate, target, "mkdir", MKDir);
SetMethod(isolate, target, "readdir", ReadDir);
SetFastMethod(isolate,
Expand Down Expand Up @@ -3468,6 +3571,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Rename);
registry->Register(FTruncate);
registry->Register(RMDir);
registry->Register(RmSync);
registry->Register(MKDir);
registry->Register(ReadDir);
registry->Register(InternalModuleStat);
Expand Down
25 changes: 0 additions & 25 deletions test/parallel/test-fs-rmdir-recursive.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,28 +217,3 @@ function removeAsync(dir) {
message: /^The value of "options\.maxRetries" is out of range\./
});
}

// It should not pass recursive option to rmdirSync, when called from
// rimraf (see: #35566)
{
// Make a non-empty directory:
const original = fs.rmdirSync;
const dir = `${nextDirPath()}/foo/bar`;
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(`${dir}/foo.txt`, 'hello world', 'utf8');

// When called the second time from rimraf, the recursive option should
// not be set for rmdirSync:
let callCount = 0;
let rmdirSyncOptionsFromRimraf;
fs.rmdirSync = (path, options) => {
if (callCount > 0) {
rmdirSyncOptionsFromRimraf = { ...options };
}
callCount++;
return original(path, options);
};
fs.rmdirSync(dir, { recursive: true });
fs.rmdirSync = original;
assert.strictEqual(rmdirSyncOptionsFromRimraf.recursive, undefined);
}
3 changes: 3 additions & 0 deletions typings/internalBinding/fs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ declare namespace InternalFSBinding {
function rmdir(path: string): void;
function rmdir(path: string, usePromises: typeof kUsePromises): Promise<void>;

function rmSync(path: StringOrBuffer, maxRetries: number, recursive: boolean, retryDelay: number): void;

function stat(path: StringOrBuffer, useBigint: boolean, req: FSReqCallback<Float64Array | BigUint64Array>): void;
function stat(path: StringOrBuffer, useBigint: true, req: FSReqCallback<BigUint64Array>): void;
function stat(path: StringOrBuffer, useBigint: false, req: FSReqCallback<Float64Array>): void;
Expand Down Expand Up @@ -279,6 +281,7 @@ export interface FsBinding {
realpath: typeof InternalFSBinding.realpath;
rename: typeof InternalFSBinding.rename;
rmdir: typeof InternalFSBinding.rmdir;
rmSync: typeof InternalFSBinding.rmSync;
stat: typeof InternalFSBinding.stat;
symlink: typeof InternalFSBinding.symlink;
unlink: typeof InternalFSBinding.unlink;
Expand Down
Loading