Skip to content
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

Closed
wants to merge 2 commits into from

Conversation

joyeecheung
Copy link
Member

  • Add an accessor property initialized to FSEventWrap to
    check the state of the handle from the JS land
  • Introduce ERR_FS_WATCHER_ALREADY_STARTED so calling start()
    on a watcher that is already started will throw instead of
    doing nothing silently.
  • Introduce ERR_FS_WATCHER_NOT_STARTED so calling close()
    on a watcher that is already closed will throw instead of
    doing nothing silently.
  • Validate the filename passed to fs.watch()
  • Assert that the handle in the watcher are instances of
    FSEvent instead of relying on the illegal invocation error
    from the VM.
  • Add more assertions in FSEventWrap methods now that we check
    initialized and the filename in JS land before invoking
    the binding.
  • Use uvException instead of errornoException to create
    the errors with the error numbers from libuv to make them
    consistent with other errors in fs.

TODO:

  • Improve fs.watchFile() the same way this patch improves fs.watch()
  • It seems possible to fire both rename and change event from libuv
    together now that we can check if the handle is closed via
    initialized in JS land.

This is a retake of #17851

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

fs

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. errors Issues and PRs related to JavaScript errors originated in Node.js core. fs Issues and PRs related to the fs subsystem / file system. labels Mar 2, 2018
@@ -1305,11 +1311,17 @@ function FSWatcher() {
this._handle.owner = this;

this._handle.onchange = function(status, eventType, filename) {
// TODO(joyeecheung): we may check self._handle.initialized here
Copy link
Member Author

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!

Copy link
Member Author

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

// We're in a bind here. libuv can set both UV_RENAME and UV_CHANGE but
// the Node API only lets us pass a single event to JS land.
//
// The obvious solution is to run the callback twice, once for each event.
// However, since the second event is not allowed to fire if the handle is
// closed after the first event, and since there is no good way to detect
// closed handles, that option is out.
//
// For now, ignore the UV_CHANGE event if UV_RENAME is also set. Make the
// assumption that a rename implicitly means an attribute change. Not too
// unreasonable, right? Still, we should revisit this before v1.0.

@joyeecheung joyeecheung added the semver-major PRs that contain breaking changes and should be released in the next major version. label Mar 2, 2018
@joyeecheung
Copy link
Member Author

Copy link
Member

@jasnell jasnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If @bnoordhuis is happy with it also, then LGTM

Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodejs/platform-aix The test is failing with ENODEV instead of ENOENT. Seems like a bug.

    AssertionError [ERR_ASSERTION]: 'ENODEV: no such device, watch \'/home/iojs/node-tmp/.tmp.2/non-existent\'' strictEqual 'ENOENT: no such file or directory, watch \'/home/iojs/node-tmp/.tmp.2/non-existent\''
        at Object.validateError (/home/iojs/build/workspace/node-test-commit-aix/nodes/aix61-ppc64/test/parallel/test-fs-watch-enoent.js:19:12)

lib/fs.js Outdated
persistent,
recursive,
!!persistent,
!!recursive,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor if correct change in behavior.

Right now, C++ land calls IsTrue(), which is essentially a strict x === true check. Using coercion here changes the observable behavior to a non-strict x == true check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis Yes, generally I think for boolean options it seems to be more intuitive to coerce them as if they are handled like if (options.something). The default cases are set in fs.watch so we should be fine in the common cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think it is best to have a strict check. Why would we want to accept any thruthy value?

FIXED_ONE_BYTE_STRING(env->isolate(), "initialized"),
get_initialized_templ,
Local<FunctionTemplate>(),
static_cast<PropertyAttribute>(ReadOnly | DontDelete | v8::DontEnum));
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

@joyeecheung
Copy link
Member Author

@nodejs/tsc Needs one more TSC approval to land since it's semver-major..thanks!

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a nit.

Can you also do a full citgm run?

@@ -105,22 +127,19 @@ void FSEventWrap::New(const FunctionCallbackInfo<Value>& args) {
new FSEventWrap(env, args.This());
}


// wrap.start(filename, persistent, recursive, encoding)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is commented out. Can you remove it?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have a strong opinion... hm... maybe change wrap.start to: Arguments: ?

@joyeecheung
Copy link
Member Author

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just a few suggestions.

assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(err.syscall, 'watch');
return true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a suggestion: assert.throws supports the object notation. So instead of using the function here, it could also be written as:

assert.throws(
  () => assert.fail(),
  {
    path: 'foo',
    filename: 'bar',
    ...
  }
)

Copy link
Member Author

@joyeecheung joyeecheung Mar 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With functions it will be easier to adjust the validations to different platforms, e.g. some throws ENOENT while some throws ENODEV

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure in what way that is easier to validate but it is only a suggestion, so shrug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BridgeAR It can be written like this

if (err.code === 'ENOENT') {
   // validate ENOENT
} else {
  // validate ENODEV
}

@@ -105,22 +127,19 @@ void FSEventWrap::New(const FunctionCallbackInfo<Value>& args) {
new FSEventWrap(env, args.This());
}


// wrap.start(filename, persistent, recursive, encoding)
Copy link
Member

Choose a reason for hiding this comment

The 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?

lib/fs.js Outdated
persistent,
recursive,
!!persistent,
!!recursive,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think it is best to have a strict check. Why would we want to accept any thruthy value?

@@ -1328,21 +1340,34 @@ FSWatcher.prototype.start = function(filename,
persistent,
recursive,
encoding) {
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since very recently we have the (D)CHECK macro. I think it would be nice to replace these assert calls with those instead :-).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BridgeAR Not unless CHECK throws instead of aborting...at least currently we only expects an error thrown in sequential/test-fs-watch.js

Copy link
Member

Choose a reason for hiding this comment

The 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.

@@ -112,12 +112,15 @@ tmpdir.refresh();
// https://github.com/joyent/node/issues/6690
{
let oldhandle;
assert.throws(function() {
common.expectsError(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is actually not necessary. assert.throws does accept objects as well.

@BridgeAR BridgeAR added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Mar 6, 2018
- Add an accessor property `initialized `to FSEventWrap to
  check the state of the handle from the JS land
- Introduce ERR_FS_WATCHER_ALREADY_STARTED so calling start()
  on a watcher that is already started will throw instead of
  doing nothing silently.
- Introduce ERR_FS_WATCHER_NOT_STARTED so calling close()
  on a watcher that is already closed will throw instead of
  doing nothing silently.
- Validate the filename passed to fs.watch()
- Assert that the handle in the watcher are instances of
  FSEvent instead of relying on the illegal invocation error
  from the VM.
- Add more assertions in FSEventWrap methods now that we check
  `initialized` and the filename in JS land before invoking
  the binding.
- Use uvException instead of errornoException to create
  the errors with the error numbers from libuv to make them
  consistent with other errors in fs.

TODO:

- Improve fs.watchFile() the same way this patch improves fs.watch()
- It seems possible to fire both rename and change event from libuv
  together now that we can check if the handle is closed via
  `initialized` in JS land.
@joyeecheung
Copy link
Member Author

Reverted the non-strict check and added ENODEV checks to work around the AIX bug.

New CI: https://ci.nodejs.org/job/node-test-pull-request/13549/


const validateError = (err) => {
assert.strictEqual(nonexistentFile, err.path);
assert.strictEqual(nonexistentFile, err.filename);
Copy link
Member

@BridgeAR BridgeAR Mar 6, 2018

Choose a reason for hiding this comment

The 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.

err.message,
`ENODEV: no such device, watch '${nonexistentFile}'`);
assert.strictEqual(err.errno, uv.UV_ENODEV);
assert.strictEqual(err.code, 'ENODEV');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodejs/platform-aix is this expected behaviour?

Copy link
Member

Choose a reason for hiding this comment

The 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:

{ Error: watch non-existent-file ENODEV
    at FSWatcher.start (fs.js:1346:19)
    at Object.fs.watch (fs.js:1369:11)
    at /tmp/gireesh/node/test/parallel/test-fs-watch-enoent.js:7:6
    at getActual (assert.js:421:5)
    at Function.throws (assert.js:430:18)
    at Object.<anonymous> (/tmp/gireesh/node/test/parallel/test-fs-watch-enoent.js:6:8)
    at Module._compile (module.js:670:30)
    at Object.Module._extensions..js (module.js:681:10)
    at Module.load (module.js:581:32)
    at tryModuleLoad (module.js:521:12)
  errno: 'ENODEV',
  code: 'ENODEV',
  syscall: 'watch non-existent-file',
  filename: 'non-existent-file' }

hope this helps.

Copy link
Member

Choose a reason for hiding this comment

The 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!

@joyeecheung
Copy link
Member Author

Only an infra issue on ubuntu1604-32 but it was green before the update. Just in case, another Linux CI: https://ci.nodejs.org/job/node-test-commit-linux/16930/

@joyeecheung
Copy link
Member Author

There seem to be no new CITGM errors. Linux CI is clean, landing..

joyeecheung added a commit that referenced this pull request Mar 7, 2018
- Add an accessor property `initialized `to FSEventWrap to
  check the state of the handle from the JS land
- Introduce ERR_FS_WATCHER_ALREADY_STARTED so calling start()
  on a watcher that is already started will throw instead of
  doing nothing silently.
- Introduce ERR_FS_WATCHER_NOT_STARTED so calling close()
  on a watcher that is already closed will throw instead of
  doing nothing silently.
- Validate the filename passed to fs.watch()
- Assert that the handle in the watcher are instances of
  FSEvent instead of relying on the illegal invocation error
  from the VM.
- Add more assertions in FSEventWrap methods now that we check
  `initialized` and the filename in JS land before invoking
  the binding.
- Use uvException instead of errornoException to create
  the errors with the error numbers from libuv to make them
  consistent with other errors in fs.

TODO:

- Improve fs.watchFile() the same way this patch improves fs.watch()
- It seems possible to fire both rename and change event from libuv
  together now that we can check if the handle is closed via
  `initialized` in JS land.

PR-URL: #19089
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
@joyeecheung
Copy link
Member Author

Landed in 6c25f2e, thanks!

@joyeecheung joyeecheung closed this Mar 7, 2018
joyeecheung added a commit to joyeecheung/node that referenced this pull request Mar 14, 2018
- Remove ERR_FS_WATCHER_ALREADY_STARTED and
  ERR_FS_WATCHER_NOT_STARTED because those two situations should
  result in noop instead of errors for consistency with the
  documented behavior of fs.watchFile.
  This partially reverts nodejs#19089
- Update comments about this behavior.

Refs: nodejs#19089
joyeecheung added a commit that referenced this pull request Mar 18, 2018
- Remove ERR_FS_WATCHER_ALREADY_STARTED and
  ERR_FS_WATCHER_NOT_STARTED because those two situations should
  result in noop instead of errors for consistency with the
  documented behavior of fs.watchFile.
  This partially reverts #19089
- Update comments about this behavior.

Refs: #19089

PR-URL: #19345
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
joyeecheung added a commit that referenced this pull request Mar 18, 2018
- 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>
MayaLekova pushed a commit to MayaLekova/node that referenced this pull request May 8, 2018
- Add an accessor property `initialized `to FSEventWrap to
  check the state of the handle from the JS land
- Introduce ERR_FS_WATCHER_ALREADY_STARTED so calling start()
  on a watcher that is already started will throw instead of
  doing nothing silently.
- Introduce ERR_FS_WATCHER_NOT_STARTED so calling close()
  on a watcher that is already closed will throw instead of
  doing nothing silently.
- Validate the filename passed to fs.watch()
- Assert that the handle in the watcher are instances of
  FSEvent instead of relying on the illegal invocation error
  from the VM.
- Add more assertions in FSEventWrap methods now that we check
  `initialized` and the filename in JS land before invoking
  the binding.
- Use uvException instead of errornoException to create
  the errors with the error numbers from libuv to make them
  consistent with other errors in fs.

TODO:

- Improve fs.watchFile() the same way this patch improves fs.watch()
- It seems possible to fire both rename and change event from libuv
  together now that we can check if the handle is closed via
  `initialized` in JS land.

PR-URL: nodejs#19089
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. c++ Issues and PRs that require attention from people who are familiar with C++. errors Issues and PRs related to JavaScript errors originated in Node.js core. fs Issues and PRs related to the fs subsystem / file system. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants