-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
fs: improve errors thrown from fs.watch() #19089
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
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'); | ||
|
||
| 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)); | ||
|
||
|
|
||
| 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) | ||
|
||
| 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'); | ||
|
Member
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?
Member
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.
Member
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); | ||
|
Member
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