Skip to content

Commit 6c25f2e

Browse files
committed
fs: improve errors thrown from fs.watch()
- 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 <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]>
1 parent 48b5c11 commit 6c25f2e

File tree

7 files changed

+179
-55
lines changed

7 files changed

+179
-55
lines changed

doc/api/errors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,18 @@ falsy value.
783783
An invalid symlink type was passed to the [`fs.symlink()`][] or
784784
[`fs.symlinkSync()`][] methods.
785785

786+
<a id="ERR_FS_WATCHER_ALREADY_STARTED"></a>
787+
### ERR_FS_WATCHER_ALREADY_STARTED
788+
789+
An attempt was made to start a watcher returned by `fs.watch()` that has
790+
already been started.
791+
792+
<a id="ERR_FS_WATCHER_NOT_STARTED"></a>
793+
### ERR_FS_WATCHER_NOT_STARTED
794+
795+
An attempt was made to initiate operations on a watcher returned by
796+
`fs.watch()` that has not yet been started.
797+
786798
<a id="ERR_HTTP_HEADERS_SENT"></a>
787799
### ERR_HTTP_HEADERS_SENT
788800

lib/fs.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,19 @@ Object.defineProperty(exports, 'constants', {
7777
value: constants
7878
});
7979

80+
let assert_ = null;
81+
function lazyAssert() {
82+
if (assert_ === null) {
83+
assert_ = require('assert');
84+
}
85+
return assert_;
86+
}
87+
8088
const kMinPoolSpace = 128;
8189
const { kMaxLength } = require('buffer');
8290

8391
const isWindows = process.platform === 'win32';
8492

85-
const errnoException = errors.errnoException;
86-
8793
let truncateWarn = true;
8894

