Skip to content

Add support for fs.watch(). #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
14 changes: 13 additions & 1 deletion lib/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ Binding.prototype.open = function(pathname, flags, mode, callback, ctx) {

return maybeCallback(normalizeCallback(callback), ctx, this, function() {
pathname = deBuffer(pathname);
const descriptor = new FileDescriptor(flags);
const descriptor = new FileDescriptor(flags, pathname);
let item = this._system.getItem(pathname);
while (item instanceof SymbolicLink) {
item = this._system.getItem(
Expand All @@ -553,6 +553,7 @@ Binding.prototype.open = function(pathname, flags, mode, callback, ctx) {
item.setMode(mode);
}
parent.addItem(path.basename(pathname), item);
parent.notifyRename(pathname);
}
if (descriptor.isRead()) {
if (!item) {
Expand All @@ -576,6 +577,7 @@ Binding.prototype.open = function(pathname, flags, mode, callback, ctx) {
throw new FSError('EBADF');
}
item.setContent('');
item.notifyChange(pathname);
}
if (descriptor.isTruncate() || descriptor.isAppend()) {
descriptor.setPosition(item.getContent().length);
Expand Down Expand Up @@ -748,6 +750,7 @@ Binding.prototype.writeBuffers = function(
const written = newContent.copy(content, position);
file.setContent(content);
descriptor.setPosition(newLength);
file.notifyChange(descriptor.getPath());
return written;
});
};
Expand Down Expand Up @@ -805,6 +808,7 @@ Binding.prototype.writeBuffer = function(
);
file.setContent(content);
descriptor.setPosition(newLength);
file.notifyChange(descriptor.getPath());
return written;
});
};
Expand Down Expand Up @@ -906,6 +910,9 @@ Binding.prototype.rename = function(oldPath, newPath, callback, ctx) {
}
oldParent.removeItem(oldName);
newParent.addItem(newName, oldItem);
oldItem.notifyRename(newPath);
oldParent.notifyRename(oldPath);
newParent.notifyRename(newPath);
});
};

Expand Down Expand Up @@ -1021,6 +1028,8 @@ Binding.prototype.mkdir = function(pathname, mode, recursive, callback, ctx) {
}.bind(this);

_mkdir(pathname);

// TODO: Should we generate "rename" watch events? If so, for which items?
});
};

Expand Down Expand Up @@ -1139,6 +1148,7 @@ Binding.prototype.ftruncate = function(fd, len, callback, ctx) {
const newContent = bufferAlloc(len);
content.copy(newContent);
file.setContent(newContent);
file.notifyChange(descriptor.getPath());
});
};

Expand Down Expand Up @@ -1352,6 +1362,7 @@ Binding.prototype.link = function(srcPath, destPath, callback, ctx) {
throw new FSError('ENOTDIR', destPath);
}
parent.addItem(path.basename(destPath), item);
parent.notifyRename(destPath);
});
};

Expand Down Expand Up @@ -1382,6 +1393,7 @@ Binding.prototype.symlink = function(srcPath, destPath, type, callback, ctx) {
const link = new SymbolicLink();
link.setPath(srcPath);
parent.addItem(path.basename(destPath), link);
parent.notifyRename(destPath);
});
};

