Skip to content

Commit

Permalink
fs: improve errors in watchFile and unwatchFile
Browse files Browse the repository at this point in the history
- 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

PR-URL: #19345
Refs: #19089
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
joyeecheung committed Mar 18, 2018
1 parent 301f6cc commit 897f7b6
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 22 deletions.
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
}

0 comments on commit 897f7b6

Please sign in to comment.