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: do not crash when using a closed fs event watcher #20985

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ fs.watch('./tmp', { encoding: 'buffer' }, (eventType, filename) => {
added: v10.0.0
-->

Emitted when the watcher stops watching for changes.
Emitted when the watcher stops watching for changes. The closed
`fs.FSWatcher` object is no longer usable in the event handler.

### Event: 'error'
<!-- YAML
Expand All @@ -334,7 +335,8 @@ added: v0.5.8

* `error` {Error}

Emitted when an error occurs while watching the file.
Emitted when an error occurs while watching the file. The errored
Copy link
Member Author

@joyeecheung joyeecheung May 27, 2018

Choose a reason for hiding this comment

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

Maybe we should also document that the errorred watcher will be closed by core? I have not looked into the user land patterns yet but I am guessing they hit the crashes because they attempt to close the watcher in the error event handler, even though we have been closing the errorred watchers ourselves.

`fs.FSWatcher` object is no longer usable in the event handler.

### watcher.close()
<!-- YAML
Expand Down
21 changes: 17 additions & 4 deletions lib/internal/fs/watchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ function FSWatcher() {
// 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) {
this._handle.close();
if (this._handle !== null) {
// We don't use this.close() here to avoid firing the close event.
this._handle.close();
this._handle = null; // make the handle garbage collectable
}
const error = errors.uvException({
errno: status,
syscall: 'watch',
Expand All @@ -120,13 +124,17 @@ util.inherits(FSWatcher, EventEmitter);
// 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
// 3. Return silently if the watcher has already been closed
// This method is a noop if the watcher has already been started.
FSWatcher.prototype.start = function(filename,
persistent,
recursive,
encoding) {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (this._handle.initialized) {
if (this._handle.initialized) { // already started
return;
}

Expand All @@ -148,13 +156,18 @@ FSWatcher.prototype.start = function(filename,
}
};

// This method is a noop if the watcher has not been started.
// This method is a noop if the watcher has not been started or
// has already been closed.
FSWatcher.prototype.close = function() {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (!this._handle.initialized) {
if (!this._handle.initialized) { // not started
return;
}
this._handle.close();
this._handle = null; // make the handle garbage collectable
process.nextTick(emitCloseNT, this);
};

Expand Down
38 changes: 38 additions & 0 deletions test/parallel/test-fs-watch-close-when-destroyed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

// This tests that closing a watcher when the underlying handle is
// already destroyed will result in a noop instead of a crash.

const common = require('../common');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
const path = require('path');

tmpdir.refresh();
const root = path.join(tmpdir.path, 'watched-directory');
fs.mkdirSync(root);

const watcher = fs.watch(root, { persistent: false, recursive: false });

// The following listeners may or may not be invoked.

watcher.addListener('error', () => {
setTimeout(
() => { watcher.close(); }, // Should not crash if it's invoked
common.platformTimeout(10)
);
});

watcher.addListener('change', () => {
setTimeout(
() => { watcher.close(); },
common.platformTimeout(10)
);
});

fs.rmdirSync(root);
// Wait for the listener to hit
setTimeout(
common.mustCall(() => {}),
common.platformTimeout(100)
);
19 changes: 15 additions & 4 deletions test/parallel/test-fs-watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@ for (const testCase of cases) {
fs.writeFileSync(testCase.filePath, content1);

let interval;
const watcher = fs.watch(testCase[testCase.field]);
const pathToWatch = testCase[testCase.field];
const watcher = fs.watch(pathToWatch);
watcher.on('error', (err) => {
if (interval) {
clearInterval(interval);
interval = null;
}
assert.fail(err);
});
watcher.on('close', common.mustCall());
watcher.on('close', common.mustCall(() => {
watcher.close(); // Closing a closed watcher should be a noop
// Starting a closed watcher should be a noop
watcher.start();
}));
watcher.on('change', common.mustCall(function(eventType, argFilename) {
if (interval) {
clearInterval(interval);
Expand All @@ -66,10 +71,16 @@ for (const testCase of cases) {
assert.strictEqual(eventType, 'change');
assert.strictEqual(argFilename, testCase.fileName);

watcher.start(); // Starting a started watcher should be a noop
// End of test case
// Starting a started watcher should be a noop
watcher.start();
watcher.start(pathToWatch);

watcher.close();

// We document that watchers cannot be used anymore when it's closed,
// here we turn the methods into noops instead of throwing
watcher.close(); // Closing a closed watcher should be a noop
watcher.start(); // Starting a closed watcher should be a noop
}));

// Long content so it's actually flushed. toUpperCase so there's real change.
Expand Down