Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
fs: improve errors in watchFile and unwatchFile
- Check if the watcher is active in JS land before
  invoking the binding, act as a noop if the state of
  the watcher does not match the expectation. This
  avoids firing 'stop' when the watcher is already
  stopped.
- Update comments, validate more arguments and
  the type of the handle.
- Handle the errors from uv_fs_poll_start
- Create an `IsActive` helper method on StatWatcher
  • Loading branch information
joyeecheung committed Mar 14, 2018
commit 9a41128cc8a437763abde944120cf9a67c45cbb0
37 changes: 31 additions & 6 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1449,18 +1449,43 @@ util.inherits(StatWatcher, EventEmitter);

// FIXME(joyeecheung): this method is not documented.
// At the moment if filename is undefined, we
// 1. Throw an Error from C++ land if it's the first time .start() is called
// 2. Return silently from C++ land if .start() has already been called
// 1. Throw an Error if it's the first time .start() is called
// 2. Return silently if .start() has already been called
// on a valid filename and the wrap has been initialized
// This method is a noop if the watcher has already been started.
StatWatcher.prototype.start = function(filename, persistent, interval) {
lazyAssert()(this._handle instanceof binding.StatWatcher,
'handle must be a StatWatcher');
if (this._handle.isActive) {
return;
}

filename = getPathFromURL(filename);
nullCheck(filename, 'filename');
this._handle.start(pathModule.toNamespacedPath(filename),
persistent, interval);
validatePath(filename, 'filename');
validateUint32(interval, 'interval');
const err = this._handle.start(pathModule.toNamespacedPath(filename),
persistent, interval);
if (err) {
const error = errors.uvException({
errno: err,
syscall: 'watch',
path: filename
});
error.filename = filename;
throw error;
}
};


// FIXME(joyeecheung): this method is not documented while there is
// another documented fs.unwatchFile(). The counterpart in
// FSWatcher is .close()
// This method is a noop if the watcher has not been started.
StatWatcher.prototype.stop = function() {
lazyAssert()(this._handle instanceof binding.StatWatcher,
'handle must be a StatWatcher');
if (!this._handle.isActive) {
return;
}
this._handle.stop();
};

Expand Down
72 changes: 60 additions & 12 deletions src/node_stat_watcher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@
namespace node {

using v8::Context;
using v8::DontDelete;
using v8::DontEnum;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Integer;
using v8::Local;
using v8::Object;
using v8::PropertyAttribute;
using v8::ReadOnly;
using v8::Signature;
using v8::String;
using v8::Uint32;
using v8::Value;


Expand All @@ -53,6 +59,17 @@ void StatWatcher::Initialize(Environment* env, Local<Object> target) {
env->SetProtoMethod(t, "start", StatWatcher::Start);
env->SetProtoMethod(t, "stop", StatWatcher::Stop);

Local<FunctionTemplate> is_active_templ =
FunctionTemplate::New(env->isolate(),
IsActive,
env->as_external(),
Signature::New(env->isolate(), t));
t->PrototypeTemplate()->SetAccessorProperty(
FIXED_ONE_BYTE_STRING(env->isolate(), "isActive"),
is_active_templ,
Local<FunctionTemplate>(),
static_cast<PropertyAttribute>(ReadOnly | DontDelete | DontEnum));

target->Set(statWatcherString, t->GetFunction());
}

Expand All @@ -73,7 +90,9 @@ StatWatcher::StatWatcher(Environment* env, Local<Object> wrap)


StatWatcher::~StatWatcher() {
Stop();
if (IsActive()) {
Stop();
}
uv_close(reinterpret_cast<uv_handle_t*>(watcher_), Delete);
}

Expand Down Expand Up @@ -101,32 +120,63 @@ void StatWatcher::New(const FunctionCallbackInfo<Value>& args) {
new StatWatcher(env, args.This());
}

bool StatWatcher::IsActive() {
return uv_is_active(reinterpret_cast<uv_handle_t*>(watcher_)) != 0;
}

void StatWatcher::IsActive(const v8::FunctionCallbackInfo<v8::Value>& args) {
StatWatcher* wrap = Unwrap<StatWatcher>(args.This());
CHECK(wrap != nullptr);
args.GetReturnValue().Set(wrap->IsActive());
}

// wrap.start(filename, persistent, interval)
void StatWatcher::Start(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 3);

StatWatcher* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
StatWatcher* wrap = Unwrap<StatWatcher>(args.Holder());
CHECK_NE(wrap, nullptr);
if (wrap->IsActive()) {
return;
}

const int argc = args.Length();
CHECK_GE(argc, 3);

node::Utf8Value path(args.GetIsolate(), args[0]);
const bool persistent = args[1]->BooleanValue();
const uint32_t interval = args[2]->Uint32Value();
CHECK_NE(*path, nullptr);

bool persistent = true;
if (args[1]->IsFalse()) {
persistent = false;
}

CHECK(args[2]->IsUint32());
const uint32_t interval = args[2].As<Uint32>()->Value();

if (uv_is_active(reinterpret_cast<uv_handle_t*>(wrap->watcher_)))
return;
// Safe, uv_ref/uv_unref are idempotent.
if (persistent)
uv_ref(reinterpret_cast<uv_handle_t*>(wrap->watcher_));
else
uv_unref(reinterpret_cast<uv_handle_t*>(wrap->watcher_));
uv_fs_poll_start(wrap->watcher_, Callback, *path, interval);

// Note that uv_fs_poll_start does not return ENOENT, we are handling
// mostly memory errors here.
const int err = uv_fs_poll_start(wrap->watcher_, Callback, *path, interval);
if (err != 0) {
args.GetReturnValue().Set(err);
}
wrap->ClearWeak();
}


