Skip to content

Commit d44a518

Browse files
committed
emit CREATED event for new directories
Most clients expect every new file from a new tree being added in the watched directory to be reported. This is currently not the case if the tree appears too quickly on disk. This commit propagates a callback down the recursive tree building to make sure that all files added are being reported as created to clients. I also added an environment variable check for use when testing: `NSFW_TEST_SLOW`. When set to `1` nsfw will artificially sleep before crawling folders on the fs. This is to simulate the case when nsfw is late to the party and sub-folders were created before we had time to allocate inotify watchers. In such a case, we need to manually emit CREATED events for new files and folders discovered by scandir. Signed-off-by: Paul Maréchal <[email protected]>
1 parent 928d9e1 commit d44a518

File tree

12 files changed

+199
-62
lines changed

12 files changed

+199
-62
lines changed

binding.gyp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
}
6363
}],
6464
["OS=='linux' or OS=='freebsd'", {
65+
"defines": [
66+
"NSFW_TEST_SLOW_<!(node -p process.env.NSFW_TEST_SLOW)"
67+
],
6568
"sources": [
6669
"src/linux/InotifyEventLoop.cpp",
6770
"src/linux/InotifyTree.cpp",
@@ -102,7 +105,7 @@
102105
],
103106
"libraries": [
104107
"-L/usr/local/lib",
105-
"-linotify"
108+
"-linotify"
106109
]
107110
}],
108111
]

includes/linux/InotifyTree.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@
1010
#include <vector>
1111
#include <map>
1212
#include <unordered_set>
13+
#include <functional>
14+
#include <chrono>
15+
#include <thread>
16+
17+
/**
18+
* void EmitCreatedEvent(std::string directory, std::string file);
19+
*/
20+
using EmitCreatedEvent = std::function<void(std::string, std::string)>;
1321

1422
class InotifyTree {
1523
public:
1624
InotifyTree(int inotifyInstance, std::string path);
1725

18-
void addDirectory(int wd, std::string name);
26+
void addDirectory(int wd, std::string name, EmitCreatedEvent emitCreatedEvent = nullptr);
1927
std::string getError();
2028
bool getPath(std::string &out, int wd);
2129
bool hasErrored();
@@ -34,10 +42,11 @@ class InotifyTree {
3442
InotifyNode *parent,
3543
std::string directory,
3644
std::string name,
37-
ino_t inodeNumber
45+
ino_t inodeNumber,
46+
EmitCreatedEvent emitCreatedEvent = nullptr
3847
);
3948

40-
void addChild(std::string name);
49+
void addChild(std::string name, EmitCreatedEvent emitCreatedEvent = nullptr);
4150
void fixPaths();
4251
std::string getFullPath();
4352
std::string getName();

js/scripts/test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable no-console */
2+
3+
const cp = require('child_process');
4+
5+
main().catch(error => {
6+
console.error(error);
7+
process.exitCode = 1;
8+
});
9+
10+
async function main() {
11+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
12+
// When ran as a npm script we can invoke npm bins such as node-gyp and mocha directly
13+
await exec('node-gyp rebuild', { env: { ...process.env, NSFW_TEST_SLOW: 1 } });
14+
await exec('mocha --expose-gc js/spec/index-slow-spec.js');
15+
await exec('node-gyp rebuild');
16+
}
17+
await exec('mocha --expose-gc js/spec/index-spec.js');
18+
}
19+
20+
/**
21+
* @param {string} commandline ...
22+
* @param {cp.SpawnOptions} options ...
23+
* @returns {Promise<void>} ...
24+
*/
25+
function exec(commandline, options = {}) {
26+
return new Promise((resolve, reject) => {
27+
const command = cp.spawn(commandline, {
28+
shell: true,
29+
stdio: 'inherit',
30+
...options
31+
});
32+
command.on('error', reject);
33+
command.on('exit', (code, signal) => {
34+
if (code === 0) {
35+
resolve();
36+
} else {
37+
reject(new Error(`${JSON.stringify(commandline)} exited with ${signal || code}`));
38+
}
39+
});
40+
});
41+
}

