Skip to content

Commit

Permalink
Fix renames of files that existed before we started watching on macOS (
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Feb 14, 2021
1 parent 5bfafd1 commit cca5ca3
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 50 deletions.
21 changes: 8 additions & 13 deletions src/macos/FSEventsBackend.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,29 +71,24 @@ void FSEventsCallback(
list->update(paths[i]);
} else {
// If multiple flags were set, then we need to call `stat` to determine if the file really exists.
// We also check our local cache of recently modified files to see if we knew about it before.
// This helps disambiguate creates, updates, and deletes.
auto existed = !since && state->tree->find(paths[i]);
struct stat file;
if (stat(paths[i], &file)) {
// File does not exist now. If it existed before in our local cache,
// or was removed/renamed and we don't have a cache (querying since a snapshot)
// then the file was probably removed. This is not exact since the flags set by
// fsevents get coalesced together (e.g. created & deleted), so there is no way to
// File does not exist, so we have to assume it was removed. This is not exact since the
// flags set by fsevents get coalesced together (e.g. created & deleted), so there is no way to
// know whether the create and delete both happened since our snapshot (in which case
// we'd rather ignore this event completely). This will result in some extra delete events
// being emitted for files we don't know about, but that is the best we can do.
if (existed || (since && (isRemoved || isRenamed))) {
state->tree->remove(paths[i]);
list->remove(paths[i]);
}

state->tree->remove(paths[i]);
list->remove(paths[i]);
continue;
}

// If the file was modified, and existed before, then this is an update, otherwise a create.
uint64_t mtime = CONVERT_TIME(file.st_birthtimespec);
if (isModified && (existed || mtime <= since)) {
uint64_t ctime = CONVERT_TIME(file.st_birthtimespec);
uint64_t mtime = CONVERT_TIME(file.st_mtimespec);
auto existed = !since && state->tree->find(paths[i]);
if (isModified && (existed || ctime <= since)) {
state->tree->update(paths[i], mtime);
list->update(paths[i]);
} else {
Expand Down
9 changes: 7 additions & 2 deletions test/since.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,20 @@ function testPrecision() {
const isSecondPrecision = testPrecision();

describe('since', () => {
const sleep = (ms = 20) => new Promise(resolve => setTimeout(resolve, ms));

before(async () => {
// wait for tmp dir to be created.
await sleep();
});

after(async () => {
try {
await fs.unlink(snapshotPath);
} catch (err) {}
});

backends.forEach(backend => {
const sleep = (ms = 10) => new Promise(resolve => setTimeout(resolve, ms));

describe(backend, () => {
describe('files', () => {
it('should emit when a file is created', async () => {
Expand Down
96 changes: 61 additions & 35 deletions test/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('watcher', () => {
.toString(31)
.slice(2)}`,
);
let ignoreDir, ignoreFile, sub;
let ignoreDir, ignoreFile, fileToRename, dirToRename, sub;

before(async () => {
tmpDir = path.join(
Expand All @@ -60,6 +60,10 @@ describe('watcher', () => {
fs.mkdirpSync(tmpDir);
ignoreDir = getFilename();
ignoreFile = getFilename();
fileToRename = getFilename();
dirToRename = getFilename();
fs.writeFileSync(fileToRename, 'hi');
fs.mkdirpSync(dirToRename);
await new Promise(resolve => setTimeout(resolve, 100));
sub = await watcher.subscribe(tmpDir, fn, {
backend,
Expand Down Expand Up @@ -103,6 +107,16 @@ describe('watcher', () => {
]);
});

it('should emit when an existing file is renamed', async () => {
let f2 = getFilename();
fs.rename(fileToRename, f2);
let res = await nextEvent();
assert.deepEqual(res, [
{type: 'delete', path: fileToRename},
{type: 'create', path: f2},
]);
});

it('should emit when a file is deleted', async () => {
let f = getFilename();
fs.writeFile(f, 'hello world');
Expand Down Expand Up @@ -137,6 +151,16 @@ describe('watcher', () => {
]);
});

it('should emit when an existing directory is renamed', async () => {
let f2 = getFilename();
fs.rename(dirToRename, f2);
let res = await nextEvent();
assert.deepEqual(res, [
{type: 'create', path: f2},
{type: 'delete', path: dirToRename},
]);
});

it('should emit when a directory is deleted', async () => {
let f = getFilename();
fs.mkdir(f);
Expand Down Expand Up @@ -321,17 +345,6 @@ describe('watcher', () => {
});

describe('rapid changes', () => {
it('should ignore files that are created and deleted rapidly', async () => {
let f1 = getFilename();
let f2 = getFilename();
await fs.writeFile(f1, 'hello world');
await fs.writeFile(f2, 'hello world');
fs.unlink(f2);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f1}]);
});

it('should coalese create and update events', async () => {
let f1 = getFilename();
await fs.writeFile(f1, 'hello world');
Expand All @@ -341,29 +354,42 @@ describe('watcher', () => {
assert.deepEqual(res, [{type: 'create', path: f1}]);
});

it('should coalese create and rename events', async () => {
let f1 = getFilename();
let f2 = getFilename();
await fs.writeFile(f1, 'hello world');
fs.rename(f1, f2);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f2}]);
});

it('should coalese multiple rename events', async () => {
let f1 = getFilename();
let f2 = getFilename();
let f3 = getFilename();
let f4 = getFilename();
await fs.writeFile(f1, 'hello world');
await fs.rename(f1, f2);
await fs.rename(f2, f3);
fs.rename(f3, f4);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f4}]);
});
if (backend !== 'fs-events') {
it('should ignore files that are created and deleted rapidly', async () => {
let f1 = getFilename();
let f2 = getFilename();
await fs.writeFile(f1, 'hello world');
await fs.writeFile(f2, 'hello world');
fs.unlink(f2);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f1}]);
});

it('should coalese create and rename events', async () => {
let f1 = getFilename();
let f2 = getFilename();
await fs.writeFile(f1, 'hello world');
fs.rename(f1, f2);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f2}]);
});

it('should coalese multiple rename events', async () => {
let f1 = getFilename();
let f2 = getFilename();
let f3 = getFilename();
let f4 = getFilename();
await fs.writeFile(f1, 'hello world');
await fs.rename(f1, f2);
await fs.rename(f2, f3);
fs.rename(f3, f4);

let res = await nextEvent();
assert.deepEqual(res, [{type: 'create', path: f4}]);
});
}

it('should coalese multiple update events', async () => {
let f1 = getFilename();
Expand Down

0 comments on commit cca5ca3

Please sign in to comment.