mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge pull request #485 from github/node-pathwatcher
Use node-pathwatcher to replace $native.watchPath.
This commit is contained in:
2
atom.gyp
2
atom.gyp
@@ -251,8 +251,6 @@
|
||||
'native/message_translation.cpp',
|
||||
'native/message_translation.h',
|
||||
'native/message_translation.h',
|
||||
'native/path_watcher.h',
|
||||
'native/path_watcher.mm',
|
||||
'native/v8_extensions/atom.h',
|
||||
'native/v8_extensions/atom.mm',
|
||||
'native/v8_extensions/native.h',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#import "native/v8_extensions/atom.h"
|
||||
#import "native/v8_extensions/native.h"
|
||||
#import "native/message_translation.h"
|
||||
#import "path_watcher.h"
|
||||
#import "atom_cef_render_process_handler.h"
|
||||
|
||||
|
||||
@@ -18,7 +17,6 @@ void AtomCefRenderProcessHandler::OnContextCreated(CefRefPtr<CefBrowser> browser
|
||||
void AtomCefRenderProcessHandler::OnContextReleased(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
CefRefPtr<CefV8Context> context) {
|
||||
[PathWatcher removePathWatcherForContext:context];
|
||||
}
|
||||
|
||||
bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#import "include/cef_base.h"
|
||||
#import "include/cef_v8.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef void (^WatchCallback)(NSString *, NSString *);
|
||||
|
||||
@interface PathWatcher : NSObject {
|
||||
int _kq;
|
||||
CefRefPtr<CefV8Context> _context;
|
||||
NSMutableDictionary *_callbacksByPath;
|
||||
NSMutableDictionary *_fileDescriptorsByPath;
|
||||
|
||||
bool _keepWatching;
|
||||
}
|
||||
|
||||
+ (PathWatcher *)pathWatcherForContext:(CefRefPtr<CefV8Context>)context;
|
||||
+ (void)removePathWatcherForContext:(CefRefPtr<CefV8Context>)context;
|
||||
|
||||
- (id)initWithContext:(CefRefPtr<CefV8Context>)context;
|
||||
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback;
|
||||
- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error;
|
||||
- (void)unwatchAllPaths;
|
||||
- (NSArray *)watchedPaths;
|
||||
|
||||
@end
|
||||
@@ -1,273 +0,0 @@
|
||||
#import <sys/event.h>
|
||||
#import <sys/time.h>
|
||||
#import <sys/param.h>
|
||||
#import <fcntl.h>
|
||||
|
||||
#import "path_watcher.h"
|
||||
|
||||
static NSMutableArray *gPathWatchers;
|
||||
|
||||
@interface PathWatcher ()
|
||||
- (bool)usesContext:(CefRefPtr<CefV8Context>)context;
|
||||
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId;
|
||||
- (void)stopWatching;
|
||||
- (bool)isAtomicWrite:(struct kevent)event;
|
||||
@end
|
||||
|
||||
@implementation PathWatcher
|
||||
|
||||
+ (PathWatcher *)pathWatcherForContext:(CefRefPtr<CefV8Context>)context {
|
||||
if (!gPathWatchers) gPathWatchers = [[NSMutableArray alloc] init];
|
||||
|
||||
PathWatcher *pathWatcher = nil;
|
||||
for (PathWatcher *p in gPathWatchers) {
|
||||
if ([p usesContext:context]) {
|
||||
pathWatcher = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathWatcher) {
|
||||
pathWatcher = [[[PathWatcher alloc] initWithContext:context] autorelease];
|
||||
[gPathWatchers addObject:pathWatcher];
|
||||
}
|
||||
|
||||
return pathWatcher;
|
||||
}
|
||||
|
||||
+ (void)removePathWatcherForContext:(CefRefPtr<CefV8Context>)context {
|
||||
PathWatcher *pathWatcher = nil;
|
||||
for (PathWatcher *p in gPathWatchers) {
|
||||
if ([p usesContext:context]) {
|
||||
pathWatcher = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathWatcher) {
|
||||
[pathWatcher stopWatching];
|
||||
[gPathWatchers removeObject:pathWatcher];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
@synchronized(self) {
|
||||
close(_kq);
|
||||
for (NSString *path in [_callbacksByPath allKeys]) {
|
||||
[self removeKeventForPath:path];
|
||||
}
|
||||
[_callbacksByPath release];
|
||||
_context = nil;
|
||||
_keepWatching = false;
|
||||
}
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (id)initWithContext:(CefRefPtr<CefV8Context>)context {
|
||||
self = [super init];
|
||||
|
||||
_keepWatching = YES;
|
||||
_callbacksByPath = [[NSMutableDictionary alloc] init];
|
||||
_fileDescriptorsByPath = [[NSMutableDictionary alloc] init];
|
||||
_kq = kqueue();
|
||||
_context = context;
|
||||
|
||||
if (_kq == -1) {
|
||||
[NSException raise:@"PathWatcher" format:@"Could not create kqueue"];
|
||||
}
|
||||
|
||||
[self performSelectorInBackground:@selector(watch) withObject:NULL];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (bool)usesContext:(CefRefPtr<CefV8Context>)context {
|
||||
return _context->IsSame(context);
|
||||
}
|
||||
|
||||
- (void)stopWatching {
|
||||
@synchronized(self) {
|
||||
[self unwatchAllPaths];
|
||||
_keepWatching = false;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback {
|
||||
NSString *callbackId = [[NSProcessInfo processInfo] globallyUniqueString];
|
||||
return [self watchPath:path callback:callback callbackId:callbackId];
|
||||
}
|
||||
|
||||
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId {
|
||||
@synchronized(self) {
|
||||
if (![self createKeventForPath:path]) {
|
||||
NSLog(@"WARNING: Failed to create kevent for path '%@'", path);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path];
|
||||
if (!callbacks) {
|
||||
callbacks = [NSMutableDictionary dictionary];
|
||||
[_callbacksByPath setObject:callbacks forKey:path];
|
||||
}
|
||||
|
||||
[callbacks setObject:callback forKey:callbackId];
|
||||
}
|
||||
|
||||
return callbackId;
|
||||
}
|
||||
|
||||
- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error {
|
||||
@synchronized(self) {
|
||||
NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path];
|
||||
|
||||
if (callbacks) {
|
||||
if (callbackId) {
|
||||
[callbacks removeObjectForKey:callbackId];
|
||||
}
|
||||
else {
|
||||
[callbacks removeAllObjects];
|
||||
}
|
||||
|
||||
if (callbacks.count == 0) {
|
||||
[self removeKeventForPath:path];
|
||||
[_callbacksByPath removeObjectForKey:path];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)watchedPaths {
|
||||
return [_callbacksByPath allKeys];
|
||||
}
|
||||
|
||||
- (void)unwatchAllPaths {
|
||||
@synchronized(self) {
|
||||
NSArray *paths = [_callbacksByPath allKeys];
|
||||
for (NSString *path in paths) {
|
||||
[self unwatchPath:path callbackId:nil error:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (bool)createKeventForPath:(NSString *)path {
|
||||
@synchronized(self) {
|
||||
if ([_fileDescriptorsByPath objectForKey:path]) {
|
||||
NSLog(@"we already have a kevent");
|
||||
return YES;
|
||||
}
|
||||
|
||||
int fd = open([path fileSystemRepresentation], O_EVTONLY, 0);
|
||||
if (fd < 0) {
|
||||
NSLog(@"WARNING: Could not create file descriptor for path '%@'. Error code %d.", path, errno);
|
||||
return NO;
|
||||
}
|
||||
|
||||
[_fileDescriptorsByPath setObject:[NSNumber numberWithInt:fd] forKey:path];
|
||||
|
||||
struct timespec timeout = { 0, 0 };
|
||||
struct kevent event;
|
||||
int filter = EVFILT_VNODE;
|
||||
int flags = EV_ADD | EV_ENABLE | EV_CLEAR;
|
||||
int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME;
|
||||
EV_SET(&event, fd, filter, flags, filterFlags, 0, path);
|
||||
kevent(_kq, &event, 1, NULL, 0, &timeout);
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeKeventForPath:(NSString *)path {
|
||||
@synchronized(self) {
|
||||
NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path];
|
||||
if (!fdNumber) {
|
||||
NSLog(@"WARNING: Could not find file descriptor for path '%@'", path);
|
||||
return;
|
||||
}
|
||||
close([fdNumber integerValue]);
|
||||
[_fileDescriptorsByPath removeObjectForKey:path];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (bool)isAtomicWrite:(struct kevent)event {
|
||||
if (!event.fflags & NOTE_DELETE) return NO;
|
||||
const char *path = [(NSString *)event.udata fileSystemRepresentation];
|
||||
bool fileExists = access(path, F_OK) != -1;
|
||||
return fileExists;
|
||||
}
|
||||
|
||||
- (void)changePath:(NSString *)path toNewPath:(NSString *)newPath {
|
||||
@synchronized(self) {
|
||||
NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]];
|
||||
[self unwatchPath:path callbackId:nil error:nil];
|
||||
for (NSString *callbackId in [callbacks allKeys]) {
|
||||
[self watchPath:newPath callback:[callbacks objectForKey:callbackId] callbackId:callbackId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)watch {
|
||||
struct kevent event;
|
||||
struct timespec timeout = { 5, 0 }; // 5 seconds timeout.
|
||||
|
||||
while (_keepWatching) {
|
||||
@autoreleasepool {
|
||||
int numberOfEvents = kevent(_kq, NULL, 0, &event, 1, &timeout);
|
||||
if (numberOfEvents == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *eventFlag = nil;
|
||||
NSString *newPath = nil;
|
||||
NSString *path = [(NSString *)event.udata retain];
|
||||
|
||||
if (event.fflags & NOTE_WRITE) {
|
||||
eventFlag = @"contents-change";
|
||||
}
|
||||
else if ([self isAtomicWrite:event]) {
|
||||
eventFlag = @"contents-change";
|
||||
// Atomic writes require the kqueue to be recreated
|
||||
[self removeKeventForPath:path];
|
||||
[self createKeventForPath:path];
|
||||
}
|
||||
else if (event.fflags & NOTE_DELETE) {
|
||||
eventFlag = @"remove";
|
||||
}
|
||||
else if (event.fflags & NOTE_RENAME) {
|
||||
eventFlag = @"move";
|
||||
char pathBuffer[MAXPATHLEN];
|
||||
fcntl((int)event.ident, F_GETPATH, &pathBuffer);
|
||||
close(event.ident);
|
||||
newPath = [NSString stringWithUTF8String:pathBuffer];
|
||||
if (!newPath) {
|
||||
NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *callbacks;
|
||||
@synchronized(self) {
|
||||
callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]];
|
||||
}
|
||||
|
||||
if ([eventFlag isEqual:@"move"]) {
|
||||
[self changePath:path toNewPath:newPath];
|
||||
}
|
||||
|
||||
if ([eventFlag isEqual:@"remove"]) {
|
||||
[self unwatchPath:path callbackId:nil error:nil];
|
||||
}
|
||||
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
for (NSString *key in callbacks) {
|
||||
WatchCallback callback = [callbacks objectForKey:key];
|
||||
callback(eventFlag, newPath ? newPath : path);
|
||||
}
|
||||
});
|
||||
|
||||
[path release];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -4,7 +4,6 @@
|
||||
#import "atom_application.h"
|
||||
#import "native.h"
|
||||
#import "include/cef_base.h"
|
||||
#import "path_watcher.h"
|
||||
|
||||
#import <iostream>
|
||||
|
||||
@@ -22,8 +21,7 @@ namespace v8_extensions {
|
||||
|
||||
void Native::CreateContextBinding(CefRefPtr<CefV8Context> context) {
|
||||
const char* methodNames[] = {
|
||||
"writeToPasteboard", "readFromPasteboard", "quit", "watchPath",
|
||||
"unwatchPath", "getWatchedPaths", "unwatchAllPaths", "moveToTrash",
|
||||
"writeToPasteboard", "readFromPasteboard", "quit", "moveToTrash",
|
||||
"reload", "setWindowState", "getWindowState", "beep", "crash"
|
||||
};
|
||||
|
||||
@@ -67,67 +65,6 @@ namespace v8_extensions {
|
||||
[NSApp terminate:nil];
|
||||
return true;
|
||||
}
|
||||
else if (name == "watchPath") {
|
||||
NSString *path = stringFromCefV8Value(arguments[0]);
|
||||
CefRefPtr<CefV8Value> function = arguments[1];
|
||||
|
||||
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
|
||||
|
||||
WatchCallback callback = ^(NSString *eventType, NSString *path) {
|
||||
context->Enter();
|
||||
|
||||
CefV8ValueList args;
|
||||
|
||||
args.push_back(CefV8Value::CreateString(string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
|
||||
args.push_back(CefV8Value::CreateString(string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
|
||||
function->ExecuteFunction(function, args);
|
||||
|
||||
context->Exit();
|
||||
};
|
||||
|
||||
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
|
||||
NSString *watchId = [pathWatcher watchPath:path callback:[[callback copy] autorelease]];
|
||||
if (watchId) {
|
||||
retval = CefV8Value::CreateString([watchId UTF8String]);
|
||||
}
|
||||
else {
|
||||
exception = string("Failed to watch path '") + string([path UTF8String]) + string("' (it may not exist)");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (name == "unwatchPath") {
|
||||
NSString *path = stringFromCefV8Value(arguments[0]);
|
||||
NSString *callbackId = stringFromCefV8Value(arguments[1]);
|
||||
NSError *error = nil;
|
||||
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
|
||||
[pathWatcher unwatchPath:path callbackId:callbackId error:&error];
|
||||
|
||||
if (error) {
|
||||
exception = [[error localizedDescription] UTF8String];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (name == "getWatchedPaths") {
|
||||
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
|
||||
NSArray *paths = [pathWatcher watchedPaths];
|
||||
|
||||
CefRefPtr<CefV8Value> pathsArray = CefV8Value::CreateArray([paths count]);
|
||||
|
||||
for (int i = 0; i < [paths count]; i++) {
|
||||
CefRefPtr<CefV8Value> path = CefV8Value::CreateString([[paths objectAtIndex:i] UTF8String]);
|
||||
pathsArray->SetValue(i, path);
|
||||
}
|
||||
retval = pathsArray;
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (name == "unwatchAllPaths") {
|
||||
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
|
||||
[pathWatcher unwatchAllPaths];
|
||||
return true;
|
||||
}
|
||||
else if (name == "moveToTrash") {
|
||||
NSString *sourcePath = stringFromCefV8Value(arguments[0]);
|
||||
bool success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"async": "0.2.6",
|
||||
"nak": "0.2.12",
|
||||
"spellchecker": "0.2.0",
|
||||
"pathwatcher": "0.1.4",
|
||||
"plist": "git://github.com/nathansobo/node-plist.git",
|
||||
"space-pen": "git://github.com/nathansobo/space-pen.git"
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ describe 'File', ->
|
||||
waitsFor "remove event", (done) -> file.on 'removed', done
|
||||
|
||||
it "it updates its path", ->
|
||||
jasmine.unspy(window, "setTimeout")
|
||||
moveHandler = null
|
||||
moveHandler = jasmine.createSpy('moveHandler')
|
||||
file.on 'moved', moveHandler
|
||||
@@ -67,6 +68,7 @@ describe 'File', ->
|
||||
expect(file.getPath()).toBe newPath
|
||||
|
||||
it "maintains 'contents-changed' events set on previous path", ->
|
||||
jasmine.unspy(window, "setTimeout")
|
||||
moveHandler = null
|
||||
moveHandler = jasmine.createSpy('moveHandler')
|
||||
file.on 'moved', moveHandler
|
||||
|
||||
@@ -65,6 +65,8 @@ describe 'Buffer', ->
|
||||
expect(eventHandler).toHaveBeenCalledWith(bufferToChange)
|
||||
|
||||
it "triggers a `path-changed` event when the file is moved", ->
|
||||
jasmine.unspy(window, "setTimeout")
|
||||
|
||||
fsUtils.remove(newPath) if fsUtils.exists(newPath)
|
||||
fsUtils.move(path, newPath)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ File = require 'file'
|
||||
Editor = require 'editor'
|
||||
TokenizedBuffer = require 'tokenized-buffer'
|
||||
fsUtils = require 'fs-utils'
|
||||
pathwatcher = require 'pathwatcher'
|
||||
RootView = require 'root-view'
|
||||
Git = require 'git'
|
||||
requireStylesheet "jasmine"
|
||||
@@ -95,8 +96,8 @@ afterEach ->
|
||||
waits(0) # yield to ui thread to make screen update more frequently
|
||||
|
||||
ensureNoPathSubscriptions = ->
|
||||
watchedPaths = $native.getWatchedPaths()
|
||||
$native.unwatchAllPaths()
|
||||
watchedPaths = pathwatcher.getWatchedPaths()
|
||||
pathwatcher.closeAllWatchers()
|
||||
if watchedPaths.length > 0
|
||||
throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", "))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
_ = require 'underscore'
|
||||
fs = require 'fs'
|
||||
fsUtils = require 'fs-utils'
|
||||
pathWatcher = require 'pathwatcher'
|
||||
File = require 'file'
|
||||
EventEmitter = require 'event-emitter'
|
||||
|
||||
@@ -39,12 +40,12 @@ class Directory
|
||||
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
|
||||
|
||||
subscribeToNativeChangeEvents: ->
|
||||
@watchSubscription = fsUtils.watchPath @path, (eventType) =>
|
||||
@trigger "contents-changed" if eventType is "contents-change"
|
||||
@watchSubscription = pathWatcher.watch @path, (eventType) =>
|
||||
@trigger "contents-changed" if eventType is "change"
|
||||
|
||||
unsubscribeFromNativeChangeEvents: ->
|
||||
if @watchSubscription?
|
||||
@watchSubscription.unwatch()
|
||||
@watchSubscription.close()
|
||||
@watchSubscription = null
|
||||
|
||||
_.extend Directory.prototype, EventEmitter
|
||||
|
||||
@@ -2,6 +2,7 @@ EventEmitter = require 'event-emitter'
|
||||
|
||||
fs = require 'fs'
|
||||
fsUtils = require 'fs-utils'
|
||||
pathWatcher = require 'pathwatcher'
|
||||
_ = require 'underscore'
|
||||
|
||||
module.exports =
|
||||
@@ -45,12 +46,13 @@ class File
|
||||
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
|
||||
|
||||
handleNativeChangeEvent: (eventType, path) ->
|
||||
if eventType is "remove"
|
||||
if eventType is "delete"
|
||||
@unsubscribeFromNativeChangeEvents()
|
||||
@detectResurrectionAfterDelay()
|
||||
else if eventType is "move"
|
||||
else if eventType is "rename"
|
||||
@setPath(path)
|
||||
@trigger "moved"
|
||||
else if eventType is "contents-change"
|
||||
else if eventType is "change"
|
||||
oldContents = @read()
|
||||
newContents = @read(true)
|
||||
return if oldContents == newContents
|
||||
@@ -62,19 +64,18 @@ class File
|
||||
detectResurrection: ->
|
||||
if @exists()
|
||||
@subscribeToNativeChangeEvents()
|
||||
@handleNativeChangeEvent("contents-change", @getPath())
|
||||
@handleNativeChangeEvent("change", @getPath())
|
||||
else
|
||||
@cachedContents = null
|
||||
@unsubscribeFromNativeChangeEvents()
|
||||
@trigger "removed"
|
||||
|
||||
subscribeToNativeChangeEvents: ->
|
||||
@watchSubscription = fsUtils.watchPath @path, (eventType, path) =>
|
||||
@watchSubscription = pathWatcher.watch @path, (eventType, path) =>
|
||||
@handleNativeChangeEvent(eventType, path)
|
||||
|
||||
unsubscribeFromNativeChangeEvents: ->
|
||||
if @watchSubscription
|
||||
@watchSubscription.unwatch()
|
||||
@watchSubscription.close()
|
||||
@watchSubscription = null
|
||||
|
||||
_.extend File.prototype, EventEmitter
|
||||
|
||||
@@ -312,11 +312,3 @@ module.exports =
|
||||
cson.readObjectAsync(path, done)
|
||||
else
|
||||
@readPlistAsync(path, done)
|
||||
|
||||
watchPath: (path, callback) ->
|
||||
path = @absolute(path)
|
||||
watchCallback = (eventType, eventPath) =>
|
||||
path = @absolute(eventPath) if eventType is 'move'
|
||||
callback(arguments...)
|
||||
id = $native.watchPath(path, watchCallback)
|
||||
unwatch: -> $native.unwatchPath(path, id)
|
||||
|
||||
Reference in New Issue
Block a user