Skip to content

Commit

Permalink
fs: Expose uv_cancel() method on FSReq
Browse files Browse the repository at this point in the history
This adds a FSReqBase::Cancel() method which calls uv_cancel(). This
shows up in JS as a cancel() method on FSReqCallback and FSReqPromise.

Refs: nodejs#31971
  • Loading branch information
ptomato committed Aug 4, 2020
1 parent 66da592 commit 8236b52
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/node_file-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,17 @@ void FSReqPromise<AliasedBufferT>::Reject(v8::Local<v8::Value> reject) {
object()->Get(env()->context(),
env()->promise_string()).ToLocalChecked();
v8::Local<v8::Promise::Resolver> resolver = value.As<v8::Promise::Resolver>();
MaybeReplaceWithAbortError(&reject);
USE(resolver->Reject(env()->context(), reject).FromJust());
}

template <typename AliasedBufferT>
void FSReqPromise<AliasedBufferT>::Resolve(v8::Local<v8::Value> value) {
finished_ = true;
if (IsCanceled()) {
Reject(v8::Undefined(env()->isolate()));
return;
}
v8::HandleScope scope(env()->isolate());
InternalCallbackScope callback_scope(this);
v8::Local<v8::Value> val =
Expand Down
36 changes: 36 additions & 0 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,32 @@ FileHandleReadWrap::~FileHandleReadWrap() {}

FSReqBase::~FSReqBase() {}

void FSReqBase::Cancel(const v8::FunctionCallbackInfo<v8::Value>& args) {
FSReqBase* self;
ASSIGN_OR_RETURN_UNWRAP(&self, args.Holder());
self->ReqWrap::Cancel();
self->is_canceled_ = true;
}

void FSReqBase::MaybeReplaceWithAbortError(v8::Local<v8::Value>* reject) {
if (!is_canceled_) return;

Local<Value> exception;
Local<Function> domexception_ctor;
if (!GetDOMException(env()->context()).ToLocal(&domexception_ctor)) return;

Local<Value> argv[] = {
FIXED_ONE_BYTE_STRING(env()->isolate(), "Operation was cancelled."),
FIXED_ONE_BYTE_STRING(env()->isolate(), "AbortError"),
};
Local<Value> abort_error;
if (!domexception_ctor->NewInstance(env()->context(), arraysize(argv), argv)
.ToLocal(&abort_error))
return;

*reject = abort_error;
}

void FSReqBase::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("continuation_data", continuation_data_);
}
Expand Down Expand Up @@ -555,6 +581,7 @@ int FileHandle::DoShutdown(ShutdownWrap* req_wrap) {


void FSReqCallback::Reject(Local<Value> reject) {
MaybeReplaceWithAbortError(&reject);
MakeCallback(env()->oncomplete_string(), 1, &reject);
}

Expand All @@ -563,6 +590,13 @@ void FSReqCallback::ResolveStat(const uv_stat_t* stat) {
}

void FSReqCallback::Resolve(Local<Value> value) {
// If FSReqBase::Cancel() was called, then reject the request rather than
// resolving.
if (IsCanceled()) {
Reject(Undefined(env()->isolate()));
return;
}

Local<Value> argv[2] {
Null(env()->isolate()),
value
Expand Down Expand Up @@ -2462,6 +2496,7 @@ void Initialize(Local<Object> target,
fst->InstanceTemplate()->SetInternalFieldCount(
FSReqBase::kInternalFieldCount);
fst->Inherit(AsyncWrap::GetConstructorTemplate(env));
env->SetProtoMethod(fst, "cancel", FSReqBase::Cancel);
Local<String> wrapString =
FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback");
fst->SetClassName(wrapString);
Expand All @@ -2485,6 +2520,7 @@ void Initialize(Local<Object> target,
// Create Function Template for FSReqPromise
Local<FunctionTemplate> fpt = FunctionTemplate::New(isolate);
fpt->Inherit(AsyncWrap::GetConstructorTemplate(env));
env->SetProtoMethod(fpt, "cancel", FSReqBase::Cancel);
Local<String> promiseString =
FIXED_ONE_BYTE_STRING(isolate, "FSReqPromise");
fpt->SetClassName(promiseString);
Expand Down
8 changes: 8 additions & 0 deletions src/node_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class FSReqBase : public ReqWrap<uv_fs_t> {
virtual void SetReturnValue(
const v8::FunctionCallbackInfo<v8::Value>& args) = 0;

// JS-exposed method to cancel the UV request.
static void Cancel(const v8::FunctionCallbackInfo<v8::Value>& args);

const char* syscall() const { return syscall_; }
const char* data() const { return has_data_ ? *buffer_ : nullptr; }
enum encoding encoding() const { return encoding_; }
Expand All @@ -111,12 +114,17 @@ class FSReqBase : public ReqWrap<uv_fs_t> {

BindingData* binding_data() { return binding_data_.get(); }

protected:
bool IsCanceled() { return is_canceled_; }
void MaybeReplaceWithAbortError(v8::Local<v8::Value>* value);

private:
std::unique_ptr<FSContinuationData> continuation_data_;
enum encoding encoding_ = UTF8;
bool has_data_ = false;
bool use_bigint_ = false;
bool is_plain_open_ = false;
bool is_canceled_ = false;
const char* syscall_ = nullptr;

BaseObjectPtr<BindingData> binding_data_;
Expand Down
65 changes: 65 additions & 0 deletions test/parallel/test-fsreqcallback-cancel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';
// Flags: --no-warnings --expose-internals

const common = require('../common');
const assert = require('assert');
const { internalBinding } = require('internal/test/binding');
const path = require('path');

const { FSReqCallback, readdir } = internalBinding('fs');

{
// Return value of cancel() is undefined
const req = new FSReqCallback();
req.oncomplete = () => {};
assert.strictEqual(req.cancel(), undefined);
}


{
// Callback is called with AbortError if request is canceled
const req = new FSReqCallback();
req.oncomplete = common.mustCall((err, files) => {
assert.strictEqual(files, undefined);
assert.strictEqual(err.name, 'AbortError');
}, 1);
req.cancel();
readdir(path.toNamespacedPath('../'), 'utf8', false, req);
}


{
// Request is canceled if cancel() called before control returns to main loop
const req = new FSReqCallback();
req.oncomplete = common.mustCall((err, files) => {
assert.strictEqual(files, undefined);
assert.strictEqual(err.name, 'AbortError');
}, 1);
readdir(path.toNamespacedPath('../'), 'utf8', false, req);
req.cancel();
}


{
// Request is still canceled on next tick
const req = new FSReqCallback();
req.oncomplete = common.mustCall((err, files) => {
assert.strictEqual(files, undefined);
assert.strictEqual(err.name, 'AbortError');
}, 1);
readdir(path.toNamespacedPath('../'), 'utf8', false, req);
process.nextTick(common.mustCall(() => req.cancel()));
}


{
// Callback is not called a second time if request canceled after it has
// already completed
const req = new FSReqCallback();
req.oncomplete = common.mustCall((err, files) => {
assert.strictEqual(err, null);
assert.notStrictEqual(files, undefined);
req.cancel();
}, 1);
readdir(path.toNamespacedPath('../'), 'utf8', false, req);
}

0 comments on commit 8236b52

Please sign in to comment.