Skip to content

Commit 94163de

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 94163de

File tree

12 files changed

+176
-62
lines changed

12 files changed

+176
-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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const cp = require('child_process');
2+
3+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
4+
// When ran as a npm script we can invoke npm bins such as node-gyp and mocha directly
5+
exec('node-gyp rebuild', { env: { ...process.env, NSFW_TEST_SLOW: 1 } });
6+
exec('mocha --exit --expose-gc js/spec/index-slow-spec.js');
7+
exec('node-gyp rebuild');
8+
}
9+
exec('mocha --exit --expose-gc js/spec/index-spec.js');
10+
11+
/**
12+
* @param {string} commandline ...
13+
* @param {cp.ExecSyncOptions} options ...
14+
* @returns {Promise<void>} ...
15+
*/
16+
function exec(commandline, options = {}) {
17+
cp.execSync(commandline, { stdio: 'inherit', ...options });
18+
}

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) {

src/linux/InotifyTree.cpp

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#include "../../includes/linux/InotifyTree.h"
2+
#include <cstdio>
3+
#include <sys/stat.h>
24
/**
35
* InotifyTree ---------------------------------------------------------------------------------------------------------
46
*/
57
InotifyTree::InotifyTree(int inotifyInstance, std::string path):
68
mError(""),
79
mInotifyInstance(inotifyInstance) {
810
mInotifyNodeByWatchDescriptor = new std::map<int, InotifyNode *>;
9-
1011
std::string directory;
1112
std::string watchName;
1213
if (path.length() == 1 && path[0] == '/') {
@@ -41,14 +42,14 @@ InotifyTree::InotifyTree(int inotifyInstance, std::string path):
4142
}
4243
}
4344

44-
void InotifyTree::addDirectory(int wd, std::string name) {
45+
void InotifyTree::addDirectory(int wd, std::string name, EmitCreatedEvent emitCreatedEvent) {
4546
auto nodeIterator = mInotifyNodeByWatchDescriptor->find(wd);
4647
if (nodeIterator == mInotifyNodeByWatchDescriptor->end()) {
4748
return;
4849
}
4950

5051
InotifyNode *node = nodeIterator->second;
51-
node->addChild(name);
52+
node->addChild(name, emitCreatedEvent);
5253
}
5354

5455
void InotifyTree::addNodeReferenceByWD(int wd, InotifyNode *node) {
@@ -159,7 +160,8 @@ InotifyTree::InotifyNode::InotifyNode(
159160
InotifyNode *parent,
160161
std::string directory,
161162
std::string name,
162-
ino_t inodeNumber
163+
ino_t inodeNumber,
164+
EmitCreatedEvent emitCreatedEvent
163165
):
164166
mDirectory(directory),
165167
mInodeNumber(inodeNumber),
@@ -171,6 +173,10 @@ InotifyTree::InotifyNode::InotifyNode(
171173
mFullPath = createFullPath(mDirectory, mName);
172174
mWatchDescriptorInitialized = false;
173175

176+
#ifdef NSFW_TEST_SLOW_1
177+
std::this_thread::sleep_for(std::chrono::milliseconds(500));
178+
#endif
179+
174180
if (!inotifyInit()) {
175181
return;
176182
}
@@ -200,6 +206,10 @@ InotifyTree::InotifyNode::InotifyNode(
200206
continue;
201207
}
202208

209+
if (emitCreatedEvent) {
210+
emitCreatedEvent(mFullPath, fileName);
211+
}
212+
203213
std::string filePath = createFullPath(mFullPath, fileName);
204214

205215
struct stat file;
@@ -218,7 +228,8 @@ InotifyTree::InotifyNode::InotifyNode(
218228
this,
219229
mFullPath,
220230
fileName,
221-
file.st_ino
231+
file.st_ino,
232+
emitCreatedEvent
222233
);
223234

224235
if (child->isAlive()) {
@@ -250,7 +261,7 @@ InotifyTree::InotifyNode::~InotifyNode() {
250261
delete mChildren;
251262
}
252263

253-
void InotifyTree::InotifyNode::addChild(std::string name) {
264+
void InotifyTree::InotifyNode::addChild(std::string name, EmitCreatedEvent emitCreatedEvent) {
254265
struct stat file;
255266

256267
if (stat(createFullPath(mFullPath, name).c_str(), &file) >= 0 && mTree->addInode(file.st_ino)) {
@@ -260,7 +271,8 @@ void InotifyTree::InotifyNode::addChild(std::string name) {
260271
this,
261272
mFullPath,
262273
name,
263-
file.st_ino
274+
file.st_ino,
275+
emitCreatedEvent
264276
);
265277

266278
if (child->isAlive()) {

0 commit comments

Comments
 (0)