js/spec/common.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const path = require('path');
2+
const util = require('util');
3+
4+
exports.DEBOUNCE = 1000;
5+
exports.TIMEOUT_PER_STEP = 3000;
6+
exports.WORKDIR = path.resolve(__dirname, '../../mockfs');
7+
8+
exports.sleep = util.promisify(setTimeout);

js/spec/index-slow-spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const assert = require('assert');
2+
const fse = require('fs-extra');
3+
4+
const { WORKDIR: workDir } = require('./common');
5+
6+
const nestedFileTest = require('./nested-file-test');
7+
8+
const nsfw = require('../src');
9+
10+
describe('Node Sentinel File Watcher (slow)', function() {
11+
this.timeout(120000);
12+
13+
assert.ok(nsfw._native.NSFW_TEST_SLOW === true, 'NSFW should be built in slow mode');
14+
15+
beforeEach(function() {
16+
return fse.mkdir(workDir, { recursive: true });
17+
});
18+
19+
afterEach(function() {
20+
return fse.remove(workDir);
21+
});
22+
23+
nestedFileTest();
24+
});

js/spec/index-spec.js

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,17 @@ const assert = require('assert');
22
const exec = require('executive');
33
const fse = require('fs-extra');
44
const path = require('path');
5-
const { promisify } = require('util');
65

7-
const nsfw = require('../src/');
6+
const { DEBOUNCE, TIMEOUT_PER_STEP, WORKDIR: workDir, sleep } = require('./common');
87

9-
const DEBOUNCE = 1000;
10-
const TIMEOUT_PER_STEP = 3000;
8+
const nestedFileTest = require('./nested-file-test');
119

12-
const sleep = promisify(setTimeout);
10+
const nsfw = require('../src');
1311