Expand Down
17 changes: 16 additions & 1 deletion lib/descriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ const constants = require('constants');
/**
* Create a new file descriptor.
* @param {number} flags Flags.
* @param {string|undefined} path File path that was used to open this descriptor, if available.
* @constructor
*/
function FileDescriptor(flags) {
function FileDescriptor(flags, path) {
/**
* Flags.
* @type {number}
*/
this._flags = flags;

/**
* Path that this descriptor was opened against, if available.
* @type {string|undefined}
*/
this._path = path;

/**
* File system item.
* @type {Item}
Expand Down Expand Up @@ -43,6 +50,14 @@ FileDescriptor.prototype.getItem = function() {
return this._item;
};

/**
* Get the path that this descriptor was opened against. May be undefined.
* @return {string|undefined} File path.
*/
FileDescriptor.prototype.getPath = function() {
return this._path;
};

/**
* Get the current file position.
* @return {number} File position.
Expand Down
93 changes: 93 additions & 0 deletions lib/fsevent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

const FSError = require('./error');

/**
* Fake binding to replace fs_event_wrap with.
* @param {FileSystem} system Mock file system.
* @constructor
*/
function FSEventWrapBinding(system) {
// Note that _system here is effectively global. I don't see an easier way to do this though.
this.FSEvent._system = system;
}

/**
* Construct a new FSEvent.
* @constructor
*/
function FSEvent() {
/**
* Item we are watching.
* @type {Item}
*/
this._item = null;

/**
* Listener function that is currently subscribed (so we can unsubscribe it).
* @type {function}
*/
this._listener = null;

/**
* Whether we are currently watching. Checked by the FSWatcher implementation.
* @type {boolean}
*/
this.initialized = false;
}

/**
* Handler for change events for the item we are tracking.
* This will be replaced with a function from Node's internal/fs/watchers.js.
*/
FSEvent.prototype.onchange = function(status, eventType, filename) {};

/**
* Refcounting. Ignored because we aren't wrapping a native object.
*/
FSEvent.prototype.ref = function() {};

/**
* Refcounting. Ignored because we aren't wrapping a native object.
*/
FSEvent.prototype.unref = function() {};

/**
* Start watching a file. This function is called by fs.watch() internally, by way of the
* FSWatcher.prototype[kFSWatchStart] function.
*/
FSEvent.prototype.start = function(filepath, persistent, recursive, encoding) {
if (recursive) {
// TODO: Can we throw the correct Node internal error type here? Does it matter?
throw new FSError('Recursive watch is not supported');
}
// TODO: Implement persistent? All watchers are currently "persistent" as long as we are mocked
// TODO: Implement encoding='buffer' case
const item = FSEvent._system.getItem(filepath);
if (!item) {
throw new FSError('ENOENT', filepath);
}
if (!item.canRead()) {
throw new FSError('EACCES', filepath);
}
this._item = item;
const onchange = this.onchange;
this._listener = function(eventType, filename) {
// TODO: The stats parameter here is wrong, but fs.watch() doesn't use it
onchange(1, eventType, filename);
};
this._item.getWatcher().addListener('change', this._listener);
this.initialized = true;
};

FSEvent.prototype.close = function() {
this._item.getWatcher().removeListener('change', this._listener);
};

FSEventWrapBinding.prototype.FSEvent = FSEvent;

/**
* Export the FSEventWrapBinding constructor.
* @type {function()}
*/
exports = module.exports = FSEventWrapBinding;
23 changes: 23 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

const Binding = require('./binding');
const FSError = require('./error');
const FSEventWrapBinding = require('./fsevent');
const FileSystem = require('./filesystem');
const realBinding = process.binding('fs');
const realFSEventWrapBinding = process.binding('fs_event_wrap');
const path = require('path');
const loader = require('./loader');
const bypass = require('./bypass');
Expand Down Expand Up @@ -50,10 +52,24 @@ for (const key in Binding.prototype) {
}
}

// Pre-patch fs_event_wrap binding.
realFSEventWrapBinding.FSEvent = new Proxy(Object, {
construct: function(target, args) {
if (realFSEventWrapBinding._mockedBinding) {
return new realFSEventWrapBinding._mockedBinding.FSEvent();
}
return new realFSEventWrapBinding.FSEvent();
}
});

function overrideBinding(binding) {
realBinding._mockedBinding = binding;
}

function overrideFSEventWrapBinding(fsEventWrapBinding) {
realFSEventWrapBinding._mockedBinding = fsEventWrapBinding;
}

function overrideProcess(cwd, chdir) {
process.cwd = cwd;
process.chdir = chdir;
Expand Down Expand Up @@ -100,6 +116,10 @@ function restoreBinding() {
realBinding.StatWatcher = realStatWatcher;
}

function restoreFSEventWrapBinding() {
delete realFSEventWrapBinding._mockedBinding;
}

function restoreProcess() {
for (const key in realProcessProps) {
process[key] = realProcessProps[key];
Expand All @@ -122,8 +142,10 @@ function restoreCreateWriteStream() {
exports = module.exports = function mock(config, options) {
const system = FileSystem.create(config, options);
const binding = new Binding(system);
const fsEventWrapBinding = new FSEventWrapBinding(system);

overrideBinding(binding);
overrideFSEventWrapBinding(fsEventWrapBinding);

let currentPath = process.cwd();
overrideProcess(
Expand Down Expand Up @@ -165,6 +187,7 @@ exports.getMockRoot = function() {
*/
exports.restore = function() {
restoreBinding();
restoreFSEventWrapBinding();
restoreProcess();
restoreCreateWriteStream();
};
Expand Down
34 changes: 34 additions & 0 deletions lib/item.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const EventEmitter = require('events');

let counter = 0;

/**
Expand Down Expand Up @@ -35,6 +37,12 @@ function getGid() {
function Item() {
const now = Date.now();

/**
* Emitter for events on this Item.
* @type {EventEmitter}
*/
this._events = new EventEmitter();

/**
* Access time.
* @type {Date}
Expand Down Expand Up @@ -303,6 +311,32 @@ Item.prototype.getStats = function() {
};
};

/**
* Return an EventEmitter for events on this Item.
* @return {EventEmitter} Emitter of events.
*/
Item.prototype.getWatcher = function() {
return this._events;
};

/**
* Generate an event that this Item has changed.
* @param {string|undefined} filename The name of the file which triggered the event.
*/
Item.prototype.notifyChange = function(filename) {
const eventType = 'change';
this._events.emit('change', eventType, filename);
};

/**
* Generate an event that this Item (or a contained Item) has been renamed.
* @param {string|undefined} filename The name of the file which triggered the event.
*/
Item.prototype.notifyRename = function(filename) {
const eventType = 'rename';
this._events.emit('change', eventType, filename);
};

/**
* Get the item's string representation.
* @return {string} String representation.
Expand Down
11 changes: 11 additions & 0 deletions test/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,14 @@ exports.assertEqualPaths = function(actual, expected) {
chai.assert(actual, expected);
}
};

exports.assertEvent = function(action, emitter, eventName, ...args) {
const captured = [];
const listener = function() {
captured.push([...arguments]);
};
emitter.addListener(eventName, listener);
action();
emitter.removeListener(eventName, listener);
chai.assert.deepEqual([...args], captured[0]);
};
12 changes: 12 additions & 0 deletions test/lib/descriptor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ describe('FileDescriptor', function() {
});
});

describe('#getPath()', function() {
it('returns undefiend if no path was provided', function() {
const fd = new FileDescriptor(flags('r'));
assert.isUndefined(fd.getPath());
});

it('returns the path the descriptor was opened with', function() {
const fd = new FileDescriptor(flags('r'), '/path/to/file');
assert.equal(fd.getPath(), '/path/to/file');
});
});

describe('#getPosition()', function() {
it('returns zero by default', function() {
const fd = new FileDescriptor(flags('r'));
Expand Down
Loading