diff --git a/.travis.yml b/.travis.yml index b8ec1d9..09d4c33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,15 @@ language: node_js -before_script: "make test-prepare" +install: "npm install" +before_script: "./node_modules/.bin/cake test-prepare" +script: "./node_modules/.bin/cake test" node_js: - 0.4 - 0.6 - 0.8 + - 0.9 notifications: irc: - - "irc.freenode.org#bevry" + - "irc.freenode.org#bevry-dev" email: recipients: - watchr@bevry.me \ No newline at end of file diff --git a/Cakefile b/Cakefile new file mode 100644 index 0000000..f7af98a --- /dev/null +++ b/Cakefile @@ -0,0 +1,132 @@ +# This file was originally created by Benjamin Lupton (http://balupton.com) +# and is currently licensed under the Creative Commons Zero (http://creativecommons.org/publicdomain/zero/1.0/) +# making it public domain so you can do whatever you wish with it without worry (you can even remove this notice!) +# +# If you change something here, be sure to reflect the changes in: +# - the scripts section of the package.json file +# - the .travis.yml file + + +# ----------------- +# Variables + +WINDOWS = process.platform.indexOf('win') is 0 +NODE = process.execPath +NPM = if WINDOWS then process.execPath.replace('node.exe','npm.cmd') else 'npm' +EXT = (if WINDOWS then '.cmd' else '') +APP = process.cwd() +BIN = "#{APP}/node_modules/.bin" +CAKE = "#{BIN}/cake#{EXT}" +COFFEE = "#{BIN}/coffee#{EXT}" +OUT = "#{APP}/out" +SRC = "#{APP}/src" + + +# ----------------- +# Requires + +pathUtil = require('path') +{exec,spawn} = require('child_process') +safe = (next,fn) -> + return (err) -> + return next(err) if err + return fn() + + +# ----------------- +# Actions + +clean = (opts,next) -> + (next = opts; opts = {}) unless next? + args = [ + '-Rf' + OUT + pathUtil.join(APP,'node_modules') + pathUtil.join(APP,'*out') + pathUtil.join(APP,'*log') + ] + spawn('rm', args, {stdio:'inherit',cwd:APP}).on('exit',next) + +compile = (opts,next) -> + (next = opts; opts = {}) unless next? + spawn(COFFEE, ['-co', OUT, SRC], {stdio:'inherit',cwd:APP}).on('exit',next) + +example = (opts,next) -> + (next = opts; opts = {}) unless next? + compile opts, safe next, -> + command = pathUtil.resolve("#{__dirname}/bin/watchr") + spawn(command, [], {stdio:'inherit',cwd:APP,env:process.env}).on('exit',next) + +watch = (opts,next) -> + (next = opts; opts = {}) unless next? + spawn(COFFEE, ['-wco', OUT, SRC], {stdio:'inherit',cwd:APP}).on('exit',next) + +install = (opts,next) -> + (next = opts; opts = {}) unless next? + spawn(NPM, ['install'], {stdio:'inherit',cwd:APP}).on('exit',next) + +reset = (opts,next) -> + (next = opts; opts = {}) unless next? + clean opts, safe next, -> + install opts, safe next, -> + compile opts, next + +setup = (opts,next) -> + (next = opts; opts = {}) unless next? + install opts, safe next, -> + compile opts, next + +test = (opts,next) -> + (next = opts; opts = {}) unless next? + spawn(NPM, ['test'], {stdio:'inherit',cwd:APP}).on('exit',next) + +finish = (err) -> + throw err if err + console.log('OK') + + +# ----------------- +# Commands + +# clean +task 'clean', 'clean up instance', -> + clean finish + +# compile +task 'compile', 'compile our files', -> + compile finish + +# dev/watch +task 'dev', 'watch and recompile our files', -> + watch finish +task 'watch', 'watch and recompile our files', -> + watch finish + +# example +task 'example', 'run the example file', -> + example finish + +# install +task 'install', 'install dependencies', -> + install finish + +# reset +task 'reset', 'reset instance', -> + reset finish + +# setup +task 'setup', 'setup for development', -> + setup finish + +# test +task 'test', 'run our tests', -> + test finish + +# test-debug +task 'test-debug', 'run our tests in debug mode', -> + test {debug:true}, finish + +# test-prepare +task 'test-prepare', 'prepare out tests', -> + setup finish + diff --git a/History.md b/History.md index ae12c42..fc450a5 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,15 @@ ## History +- v2.3.4 January 8, 2013 + - Better handling and detection of failed watching operations + - Better handling of duplicated events + - Watching is now an atomic operation + - If watching fails for a descendant, we will close everything related to that watch operation of the eve + - We now prefer the `watch` method over the `watchFile` method + - This offers great reliability and way less CPU and memory foot print + - If you still wish to prefer `watchFile`, then set the new configuration option `preferredMethod` to `watchFile` + - Closes [issue #30](https://github.com/bevry/watchr/issues/30) thanks to [Howard Tyson](https://github.com/tizzo) + - v2.3.3 January 8, 2013 - Added `outputLog` option - Added `ignorePaths` option diff --git a/Makefile b/Makefile deleted file mode 100644 index b45c6c5..0000000 --- a/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -# If you change something here, be sure to reflect the changes in: -# - the scripts section of the package.json file -# - the .travis.yml file - -# ----------------- -# Variables - -BIN=node_modules/.bin -COFFEE=$(BIN)/coffee -OUT=out -SRC=src - - -# ----------------- -# Documentation - -# Usage: coffee [options] path/to/script.coffee -- [args] -# -b, --bare compile without a top-level function wrapper -# -c, --compile compile to JavaScript and save as .js files -# -o, --output set the output directory for compiled JavaScript -# -w, --watch watch scripts for changes and rerun commands - - -# ----------------- -# Commands - -# Watch and recompile our files -dev: - $(COFFEE) -cbwo $(OUT) $(SRC) - -# Compile our files -compile: - $(COFFEE) -cbo $(OUT) $(SRC) - -# Clean up -clean: - rm -Rf $(OUT) node_modules *.log - -# Install dependencies -install: - npm install - -# Reset -reset: - make clean - make install - make compile - -# Ensure everything is ready for our tests (used by things like travis) -test-prepare: - make reset - -# Run our tests -test: - npm test - -# Example -example-run: - make compile - ./bin/watchr -example: - make example-run -i - - -# Ensure the listed commands always re-run and are never cached -.PHONY: dev compile clean install reset test-prepare test example example-run \ No newline at end of file diff --git a/README.md b/README.md index bf1972e..32cf293 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,14 @@ You install it via `npm install watchr` and use it via `require('watchr').watch( - when using the `path` configuration option: `err, watcherInstance` - when using the `paths` configuration option: `err, [watcherInstance,...]` - `stat` (optional, defaults to `null`) a file stat object to use for the path, instead of fetching a new one +- `interval` (optional, defaults to `100`) for systems that poll to detect file changes, how often should it poll in millseconds +- `persistent` (optional, defaults to `true`) whether or not we should keep the node process alive for as long as files are still being watched +- `duplicateDelay` (optional, defaults to `1000`) sometimes events will fire really fast, this delay is set in place so we don't fire the same event within the timespan +- `preferredMethod` (optional, defaults to `watch`) which watching method should try first? `watch` or `watchFile` - `ignorePaths` (optional, defaults to `false`) an array of full paths to ignore - `ignoreHiddenFiles` (optional, defaults to `false`) whether or not to ignored files which filename starts with a `.` - `ignoreCommonPatterns` (optional, defaults to `true`) whether or not to ignore common undesirable file patterns (e.g. `.svn`, `.git`, `.DS_Store`, `thumbs.db`, etc) - `ignoreCustomPatterns` (optional, defaults to `null`) any custom ignore patterns that you would also like to ignore along with the common patterns -- `interval` (optional, defaults to `100`) for systems that poll to detect file changes, how often should it poll in millseconds -- `persistent` (optional, defaults to `true`) whether or not we should keep the node process alive for as long as files are still being watched - The following events are available to your via the listeners: @@ -39,7 +40,7 @@ To wrap it all together, it would look like this: ``` javascript // Require -var watchr = require('watchr') +var watchr = require('watchr'); // Watch a directory or file console.log('Watch our paths'); @@ -53,15 +54,22 @@ watchr.watch({ console.log('an error occured:', err); }, watching: function(err,watcherInstance,isWatching){ - console.log('a new watcher instance finished setting up', arguments); + if (err) { + console.log("watching the path " + watcherInstance.path + " failed with error", err); + } else { + console.log("watching the path " + watcherInstance.path + " completed"); + } }, change: function(changeType,filePath,fileCurrentStat,filePreviousStat){ console.log('a change event occured:',arguments); } }, next: function(err,watchers){ - // Watching all setup - console.log('Now watching our paths', arguments); + if (err) { + return console.log("watching everything failed with error", err); + } else { + console.log('watching everything completed', watchers); + } // Close watchers after 10 seconds setTimeout(function(){ @@ -75,7 +83,12 @@ watchr.watch({ }); ``` -You can test the above code snippet by installing watchr globally by running `npm install -g watchr` to install watchr, then `watchr ` to watchr a particular path, and performing some file system modifications on that path. +You can test the above code snippet by running the following: + +``` +npm install watchr +./node_modules/.bin/watchr +``` ## Support diff --git a/bin/watchr b/bin/watchr index 1a6e0d2..23e0246 100755 --- a/bin/watchr +++ b/bin/watchr @@ -1,2 +1,43 @@ #!/usr/bin/env node -require(__dirname+'/../out/bin/watchr'); \ No newline at end of file +// Require +var watchr = require(__dirname+'/../out/lib/watchr'); + +// Watch a directory or file +console.log('Watch our paths'); +watchr.watch({ + paths: [process.cwd()], + listeners: { + log: function(logLevel){ + console.log('a log message occured:', arguments); + }, + error: function(err){ + console.log('an error occured:', err); + }, + watching: function(err,watcherInstance,isWatching){ + if (err) { + console.log("watching the path " + watcherInstance.path + " failed with error", err); + } else { + console.log("watching the path " + watcherInstance.path + " completed"); + } + }, + change: function(changeType,filePath,fileCurrentStat,filePreviousStat){ + console.log('a change event occured:',arguments); + } + }, + next: function(err,watchers){ + if (err) { + return console.log("watching everything failed with error", err); + } else { + console.log('watching everything completed', watchers); + } + + // Close watchers after 10 seconds + setTimeout(function(){ + var i; + console.log('Stop watching our paths'); + for ( i=0; i=0.4" }, "dependencies": { - "bal-util": "~1.15.4" + "bal-util": "~1.16.3" }, "devDependencies": { "coffee-script": "~1.4.0", diff --git a/src/bin/watchr.coffee b/src/bin/watchr.coffee deleted file mode 100755 index 07cb5bf..0000000 --- a/src/bin/watchr.coffee +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env coffee -path = require('path') -watchr = require(__dirname+'/../lib/watchr') -cwd = process.cwd() -watchPath = - if process.argv.length is 3 - path.resolve(cwd,process.argv[2]) - else - cwd -changes = 0 -watchr.watch( - path: watchPath - listeners: - log: (args...) -> - console.log('a log message occured:', args); - error: (err) -> - console.log('an error occured:', err); - watching: (args...) -> - console.log('a new watcher instance finished setting up', args); - change: (args...) -> - console.log('a change event occured:',args); - next: (args...) -> - console.log('watching for all our paths has completed', args); -) \ No newline at end of file diff --git a/src/lib/watchr.coffee b/src/lib/watchr.coffee index ea82717..3f2c959 100644 --- a/src/lib/watchr.coffee +++ b/src/lib/watchr.coffee @@ -14,6 +14,27 @@ balUtil = require('bal-util') # This provides us with the event system that we use for binding and trigger events EventEmitter = require('events').EventEmitter +# Require the optional domain logic introduced in node 0.8 +try + domain = require('domain') +catch err + domain = false + +# Safe Execute +# Wrap the method in a domain if we can, otherwise wrap it in a try catch +# If an error occurs, go to the next callback +safeExecute = (next,method) -> + if domain + d = domain.create() + d.on 'error', (err) -> + return next(err,false) + d.run(method) + else + try + method() + catch err + next(err,false) + ### Now to make watching files more convient and managed, we'll create a class which we can use to attach to each file. It'll provide us with the API and abstraction we need to accomplish difficult things like recursion. @@ -25,6 +46,7 @@ Events: - `watching` for when watching of the path has completed, receives the arguments `err, watcherInstance, isWatching` - `change` for listening to change events, receives the arguments `changeType, fullPath, currentStat, previousStat` ### +watchersTotal = 0 watchers = {} Watcher = class extends EventEmitter # The path this class instance is attached to @@ -57,9 +79,6 @@ Watcher = class extends EventEmitter # A single path to watch path: null - # Should we output log messages? - outputLog: false - # Listener (optional, detaults to null) # single change listener, forwaded to @listen listener: null @@ -72,6 +91,29 @@ Watcher = class extends EventEmitter # a file stat object to use for the path, instead of fetching a new one stat: null + # Should we output log messages? + outputLog: false + + # Interval (optional, defaults to `100`) + # for systems that poll to detect file changes, how often should it poll in millseconds + # if you are watching a lot of files, make this value larger otherwise you will have huge memory load + # only appliable to the `watchFile` watching method + interval: 100 + + # Persistent (optional, defaults to `true`) + # whether or not we should keep the node process alive for as long as files are still being watched + # only appliable to the `watchFile` watching method + persistent: true + + # Duplicate Delay (optional, defaults to `1000`) + # sometimes events will fire really fast, this delay is set in place to ensure we don't fire the same event + # within the duplicateDelay timespan + duplicateDelay: 1*1000 + + # Preferred Method (optional, detaults to `watch`) + # which node.js fsUtil watching method should we attempt first, either `watch` or `watchFile` + preferredMethod: 'watch' + # Ignore Paths (optional, defaults to `false`) # array of paths that we should ignore ignorePaths: false @@ -88,13 +130,6 @@ Watcher = class extends EventEmitter # any custom ignore patterns that you would also like to ignore along with the common patterns ignoreCustomPatterns: null - # Interval (optional, defaults to `100`) - # for systems that poll to detect file changes, how often should it poll in millseconds - interval: 100 - - # Persistent (optional, defaults to `true`) - # whether or not we should keep the node process alive for as long as files are still being watched - persistent: true # Now it's time to construct our watcher # We give it a path, and give it some events to use @@ -160,7 +195,7 @@ Watcher = class extends EventEmitter # We need something to bubble events up from a child file all the way up the top bubble: (args...) => # Log - @log('debug',"bubble on #{@path} with the args:",args) + #@log('debug',"bubble on #{@path} with the args:",args) # Trigger @emit(args...) @@ -216,6 +251,42 @@ Watcher = class extends EventEmitter # Chain @ + ### + Emit Safe + Sometimes events can fire incredibly quickly in which case we'll determine multiple events + This alias for emit('change',...) will check to see if the event has already been fired recently + and if it has, then ignore it + ### + cacheTimeout: null + cachedEvents: null + emitSafe: (args...) -> + # Prepare + me = @ + config = @config + + # Clear duplicate timeout + clearTimeout(@cacheTimeout) if @cacheTimeout? + @cacheTimeout = setTimeout( + -> + me.cachedEvents = [] + me.cacheTimeout = null + config.duplicateDelay + ) + @cachedEvents ?= [] + + # Check duplicate + thisEvent = args.toString() + if thisEvent in @cachedEvents + @log('debug',"event ignored on #{@path} due to duplicate:", args) + return @ + @cachedEvents.push(thisEvent) + + # Fire the event + @emit(args...) + + # Chain + @ + ### Listener A change event has fired @@ -226,8 +297,7 @@ Watcher = class extends EventEmitter - for deleted and updated files, it will fire on the file - for created files, it will fire on the directory - fsWatcher: - - eventName is always 'change' - - 'rename' is not yet implemented by node + - eventName is either 'change' or 'rename', this value cannot be trusted - currentStat still exists even for deleted/renamed files - previousStat is accurate, however we already have this - for deleted and changed files, it will fire on the file @@ -283,53 +353,53 @@ Watcher = class extends EventEmitter if isTheSame() is false # Scan children balUtil.readdir fileFullPath, (err,newFileRelativePaths) => - # Error + # Error? return @emit('error',err) if err + # Check for deleted files + # by cycling through our known children + balUtil.each @children, (childFileWatcher,childFileRelativePath) => + # Skip if this is a new file (not a deleted file) + return if childFileRelativePath in newFileRelativePaths + + # Fetch full path + childFileFullPath = pathUtil.join(fileFullPath,childFileRelativePath) + + # Skip if ignored file + return if @isIgnoredPath(childFileFullPath) + + # Emit the event + @log('debug','determined delete:',childFileFullPath,'via:',fileFullPath) + @closeChild(childFileRelativePath,'deleted') + # Check for new files balUtil.each newFileRelativePaths, (childFileRelativePath) => - # Already watching the new file + # Skip if we are already watching this file return if @children[childFileRelativePath]? - # Not yet watching the new file + @children[childFileRelativePath] = false # reserve this file # Fetch full path childFileFullPath = pathUtil.join(fileFullPath,childFileRelativePath) - # Is ignored file? + # Skip if ignored file return if @isIgnoredPath(childFileFullPath) # Fetch the stat for the new file balUtil.stat childFileFullPath, (err,childFileStat) => - # Error + # Error? return @emit('error',err) if err # Emit the event @log('debug','determined create:',childFileFullPath,'via:',fileFullPath) - @emit('change','create',childFileFullPath,childFileStat,null) + @emitSafe('change','create',childFileFullPath,childFileStat,null) @watchChild(childFileFullPath,childFileRelativePath,childFileStat) - # Check for deleted files - balUtil.each @children, (childFileWatcher,childFileRelativePath) => - # File still exists - return if childFileRelativePath in newFileRelativePaths - # File was deleted - - # Fetch full path - childFileFullPath = pathUtil.join(fileFullPath,childFileRelativePath) - - # Is ignored file? - return if @isIgnoredPath(childFileFullPath) - - # Emit the event - @log('debug','determined delete:',childFileFullPath,'via:',fileFullPath) - @closeChild(childFileRelativePath,'deleted') - # If we are a file, lets simply emit the change event else # It has changed, so let's emit a change event @log('debug','determined update:',fileFullPath) - @emit('change','update',fileFullPath,currentStat,previousStat) + @emitSafe('change','update',fileFullPath,currentStat,previousStat) # Check if the file still exists balUtil.exists fileFullPath, (exists) -> @@ -369,35 +439,40 @@ Watcher = class extends EventEmitter for own childRelativePath of @children @closeChild(childRelativePath,reason) - # Close listener + # Close watchFile listener if @method is 'watchFile' fsUtil.unwatchFile(@path) - else if @method is 'watch' and @fswatcher + + # Close watch listener + if @fswatcher? @fswatcher.close() @fswatcher = null # Updated state if reason is 'deleted' @state = 'deleted' - @emit('change','delete',@path,null,@stat) + @emitSafe('change','delete',@path,null,@stat) + else if reason is 'failure' + @state = 'closed' + @log('warn',"Failed to watch the path #{@path}") else @state = 'closed' # Delete our watchers reference - delete watchers[@path] if watchers[@path]? + if watchers[@path]? + delete watchers[@path] + watchersTotal-- # Chain @ # Close a child closeChild: (fileRelativePath,reason) -> - # Prepare - watcher = @children[fileRelativePath] - # Check - if watcher + if @children[fileRelativePath]? + watcher = @children[fileRelativePath] + watcher.close(reason) if watcher # could be "fase" for reservation delete @children[fileRelativePath] - watcher.close(reason) # Chain @ @@ -413,37 +488,35 @@ Watcher = class extends EventEmitter me = @ config = @config - # Watch the file - watcher = watch( + # Watch the file if we aren't already + me.children[fileRelativePath] or= watch( + # Custom path: fileFullPath stat: fileStat - ignorePaths: config.ignorePaths - ignoreHiddenFiles: config.ignoreHiddenFiles - ignoreCommonPatterns: config.ignoreCommonPatterns - ignoreCustomPatterns: config.ignoreCustomPatterns listeners: - 'change': (args...) => + 'log': me.bubbler('log') + 'change': (args...) -> [changeType,path] = args if changeType is 'delete' and path is fileFullPath - @closeChild(fileRelativePath,'deleted') + me.closeChild(fileRelativePath,'deleted') me.bubble('change', args...) 'error': me.bubbler('error') - next: (args...) -> - # Prepare - [err] = args - - # Stop if an error happened - return next?(err) if err - - # Store the child watcher in us - me.children[fileRelativePath] = watcher - - # Proceed to the next file - next?(args...) + next: next + + # Inherit + outputLog: config.outputLog + interval: config.interval + persistent: config.persistent + duplicateDelay: config.duplicateDelay + preferredMethod: config.preferredMethod + ignorePaths: config.ignorePaths + ignoreHiddenFiles: config.ignoreHiddenFiles + ignoreCommonPatterns: config.ignoreCommonPatterns + ignoreCustomPatterns: config.ignoreCustomPatterns ) # Return - return watcher + return me.children[fileRelativePath] # Is Ignored Path isIgnoredPath: (path,opts={}) => @@ -461,6 +534,123 @@ Watcher = class extends EventEmitter # Return return ignore + ### + Watch Children + next(err,result) + ### + watchChildren: (next) -> + # Prepare + me = @ + config = @config + + # Cycle through the directory if necessary + if @isDirectory + balUtil.scandir( + # Path + path: @path + + # Options + ignorePaths: config.ignorePaths + ignoreHiddenFiles: config.ignoreHiddenFiles + ignoreCommonPatterns: config.ignoreCommonPatterns + ignoreCustomPatterns: config.ignoreCustomPatterns + recurse: false + + # Next + next: (err) -> + return next(err,err? is false) + + # File and Directory Actions + action: (fileFullPath,fileRelativePath,nextFile,fileStat) -> + # Check we are still releveant + if me.state isnt 'active' + return nextFile(null,true) # skip without error + + # Watch this child + me.watchChild fileFullPath, fileRelativePath, fileStat, (err) -> + nextFile(err) + ) + else + next(null,true) + + # Chain + return @ + + ### + Watch Self + ### + watchSelf: (next) -> + # Prepare + me = @ + config = @config + + # Reset the method + @method = null + + # Setup our watch methods + methods = + # Try with fsUtil.watch + watch: (next) -> + # Check + return next(null,false) unless fsUtil.watch? + + # Watch + safeExecute next, -> + me.fswatcher = fsUtil.watch me.path, (args...) -> + me.listener.apply(me,args) + me.method = 'watch' + return next(null,true) + + # Try fsUtil.watchFile + watchFile: (next) -> + # Check + return next(null,false) unless fsUtil.watchFile? + + # Watch + safeExecute next, -> + watchFileOpts = + persistent: config.persistent + interval: config.interval + fsUtil.watchFile me.path, watchFileOpts, (args...) -> + me.listener.apply(me,args) + me.method = 'watchFile' + return next(null,true) + + # Complete + complete = (success) -> + success ?= true + if success + me.state = 'active' + next(null,true) + else + me.close('failure') + next(null,false) + + # Watch + if config.preferredMethod is 'watch' + # Try watch + methods.watch (err,success) -> + me.emit('error',err) if err + return complete(success) if success + + # Try watchFile + methods.watchFile (err,success) -> + me.emit('error',err) if err + return complete(success) + else + # Try watchFile + methods.watchFile (err,success) -> + me.emit('error',err) if err + return complete(success) if success + + # Try watch + methods.watchFile (err,success) -> + me.emit('error',err) if err + return complete(success) + + # Chain + return @ + ### Watch Setup the native watching handlers for our path so we can receive updates on when things happen @@ -469,6 +659,7 @@ Watcher = class extends EventEmitter If we are a directory, let's recurse If we are deleted, then don't error but return the isWatching argument of our completion callback as false Once watching has completed for this directory and all children, then emit the watching event + next(err,watcherInstance,success) ### watch: (next) -> # Prepare @@ -501,68 +692,29 @@ Watcher = class extends EventEmitter # Log @log('debug',"watch: #{@path}") - # Prepare Start Watching - startWatching = => - # Create a set of tasks - tasks = new balUtil.Group (err) => - return @emit('watching',err,@,false) if err - return @emit('watching',err,@,true) - tasks.total = 2 - - # Cycle through the directory if necessary - if @isDirectory - balUtil.scandir( - # Path - path: @path - - # Options - ignorePaths: config.ignorePaths - ignoreHiddenFiles: config.ignoreHiddenFiles - ignoreCommonPatterns: config.ignoreCommonPatterns - ignoreCustomPatterns: config.ignoreCustomPatterns - recurse: false - - # Next - next: (err) -> - tasks.complete(err) - - # File and Directory Actions - action: (fileFullPath,fileRelativePath,nextFile,fileStat) -> - # Watch it - me.watchChild fileFullPath, fileRelativePath, fileStat, (err) -> - nextFile(err) - ) + # Prepare + complete = (err,result) -> + # Prepare + err ?= null + result ?= true + + # Handle + if err or !result + me.close() + me.emit('watching',err,me,false) else - tasks.complete() - - # Watch the current file/directory - try - # Try first with fsUtil.watchFile - watchFileOpts = - persistent: config.persistent - interval: config.interval - fsUtil.watchFile @path, watchFileOpts, (args...) -> - me.listener.apply(me,args) - @method = 'watchFile' - catch err - # Then try with fsUtil.watch - @fswatcher = fsUtil.watch @path, (args...) -> - me.listener.apply(me,args) - @method = 'watch' - - # We are now watching so set the state as active - @state = 'active' - tasks.complete() + me.emit('watching',null,me,true) # Check if we still exist - balUtil.exists @path, (exists) => + balUtil.exists @path, (exists) -> # Check - unless exists - # We don't exist anymore, move along - return @emit('watching',null,@,false) + return complete(null,false) unless exists # Start watching - startWatching() + me.watchSelf (err,result) -> + return complete(err,result) if err or !result + me.watchChildren (err,result) -> + return complete(err,result) # Chain @ @@ -606,6 +758,7 @@ createWatcher = (opts,next) -> watcher = new Watcher opts, (err) -> next?(err,watcher) watchers[path] = watcher + ++watchersTotal # Return return watcher diff --git a/src/test/everything.test.coffee b/src/test/everything.test.coffee index 2d5d098..a4d81c4 100644 --- a/src/test/everything.test.coffee +++ b/src/test/everything.test.coffee @@ -37,15 +37,16 @@ joe.suite 'watchr', (suite,test) -> watcher = null # Change detection - actualChanges = 0 + changes = [] checkChanges = (expectedChanges,next) -> wait batchDelay, -> - assert.equal(actualChanges, expectedChanges, "#{actualChanges} changes ran out of #{expectedChanges} changes") - actualChanges = 0 + console.log(changes) if changes.length isnt expectedChanges + assert.equal(changes.length, expectedChanges, "#{changes.length} changes ran out of #{expectedChanges} changes") + changes = [] next() changeHappened = (args...) -> - ++actualChanges - console.log("a watch event occured: #{actualChanges}", args) if debug + changes.push(args) + console.log("a watch event occured: #{changes.length}", args) if debug # Files changes writeFile = (fileRelativePath) -> @@ -75,9 +76,9 @@ joe.suite 'watchr', (suite,test) -> watchr.watch( path: outPath listener: changeHappened - ignorePaths: [outPath+'/blah'] + ignorePaths: [pathUtil.join(outPath,'blah')] ignoreHiddenFiles: true - #outputLog: true + outputLog: true next: (err,_watcher) -> watcher = _watcher wait batchDelay, -> done(err)