Skip to content

Commit

Permalink
fs: add recursive watch to linux
Browse files Browse the repository at this point in the history
PR-URL: #45098
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
Reviewed-By: Moshe Atlow <[email protected]>
Reviewed-By: Rich Trott <[email protected]>
Reviewed-By: James M Snell <[email protected]>
anonrig authored and RafaelGSS committed Nov 10, 2022
1 parent 97547bc commit 34bfef9
Showing 13 changed files with 801 additions and 109 deletions.
7 changes: 3 additions & 4 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
@@ -4320,6 +4320,9 @@ The `atime` and `mtime` arguments follow these rules:
<!-- YAML
added: v0.5.10
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/45098
description: Added recursive support for Linux, AIX and IBMi.
- version:
- v15.9.0
- v14.17.0
@@ -4377,10 +4380,6 @@ the returned {fs.FSWatcher}.
The `fs.watch` API is not 100% consistent across platforms, and is
unavailable in some situations.
The recursive option is only supported on macOS and Windows.
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
when the option is used on a platform that does not support it.
On Windows, no events will be emitted if the watched directory is moved or
renamed. An `EPERM` error is reported when the watched directory is deleted.
26 changes: 17 additions & 9 deletions lib/fs.js
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ const {

const pathModule = require('path');
const { isArrayBufferView } = require('internal/util/types');
const nonNativeWatcher = require('internal/fs/recursive_watch');

// We need to get the statValues from the binding at the callsite since
// it's re-initialized after deserialization.
@@ -68,7 +69,6 @@ const {
codes: {
ERR_FS_FILE_TOO_LARGE,
ERR_INVALID_ARG_VALUE,
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
},
AbortError,
uvErrmapGet,
@@ -163,7 +163,6 @@ let FileWriteStream;
const isWindows = process.platform === 'win32';
const isOSX = process.platform === 'darwin';


function showTruncateDeprecation() {
if (truncateWarn) {
process.emitWarning(
@@ -2297,13 +2296,22 @@ function watch(filename, options, listener) {

if (options.persistent === undefined) options.persistent = true;
if (options.recursive === undefined) options.recursive = false;
if (options.recursive && !(isOSX || isWindows))
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('watch recursively');
const watcher = new watchers.FSWatcher();
watcher[watchers.kFSWatchStart](filename,
options.persistent,
options.recursive,
options.encoding);

let watcher;

// TODO(anonrig): Remove this when/if libuv supports it.
// As of November 2022, libuv does not support recursive file watch on all platforms,
// e.g. Linux due to the limitations of inotify.
if (options.recursive && !isOSX && !isWindows) {
watcher = new nonNativeWatcher.FSWatcher(options);
watcher[watchers.kFSWatchStart](filename);
} else {
watcher = new watchers.FSWatcher();
watcher[watchers.kFSWatchStart](filename,
options.persistent,
options.recursive,
options.encoding);
}

if (listener) {
watcher.addListener('change', listener);
26 changes: 24 additions & 2 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
@@ -88,7 +88,8 @@ const {
} = require('internal/util');
const { EventEmitterMixin } = require('internal/event_target');
const { StringDecoder } = require('string_decoder');
const { watch } = require('internal/fs/watchers');
const { kFSWatchStart, watch } = require('internal/fs/watchers');
const nonNativeWatcher = require('internal/fs/recursive_watch');
const { isIterable } = require('internal/streams/utils');
const assert = require('internal/assert');

@@ -120,6 +121,7 @@ const getDirectoryEntriesPromise = promisify(getDirents);
const validateRmOptionsPromise = promisify(validateRmOptions);

const isWindows = process.platform === 'win32';
const isOSX = process.platform === 'darwin';

let cpPromises;
function lazyLoadCpPromises() {
@@ -917,6 +919,26 @@ async function readFile(path, options) {
return handleFdClose(readFileHandle(fd, options), fd.close);
}

async function* _watch(filename, options = kEmptyObject) {
validateObject(options, 'options');

if (options.recursive != null) {
validateBoolean(options.recursive, 'options.recursive');

// TODO(anonrig): Remove this when/if libuv supports it.
// As of November 2022, libuv does not support recursive file watch on all platforms,
// e.g. Linux due to the limitations of inotify.
if (options.recursive && !isOSX && !isWindows) {
const watcher = new nonNativeWatcher.FSWatcher(options);
await watcher[kFSWatchStart](filename);
yield* watcher;
return;
}
}

yield* watch(filename, options);
}

module.exports = {
exports: {
access,
@@ -947,7 +969,7 @@ module.exports = {
writeFile,
appendFile,
readFile,
watch,
watch: !isOSX && !isWindows ? _watch : watch,
constants,
},

281 changes: 281 additions & 0 deletions lib/internal/fs/recursive_watch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
'use strict';

const {
ArrayPrototypePush,
SafePromiseAllReturnVoid,
Promise,
PromisePrototypeThen,
SafeMap,
SafeSet,
StringPrototypeStartsWith,
SymbolAsyncIterator,
} = primordials;

const { EventEmitter } = require('events');
const assert = require('internal/assert');
const {
AbortError,
codes: {
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const { getValidatedPath } = require('internal/fs/utils');
const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
const { kEmptyObject } = require('internal/util');
const { validateBoolean, validateAbortSignal } = require('internal/validators');
const path = require('path');

let internalSync;
let internalPromises;

function lazyLoadFsPromises() {
internalPromises ??= require('fs/promises');
return internalPromises;
}

function lazyLoadFsSync() {
internalSync ??= require('fs');
return internalSync;
}

async function traverse(dir, files = new SafeMap(), symbolicLinks = new SafeSet()) {
const { opendir } = lazyLoadFsPromises();

const filenames = await opendir(dir);
const subdirectories = [];

for await (const file of filenames) {
const f = path.join(dir, file.name);

files.set(f, file);

// Do not follow symbolic links
if (file.isSymbolicLink()) {
symbolicLinks.add(f);
} else if (file.isDirectory()) {
ArrayPrototypePush(subdirectories, traverse(f, files));
}
}

await SafePromiseAllReturnVoid(subdirectories);

return files;
}

class FSWatcher extends EventEmitter {
#options = null;
#closed = false;
#files = new SafeMap();
#symbolicFiles = new SafeSet();
#rootPath = path.resolve();
#watchingFile = false;

constructor(options = kEmptyObject) {
super();

assert(typeof options === 'object');

const { persistent, recursive, signal, encoding } = options;

// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
if (recursive != null) {
validateBoolean(recursive, 'options.recursive');
}

if (persistent != null) {
validateBoolean(persistent, 'options.persistent');
}

if (signal != null) {
validateAbortSignal(signal, 'options.signal');
}

if (encoding != null) {
// This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE
if (typeof encoding !== 'string') {
throw new ERR_INVALID_ARG_VALUE(encoding, 'options.encoding');
}
}

this.#options = { persistent, recursive, signal, encoding };
}

close() {
if (this.#closed) {
return;
}

const { unwatchFile } = lazyLoadFsSync();
this.#closed = true;

for (const file of this.#files.keys()) {
unwatchFile(file);
}

this.#files.clear();
this.#symbolicFiles.clear();
this.emit('close');
}

#unwatchFiles(file) {
const { unwatchFile } = lazyLoadFsSync();

this.#symbolicFiles.delete(file);

for (const filename of this.#files.keys()) {
if (StringPrototypeStartsWith(filename, file)) {
unwatchFile(filename);
}
}
}

async #watchFolder(folder) {
const { opendir } = lazyLoadFsPromises();

try {
const files = await opendir(folder);

for await (const file of files) {
if (this.#closed) {
break;
}

const f = path.join(folder, file.name);

if (!this.#files.has(f)) {
this.emit('change', 'rename', path.relative(this.#rootPath, f));

if (file.isSymbolicLink()) {
this.#symbolicFiles.add(f);
}

if (file.isFile()) {
this.#watchFile(f);
} else {
this.#files.set(f, file);

if (file.isDirectory() && !file.isSymbolicLink()) {
await this.#watchFolder(f);
}
}
}
}
} catch (error) {
this.emit('error', error);
}
}

#watchFile(file) {
if (this.#closed) {
return;
}

const { watchFile } = lazyLoadFsSync();
const existingStat = this.#files.get(file);

watchFile(file, {
persistent: this.#options.persistent,
}, (currentStats, previousStats) => {
if (existingStat && !existingStat.isDirectory() &&
currentStats.nlink !== 0 && existingStat.mtimeMs === currentStats.mtimeMs) {
return;
}

this.#files.set(file, currentStats);

if (currentStats.birthtimeMs === 0 && previousStats.birthtimeMs !== 0) {
// The file is now deleted
this.#files.delete(file);
this.emit('change', 'rename', path.relative(this.#rootPath, file));
this.#unwatchFiles(file);
} else if (file === this.#rootPath && this.#watchingFile) {
// This case will only be triggered when watching a file with fs.watch
this.emit('change', 'change', path.basename(file));
} else if (this.#symbolicFiles.has(file)) {
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
this.emit('change', 'rename', path.relative(this.#rootPath, file));
} else if (currentStats.isDirectory()) {
this.#watchFolder(file);
}
});
}

[kFSWatchStart](filename) {
filename = path.resolve(getValidatedPath(filename));

try {
const file = lazyLoadFsSync().statSync(filename);

this.#rootPath = filename;
this.#closed = false;
this.#watchingFile = file.isFile();

if (file.isDirectory()) {
this.#files.set(filename, file);

PromisePrototypeThen(
traverse(filename, this.#files, this.#symbolicFiles),
() => {
for (const f of this.#files.keys()) {
this.#watchFile(f);
}
},
);
} else {
this.#watchFile(filename);
}
} catch (error) {
if (error.code === 'ENOENT') {
error.filename = filename;
throw error;
}
}

}

ref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.ref();
}
});
}

unref() {
this.#files.forEach((file) => {
if (file instanceof StatWatcher) {
file.unref();
}
});
}

[SymbolAsyncIterator]() {
const { signal } = this.#options;
const promiseExecutor = signal == null ?
(resolve) => {
this.once('change', (eventType, filename) => {
resolve({ __proto__: null, value: { eventType, filename } });
});
} : (resolve, reject) => {
const onAbort = () => reject(new AbortError(undefined, { cause: signal.reason }));
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort, { __proto__: null, once: true });
this.once('change', (eventType, filename) => {
signal.removeEventListener('abort', onAbort);
resolve({ __proto__: null, value: { eventType, filename } });
});
};
return {
next: () => (this.#closed ?
{ __proto__: null, done: true } :
new Promise(promiseExecutor)),
[SymbolAsyncIterator]() { return this; },
};
}
}

module.exports = {
FSWatcher,
kFSWatchStart,
};
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ const expectedModules = new Set([
'NativeModule internal/fs/dir',
'NativeModule internal/fs/promises',
'NativeModule internal/fs/read_file_context',
'NativeModule internal/fs/recursive_watch',
'NativeModule internal/fs/rimraf',
'NativeModule internal/fs/utils',
'NativeModule internal/fs/watchers',
7 changes: 7 additions & 0 deletions test/parallel/test-fs-watch-close-when-destroyed.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,13 @@

const common = require('../common');

// fs-watch on folders have limited capability in AIX.
// The testcase makes use of folder watching, and causes
// hang. This behavior is documented. Skip this for AIX.

if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

3 changes: 3 additions & 0 deletions test/parallel/test-fs-watch-encoding.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,9 @@ const common = require('../common');
if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

const fs = require('fs');
const path = require('path');

44 changes: 22 additions & 22 deletions test/parallel/test-fs-watch-enoent.js
Original file line number Diff line number Diff line change
@@ -25,11 +25,9 @@ tmpdir.refresh();
const validateError = (err) => {
assert.strictEqual(err.path, nonexistentFile);
assert.strictEqual(err.filename, nonexistentFile);
assert.strictEqual(err.syscall, 'watch');
assert.ok(err.syscall === 'watch' || err.syscall === 'stat');
if (err.code === 'ENOENT') {
assert.strictEqual(
err.message,
`ENOENT: no such file or directory, watch '${nonexistentFile}'`);
assert.ok(err.message.startsWith('ENOENT: no such file or directory'));
assert.strictEqual(err.errno, UV_ENOENT);
assert.strictEqual(err.code, 'ENOENT');
} else { // AIX
@@ -49,25 +47,27 @@ tmpdir.refresh();
}

{
const file = path.join(tmpdir.path, 'file-to-watch');
fs.writeFileSync(file, 'test');
const watcher = fs.watch(file, common.mustNotCall());
if (common.isOSX || common.isWindows) {
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(err.path, nonexistentFile);
assert.strictEqual(err.filename, nonexistentFile);
assert.strictEqual(
err.message,
`ENOENT: no such file or directory, watch '${nonexistentFile}'`);
assert.strictEqual(err.errno, UV_ENOENT);
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(err.syscall, 'watch');
fs.unlinkSync(file);
return true;
};
const validateError = (err) => {
assert.strictEqual(err.path, nonexistentFile);
assert.strictEqual(err.filename, nonexistentFile);
assert.strictEqual(
err.message,
`ENOENT: no such file or directory, watch '${nonexistentFile}'`);
assert.strictEqual(err.errno, UV_ENOENT);
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(err.syscall, 'watch');
fs.unlinkSync(file);
return true;
};

watcher.on('error', common.mustCall(validateError));
watcher.on('error', common.mustCall(validateError));

// Simulate the invocation from the binding
watcher._handle.onchange(UV_ENOENT, 'ENOENT', nonexistentFile);
// Simulate the invocation from the binding
watcher._handle.onchange(UV_ENOENT, 'ENOENT', nonexistentFile);
}
}
94 changes: 94 additions & 0 deletions test/parallel/test-fs-watch-recursive-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

const common = require('../common');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

// fs-watch on folders have limited capability in AIX.
// The testcase makes use of folder watching, and causes
// hang. This behavior is documented. Skip this for AIX.

if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');

const assert = require('assert');
const path = require('path');
const fs = require('fs/promises');
const fsSync = require('fs');

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

(async function run() {
// Add a file to already watching folder

const testsubdir = await fs.mkdtemp(testDir + path.sep);
const file = '1.txt';
const filePath = path.join(testsubdir, file);
const watcher = fs.watch(testsubdir, { recursive: true });

let interval;

process.on('exit', function() {
assert.ok(interval === null, 'watcher Object was not closed');
});

process.nextTick(common.mustCall(() => {
interval = setInterval(() => {
fsSync.writeFileSync(filePath, 'world');
}, 500);
}));

for await (const payload of watcher) {
const { eventType, filename } = payload;

assert.ok(eventType === 'change' || eventType === 'rename');

if (filename === file) {
break;
}
}

clearInterval(interval);
interval = null;
})().then(common.mustCall());

(async function() {
// Test that aborted AbortSignal are reported.
const testsubdir = await fs.mkdtemp(testDir + path.sep);
const error = new Error();
const watcher = fs.watch(testsubdir, { recursive: true, signal: AbortSignal.abort(error) });
await assert.rejects(async () => {
// eslint-disable-next-line no-unused-vars
for await (const _ of watcher);
}, { code: 'ABORT_ERR', cause: error });
})().then(common.mustCall());

(async function() {
// Test that with AbortController.
const testsubdir = await fs.mkdtemp(testDir + path.sep);
const file = '2.txt';
const filePath = path.join(testsubdir, file);
const error = new Error();
const ac = new AbortController();
const watcher = fs.watch(testsubdir, { recursive: true, signal: ac.signal });
let interval;
process.on('exit', function() {
assert.ok(interval === null, 'watcher Object was not closed');
});
process.nextTick(common.mustCall(() => {
interval = setInterval(() => {
fsSync.writeFileSync(filePath, 'world');
}, 50);
ac.abort(error);
}));
await assert.rejects(async () => {
for await (const { eventType } of watcher) {
assert.ok(eventType === 'change' || eventType === 'rename');
}
}, { code: 'ABORT_ERR', cause: error });
clearInterval(interval);
interval = null;
})().then(common.mustCall());
100 changes: 100 additions & 0 deletions test/parallel/test-fs-watch-recursive-symlink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

// fs-watch on folders have limited capability in AIX.
// The testcase makes use of folder watching, and causes
// hang. This behavior is documented. Skip this for AIX.

if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');

const assert = require('assert');
const path = require('path');
const fs = require('fs');

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

(async () => {
// Add a recursive symlink to the parent folder

const testDirectory = fs.mkdtempSync(testDir + path.sep);

// Do not use `testDirectory` as base. It will hang the tests.
const rootDirectory = path.join(testDirectory, 'test-1');
fs.mkdirSync(rootDirectory);

const filePath = path.join(rootDirectory, 'file.txt');

const symlinkFolder = path.join(rootDirectory, 'symlink-folder');
fs.symlinkSync(rootDirectory, symlinkFolder);


const watcher = fs.watch(rootDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename', `Received ${event}`);
assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(filePath), `Received ${filename}`);

if (filename === path.basename(filePath)) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.writeFileSync(filePath, 'world');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// This test checks how a symlink to outside the tracking folder can trigger change
// tmp/sub-directory/tracking-folder/symlink-folder -> tmp/sub-directory

const rootDirectory = fs.mkdtempSync(testDir + path.sep);

const subDirectory = path.join(rootDirectory, 'sub-directory');
fs.mkdirSync(subDirectory);

const trackingSubDirectory = path.join(subDirectory, 'tracking-folder');
fs.mkdirSync(trackingSubDirectory);

const symlinkFolder = path.join(trackingSubDirectory, 'symlink-folder');
fs.symlinkSync(subDirectory, symlinkFolder);

const forbiddenFile = path.join(subDirectory, 'forbidden.txt');
const acceptableFile = path.join(trackingSubDirectory, 'acceptable.txt');

const watcher = fs.watch(trackingSubDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
// macOS will only change the following events:
// { event: 'rename', filename: 'symlink-folder' }
// { event: 'rename', filename: 'acceptable.txt' }
assert.ok(event === 'rename', `Received ${event}`);
assert.ok(filename === path.basename(symlinkFolder) || filename === path.basename(acceptableFile), `Received ${filename}`);

if (filename === path.basename(acceptableFile)) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.writeFileSync(forbiddenFile, 'world');
await setTimeout(common.platformTimeout(100));
fs.writeFileSync(acceptableFile, 'acceptable');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());
245 changes: 209 additions & 36 deletions test/parallel/test-fs-watch-recursive.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,227 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

// fs-watch on folders have limited capability in AIX.
// The testcase makes use of folder watching, and causes
// hang. This behavior is documented. Skip this for AIX.

if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');

const assert = require('assert');
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');

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

const testDir = tmpdir.path;
const filenameOne = 'watch.txt';

tmpdir.refresh();

const testsubdir = fs.mkdtempSync(testDir + path.sep);
const relativePathOne = path.join(path.basename(testsubdir), filenameOne);
const filepathOne = path.join(testsubdir, filenameOne);
(async () => {
// Add a file to already watching folder

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-1');
fs.mkdirSync(testDirectory);

const testFile = path.join(testDirectory, 'file-1.txt');

const watcher = fs.watch(testDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename');

if (filename === path.basename(testFile)) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.writeFileSync(testFile, 'world');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// Add a folder to already watching folder

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-2');
fs.mkdirSync(testDirectory);

const testFile = path.join(testDirectory, 'folder-2');

const watcher = fs.watch(testDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename');

if (filename === path.basename(testFile)) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.mkdirSync(testFile);

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// Add a file to newly created folder to already watching folder

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-3');
fs.mkdirSync(testDirectory);

const filePath = path.join(testDirectory, 'folder-3');

const childrenFile = 'file-4.txt';
const childrenAbsolutePath = path.join(filePath, childrenFile);
const childrenRelativePath = path.join(path.basename(filePath), childrenFile);

const watcher = fs.watch(testDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename');
assert.ok(filename === path.basename(filePath) || filename === childrenRelativePath);

if (filename === childrenRelativePath) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.mkdirSync(filePath);
await setTimeout(common.platformTimeout(100));
fs.writeFileSync(childrenAbsolutePath, 'world');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// Add a file to subfolder of a watching folder

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-4');
fs.mkdirSync(testDirectory);

const file = 'folder-5';
const filePath = path.join(testDirectory, file);
fs.mkdirSync(filePath);

const subfolderPath = path.join(filePath, 'subfolder-6');
fs.mkdirSync(subfolderPath);

const childrenFile = 'file-7.txt';
const childrenAbsolutePath = path.join(subfolderPath, childrenFile);
const relativePath = path.join(file, path.basename(subfolderPath), childrenFile);

const watcher = fs.watch(testDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename');

if (filename === relativePath) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.writeFileSync(childrenAbsolutePath, 'world');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// Add a file to already watching folder, and use URL as the path

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-5');
fs.mkdirSync(testDirectory);

const filePath = path.join(testDirectory, 'file-8.txt');
const url = pathToFileURL(testDirectory);

const watcher = fs.watch(url, { recursive: true });
let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'rename');

if (filename === path.basename(filePath)) {
watcher.close();
watcherClosed = true;
}
});

await setTimeout(common.platformTimeout(100));
fs.writeFileSync(filePath, 'world');

process.on('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());

(async () => {
// Watch a file (not a folder) using fs.watch

const rootDirectory = fs.mkdtempSync(testDir + path.sep);
const testDirectory = path.join(rootDirectory, 'test-6');
fs.mkdirSync(testDirectory);

const filePath = path.join(testDirectory, 'only-file.txt');
fs.writeFileSync(filePath, 'hello');

const watcher = fs.watch(filePath, { recursive: true });
let watcherClosed = false;
let interval;
watcher.on('change', function(event, filename) {
assert.ok(event === 'change');

if (filename === path.basename(filePath)) {
clearInterval(interval);
interval = null;
watcher.close();
watcherClosed = true;
}
});

if (!common.isOSX && !common.isWindows) {
assert.throws(() => { fs.watch(testDir, { recursive: true }); },
{ code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM' });
return;
}
const watcher = fs.watch(testDir, { recursive: true });
interval = setInterval(() => {
fs.writeFileSync(filePath, 'world');
}, common.platformTimeout(10));

let watcherClosed = false;
watcher.on('change', function(event, filename) {
assert.ok(event === 'change' || event === 'rename');
process.on('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
assert.ok(interval === null, 'interval should have been null');
});
})().then(common.mustCall());

// Ignore stale events generated by mkdir and other tests
if (filename !== relativePathOne)
return;
(async () => {
// Handle non-boolean values for options.recursive

if (common.isOSX) {
clearInterval(interval);
if (!common.isWindows && !common.isOSX) {
assert.throws(() => {
const testsubdir = fs.mkdtempSync(testDir + path.sep);
fs.watch(testsubdir, { recursive: '1' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
});
}
watcher.close();
watcherClosed = true;
});

let interval;
if (common.isOSX) {
interval = setInterval(function() {
fs.writeFileSync(filepathOne, 'world');
}, 10);
} else {
fs.writeFileSync(filepathOne, 'world');
}

process.on('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
})().then(common.mustCall());
4 changes: 2 additions & 2 deletions test/parallel/test-fs-watchfile.js
Original file line number Diff line number Diff line change
@@ -90,9 +90,9 @@ if (common.isLinux || common.isOSX || common.isWindows) {
fs.mkdir(dir, common.mustCall(function(err) {
if (err) assert.fail(err);

fs.watch(dir, common.mustCall(function(eventType, filename) {
const handle = fs.watch(dir, common.mustCall(function(eventType, filename) {
clearInterval(interval);
this._handle.close();
handle.close();
assert.strictEqual(filename, 'foo.txt');
}));

72 changes: 38 additions & 34 deletions test/sequential/test-fs-watch.js
Original file line number Diff line number Diff line change
@@ -124,41 +124,45 @@ function repeat(fn) {
// Whitebox test to ensure that wrapped FSEvent is safe
// https://github.com/joyent/node/issues/6690
{
let oldhandle;
assert.throws(
() => {
const w = fs.watch(__filename, common.mustNotCall());
oldhandle = w._handle;
w._handle = { close: w._handle.close };
w.close();
},
{
name: 'Error',
code: 'ERR_INTERNAL_ASSERTION',
message: /^handle must be a FSEvent/,
}
);
oldhandle.close(); // clean up
if (common.isOSX || common.isWindows) {
let oldhandle;
assert.throws(
() => {
const w = fs.watch(__filename, common.mustNotCall());
oldhandle = w._handle;
w._handle = { close: w._handle.close };
w.close();
},
{
name: 'Error',
code: 'ERR_INTERNAL_ASSERTION',
message: /^handle must be a FSEvent/,
}
);
oldhandle.close(); // clean up
}
}

{
let oldhandle;
assert.throws(
() => {
const w = fs.watch(__filename, common.mustNotCall());
oldhandle = w._handle;
const protoSymbols =
Object.getOwnPropertySymbols(Object.getPrototypeOf(w));
const kFSWatchStart =
protoSymbols.find((val) => val.toString() === 'Symbol(kFSWatchStart)');
w._handle = {};
w[kFSWatchStart]();
},
{
name: 'Error',
code: 'ERR_INTERNAL_ASSERTION',
message: /^handle must be a FSEvent/,
}
);
oldhandle.close(); // clean up
if (common.isOSX || common.isWindows) {
let oldhandle;
assert.throws(
() => {
const w = fs.watch(__filename, common.mustNotCall());
oldhandle = w._handle;
const protoSymbols =
Object.getOwnPropertySymbols(Object.getPrototypeOf(w));
const kFSWatchStart =
protoSymbols.find((val) => val.toString() === 'Symbol(kFSWatchStart)');
w._handle = {};
w[kFSWatchStart]();
},
{
name: 'Error',
code: 'ERR_INTERNAL_ASSERTION',
message: /^handle must be a FSEvent/,
}
);
oldhandle.close(); // clean up
}
}

0 comments on commit 34bfef9

Please sign in to comment.