void StatWatcher::Stop(const FunctionCallbackInfo<Value>& args) {
StatWatcher* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
StatWatcher* wrap = Unwrap<StatWatcher>(args.Holder());
CHECK_NE(wrap, nullptr);
if (!wrap->IsActive()) {
return;
}

Environment* env = wrap->env();
Context::Scope context_scope(env->context());
wrap->MakeCallback(env->onstop_string(), 0, nullptr);
Expand All @@ -135,8 +185,6 @@ void StatWatcher::Stop(const FunctionCallbackInfo<Value>& args) {


void StatWatcher::Stop() {
if (!uv_is_active(reinterpret_cast<uv_handle_t*>(watcher_)))
return;
uv_fs_poll_stop(watcher_);
MakeWeak<StatWatcher>(this);
}
Expand Down
2 changes: 2 additions & 0 deletions src/node_stat_watcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class StatWatcher : public AsyncWrap {
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Start(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Stop(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsActive(const v8::FunctionCallbackInfo<v8::Value>& args);

size_t self_size() const override { return sizeof(*this); }

Expand All @@ -53,6 +54,7 @@ class StatWatcher : public AsyncWrap {
const uv_stat_t* prev,
const uv_stat_t* curr);
void Stop();
bool IsActive();

uv_fs_poll_t* watcher_;
};
Expand Down
7 changes: 6 additions & 1 deletion test/parallel/test-fs-watchfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,15 @@ const watcher =
assert(prev.ino <= 0);
// Stop watching the file
fs.unwatchFile(enoentFile);
watcher.stop(); // stopping a stopped watcher should be a noop
}
}, 2));

watcher.start(); // should not crash
// 'stop' should only be emitted once - stopping a stopped watcher should
// not trigger a 'stop' event.
watcher.on('stop', common.mustCall(function onStop() {}));

watcher.start(); // starting a started watcher should be a noop

// Watch events should callback with a filename on supported systems.
// Omitting AIX. It works but not reliably.
Expand Down
13 changes: 10 additions & 3 deletions test/sequential/test-fs-watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,20 @@ tmpdir.refresh();
code: 'ERR_ASSERTION'
});
oldhandle.close(); // clean up
}

assert.throws(function() {
const w = fs.watchFile(__filename, { persistent: false },
{
let oldhandle;
assert.throws(() => {
const w = fs.watchFile(__filename,
{ persistent: false },
common.mustNotCall());
oldhandle = w._handle;
w._handle = { stop: w._handle.stop };
w.stop();
}, /^TypeError: Illegal invocation$/);
}, {
message: 'handle must be a StatWatcher',
code: 'ERR_ASSERTION'
});
oldhandle.stop(); // clean up
}