1412
describe('Node Sentinel File Watcher', function() {
1513
this.timeout(120000);
1614

17-
const workDir = path.resolve('./mockfs');
15+
assert.ok(typeof nsfw._native.NSFW_TEST_SLOW === 'undefined', 'NSFW should NOT be built in slow mode');
1816

1917
beforeEach(async function() {
2018
async function makeDir(identifier) {
@@ -438,49 +436,7 @@ describe('Node Sentinel File Watcher', function() {
438436
});
439437

440438
describe('Recursive', function() {
441-
it('can listen for the creation of a deeply nested file', async function() {
442-
const folders = 'a_very_deep_tree'.split(''); // ['a', '_', ...]
443-
const file = 'a_file.txt';
444-
445-
const events = new Map();
446-
let directory = workDir; for (const folder of folders) {
447-
events.set(directory, { file: folder, found: false });
448-
directory = path.join(directory, folder);
449-
}
450-
events.set(directory, { file, found: false });
451-
452-
function findEvent(element) {
453-
if (element.action === nsfw.actions.CREATED) {
454-
const item = events.get(element.directory);
455-
if (item === undefined || item.file !== element.file) {
456-
return;
457-
}
458-
item.found = true;
459-
}
460-
}
461-
462-
let watch = await nsfw(
463-
workDir,
464-
events => events.forEach(findEvent),
465-
{ debounceMS: DEBOUNCE }
466-
);
467-
468-
try {
469-
await watch.start();
470-
await sleep(TIMEOUT_PER_STEP);
471-
await fse.mkdirp(directory);
472-
await fse.close(await fse.open(path.join(directory, file), 'w'));
473-
await sleep(TIMEOUT_PER_STEP);
474-
475-
for (const [directory, item] of events.entries()) {
476-
assert.ok(item.found, `${path.join(directory, item.file)} wasn't found`);
477-
}
478-
} finally {
479-
await watch.stop();
480-
watch = null;
481-
}
482-
});
483-
439+
nestedFileTest();
484440
it('can listen for the destruction of a directory and its subtree', async function() {
485441
const inPath = path.resolve(workDir, 'test4');
486442
let deletionCount = 0;

js/spec/nested-file-test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const path = require('path');
2+
const assert = require('assert');
3+
const fse = require('fs-extra');
4+
5+
const { DEBOUNCE, TIMEOUT_PER_STEP, WORKDIR: workDir, sleep } = require('./common');
6+
7+
const nsfw = require('../src');
8+
9+
/**
10+
* Run this function to register the test
11+
* @returns {void}
12+
*/
13+
module.exports = function() {
14+
it('can listen for the creation of a deeply nested file', async function () {
15+
const folders = 'a_very_deep_tree'.split(''); // ['a', '_', ...]
16+
const file = 'a_file.txt';
17+
18+
// Resolve a promise as soon as all events have been found
19+
let done; const promise = new Promise(resolve => {
20+
done = resolve;
21+
});
22+
23+
/**
24+
* We will remove or pop entries as we find them.
25+
* This set should be empty at the end of the test.
26+
* @type {Set<string>}
27+
*/
28+
const events = new Set();
29+
let directory = workDir; for (const folder of folders) {
30+
directory = path.join(directory, folder);
31+
events.add(directory);
32+
}
33+
const filePath = path.join(directory, file);
34+
events.add(filePath);
35+
36+
function findEvent(element) {
37+
if (element.action === nsfw.actions.CREATED) {
38+
const file = path.join(element.directory, element.file);
39+
if (events.has(file)) {
40+
events.delete(file);
41+
if (events.size === 0) {
42+
done();
43+
}
44+
}
45+
}
46+
}
47+
48+
let watch = await nsfw(
49+
workDir,
50+
events => events.forEach(findEvent),
51+
{ debounceMS: DEBOUNCE }
52+
);
53+
54+
try {
55+
await watch.start();
56+
await sleep(TIMEOUT_PER_STEP);
57+
await fse.mkdirp(directory);
58+
await fse.close(await fse.open(filePath, 'w'));
59+
await Promise.race([
60+
sleep(folders.length * 500 * 1.5),
61+
promise,
62+
]);
63+
64+
// Make sure that we got the events for each nested directory
65+
assert.ok(events.size === 0, `Some files were not detected:\n${
66+
Array.from(events, file => `- ${file}`).join('\n')
67+
}`);
68+
} finally {
69+
await watch.stop();
70+
watch = null;
71+
}
72+
});
73+
};

js/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ nsfw.actions = {
102102
RENAMED: 3
103103
};
104104

105+
nsfw._native = NSFW;
106+
105107
if (NSFW.getAllocatedInstanceCount) {
106108
nsfw.getAllocatedInstanceCount = NSFW.getAllocatedInstanceCount;
107109
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "js/src/index.js",
66
"scripts": {
77
"lint": "eslint js/src js/spec",
8-
"test": "yarn lint && mocha --expose-gc js/spec"
8+
"test": "yarn lint && node js/scripts/test.js"
99
},
1010
"repository": {
1111
"type": "git",

src/NSFW.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@ Napi::Object NSFW::Init(Napi::Env env, Napi::Object exports) {
339339
));
340340
}
341341

342+
#ifdef NSFW_TEST_SLOW_1
343+
nsfwConstructor.DefineProperty(Napi::PropertyDescriptor::Value(
344+
"NSFW_TEST_SLOW",
345+
Napi::Boolean::New(env, true)
346+
));
347+
#endif
348+
342349
constructor = Napi::Persistent(nsfwConstructor);
343350
constructor.SuppressDestruct();
344351

src/linux/InotifyService.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ void InotifyService::createDirectory(int wd, std::string name) {
9898
return;
9999
}
100100

101-
mTree->addDirectory(wd, name);
102101
dispatch(CREATED, wd, name);
102+
mTree->addDirectory(wd, name, [this](std::string directory, std::string file) {
103+
mQueue->enqueue(CREATED, directory, file);
104+
});
103105
}
104106

105107
void InotifyService::removeDirectory(int wd) {

0 commit comments

Comments
 (0)