8995
function showTruncateDeprecation() {
@@ -1312,11 +1318,17 @@ function FSWatcher() {
13121318
this._handle.owner = this;
13131319

13141320
this._handle.onchange = function(status, eventType, filename) {
1321+
// TODO(joyeecheung): we may check self._handle.initialized here
1322+
// and return if that is false. This allows us to avoid firing the event
1323+
// after the handle is closed, and to fire both UV_RENAME and UV_CHANGE
1324+
// if they are set by libuv at the same time.
13151325
if (status < 0) {
13161326
self._handle.close();
1317-
const error = !filename ?
1318-
errnoException(status, 'Error watching file for changes:') :
1319-
errnoException(status, `Error watching file ${filename} for changes:`);
1327+
const error = errors.uvException({
1328+
errno: status,
1329+
syscall: 'watch',
1330+
path: filename
1331+
});
13201332
error.filename = filename;
13211333
self.emit('error', error);
13221334
} else {
@@ -1335,21 +1347,34 @@ FSWatcher.prototype.start = function(filename,
13351347
persistent,
13361348
recursive,
13371349
encoding) {
1350+
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent');
1351+
if (this._handle.initialized) {
1352+
throw new errors.Error('ERR_FS_WATCHER_ALREADY_STARTED');
1353+
}
1354+
13381355
filename = getPathFromURL(filename);
1339-
nullCheck(filename, 'filename');
1356+
validatePath(filename, 'filename');
1357+
13401358
var err = this._handle.start(pathModule.toNamespacedPath(filename),
13411359
persistent,
13421360
recursive,
13431361
encoding);
13441362
if (err) {
1345-
this._handle.close();
1346-
const error = errnoException(err, `watch ${filename}`);
1363+
const error = errors.uvException({
1364+
errno: err,
1365+
syscall: 'watch',
1366+
path: filename
1367+
});
13471368
error.filename = filename;
13481369
throw error;
13491370
}
13501371
};
13511372

13521373
FSWatcher.prototype.close = function() {
1374+
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent');
1375+
if (!this._handle.initialized) {
1376+
throw new errors.Error('ERR_FS_WATCHER_NOT_STARTED');
1377+
}
13531378
this._handle.close();
13541379
};
13551380

lib/internal/errors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,10 @@ E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value', Error);
658658
E('ERR_FS_INVALID_SYMLINK_TYPE',
659659
'Symlink type must be one of "dir", "file", or "junction". Received "%s"',
660660
Error); // Switch to TypeError. The current implementation does not seem right
661+
E('ERR_FS_WATCHER_ALREADY_STARTED',
662+
'The watcher has already been started', Error);
663+
E('ERR_FS_WATCHER_NOT_STARTED',
664+
'The watcher has not been started', Error);
661665
E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
662666
'HTTP/2 ALTSVC frames require a valid origin', TypeError);
663667
E('ERR_HTTP2_ALTSVC_LENGTH',

src/fs_event_wrap.cc

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@
3131
namespace node {
3232

3333
using v8::Context;
34+
using v8::DontDelete;
35+
using v8::DontEnum;
3436
using v8::FunctionCallbackInfo;
3537
using v8::FunctionTemplate;
3638
using v8::HandleScope;
3739
using v8::Integer;
3840
using v8::Local;
3941
using v8::MaybeLocal;
4042
using v8::Object;
43+
using v8::PropertyAttribute;
44+
using v8::ReadOnly;
45+
using v8::Signature;
4146
using v8::String;
4247
using v8::Value;
4348

@@ -51,7 +56,7 @@ class FSEventWrap: public HandleWrap {
5156
static void New(const FunctionCallbackInfo<Value>& args);
5257
static void Start(const FunctionCallbackInfo<Value>& args);
5358
static void Close(const FunctionCallbackInfo<Value>& args);
54-
59+
static void GetInitialized(const FunctionCallbackInfo<Value>& args);
5560
size_t self_size() const override { return sizeof(*this); }
5661

5762
private:
@@ -80,6 +85,11 @@ FSEventWrap::~FSEventWrap() {
8085
CHECK_EQ(initialized_, false);
8186
}
8287

88+
void FSEventWrap::GetInitialized(const FunctionCallbackInfo<Value>& args) {
89+
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.This());
90+
CHECK(wrap != nullptr);
91+
args.GetReturnValue().Set(wrap->initialized_);
92+
}
8393

8494
void FSEventWrap::Initialize(Local<Object> target,
8595
Local<Value> unused,
@@ -95,6 +105,18 @@ void FSEventWrap::Initialize(Local<Object> target,
95105
env->SetProtoMethod(t, "start", Start);
96106
env->SetProtoMethod(t, "close", Close);
97107

108+
Local<FunctionTemplate> get_initialized_templ =
109+
FunctionTemplate::New(env->isolate(),
110+
GetInitialized,
111+
env->as_external(),
112+
Signature::New(env->isolate(), t));
113+
114+
t->PrototypeTemplate()->SetAccessorProperty(
115+
FIXED_ONE_BYTE_STRING(env->isolate(), "initialized"),
116+
get_initialized_templ,
117+
Local<FunctionTemplate>(),
118+
static_cast<PropertyAttribute>(ReadOnly | DontDelete | v8::DontEnum));
119+
98120
target->Set(fsevent_string, t->GetFunction());
99121
}
100122

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

108-
130+
// wrap.start(filename, persistent, recursive, encoding)
109131
void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) {
110132
Environment* env = Environment::GetCurrent(args);
111133

112-
FSEventWrap* wrap;
113-
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
114-
if (wrap->initialized_)
115-
return args.GetReturnValue().Set(0);
134+
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.Holder());
135+
CHECK_NE(wrap, nullptr);
136+
CHECK(!wrap->initialized_);
116137

117-
static const char kErrMsg[] = "filename must be a string or Buffer";
118-
if (args.Length() < 1)
119-
return env->ThrowTypeError(kErrMsg);
138+
const int argc = args.Length();
139+
CHECK_GE(argc, 4);
120140

121141
BufferValue path(env->isolate(), args[0]);
122-
if (*path == nullptr)
123-
return env->ThrowTypeError(kErrMsg);
142+
CHECK_NE(*path, nullptr);
124143

125144
unsigned int flags = 0;
126145
if (args[2]->IsTrue())
@@ -129,19 +148,21 @@ void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) {
129148
wrap->encoding_ = ParseEncoding(env->isolate(), args[3], kDefaultEncoding);
130149

131150
int err = uv_fs_event_init(wrap->env()->event_loop(), &wrap->handle_);
132-
if (err == 0) {
133-
wrap->initialized_ = true;
151+
if (err != 0) {
152+
return args.GetReturnValue().Set(err);
153+
}
134154

135-
err = uv_fs_event_start(&wrap->handle_, OnEvent, *path, flags);
155+
err = uv_fs_event_start(&wrap->handle_, OnEvent, *path, flags);
156+
wrap->initialized_ = true;
136157

137-
if (err == 0) {
138-
// Check for persistent argument
139-
if (!args[1]->IsTrue()) {
140-
uv_unref(reinterpret_cast<uv_handle_t*>(&wrap->handle_));
141-
}
142-
} else {
143-
FSEventWrap::Close(args);
144-
}
158+
if (err != 0) {
159+
FSEventWrap::Close(args);
160+
return args.GetReturnValue().Set(err);
161+
}
162+
163+
// Check for persistent argument
164+
if (!args[1]->IsTrue()) {
165+
uv_unref(reinterpret_cast<uv_handle_t*>(&wrap->handle_));
145166
}
146167

147168
args.GetReturnValue().Set(err);
@@ -209,13 +230,11 @@ void FSEventWrap::OnEvent(uv_fs_event_t* handle, const char* filename,
209230

210231

211232
void FSEventWrap::Close(const FunctionCallbackInfo<Value>& args) {
212-
FSEventWrap* wrap;
213-
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
233+
FSEventWrap* wrap = Unwrap<FSEventWrap>(args.Holder());
234+
CHECK_NE(wrap, nullptr);
235+
CHECK(wrap->initialized_);
214236

215-
if (wrap == nullptr || wrap->initialized_ == false)
216-
return;
217237
wrap->initialized_ = false;
218-
219238
HandleWrap::Close(args);
220239
}
221240

test/parallel/test-fs-watch-enoent.js

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,64 @@
11
'use strict';
2+
3+
// This verifies the error thrown by fs.watch.
4+
25
const common = require('../common');
36
const assert = require('assert');
47
const fs = require('fs');
8+
const tmpdir = require('../common/tmpdir');
9+
const path = require('path');
10+
const nonexistentFile = path.join(tmpdir.path, 'non-existent');
11+
const uv = process.binding('uv');
12+
13+
tmpdir.refresh();
14+
15+
{
16+
const validateError = (err) => {
17+
assert.strictEqual(err.path, nonexistentFile);
18+
assert.strictEqual(err.filename, nonexistentFile);
19+
assert.strictEqual(err.syscall, 'watch');
20+
if (err.code === 'ENOENT') {
21+
assert.strictEqual(
22+
err.message,
23+
`ENOENT: no such file or directory, watch '${nonexistentFile}'`);
24+
assert.strictEqual(err.errno, uv.UV_ENOENT);
25+
assert.strictEqual(err.code, 'ENOENT');
26+
} else { // AIX
27+
assert.strictEqual(
28+
err.message,
29+
`ENODEV: no such device, watch '${nonexistentFile}'`);
30+
assert.strictEqual(err.errno, uv.UV_ENODEV);
31+
assert.strictEqual(err.code, 'ENODEV');
32+
}
33+
return true;
34+
};
35+
36+
assert.throws(
37+
() => fs.watch(nonexistentFile, common.mustNotCall()),
38+
validateError
39+
);
40+
}
41+
42+
{
43+
const file = path.join(tmpdir.path, 'file-to-watch');
44+
fs.writeFileSync(file, 'test');
45+
const watcher = fs.watch(file, common.mustNotCall());
46+
47+
const validateError = (err) => {
48+
assert.strictEqual(err.path, nonexistentFile);
49+
assert.strictEqual(err.filename, nonexistentFile);
50+
assert.strictEqual(
51+
err.message,
52+
`ENOENT: no such file or directory, watch '${nonexistentFile}'`);
53+
assert.strictEqual(err.errno, uv.UV_ENOENT);
54+
assert.strictEqual(err.code, 'ENOENT');
55+
assert.strictEqual(err.syscall, 'watch');
56+
fs.unlinkSync(file);
57+
return true;
58+
};
59+
60+
watcher.on('error', common.mustCall(validateError));
561

6-
assert.throws(function() {
7-
fs.watch('non-existent-file');
8-
}, function(err) {
9-
assert(err);
10-
assert(/non-existent-file/.test(err));
11-
assert.strictEqual(err.filename, 'non-existent-file');
12-
return true;
13-
});
14-
15-
const watcher = fs.watch(__filename);
16-
watcher.on('error', common.mustCall(function(err) {
17-
assert(err);
18-
assert(/non-existent-file/.test(err));
19-
assert.strictEqual(err.filename, 'non-existent-file');
20-
}));
21-
watcher._handle.onchange(-1, 'ENOENT', 'non-existent-file');
62+
// Simulate the invocation from the binding
63+
watcher._handle.onchange(uv.UV_ENOENT, 'ENOENT', nonexistentFile);
64+
}

test/parallel/test-fs-watch.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,16 @@ for (const testCase of cases) {
6565
assert.strictEqual(eventType, 'change');
6666
assert.strictEqual(argFilename, testCase.fileName);
6767

68-
watcher.start(); // should not crash
69-
68+
common.expectsError(() => watcher.start(), {
69+
code: 'ERR_FS_WATCHER_ALREADY_STARTED',
70+
message: 'The watcher has already been started'
71+
});
7072
// end of test case
7173
watcher.close();
74+
common.expectsError(() => watcher.close(), {
75+
code: 'ERR_FS_WATCHER_NOT_STARTED',
76+
message: 'The watcher has not been started'
77+
});
7278
}));
7379

7480
// long content so it's actually flushed. toUpperCase so there's real change.
@@ -78,3 +84,15 @@ for (const testCase of cases) {
7884
fs.writeFileSync(testCase.filePath, content2);
7985
}, 100);
8086
}
87+
88+
[false, 1, {}, [], null, undefined].forEach((i) => {
89+
common.expectsError(
90+
() => fs.watch(i, common.mustNotCall()),
91+
{
92+
code: 'ERR_INVALID_ARG_TYPE',
93+
type: TypeError,
94+
message: 'The "filename" argument must be one of ' +
95+
'type string, Buffer, or URL'
96+
}
97+
);
98+
});

test/sequential/test-fs-watch.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,15 @@ tmpdir.refresh();
112112
// https://github.com/joyent/node/issues/6690
113113
{
114114
let oldhandle;
115-
assert.throws(function() {
115+
assert.throws(() => {
116116
const w = fs.watch(__filename, common.mustNotCall());
117117
oldhandle = w._handle;
118118
w._handle = { close: w._handle.close };
119119
w.close();
120-
}, /^TypeError: Illegal invocation$/);
120+
}, {
121+
message: 'handle must be a FSEvent',
122+
code: 'ERR_ASSERTION'
123+
});
121124
oldhandle.close(); // clean up
122125

123126
assert.throws(function() {

0 commit comments

Comments
 (0)