-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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: improve errors thrown from fs.watch() #19089
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,13 +77,19 @@ Object.defineProperty(exports, 'constants', { | |
value: constants | ||
}); | ||
|
||
let assert_ = null; | ||
function lazyAssert() { | ||
if (assert_ === null) { | ||
assert_ = require('assert'); | ||
} | ||
return assert_; | ||
} | ||
|
||
const kMinPoolSpace = 128; | ||
const { kMaxLength } = require('buffer'); | ||
|
||
const isWindows = process.platform === 'win32'; | ||
|
||
const errnoException = errors.errnoException; | ||
|
||
let truncateWarn = true; | ||
|
||
function showTruncateDeprecation() { | ||
|
@@ -1312,11 +1318,17 @@ function FSWatcher() { | |
this._handle.owner = this; | ||
|
||
this._handle.onchange = function(status, eventType, filename) { | ||
// TODO(joyeecheung): we may check self._handle.initialized here | ||
// and return if that is false. This allows us to avoid firing the event | ||
// after the handle is closed, and to fire both UV_RENAME and UV_CHANGE | ||
// if they are set by libuv at the same time. | ||
if (status < 0) { | ||
self._handle.close(); | ||
const error = !filename ? | ||
errnoException(status, 'Error watching file for changes:') : | ||
errnoException(status, `Error watching file ${filename} for changes:`); | ||
const error = errors.uvException({ | ||
errno: status, | ||
syscall: 'watch', | ||
path: filename | ||
}); | ||
error.filename = filename; | ||
self.emit('error', error); | ||
} else { | ||
|
@@ -1335,21 +1347,34 @@ FSWatcher.prototype.start = function(filename, | |
persistent, | ||
recursive, | ||
encoding) { | ||
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since very recently we have the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @BridgeAR Not unless There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well... it is a semver-major PR. But this is also just a suggestion. |
||
if (this._handle.initialized) { | ||
throw new errors.Error('ERR_FS_WATCHER_ALREADY_STARTED'); | ||
} | ||
|
||
filename = getPathFromURL(filename); | ||
nullCheck(filename, 'filename'); | ||
validatePath(filename, 'filename'); | ||
|
||
var err = this._handle.start(pathModule.toNamespacedPath(filename), | ||
persistent, | ||
recursive, | ||
encoding); | ||
if (err) { | ||
this._handle.close(); | ||
const error = errnoException(err, `watch ${filename}`); | ||
const error = errors.uvException({ | ||
errno: err, | ||
syscall: 'watch', | ||
path: filename | ||
}); | ||
error.filename = filename; | ||
throw error; | ||
} | ||
}; | ||
|
||
FSWatcher.prototype.close = function() { | ||
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent'); | ||
if (!this._handle.initialized) { | ||
throw new errors.Error('ERR_FS_WATCHER_NOT_STARTED'); | ||
} | ||
this._handle.close(); | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,13 +31,18 @@ | |
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::MaybeLocal; | ||
using v8::Object; | ||
using v8::PropertyAttribute; | ||
using v8::ReadOnly; | ||
using v8::Signature; | ||
using v8::String; | ||
using v8::Value; | ||
|
||
|
@@ -51,7 +56,7 @@ class FSEventWrap: public HandleWrap { | |
static void New(const FunctionCallbackInfo<Value>& args); | ||
static void Start(const FunctionCallbackInfo<Value>& args); | ||
static void Close(const FunctionCallbackInfo<Value>& args); | ||
|
||
static void GetInitialized(const FunctionCallbackInfo<Value>& args); | ||
size_t self_size() const override { return sizeof(*this); } | ||
|
||
private: | ||
|
@@ -80,6 +85,11 @@ FSEventWrap::~FSEventWrap() { | |
CHECK_EQ(initialized_, false); | ||
} | ||
|
||
void FSEventWrap::GetInitialized(const FunctionCallbackInfo<Value>& args) { | ||
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.This()); | ||
CHECK(wrap != nullptr); | ||
args.GetReturnValue().Set(wrap->initialized_); | ||
} | ||
|
||
void FSEventWrap::Initialize(Local<Object> target, | ||
Local<Value> unused, | ||
|
@@ -95,6 +105,18 @@ void FSEventWrap::Initialize(Local<Object> target, | |
env->SetProtoMethod(t, "start", Start); | ||
env->SetProtoMethod(t, "close", Close); | ||
|
||
Local<FunctionTemplate> get_initialized_templ = | ||
FunctionTemplate::New(env->isolate(), | ||
GetInitialized, | ||
env->as_external(), | ||
Signature::New(env->isolate(), t)); | ||
|
||
t->PrototypeTemplate()->SetAccessorProperty( | ||
FIXED_ONE_BYTE_STRING(env->isolate(), "initialized"), | ||
get_initialized_templ, | ||
Local<FunctionTemplate>(), | ||
static_cast<PropertyAttribute>(ReadOnly | DontDelete | v8::DontEnum)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to make it an accessor instead of a method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bnoordhuis No strong reasons other than this way you can't assign it to something else or wrap it...probably plays more nicely with inspector? |
||
|
||
target->Set(fsevent_string, t->GetFunction()); | ||
} | ||
|
||
|
@@ -105,22 +127,19 @@ void FSEventWrap::New(const FunctionCallbackInfo<Value>& args) { | |
new FSEventWrap(env, args.This()); | ||
} | ||
|
||
|
||
// wrap.start(filename, persistent, recursive, encoding) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this line is commented out. Can you remove it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mcollina It's the signature of this method...probably less obvious than I thought There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is nice to have the signature. I am not sure what the convention for this is in C++ though. Something similar to JSDoc might be nicer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is more about the positions, the types are pretty obvious given the assertions below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not have a strong opinion... hm... maybe change |
||
void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) { | ||
Environment* env = Environment::GetCurrent(args); | ||
|
||
FSEventWrap* wrap; | ||
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); | ||
if (wrap->initialized_) | ||
return args.GetReturnValue().Set(0); | ||
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.Holder()); | ||
CHECK_NE(wrap, nullptr); | ||
CHECK(!wrap->initialized_); | ||
|
||
static const char kErrMsg[] = "filename must be a string or Buffer"; | ||
if (args.Length() < 1) | ||
return env->ThrowTypeError(kErrMsg); | ||
const int argc = args.Length(); | ||
CHECK_GE(argc, 4); | ||
|
||
BufferValue path(env->isolate(), args[0]); | ||
if (*path == nullptr) | ||
return env->ThrowTypeError(kErrMsg); | ||
CHECK_NE(*path, nullptr); | ||
|
||
unsigned int flags = 0; | ||
if (args[2]->IsTrue()) | ||
|
@@ -129,19 +148,21 @@ void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) { | |
wrap->encoding_ = ParseEncoding(env->isolate(), args[3], kDefaultEncoding); | ||
|
||
int err = uv_fs_event_init(wrap->env()->event_loop(), &wrap->handle_); | ||
if (err == 0) { | ||
wrap->initialized_ = true; | ||
if (err != 0) { | ||
return args.GetReturnValue().Set(err); | ||
} | ||
|
||
err = uv_fs_event_start(&wrap->handle_, OnEvent, *path, flags); | ||
err = uv_fs_event_start(&wrap->handle_, OnEvent, *path, flags); | ||
wrap->initialized_ = true; | ||
|
||
if (err == 0) { | ||
// Check for persistent argument | ||
if (!args[1]->IsTrue()) { | ||
uv_unref(reinterpret_cast<uv_handle_t*>(&wrap->handle_)); | ||
} | ||
} else { | ||
FSEventWrap::Close(args); | ||
} | ||
if (err != 0) { | ||
FSEventWrap::Close(args); | ||
return args.GetReturnValue().Set(err); | ||
} | ||
|
||
// Check for persistent argument | ||
if (!args[1]->IsTrue()) { | ||
uv_unref(reinterpret_cast<uv_handle_t*>(&wrap->handle_)); | ||
} | ||
|
||
args.GetReturnValue().Set(err); | ||
|
@@ -209,13 +230,11 @@ void FSEventWrap::OnEvent(uv_fs_event_t* handle, const char* filename, | |
|
||
|
||
void FSEventWrap::Close(const FunctionCallbackInfo<Value>& args) { | ||
FSEventWrap* wrap; | ||
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); | ||
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.Holder()); | ||
CHECK_NE(wrap, nullptr); | ||
CHECK(wrap->initialized_); | ||
|
||
if (wrap == nullptr || wrap->initialized_ == false) | ||
return; | ||
wrap->initialized_ = false; | ||
|
||
HandleWrap::Close(args); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,64 @@ | ||
'use strict'; | ||
|
||
// This verifies the error thrown by fs.watch. | ||
|
||
const common = require('../common'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const tmpdir = require('../common/tmpdir'); | ||
const path = require('path'); | ||
const nonexistentFile = path.join(tmpdir.path, 'non-existent'); | ||
const uv = process.binding('uv'); | ||
|
||
tmpdir.refresh(); | ||
|
||
{ | ||
const validateError = (err) => { | ||
assert.strictEqual(nonexistentFile, err.path); | ||
assert.strictEqual(nonexistentFile, err.filename); | ||
assert.strictEqual(err.syscall, 'watch'); | ||
if (err.code === 'ENOENT') { | ||
assert.strictEqual( | ||
err.message, | ||
`ENOENT: no such file or directory, watch '${nonexistentFile}'`); | ||
assert.strictEqual(err.errno, uv.UV_ENOENT); | ||
assert.strictEqual(err.code, 'ENOENT'); | ||
} else { // AIX | ||
assert.strictEqual( | ||
err.message, | ||
`ENODEV: no such device, watch '${nonexistentFile}'`); | ||
assert.strictEqual(err.errno, uv.UV_ENODEV); | ||
assert.strictEqual(err.code, 'ENODEV'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nodejs/platform-aix is this expected behaviour? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. --- a/test/parallel/test-fs-watch-enoent.js
+++ b/test/parallel/test-fs-watch-enoent.js
@@ -6,6 +6,7 @@ const fs = require('fs');
assert.throws(function() {
fs.watch('non-existent-file');
}, function(err) {
+ console.log(err)
assert(err);
assert(/non-existent-file/.test(err));
assert.strictEqual(err.filename, 'non-existent-file'); with a console.log in the callback of the existing code, this is what I get in AIX:
hope this helps. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just tested with the changes from this PR on a local AIX box, and this test looks good - the error messages are matching, thanks! |
||
} | ||
return true; | ||
}; | ||
|
||
assert.throws( | ||
() => fs.watch(nonexistentFile, common.mustNotCall()), | ||
validateError | ||
); | ||
} | ||
|
||
{ | ||
const file = path.join(tmpdir.path, 'file-to-watch'); | ||
fs.writeFileSync(file, 'test'); | ||
const watcher = fs.watch(file, common.mustNotCall()); | ||
|
||
const validateError = (err) => { | ||
assert.strictEqual(nonexistentFile, err.path); | ||
assert.strictEqual(nonexistentFile, err.filename); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the arguments should be the other way around. Otherwise the error message would be weird in case of an error. The same in other places. |
||
assert.strictEqual( | ||
err.message, | ||
`ENOENT: no such file or directory, watch '${nonexistentFile}'`); | ||
assert.strictEqual(err.errno, uv.UV_ENOENT); | ||
assert.strictEqual(err.code, 'ENOENT'); | ||
assert.strictEqual(err.syscall, 'watch'); | ||
fs.unlinkSync(file); | ||
return true; | ||
}; | ||
|
||
watcher.on('error', common.mustCall(validateError)); | ||
|
||
assert.throws(function() { | ||
fs.watch('non-existent-file'); | ||
}, function(err) { | ||
assert(err); | ||
assert(/non-existent-file/.test(err)); | ||
assert.strictEqual(err.filename, 'non-existent-file'); | ||
return true; | ||
}); | ||
|
||
const watcher = fs.watch(__filename); | ||
watcher.on('error', common.mustCall(function(err) { | ||
assert(err); | ||
assert(/non-existent-file/.test(err)); | ||
assert.strictEqual(err.filename, 'non-existent-file'); | ||
})); | ||
watcher._handle.onchange(-1, 'ENOENT', 'non-existent-file'); | ||
// Simulate the invocation from the binding | ||
watcher._handle.onchange(uv.UV_ENOENT, 'ENOENT', nonexistentFile); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @bnoordhuis can you check that this can be fixed this way? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, this is coming from:
node/src/fs_event_wrap.cc
Lines 161 to 171 in 9e9c516