Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
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 the watcher stops watching for changes. The errored
`fs.FSWatcher` object is no longer usable in the event handler.

### watcher.close()
<!-- YAML
Expand Down
17 changes: 13 additions & 4 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1380,7 +1380,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) {
self._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 @@ -1400,13 +1404,14 @@ 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) {
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (this._handle.initialized) {
if (this._handle.initialized) { // already started
return;
}

Expand All @@ -1428,10 +1433,14 @@ 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;
}
lazyAssert()(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (!this._handle.initialized) {
if (!this._handle.initialized) { // not started
return;
}
this._handle.close();
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