From ec18bce4050a3323ddd649b01cbfa751443c5c6b Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 12 Nov 2013 11:33:41 -0800 Subject: [PATCH 01/94] Cache file contents; add hash property to stats; reject writes for inconsistent hashes --- src/filesystem/File.js | 31 +++++++++--- src/filesystem/FileIndex.js | 2 + src/filesystem/FileSystem.js | 48 ++++++++++++------- src/filesystem/FileSystemEntry.js | 10 ++++ src/filesystem/FileSystemError.js | 3 +- src/filesystem/FileSystemStats.js | 1 + .../impls/appshell/AppshellFileSystem.js | 36 ++++++++++---- test/spec/FileSystem-test.js | 5 ++ test/spec/MockFileSystemImpl.js | 48 +++++++++++-------- 9 files changed, 131 insertions(+), 53 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 5b5e1a084fe..29fd4ce0665 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -80,11 +80,21 @@ define(function (require, exports, module) { options = {}; } + if (this._contents && this._stat) { + callback(null, this._contents, this._stat); + return; + } + this._impl.readFile(this._path, options, function (err, data, stat) { - if (!err) { - this._stat = stat; - // this._contents = data; + if (err) { + this._clearCachedData(); + callback(err); + return; } + + this._stat = stat; + this._contents = data; + callback(err, data, stat); }.bind(this)); }; @@ -107,12 +117,19 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); - this._impl.writeFile(this._path, data, options, function (err, stat) { + var hash = this._stat ? this._stat._hash : null; + + this._impl.writeFile(this._path, data, hash, options, function (err, stat) { try { - if (!err) { - this._stat = stat; - // this._contents = data; + if (err) { + this._clearCachedData(); + callback(err); + return; } + + this._stat = stat; + this._contents = data; + callback(err, stat); } finally { this._fileSystem._endWrite(); // unblock generic change events diff --git a/src/filesystem/FileIndex.js b/src/filesystem/FileIndex.js index e6af676c600..06630e5500a 100644 --- a/src/filesystem/FileIndex.js +++ b/src/filesystem/FileIndex.js @@ -96,6 +96,8 @@ define(function (require, exports, module) { } } + entry._clearCachedData(); + delete this._index[path]; for (property in entry) { diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index b3b2b24de40..03a80520759 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -387,6 +387,14 @@ define(function (require, exports, module) { FileSystem.isAbsolutePath = function (fullPath) { return (fullPath[0] === "/" || fullPath[1] === ":"); }; + + function _appendTrailingSlash(path) { + if (path[path.length - 1] !== "/") { + path += "/"; + } + + return path; + } /* * Matches continguous groups of forward slashes @@ -430,9 +438,7 @@ define(function (require, exports, module) { if (isDirectory) { // Make sure path DOES include trailing slash - if (path[path.length - 1] !== "/") { - path += "/"; - } + path = _appendTrailingSlash(path); } if (isUNCPath) { @@ -489,20 +495,32 @@ define(function (require, exports, module) { * with a FileSystemError string or with the entry for the provided path. */ FileSystem.prototype.resolve = function (path, callback) { - // No need to normalize path here: assume underlying stat() does it internally, - // and it will be normalized anyway when ingested by get*ForPath() afterward + var normalizedPath = _normalizePath(path, false), + item = this._index.getEntry(normalizedPath); + + if (!item) { + normalizedPath = _appendTrailingSlash(normalizedPath); + item = this._index.getEntry(normalizedPath); + } + + if (item && item._stat) { + callback(null, item, item._stat); + return; + } this._impl.stat(path, function (err, stat) { - var item; + if (err) { + callback(err); + return; + } - if (!err) { - if (stat.isFile) { - item = this.getFileForPath(path); - } else { - item = this.getDirectoryForPath(path); - } + if (stat.isFile) { + item = this.getFileForPath(path); + } else { + item = this.getDirectoryForPath(path); } - callback(err, item, stat); + + callback(null, item, stat); }.bind(this)); }; @@ -583,9 +601,7 @@ define(function (require, exports, module) { // This is a "wholesale" change event // Clear all caches (at least those that won't do a stat() double-check before getting used) this._index.visitAll(function (entry) { - if (entry.isDirectory) { - entry._clearCachedData(); - } + entry._clearCachedData(); }); fireChangeEvent(null); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index a246db4d7d5..f6af6d6ef34 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -176,6 +176,11 @@ define(function (require, exports, module) { * string or a boolean indicating whether or not the file exists. */ FileSystemEntry.prototype.exists = function (callback) { + if (this._stat) { + callback(true); + return; + } + this._impl.exists(this._path, callback); }; @@ -186,6 +191,11 @@ define(function (require, exports, module) { * FileSystemError string or FileSystemStats object. */ FileSystemEntry.prototype.stat = function (callback) { + if (this._stat) { + callback(null, this._stat); + return; + } + this._impl.stat(this._path, function (err, stat) { if (!err) { this._stat = stat; diff --git a/src/filesystem/FileSystemError.js b/src/filesystem/FileSystemError.js index d54130b21b3..9636c981a05 100644 --- a/src/filesystem/FileSystemError.js +++ b/src/filesystem/FileSystemError.js @@ -42,7 +42,8 @@ define(function (require, exports, module) { NOT_WRITABLE : "NotWritable", OUT_OF_SPACE : "OutOfSpace", TOO_MANY_ENTRIES : "TooManyEntries", - ALREADY_EXISTS : "AlreadyExists" + ALREADY_EXISTS : "AlreadyExists", + CONTENTS_MODIFIED : "ContentsModified" // FUTURE: Add remote connection errors: timeout, not logged in, connection err, etc. }; }); diff --git a/src/filesystem/FileSystemStats.js b/src/filesystem/FileSystemStats.js index 58160f8b01c..13131f26a0c 100644 --- a/src/filesystem/FileSystemStats.js +++ b/src/filesystem/FileSystemStats.js @@ -42,6 +42,7 @@ define(function (require, exports, module) { this._isDirectory = !isFile; this._mtime = options.mtime; this._size = options.size; + this._hash = options.hash; var realPath = options.realPath; if (realPath) { diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index de4c0b7be5f..731e8a4d923 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -157,9 +157,10 @@ define(function (require, exports, module) { isFile: stats.isFile(), mtime: stats.mtime, size: stats.size, - realPath: stats.realPath + realPath: stats.realPath, + hash: stats.mtime.getTime() }; - + var fsStats = new FileSystemStats(options); callback(null, fsStats); @@ -270,15 +271,10 @@ define(function (require, exports, module) { }); } - function writeFile(path, data, options, callback) { + function writeFile(path, data, hash, options, callback) { var encoding = options.encoding || "utf8"; - exists(path, function (err, alreadyExists) { - if (err) { - callback(err); - return; - } - + function _finishWrite(alreadyExists) { appshell.fs.writeFile(path, data, encoding, function (err) { if (err) { callback(_mapError(err)); @@ -297,8 +293,28 @@ define(function (require, exports, module) { }); } }); - }); + } + stat(path, function (err, stats) { + if (err) { + switch (err) { + case FileSystemError.NOT_FOUND: + _finishWrite(false); + break; + default: + callback(err); + } + return; + } + + if (hash !== stats._hash) { + console.warn("Blind write attempted: ", path, stats._hash, hash); + callback(FileSystemError.CONTENTS_MODIFIED); + return; + } + + _finishWrite(path, data, encoding, true, callback); + }); } function unlink(path, callback) { diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 490c31acb73..61591218ce4 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -499,6 +499,7 @@ define(function (require, exports, module) { runs(function () { expect(firstReadCB.error).toBeFalsy(); expect(firstReadCB.data).toBe("File 4 Contents"); + expect(firstReadCB.stat).toBeTruthy(); }); // Write new contents @@ -518,6 +519,7 @@ define(function (require, exports, module) { runs(function () { expect(secondReadCB.error).toBeFalsy(); expect(secondReadCB.data).toBe(newContents); + expect(secondReadCB.stat).toBeTruthy(); }); }); @@ -531,6 +533,8 @@ define(function (require, exports, module) { waitsFor(function () { return cb.wasCalled; }); runs(function () { expect(cb.error).toBe(FileSystemError.NOT_FOUND); + expect(cb.data).toBeFalsy(); + expect(cb.stat).toBeFalsy(); }); }); @@ -561,6 +565,7 @@ define(function (require, exports, module) { runs(function () { expect(readCb.error).toBeFalsy(); expect(readCb.data).toBe(newContents); + expect(readCb.stat).toBeTruthy(); }); }); }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index a92f76b14ab..9eb36b960ee 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -37,30 +37,30 @@ define(function (require, exports, module) { var _initialData = { "/": { isFile: false, - mtime: Date.now() + mtime: new Date() }, "/file1.txt": { isFile: true, - mtime: Date.now(), + mtime: new Date(), contents: "File 1 Contents" }, "/file2.txt": { isFile: true, - mtime: Date.now(), + mtime: new Date(), contents: "File 2 Contents" }, "/subdir/": { isFile: false, - mtime: Date.now() + mtime: new Date() }, "/subdir/file3.txt": { isFile: true, - mtime: Date.now(), + mtime: new Date(), contents: "File 3 Contents" }, "/subdir/file4.txt": { isFile: true, - mtime: Date.now(), + mtime: new Date(), contents: "File 4 Contents" } }; @@ -107,7 +107,8 @@ define(function (require, exports, module) { stat = new FileSystemStats({ isFile: entry.isFile, mtime: entry.mtime, - size: 0 + size: entry.contents ? entry.contents.length : 0, + hash: entry.mtime.getTime() }); } @@ -192,7 +193,7 @@ define(function (require, exports, module) { } else { var entry = { isFile: false, - mtime: Date.now() + mtime: new Date() }; _data[path] = entry; cb(null, _getStat(path)); @@ -266,35 +267,44 @@ define(function (require, exports, module) { if (!_data[path]) { cb(FileSystemError.NOT_FOUND); } else { - cb(null, _data[path].contents); + cb(null, _data[path].contents, _getStat(path)); } } - function writeFile(path, data, options, callback) { + function writeFile(path, data, hash, options, callback) { if (typeof (options) === "function") { callback = options; options = null; } - exists(path, function (err, exists) { + stat(path, function (err, stats) { var cb = _getCallback("writeFile", path, callback); - if (err) { + if (err && err !== FileSystemError.NOT_FOUND) { cb(err); return; } + var exists = !!stats; + if (exists && hash !== stats._hash) { + cb(FileSystemError.CONTENTS_MODIFIED); + return; + } + var notification = exists ? _sendWatcherNotification : _sendDirectoryWatcherNotification, notify = _getNotification("writeFile", path, notification); - - if (!_data[path]) { - _data[path] = { - isFile: true - }; + + if (!exists) { + if (!_data[path]) { + _data[path] = { + isFile: true + }; + } } + _data[path].contents = data; - _data[path].mtime = Date.now(); - cb(null); + _data[path].mtime = new Date(); + cb(null, _getStat(path)); notify(path); }); } From d5893e5b93ecfadf80b5e3980ad78385be51b028 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 14 Nov 2013 11:27:45 -0800 Subject: [PATCH 02/94] Use the FileSystem from the testWindow when possible --- test/spec/SpecRunnerUtils.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 7af6d890f07..5024f0f51fb 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -50,10 +50,13 @@ define(function (require, exports, module) { _testSuites = {}, _testWindow, _doLoadExtensions, - nfs, _rootSuite = { id: "__brackets__" }, _unitTestReporter; + function _getFileSystem() { + return _testWindow ? _testWindow.brackets.test.FileSystem : FileSystem; + } + /** * Delete a path * @param {string} fullPath @@ -62,7 +65,7 @@ define(function (require, exports, module) { */ function deletePath(fullPath, silent) { var result = new $.Deferred(); - FileSystem.resolve(fullPath, function (err, item) { + _getFileSystem().resolve(fullPath, function (err, item) { if (!err) { item.unlink(function (err) { if (!err) { @@ -143,7 +146,7 @@ define(function (require, exports, module) { function resolveNativeFileSystemPath(path) { var result = new $.Deferred(); - FileSystem.resolve(path, function (err, item) { + _getFileSystem().resolve(path, function (err, item) { if (!err) { result.resolve(item); } else { @@ -227,7 +230,7 @@ define(function (require, exports, module) { var deferred = new $.Deferred(); runs(function () { - var dir = FileSystem.getDirectoryForPath(getTempDirectory()).create(function (err) { + var dir = _getFileSystem().getDirectoryForPath(getTempDirectory()).create(function (err) { if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(err); } else { @@ -251,7 +254,7 @@ define(function (require, exports, module) { promise = Async.doSequentially(folders, function (folder) { var deferred = new $.Deferred(); - FileSystem.resolve(folder, function (err, entry) { + _getFileSystem().resolve(folder, function (err, entry) { if (!err) { // Change permissions if the directory exists chmod(folder, "777").then(deferred.resolve, deferred.reject); @@ -317,7 +320,7 @@ define(function (require, exports, module) { content = options.content || ""; // Use unique filename to avoid collissions in open documents list - var dummyFile = FileSystem.getFileForPath(filename); + var dummyFile = _getFileSystem().getFileForPath(filename); var docToShim = new DocumentManager.Document(dummyFile, new Date(), content); // Prevent adding doc to working set @@ -782,7 +785,7 @@ define(function (require, exports, module) { } // create the new File - createTextFile(destination, text, FileSystem).done(function (entry) { + createTextFile(destination, text, _getFileSystem()).done(function (entry) { deferred.resolve(entry, offsets, text); }).fail(function (err) { deferred.reject(err); @@ -819,7 +822,7 @@ define(function (require, exports, module) { var parseOffsets = options.parseOffsets || false, removePrefix = options.removePrefix || true, deferred = new $.Deferred(), - destDir = FileSystem.getDirectoryForPath(destination); + destDir = _getFileSystem().getDirectoryForPath(destination); // create the destination folder destDir.create(function (err) { From 8a80572d0885e4cbe05ffb8cc6d3b6614c29eb36 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 15 Nov 2013 14:14:38 -0800 Subject: [PATCH 03/94] Only update the file hash on read or write; add blind write support. --- src/filesystem/File.js | 17 ++++++++++++----- src/filesystem/FileSystemEntry.js | 4 ++-- .../impls/appshell/AppshellFileSystem.js | 2 +- test/spec/FileSystem-test.js | 6 +++--- test/spec/MockFileSystemImpl.js | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 29fd4ce0665..8e6ade9f1ae 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -28,7 +28,7 @@ define(function (require, exports, module) { "use strict"; - var FileSystemEntry = require("filesystem/FileSystemEntry"); + var FileSystemEntry = require("filesystem/FileSystemEntry"); /* @@ -58,7 +58,14 @@ define(function (require, exports, module) { File.prototype._contents = null; /** - * Clear any cached data for this file + * Consistency hash for this file. Reads and writes update this value, and + * writes confirm the hash before overwriting existing files. + */ + File.prototype._hash = null; + + /** + * Clear any cached data for this file. Note that this explicitly does NOT + * clear the file's hash. * @private */ File.prototype._clearCachedData = function () { @@ -93,6 +100,7 @@ define(function (require, exports, module) { } this._stat = stat; + this._hash = stat._hash; this._contents = data; callback(err, data, stat); @@ -117,9 +125,7 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); - var hash = this._stat ? this._stat._hash : null; - - this._impl.writeFile(this._path, data, hash, options, function (err, stat) { + this._impl.writeFile(this._path, data, this._hash, options, function (err, stat) { try { if (err) { this._clearCachedData(); @@ -128,6 +134,7 @@ define(function (require, exports, module) { } this._stat = stat; + this._hash = stat._hash; this._contents = data; callback(err, stat); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index f6af6d6ef34..432907471e1 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -89,7 +89,7 @@ define(function (require, exports, module) { * @type {?FileSystemStats} */ FileSystemEntry.prototype._stat = null; - + /** * Parent file system. * @type {!FileSystem} @@ -177,7 +177,7 @@ define(function (require, exports, module) { */ FileSystemEntry.prototype.exists = function (callback) { if (this._stat) { - callback(true); + callback(null, true); return; } diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 731e8a4d923..1646fc1d407 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -307,7 +307,7 @@ define(function (require, exports, module) { return; } - if (hash !== stats._hash) { + if (hash !== stats._hash && !options.blind) { console.warn("Blind write attempted: ", path, stats._hash, hash); callback(FileSystemError.CONTENTS_MODIFIED); return; diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 61591218ce4..42854fd1ca2 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -820,7 +820,7 @@ define(function (require, exports, module) { changeDone = true; }); - testFile.write("Foobar", function (err) { + testFile.write("Foobar", { blind: true }, function (err) { expect(err).toBeFalsy(); writeDone = true; }); @@ -863,11 +863,11 @@ define(function (require, exports, module) { // We always *start* both operations together, synchronously // What varies is when the impl callbacks for for each op, and when the impl's watcher notices each op - fileSystem.getFileForPath("/file1.txt").write("Foobar 1", function (err) { + fileSystem.getFileForPath("/file1.txt").write("Foobar 1", { blind: true }, function (err) { expect(err).toBeFalsy(); write1Done = true; }); - fileSystem.getFileForPath("/file2.txt").write("Foobar 2", function (err) { + fileSystem.getFileForPath("/file2.txt").write("Foobar 2", { blind: true }, function (err) { expect(err).toBeFalsy(); write2Done = true; }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index 9eb36b960ee..f8a1a550bbe 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -286,7 +286,7 @@ define(function (require, exports, module) { } var exists = !!stats; - if (exists && hash !== stats._hash) { + if (exists && hash !== stats._hash && !options.blind) { cb(FileSystemError.CONTENTS_MODIFIED); return; } From a3fbbe50b97992d3784e260999a26d3bce368101 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 15 Nov 2013 17:07:56 -0800 Subject: [PATCH 04/94] Only cache stats and contents for watched entries --- src/filesystem/Directory.js | 45 ++++++++----- src/filesystem/File.js | 33 +++++++--- src/filesystem/FileSystem.js | 15 ++--- src/filesystem/FileSystemEntry.js | 63 ++++++++++++++----- .../impls/appshell/AppshellFileSystem.js | 8 +-- test/spec/MockFileSystemImpl.js | 4 +- 6 files changed, 115 insertions(+), 53 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 293cf074e43..77234f14911 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -113,15 +113,15 @@ define(function (require, exports, module) { this._contentsCallbacks = [callback]; - this._impl.readdir(this.fullPath, function (err, contents, stats) { + this._impl.readdir(this.fullPath, function (err, entries, stats) { + var contents = [], + contentsStats = [], + contentsStatsErrors; + if (err) { this._clearCachedData(); } else { - this._contents = []; - this._contentsStats = []; - this._contentsStatsErrors = undefined; - - contents.forEach(function (name, index) { + entries.forEach(function (name, index) { var entryPath = this.fullPath + name, entry; @@ -131,10 +131,10 @@ define(function (require, exports, module) { // Note: not all entries necessarily have associated stats. if (typeof entryStats === "string") { // entryStats is an error string - if (this._contentsStatsErrors === undefined) { - this._contentsStatsErrors = {}; + if (contentsStatsErrors === undefined) { + contentsStatsErrors = {}; } - this._contentsStatsErrors[entryPath] = entryStats; + contentsStatsErrors[entryPath] = entryStats; } else { // entryStats is a FileSystemStats object if (entryStats.isFile) { @@ -149,13 +149,22 @@ define(function (require, exports, module) { entry = this._fileSystem.getDirectoryForPath(entryPath); } - entry._stat = entryStats; - this._contents.push(entry); - this._contentsStats.push(entryStats); + if (entry._isWatched) { + entry._stat = entryStats; + } + + contents.push(entry); + contentsStats.push(entryStats); } } }, this); + + if (this._isWatched) { + this._contents = contents; + this._contentsStats = contentsStats; + this._contentsStatsErrors = contentsStatsErrors; + } } // Reset the callback list before we begin calling back so that @@ -167,7 +176,7 @@ define(function (require, exports, module) { // Invoke all saved callbacks currentCallbacks.forEach(function (cb) { try { - cb(err, this._contents, this._contentsStats, this._contentsStatsErrors); + cb(err, contents, contentsStats, contentsStatsErrors); } catch (ex) { console.warn("Unhandled exception in callback: ", ex); } @@ -184,11 +193,17 @@ define(function (require, exports, module) { Directory.prototype.create = function (callback) { callback = callback || function () {}; this._impl.mkdir(this._path, function (err, stat) { - if (!err) { + if (err) { + this._clearCachedData(); + callback(err); + return; + } + + if (this._isWatched) { this._stat = stat; } - callback(err, stat); + callback(null, stat); }.bind(this)); }; diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 8e6ade9f1ae..848686a1acf 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -72,7 +72,6 @@ define(function (require, exports, module) { this.parentClass._clearCachedData.apply(this); this._contents = undefined; }; - /** * Read a file. @@ -99,9 +98,12 @@ define(function (require, exports, module) { return; } - this._stat = stat; - this._hash = stat._hash; - this._contents = data; + // Only cache the stats for and contents of watched files + if (this._isWatched) { + this._stat = stat; + this._hash = stat._hash; + this._contents = data; + } callback(err, data, stat); }.bind(this)); @@ -122,22 +124,33 @@ define(function (require, exports, module) { } callback = callback || function () {}; + + // Hashes are only saved for watched files; the first disjunct is an optimization + var watched = this._isWatched; + // Request a consistency check if the file is watched and the write is not blind + if (watched && !options.blind) { + options.hash = this._hash; + } + this._fileSystem._beginWrite(); - this._impl.writeFile(this._path, data, this._hash, options, function (err, stat) { + this._impl.writeFile(this._path, data, options, function (err, stat) { try { if (err) { this._clearCachedData(); callback(err); return; } - - this._stat = stat; - this._hash = stat._hash; - this._contents = data; - callback(err, stat); + // Only cache the stats for and contents of watched files + if (watched) { + this._stat = stat; + this._hash = stat._hash; + this._contents = data; + } + + callback(null, stat); } finally { this._fileSystem._endWrite(); // unblock generic change events } diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 03a80520759..68f01a21771 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -222,11 +222,7 @@ define(function (require, exports, module) { */ FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { var watchPaths = [], - allChildren; - - if (!shouldWatch) { allChildren = []; - } var visitor = function (child) { if (watchedRoot.filter(child.name)) { @@ -234,9 +230,7 @@ define(function (require, exports, module) { watchPaths.push(child.fullPath); } - if (!shouldWatch) { - allChildren.push(child); - } + allChildren.push(child); return true; } @@ -267,6 +261,9 @@ define(function (require, exports, module) { watchPaths.forEach(function (path, index) { this._impl.watchPath(path); }, this); + allChildren.forEach(function (child) { + child._setWatched(); + }); } else { watchPaths.forEach(function (path, index) { this._impl.unwatchPath(path); @@ -801,6 +798,10 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; + + // Private methods + exports._findWatchedRootForPath = _wrap(FileSystem.prototype._findWatchedRootForPath); + // Export "on" and "off" methods diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 432907471e1..2a05c0b8674 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -50,6 +50,7 @@ define(function (require, exports, module) { this._setPath(path); this._fileSystem = fileSystem; this._id = nextId++; + this._watched = !!fileSystem._findWatchedRootForPath(path); } // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters @@ -126,6 +127,12 @@ define(function (require, exports, module) { */ FileSystemEntry.prototype._isDirectory = false; + /** + * Whether or not the entry is watched. + * @type {boolean} + */ + FileSystemEntry.prototype._isWatched = false; + /** * Update the path for this entry * @private @@ -149,6 +156,16 @@ define(function (require, exports, module) { this._path = newPath; }; + /** + * Mark this entry as being watched after construction. There is no way to + * set an entry as being unwatched after construction because entries should + * be discarded upon being unwatched. + * @private + */ + FileSystemEntry.prototype._setWatched = function () { + this._isWatched = true; + }; + /** * Clear any cached data for this entry * @private @@ -181,7 +198,15 @@ define(function (require, exports, module) { return; } - this._impl.exists(this._path, callback); + this._impl.exists(this._path, function (err, exists) { + if (err) { + this._clearCachedData(); + callback(err); + return; + } + + callback(null, exists); + }.bind(this)); }; /** @@ -197,10 +222,17 @@ define(function (require, exports, module) { } this._impl.stat(this._path, function (err, stat) { - if (!err) { + if (err) { + this._clearCachedData(); + callback(err); + return; + } + + if (this._isWatched) { this._stat = stat; } - callback(err, stat); + + callback(null, stat); }.bind(this)); }; @@ -216,11 +248,16 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); this._impl.rename(this._path, newFullPath, function (err) { try { - if (!err) { - // Notify the file system of the name change - this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); + if (err) { + this._clearCachedData(); + callback(err); + return; } - callback(err); // notify caller + + // Notify the file system of the name change + this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); + + callback(null); // notify caller } finally { this._fileSystem._endWrite(); // unblock generic change events } @@ -240,11 +277,9 @@ define(function (require, exports, module) { this._clearCachedData(); this._impl.unlink(this._path, function (err) { - if (!err) { - this._fileSystem._index.removeEntry(this); - } + this._fileSystem._index.removeEntry(this); - callback.apply(undefined, arguments); + callback(err); }.bind(this)); }; @@ -265,11 +300,9 @@ define(function (require, exports, module) { this._clearCachedData(); this._impl.moveToTrash(this._path, function (err) { - if (!err) { - this._fileSystem._index.removeEntry(this); - } + this._fileSystem._index.removeEntry(this); - callback.apply(undefined, arguments); + callback(err); }.bind(this)); }; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 1646fc1d407..fada170b56d 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -271,7 +271,7 @@ define(function (require, exports, module) { }); } - function writeFile(path, data, hash, options, callback) { + function writeFile(path, data, options, callback) { var encoding = options.encoding || "utf8"; function _finishWrite(alreadyExists) { @@ -307,13 +307,13 @@ define(function (require, exports, module) { return; } - if (hash !== stats._hash && !options.blind) { - console.warn("Blind write attempted: ", path, stats._hash, hash); + if (options.hasOwnProperty("hash") && options.hash !== stats._hash) { + console.warn("Blind write attempted: ", path, stats._hash, options.hash); callback(FileSystemError.CONTENTS_MODIFIED); return; } - _finishWrite(path, data, encoding, true, callback); + _finishWrite(true); }); } diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index f8a1a550bbe..bc1c7f60bb8 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -271,7 +271,7 @@ define(function (require, exports, module) { } } - function writeFile(path, data, hash, options, callback) { + function writeFile(path, data, options, callback) { if (typeof (options) === "function") { callback = options; options = null; @@ -286,7 +286,7 @@ define(function (require, exports, module) { } var exists = !!stats; - if (exists && hash !== stats._hash && !options.blind) { + if (exists && options.hasOwnProperty("hash") && options.hash !== stats._hash) { cb(FileSystemError.CONTENTS_MODIFIED); return; } From 277f6f7add455b425a8653f75e0ee273fd376d8d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 18 Nov 2013 14:00:01 -0800 Subject: [PATCH 05/94] Add file caching unit tests --- test/spec/FileSystem-test.js | 343 ++++++++++++++++++++++++++++++-- test/spec/MockFileSystemImpl.js | 9 +- 2 files changed, 326 insertions(+), 26 deletions(-) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 42854fd1ca2..bf53b5772f7 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -35,19 +35,7 @@ define(function (require, exports, module) { MockFileSystemImpl = require("./MockFileSystemImpl"); describe("FileSystem", function () { - - // Setup - - var fileSystem; - - beforeEach(function () { - // Create an FS instance for testing - MockFileSystemImpl.reset(); - fileSystem = new FileSystem._FileSystem(); - fileSystem.init(MockFileSystemImpl); - fileSystem.watch(fileSystem.getDirectoryForPath("/"), function () {return true; }, function () {}); - }); - + // Callback factories function resolveCallback() { var callback = function (err, entry) { @@ -75,6 +63,24 @@ define(function (require, exports, module) { }; return callback; } + + function writeCallback() { + var callback = function (err, stat) { + callback.error = err; + callback.stat = stat; + callback.wasCalled = true; + }; + return callback; + } + + function getContentsCallback() { + var callback = function (err, contents) { + callback.error = err; + callback.contents = contents; + callback.wasCalled = true; + }; + return callback; + } // Utilities @@ -91,15 +97,28 @@ define(function (require, exports, module) { } return generateCbWrapper; } - - function getContentsCallback() { - var callback = function (err, contents) { - callback.error = err; - callback.contents = contents; - callback.wasCalled = true; - }; - return callback; + + // Setup + + var fileSystem; + + function permissiveFilter() { + return true; } + + beforeEach(function () { + // Create an FS instance for testing + MockFileSystemImpl.reset(); + fileSystem = new FileSystem._FileSystem(); + fileSystem.init(MockFileSystemImpl); + + var cb = errorCallback(); + fileSystem.watch(fileSystem.getDirectoryForPath("/"), permissiveFilter, cb); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + }); + }); describe("Path normalization", function () { // Auto-prepended to both origPath & normPath in all the test helpers below @@ -920,6 +939,286 @@ define(function (require, exports, module) { }); }); - + describe("File contents caching", function () { + var filename = "/file1.txt", + readCalls, + writeCalls; + + beforeEach(function () { + readCalls = 0; + writeCalls = 0; + + MockFileSystemImpl.when("readFile", filename, { + callback: function (cb) { + return function () { + var args = arguments; + readCalls++; + cb.apply(undefined, args); + }; + } + }); + + MockFileSystemImpl.when("writeFile", filename, { + callback: function (cb) { + return function () { + var args = arguments; + writeCalls++; + cb.apply(undefined, args); + }; + } + }); + }); + + it("should only read from the impl once", function () { + var file = fileSystem.getFileForPath(filename), + cb1 = readCallback(), + cb2 = readCallback(); + + // confirm empty cached data and then read + runs(function () { + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(0); + + file.read(cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm impl read and cached data and then read again + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb1.stat); + expect(file._contents).toBe(cb1.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + + file.read(cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm no impl read and cached data + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(cb2.stat).toBe(cb1.stat); + expect(cb2.data).toBe(cb1.data); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb2.stat); + expect(file._contents).toBe(cb2.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); // The impl should NOT be called a second time + }); + }); + + it("should support blind writes", function () { + var file = fileSystem.getFileForPath(filename), + cb1 = writeCallback(), + cb2 = writeCallback(), + newFileContent = "Computer programming is an exact science"; + + // confirm empty cached data and then write blindly + runs(function () { + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(writeCalls).toBe(0); + + file.write(newFileContent, cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm error, empty cache and then force write + runs(function () { + expect(cb1.error).toBe(FileSystemError.CONTENTS_MODIFIED); + expect(cb1.stat).toBeFalsy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(writeCalls).toBe(1); + + file.write(newFileContent, { blind: true }, cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm impl write and updated cache + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(cb2.stat).toBeTruthy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb2.stat); + expect(file._contents).toBe(newFileContent); + expect(file._hash).toBeTruthy(); + expect(writeCalls).toBe(2); + }); + }); + + it("should persist data on write and update cached data", function () { + var file = fileSystem.getFileForPath(filename), + cb1 = readCallback(), + cb2 = writeCallback(), + newFileContent = "I propose to consider the question, 'Can machines think?'", + savedHash; + + // confirm empty cached data and then read + runs(function () { + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(0); + expect(writeCalls).toBe(0); + + file.read(cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm impl read and cached data and then write + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb1.stat); + expect(file._contents).toBe(cb1.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + expect(writeCalls).toBe(0); + + savedHash = file._hash; + file.write(newFileContent, cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm impl write and updated cache + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(cb2.stat).not.toBe(cb1.stat); + expect(cb2.stat).toBeTruthy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb2.stat); + expect(file._contents).toBe(newFileContent); + expect(file._hash).not.toBe(savedHash); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + expect(writeCalls).toBe(1); + }); + }); + + it("should invalidate cached data on change", function () { + var file = fileSystem.getFileForPath(filename), + cb1 = readCallback(), + cb2 = readCallback(), + fileChanged = false, + savedHash; + + // confirm empty cached data and then read + runs(function () { + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(0); + + file.read(cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm impl read and cached data and then fire a synthetic change event + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb1.stat); + expect(file._contents).toBe(cb1.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + + savedHash = file._hash; + + $(fileSystem).on("change", function (event, filename) { + fileChanged = true; + }); + + // Fire a whole-sale change event + fileSystem._handleWatchResult(null); + }); + waitsFor(function () { return fileChanged; }); + + // confirm now-empty cached data and then read + runs(function () { + expect(file._isWatched).toBe(true); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); // contents and stat should be cleared + expect(file._hash).toBe(savedHash); // but hash should not be cleared + + file.read(cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm impl read and new cached data + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(file._isWatched).toBe(true); + expect(file._stat).toBe(cb2.stat); + expect(file._contents).toBe(cb2.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(2); // The impl should have been called a second time + }); + }); + + it("should not cache data for unwatched files", function () { + var file, + cb0 = errorCallback(), + cb1 = readCallback(), + cb2 = readCallback(); + + // unwatch root directory + runs(function () { + fileSystem.unwatch(fileSystem.getDirectoryForPath("/"), cb0); + }); + waitsFor(function () { return cb0.wasCalled; }); + + // confirm empty cached data and then read + runs(function () { + expect(cb0.error).toBeFalsy(); + + file = fileSystem.getFileForPath(filename); + + expect(file._isWatched).toBe(false); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(0); + + file.read(cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm impl read, empty cached data and then read again + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(file._isWatched).toBe(false); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(1); + + file.read(cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm impl read and empty cached data + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(file._isWatched).toBe(false); + expect(file._stat).toBeFalsy(); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + expect(readCalls).toBe(2); + }); + }); + + }); }); }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index bc1c7f60bb8..5ec041d6554 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -115,9 +115,9 @@ define(function (require, exports, module) { return stat; } - function _sendWatcherNotification(path) { + function _sendWatcherNotification(path, stats) { if (_watcherCallback) { - _watcherCallback(path); + _watcherCallback(path, stats); } } @@ -304,8 +304,9 @@ define(function (require, exports, module) { _data[path].contents = data; _data[path].mtime = new Date(); - cb(null, _getStat(path)); - notify(path); + var newStat = _getStat(path); + cb(null, newStat); + notify(path, newStat); }); } From 771e73ce54cde3f21c99e287fd9ea481906dbad3 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 21 Nov 2013 07:56:01 -0800 Subject: [PATCH 06/94] work in progress --- src/filesystem/FileSystem.js | 26 +- .../impls/appshell/AppshellFileSystem.js | 241 ++++++++---------- .../impls/appshell/node/FileWatcherDomain.js | 145 +++++++++++ 3 files changed, 257 insertions(+), 155 deletions(-) create mode 100644 src/filesystem/impls/appshell/node/FileWatcherDomain.js diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 68f01a21771..3232b3c4a1e 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -307,25 +307,16 @@ define(function (require, exports, module) { }; /** - * @param {function(?string)=} callback Callback resolved, possibly with a - * FileSystemError string. + * Initialize this FileSystem instance. + * + * @param {FileSystemImpl} impl The back-end implementation for this + * FileSystem instance. */ - FileSystem.prototype.init = function (impl, callback) { + FileSystem.prototype.init = function (impl) { console.assert(!this._impl, "This FileSystem has already been initialized!"); - callback = callback || function () {}; - this._impl = impl; - this._impl.init(function (err) { - if (err) { - callback(err); - return; - } - - // Initialize watchers - this._impl.initWatchers(this._enqueueWatchResult.bind(this)); - callback(null); - }.bind(this)); + this._impl.initWatchers(this._enqueueWatchResult.bind(this)); }; /** @@ -611,12 +602,11 @@ define(function (require, exports, module) { if (entry) { if (entry.isFile) { // Update stat and clear contents, but only if out of date - if (!stat || !entry._stat || (stat.mtime.getTime() !== entry._stat.mtime.getTime())) { + if (!(stat && entry._stat && stat.mtime.getTime() === entry._stat.mtime.getTime())) { entry._clearCachedData(); entry._stat = stat; + fireChangeEvent(entry); } - - fireChangeEvent(entry); } else { var oldContents = entry._contents || []; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index fada170b56d..5eadcc176aa 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -33,26 +33,93 @@ define(function (require, exports, module) { FileSystemError = require("filesystem/FileSystemError"), NodeConnection = require("utils/NodeConnection"); - /** - * @const - * Amount of time to wait before automatically rejecting the connection - * deferred. If we hit this timeout, we'll never have a node connection - * for the file watcher in this run of Brackets. - */ - var NODE_CONNECTION_TIMEOUT = 30000, // 30 seconds - TODO: share with StaticServer & Package? - FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes - - /** - * @private - * @type{jQuery.Deferred.} - * A deferred which is resolved with a NodeConnection or rejected if - * we are unable to connect to Node. - */ - var _nodeConnectionDeferred; + var FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes var _changeCallback, // Callback to notify FileSystem of watcher changes _changeTimeout, // Timeout used to batch up file watcher changes _pendingChanges = {}; // Pending file watcher changes + + var _bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), + _modulePath = FileUtils.getNativeModuleDirectoryPath(module), + _nodePath = "node/FileWatcherDomain", + _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), + _nodeConnection = new NodeConnection(); + + function _loadDomains() { + return _nodeConnection.loadDomains(_domainPath, true); + } + + var _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); + + function _enqueueChange(change, needsStats) { + _pendingChanges[change] = _pendingChanges[change] || needsStats; + + if (!_changeTimeout) { + _changeTimeout = window.setTimeout(function () { + if (_changeCallback) { + Object.keys(_pendingChanges).forEach(function (path) { + var needsStats = _pendingChanges[path]; + if (needsStats) { + exports.stat(path, function (err, stats) { + console.warn("Unable to stat changed path: ", path, err); + _changeCallback(path, stats); + }); + } else { + _changeCallback(path); + } + }); + } + + _changeTimeout = null; + _pendingChanges = {}; + }, FILE_WATCHER_BATCH_TIMEOUT); + } + } + + function _fileWatcherChange(evt, path, event, filename) { + var change; + + console.log("change!"); + console.log(arguments); + + if (event === "change") { + // Only register change events if filename is passed + if (filename) { + // an existing file was created; stats are needed + change = path + "/" + filename; + _enqueueChange(change, true); + } + } else if (event === "rename") { + // a new file was created; no stats are needed + change = path; + _enqueueChange(change, false); + } + } + + $(_nodeConnection).on("fileWatcher.change", _fileWatcherChange); + + $(_nodeConnection).on("close", function (event, promise) { + _nodeConnectionPromise = promise.then(_loadDomains); + }); + + function _execWhenConnected(name, args, callback, errback) { + function execConnected() { + var domain = _nodeConnection.domains.fileWatcher, + fn = domain[name]; + + return fn.apply(domain, args) + .done(callback) + .fail(errback); + } + + if (_nodeConnection.connected()) { + execConnected(); + } else { + _nodeConnectionPromise + .done(execConnected) + .fail(errback); + } + } function _mapError(err) { if (!err) { @@ -87,51 +154,6 @@ define(function (require, exports, module) { return path.substr(0, lastSlash + 1); } - - function init(callback) { - /* Temporarily disable file watchers - if (!_nodeConnectionDeferred) { - _nodeConnectionDeferred = new $.Deferred(); - - // TODO: This code is a copy of the AppInit function in extensibility/Package.js. This should be refactored - // into common code. - - - // Start up the node connection, which is held in the - // _nodeConnectionDeferred module variable. (Use - // _nodeConnectionDeferred.done() to access it. - var connectionTimeout = window.setTimeout(function () { - console.error("[AppshellFileSystem] Timed out while trying to connect to node"); - _nodeConnectionDeferred.reject(); - }, NODE_CONNECTION_TIMEOUT); - - var _nodeConnection = new NodeConnection(); - _nodeConnection.connect(true).then(function () { - var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/FileWatcherDomain"; - - _nodeConnection.loadDomains(domainPath, true) - .then( - function () { - window.clearTimeout(connectionTimeout); - _nodeConnectionDeferred.resolve(_nodeConnection); - }, - function () { // Failed to connect - console.error("[AppshellFileSystem] Failed to connect to node", arguments); - window.clearTimeout(connectionTimeout); - _nodeConnectionDeferred.reject(); - } - ); - }); - } - */ - - // Don't want to block on _nodeConnectionDeferred because we're needed as the 'root' fs - // at startup -- and the Node-side stuff isn't needed for most functionality anyway. - if (callback) { - callback(); - } - } - function _wrap(cb) { return function (err) { var args = Array.prototype.slice.call(arguments); @@ -339,93 +361,35 @@ define(function (require, exports, module) { }); } - /* File watchers are temporarily disabled - function _notifyChanges(callback) { - var change; - - for (change in _pendingChanges) { - if (_pendingChanges.hasOwnProperty(change)) { - callback(change); - delete _pendingChanges[change]; - } - } - } - - function _fileWatcherChange(evt, path, event, filename) { - var change; - - if (event === "change") { - // Only register change events if filename is passed - if (filename) { - change = path + "/" + filename; - } - } else if (event === "rename") { - change = path; - } - if (change && !_pendingChanges.hasOwnProperty(change)) { - if (!_changeTimeout) { - _changeTimeout = window.setTimeout(function () { - _changeTimeout = null; - _notifyChanges(_fileWatcherChange.callback); - }, FILE_WATCHER_BATCH_TIMEOUT); - } - - _pendingChanges[change] = true; - } - } - */ - function initWatchers(callback) { _changeCallback = callback; - - /* File watchers are temporarily disabled. For now, send - a "wholesale" change when the window is focused. */ - $(window).on("focus", function () { - callback(null); - }); - - /* - _nodeConnectionDeferred.done(function (nodeConnection) { - if (nodeConnection.connected()) { - _fileWatcherChange.callback = callback; - $(nodeConnection).on("fileWatcher.change", _fileWatcherChange); - } - }); - */ } - function watchPath(path) { - /* - _nodeConnectionDeferred.done(function (nodeConnection) { - if (nodeConnection.connected()) { - nodeConnection.domains.fileWatcher.watchPath(path); - } - }); - */ + function watchPath(path, callback) { + callback = callback || function () {}; + + _execWhenConnected("watchPath", [path], + callback.bind(undefined, null), + callback.bind(undefined)); } - function unwatchPath(path) { - /* - _nodeConnectionDeferred.done(function (nodeConnection) { - if (nodeConnection.connected()) { - nodeConnection.domains.fileWatcher.unwatchPath(path); - } - }); - */ + function unwatchPath(path, callback) { + callback = callback || function () {}; + + _execWhenConnected("unwatchPath", [path], + callback.bind(undefined, null), + callback.bind(undefined)); } - function unwatchAll() { - /* - _nodeConnectionDeferred.done(function (nodeConnection) { - if (nodeConnection.connected()) { - nodeConnection.domains.fileWatcher.unwatchAll(); - } - }); - */ + function unwatchAll(callback) { + callback = callback || function () {}; + + _execWhenConnected("watchPath", [], + callback.bind(undefined, null), + callback.bind(undefined)); } // Export public API - exports.init = init; exports.showOpenDialog = showOpenDialog; exports.showSaveDialog = showSaveDialog; exports.exists = exists; @@ -442,6 +406,9 @@ define(function (require, exports, module) { exports.unwatchPath = unwatchPath; exports.unwatchAll = unwatchAll; + // Node only supports recursive file watching on the Darwin + exports.recursiveWatch = appshell.platform === "mac"; + // Only perform UNC path normalization on Windows exports.normalizeUNCPaths = appshell.platform === "win"; }); diff --git a/src/filesystem/impls/appshell/node/FileWatcherDomain.js b/src/filesystem/impls/appshell/node/FileWatcherDomain.js new file mode 100644 index 00000000000..cafc95442ed --- /dev/null +++ b/src/filesystem/impls/appshell/node/FileWatcherDomain.js @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, node: true, nomen: true, indent: 4, maxerr: 50 */ + +"use strict"; + +var fs = require("fs"); + +var _domainManager, + _watcherMap = {}; + +/** + * Un-watch a file or directory. + * @param {string} path File or directory to unwatch. + */ +function unwatchPath(path) { + var watcher = _watcherMap[path]; + + if (watcher) { + try { + watcher.close(); + } catch (err) { + console.warn("Failed to unwatch file " + path + ": " + (err && err.message)); + } finally { + delete _watcherMap[path]; + } + } +} + +/** + * Watch a file or directory. + * @param {string} path File or directory to watch. + */ +function watchPath(path) { + if (_watcherMap.hasOwnProperty(path)) { + return; + } + + try { + var watcher = fs.watch(path, {persistent: false}, function (event, filename) { + // File/directory changes are emitted as "change" events on the fileWatcher domain. + _domainManager.emitEvent("fileWatcher", "change", [path, event, filename]); + }); + + _watcherMap[path] = watcher; + + watcher.on("error", function (err) { + console.error("Error watching file " + path + ": " + (err && err.message)); + unwatchPath(path); + }); + } catch (err) { + console.warn("Failed to watch file " + path + ": " + (err && err.message)); + } +} + +/** + * Un-watch all files and directories. + */ +function unwatchAll() { + var path; + + for (path in _watcherMap) { + if (_watcherMap.hasOwnProperty(path)) { + unwatchPath(path); + } + } +} + +/** + * Initialize the "fileWatcher" domain. + * The fileWatcher domain handles watching and un-watching directories. + */ +function init(domainManager) { + if (!domainManager.hasDomain("fileWatcher")) { + domainManager.registerDomain("fileWatcher", {major: 0, minor: 1}); + } + + domainManager.registerCommand( + "fileWatcher", + "watchPath", + watchPath, + false, + "Start watching a file or directory", + [{ + name: "path", + type: "string", + description: "absolute filesystem path of the file or directory to watch" + }] + ); + domainManager.registerCommand( + "fileWatcher", + "unwatchPath", + unwatchPath, + false, + "Stop watching a file or directory", + [{ + name: "path", + type: "string", + description: "absolute filesystem path of the file or directory to unwatch" + }] + ); + domainManager.registerCommand( + "fileWatcher", + "unwatchAll", + unwatchAll, + false, + "Stop watching all files and directories" + ); + domainManager.registerEvent( + "fileWatcher", + "change", + [ + {name: "path", type: "string"}, + {name: "event", type: "string"}, + {name: "filename", type: "string"} + ] + ); + + _domainManager = domainManager; +} + +exports.init = init; + From 0ea50d4b94013a2b345c55dbb21c37cac10b4c28 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 21 Nov 2013 14:03:37 -0800 Subject: [PATCH 07/94] Do not exec until connection is open and domains have been loaded; add a reminder to refresh watched paths upon domain reload --- .../impls/appshell/AppshellFileSystem.js | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 5eadcc176aa..0a3593fce21 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -43,13 +43,8 @@ define(function (require, exports, module) { _modulePath = FileUtils.getNativeModuleDirectoryPath(module), _nodePath = "node/FileWatcherDomain", _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), - _nodeConnection = new NodeConnection(); - - function _loadDomains() { - return _nodeConnection.loadDomains(_domainPath, true); - } - - var _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); + _nodeConnection = new NodeConnection(), + _domainsLoaded = false; function _enqueueChange(change, needsStats) { _pendingChanges[change] = _pendingChanges[change] || needsStats; @@ -61,7 +56,10 @@ define(function (require, exports, module) { var needsStats = _pendingChanges[path]; if (needsStats) { exports.stat(path, function (err, stats) { - console.warn("Unable to stat changed path: ", path, err); + if (err) { + console.warn("Unable to stat changed path: ", path, err); + return; + } _changeCallback(path, stats); }); } else { @@ -79,14 +77,13 @@ define(function (require, exports, module) { function _fileWatcherChange(evt, path, event, filename) { var change; - console.log("change!"); - console.log(arguments); + console.log.bind(console, "Change!").apply(undefined, arguments); if (event === "change") { // Only register change events if filename is passed if (filename) { // an existing file was created; stats are needed - change = path + "/" + filename; + change = path + filename; _enqueueChange(change, true); } } else if (event === "rename") { @@ -96,10 +93,27 @@ define(function (require, exports, module) { } } + function _loadDomains() { + return _nodeConnection + .loadDomains(_domainPath, true) + .done(function () { + _domainsLoaded = true; + }); + } + + function _reloadDomains() { + return _loadDomains().done(function () { + // call back into the filesystem here to restore any previously watched paths. + }); + } + + var _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); + $(_nodeConnection).on("fileWatcher.change", _fileWatcherChange); $(_nodeConnection).on("close", function (event, promise) { - _nodeConnectionPromise = promise.then(_loadDomains); + _domainsLoaded = false; + _nodeConnectionPromise = promise.then(_reloadDomains); }); function _execWhenConnected(name, args, callback, errback) { @@ -112,7 +126,7 @@ define(function (require, exports, module) { .fail(errback); } - if (_nodeConnection.connected()) { + if (_domainsLoaded && _nodeConnection.connected()) { execConnected(); } else { _nodeConnectionPromise From 9c7d7d7e373416ec3a884386dc4d25483fe98c9f Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 22 Nov 2013 11:48:56 -0800 Subject: [PATCH 08/94] work in progress, moving synthetic events out of the impl and up to the FileSystem level --- src/filesystem/Directory.js | 6 ++- src/filesystem/File.js | 39 +++++++++++++------ src/filesystem/FileSystemEntry.js | 18 ++++++--- .../impls/appshell/AppshellFileSystem.js | 38 ++++-------------- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 77234f14911..aea991146db 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -203,7 +203,11 @@ define(function (require, exports, module) { this._stat = stat; } - callback(null, stat); + try { + callback(null, stat); + } finally { + this._fileSystem._handleWatchResult(this.parent, stat); + } }.bind(this)); }; diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 848686a1acf..390221d9e98 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -28,7 +28,8 @@ define(function (require, exports, module) { "use strict"; - var FileSystemEntry = require("filesystem/FileSystemEntry"); + var FileSystemEntry = require("filesystem/FileSystemEntry"), + FileSystemError = require("filesystem/FileSystemError"); /* @@ -98,10 +99,11 @@ define(function (require, exports, module) { return; } + this._hash = stat._hash; + // Only cache the stats for and contents of watched files if (this._isWatched) { this._stat = stat; - this._hash = stat._hash; this._contents = data; } @@ -132,26 +134,41 @@ define(function (require, exports, module) { if (watched && !options.blind) { options.hash = this._hash; } - + this._fileSystem._beginWrite(); - this._impl.writeFile(this._path, data, options, function (err, stat) { - try { - if (err) { - this._clearCachedData(); + this._impl.writeFile(this._path, data, options, function (err, stat, created) { + + if (err) { + this._clearCachedData(); + + try { callback(err); - return; + } finally { + this._fileSystem._endWrite(); // unblock generic change events + } + return; + } + + this._hash = stat._hash; + + try { + callback(null, stat); + } finally { + if (created) { + // new file created + this._fileSystem._handleWatchResult(this._parentPath); + } else { + // existing file modified + this._fileSystem._handleWatchResult(this._path, stat); } // Only cache the stats for and contents of watched files if (watched) { this._stat = stat; - this._hash = stat._hash; this._contents = data; } - callback(null, stat); - } finally { this._fileSystem._endWrite(); // unblock generic change events } }.bind(this)); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 2a05c0b8674..996155418f6 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -277,9 +277,12 @@ define(function (require, exports, module) { this._clearCachedData(); this._impl.unlink(this._path, function (err) { - this._fileSystem._index.removeEntry(this); - - callback(err); + try { + callback(err); + } finally { + this._fileSystem._handleWatchResult(this._parentPath); + this._fileSystem._index.removeEntry(this); + } }.bind(this)); }; @@ -300,9 +303,12 @@ define(function (require, exports, module) { this._clearCachedData(); this._impl.moveToTrash(this._path, function (err) { - this._fileSystem._index.removeEntry(this); - - callback(err); + try { + callback(err); + } finally { + this._fileSystem._handleWatchResult(this._parentPath); + this._fileSystem._index.removeEntry(this); + } }.bind(this)); }; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 0a3593fce21..819edb65904 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -255,12 +255,7 @@ define(function (require, exports, module) { callback(_mapError(err)); } else { stat(path, function (err, stat) { - try { - callback(err, stat); - } finally { - // Fake a file-watcher result until real watchers respond quickly - _changeCallback(_parentPath(path)); - } + callback(err, stat); }); } }); @@ -310,22 +305,13 @@ define(function (require, exports, module) { function writeFile(path, data, options, callback) { var encoding = options.encoding || "utf8"; - function _finishWrite(alreadyExists) { + function _finishWrite(created) { appshell.fs.writeFile(path, data, encoding, function (err) { if (err) { callback(_mapError(err)); } else { stat(path, function (err, stat) { - try { - callback(err, stat); - } finally { - // Fake a file-watcher result until real watchers respond quickly - if (alreadyExists) { - _changeCallback(path, stat); // existing file modified - } else { - _changeCallback(_parentPath(path)); // new file created - } - } + callback(err, stat, created); }); } }); @@ -335,7 +321,7 @@ define(function (require, exports, module) { if (err) { switch (err) { case FileSystemError.NOT_FOUND: - _finishWrite(false); + _finishWrite(true); break; default: callback(err); @@ -349,29 +335,19 @@ define(function (require, exports, module) { return; } - _finishWrite(true); + _finishWrite(false); }); } function unlink(path, callback) { appshell.fs.unlink(path, function (err) { - try { - callback(_mapError(err)); - } finally { - // Fake a file-watcher result until real watchers respond quickly - _changeCallback(_parentPath(path)); - } + callback(_mapError(err)); }); } function moveToTrash(path, callback) { appshell.fs.moveToTrash(path, function (err) { - try { - callback(_mapError(err)); - } finally { - // Fake a file-watcher result until real watchers respond quickly - _changeCallback(_parentPath(path)); - } + callback(_mapError(err)); }); } From 24e3686a9d96a9383209148260afa72ffcc86054 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Sun, 24 Nov 2013 08:42:00 -0800 Subject: [PATCH 09/94] Clean up + some unit test fixes --- src/filesystem/File.js | 56 +++++++++++++++---------------- src/filesystem/FileSystemEntry.js | 13 ++++--- test/spec/FileSystem-test.js | 9 +++-- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 390221d9e98..a33e0a73985 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -127,7 +127,7 @@ define(function (require, exports, module) { callback = callback || function () {}; - // Hashes are only saved for watched files; the first disjunct is an optimization + // Hashes are only saved for watched files var watched = this._isWatched; // Request a consistency check if the file is watched and the write is not blind @@ -135,41 +135,41 @@ define(function (require, exports, module) { options.hash = this._hash; } + // Block external change events until after the write has finished this._fileSystem._beginWrite(); this._impl.writeFile(this._path, data, options, function (err, stat, created) { - - if (err) { - this._clearCachedData(); + try { + if (err) { + this._clearCachedData(); + + callback(err); + return; + } + + this._hash = stat._hash; try { - callback(err); + callback(null, stat); } finally { - this._fileSystem._endWrite(); // unblock generic change events + // If the write succeeded, fire a synthetic change event + if (created) { + // new file created + this._fileSystem._handleWatchResult(this._parentPath); + } else { + // existing file modified + this._fileSystem._handleWatchResult(this._path, stat); + } + + // Update cached stats and contents if the file is watched + if (watched) { + this._stat = stat; + this._contents = data; + } } - return; - } - - this._hash = stat._hash; - - try { - callback(null, stat); } finally { - if (created) { - // new file created - this._fileSystem._handleWatchResult(this._parentPath); - } else { - // existing file modified - this._fileSystem._handleWatchResult(this._path, stat); - } - - // Only cache the stats for and contents of watched files - if (watched) { - this._stat = stat; - this._contents = data; - } - - this._fileSystem._endWrite(); // unblock generic change events + // Always unblock external change events + this._fileSystem._endWrite(); } }.bind(this)); }; diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 996155418f6..69eea8e4aff 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -254,12 +254,15 @@ define(function (require, exports, module) { return; } - // Notify the file system of the name change - this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); - - callback(null); // notify caller + try { + callback(null); // notify caller + } finally { + // Notify the file system of the name change + this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); + } } finally { - this._fileSystem._endWrite(); // unblock generic change events + // Unblock external change events + this._fileSystem._endWrite(); } }.bind(this)); }; diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index bf53b5772f7..d790786f19b 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1171,7 +1171,8 @@ define(function (require, exports, module) { var file, cb0 = errorCallback(), cb1 = readCallback(), - cb2 = readCallback(); + cb2 = readCallback(), + savedHash; // unwatch root directory runs(function () { @@ -1201,9 +1202,11 @@ define(function (require, exports, module) { expect(file._isWatched).toBe(false); expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); - expect(file._hash).toBeFalsy(); + expect(file._hash).toBeTruthy(); expect(readCalls).toBe(1); + // Save a file hash even if we aren't watching the file + savedHash = file._hash; file.read(cb2); }); waitsFor(function () { return cb2.wasCalled; }); @@ -1214,7 +1217,7 @@ define(function (require, exports, module) { expect(file._isWatched).toBe(false); expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); - expect(file._hash).toBeFalsy(); + expect(file._hash).toBe(savedHash); expect(readCalls).toBe(2); }); }); From 32c37c0b006b6a601be124182e10161e8c708ebc Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 25 Nov 2013 11:29:10 -0800 Subject: [PATCH 10/94] Do not catch exceptions thrown by Directory.getContents callbacks --- src/filesystem/Directory.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index aea991146db..9cdf1afbf88 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -83,6 +83,25 @@ define(function (require, exports, module) { this._contentsStatsErrors = undefined; }; + /** + * Apply each callback in a list to the provided arguments. Callbacks + * can throw without preventing other callbacks from being applied. + * + * @private + * @param {Array.} callbacks The callbacks to apply + * @param {Array} args The arguments to which each callback is applied + */ + function _applyAllCallbacks(callbacks, args) { + if (callbacks.length > 0) { + var callback = callbacks.pop(); + try { + callback.apply(undefined, args); + } finally { + _applyAllCallbacks(callbacks, args); + } + } + } + /** * Read the contents of a Directory. * @@ -174,13 +193,8 @@ define(function (require, exports, module) { this._contentsCallbacks = null; // Invoke all saved callbacks - currentCallbacks.forEach(function (cb) { - try { - cb(err, contents, contentsStats, contentsStatsErrors); - } catch (ex) { - console.warn("Unhandled exception in callback: ", ex); - } - }, this); + var callbackArgs = [err, contents, contentsStats, contentsStatsErrors]; + _applyAllCallbacks(currentCallbacks, callbackArgs); }.bind(this)); }; From 3c1e7b3f580449b63aded69dcc81dac9f849742e Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 25 Nov 2013 11:31:04 -0800 Subject: [PATCH 11/94] Include added and removed sets in change events --- src/filesystem/FileSystem.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 3232b3c4a1e..ab162080f61 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -580,9 +580,9 @@ define(function (require, exports, module) { */ FileSystem.prototype._handleWatchResult = function (path, stat) { - var fireChangeEvent = function (entry) { + var fireChangeEvent = function (entry, added, removed) { // Trigger a change event - $(this).trigger("change", entry); + $(this).trigger("change", [entry, added, removed]); }.bind(this); if (!path) { @@ -606,6 +606,8 @@ define(function (require, exports, module) { entry._clearCachedData(); entry._stat = stat; fireChangeEvent(entry); + } else { + console.info("Detected duplicate file change event: ", path); } } else { var oldContents = entry._contents || []; @@ -633,12 +635,12 @@ define(function (require, exports, module) { var addCounter = entriesToAdd.length; if (addCounter === 0) { - callback(); + callback([]); } else { entriesToAdd.forEach(function (entry) { this._watchEntry(entry, watchedRoot, function (err) { if (--addCounter === 0) { - callback(); + callback(entriesToAdd); } }); }, this); @@ -653,12 +655,12 @@ define(function (require, exports, module) { var removeCounter = entriesToRemove.length; if (removeCounter === 0) { - callback(); + callback([]); } else { entriesToRemove.forEach(function (entry) { this._unwatchEntry(entry, watchedRoot, function (err) { if (--removeCounter === 0) { - callback(); + callback(entriesToRemove); } }); }, this); @@ -668,9 +670,13 @@ define(function (require, exports, module) { if (err) { console.warn("Unable to get contents of changed directory: ", path, err); } else { - removeOldEntries(function () { - addNewEntries(function () { - fireChangeEvent(entry); + removeOldEntries(function (removed) { + addNewEntries(function (added) { + if (added.length > 0 || removed.length > 0) { + fireChangeEvent(entry, added, removed); + } else { + console.info("Detected duplicate directory change event: ", path); + } }); }); } From 57f3085a3749cc2d944bb2ba2ee377a485514064 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 25 Nov 2013 11:31:28 -0800 Subject: [PATCH 12/94] Update search results using filesystem change event added and removed sets --- src/search/FindInFiles.js | 164 ++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 76 deletions(-) diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index e2eebf4275e..0e9113c524e 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -111,6 +111,14 @@ define(function (require, exports, module) { /** @type {FindInFilesDialog} dialog having the modalbar for search */ var dialog = null; + + /** + * FileSystem change event handler. Updates the search results based on a changed + * entry and optionally sets of added and removed child entries. + * + * @type {function(FileSystemEntry, Array.=, Array.=)} + **/ + var _fileSystemChangeHandler; /** * @private @@ -171,7 +179,7 @@ define(function (require, exports, module) { return Strings.FIND_IN_FILES_NO_SCOPE; } } - + /** * @private * Hides the Search Results Panel @@ -181,9 +189,10 @@ define(function (require, exports, module) { searchResultsPanel.hide(); $(DocumentModule).off(".findInFiles"); } + + FileSystem.off("change", _fileSystemChangeHandler); } - /** * @private * Searches through the contents an returns an array of matches @@ -234,6 +243,7 @@ define(function (require, exports, module) { * @param {string} fullPath * @param {string} contents * @param {RegExp} queryExpr + * @return {boolean} True iff matches were added to the search results */ function _addSearchMatches(fullPath, contents, queryExpr) { var matches = _getSearchMatches(contents, queryExpr); @@ -243,7 +253,9 @@ define(function (require, exports, module) { matches: matches, collapsed: false }; + return true; } + return false; } /** @@ -271,7 +283,6 @@ define(function (require, exports, module) { return Math.floor((numMatches - 1) / RESULTS_PER_PAGE) * RESULTS_PER_PAGE; } - /** * @private * Shows the results in a table and adds the necessary event listeners @@ -479,6 +490,8 @@ define(function (require, exports, module) { dialog._close(); dialog = null; } + + FileSystem.on("change", _fileSystemChangeHandler); } else { _hideSearchResults(); @@ -493,9 +506,7 @@ define(function (require, exports, module) { } } } - - - + /** * @private * Shows the search results and tries to restore the previous scroll and selection @@ -518,7 +529,7 @@ define(function (require, exports, module) { } } } - + /** * @private * Update the search results using the given list of changes fr the given document @@ -652,6 +663,26 @@ define(function (require, exports, module) { } } } + + function _doSearchInOneFile(addMatches, file) { + var result = new $.Deferred(); + + if (!_inScope(file, currentScope)) { + result.resolve(); + } else { + DocumentManager.getDocumentText(file) + .done(function (text) { + addMatches(file.fullPath, text, currentQueryExpr); + result.resolve(); + }) + .fail(function (error) { + // Always resolve. If there is an error, this file + // is skipped and we move on to the next file. + result.resolve(); + }); + } + return result.promise(); + } /** * @private @@ -673,38 +704,21 @@ define(function (require, exports, module) { perfTimer = PerfUtils.markStart("FindIn: " + scopeName + " - " + query); ProjectManager.getAllFiles(true) - .done(function (fileListResult) { - Async.doInParallel(fileListResult, function (file) { - var result = new $.Deferred(); - - if (!_inScope(file, currentScope)) { - result.resolve(); - } else { - DocumentManager.getDocumentText(file) - .done(function (text) { - _addSearchMatches(file.fullPath, text, currentQueryExpr); - result.resolve(); - }) - .fail(function (error) { - // Always resolve. If there is an error, this file - // is skipped and we move on to the next file. - result.resolve(); - }); - } - return result.promise(); - }) - .done(function () { - // Done searching all files: show results - _showSearchResults(); - StatusBar.hideBusyIndicator(); - PerfUtils.addMeasurement(perfTimer); - $(DocumentModule).on("documentChange.findInFiles", _documentChangeHandler); - }) - .fail(function () { - console.log("find in files failed."); - StatusBar.hideBusyIndicator(); - PerfUtils.finalizeMeasurement(perfTimer); - }); + .then(function (fileListResult) { + var doSearch = _doSearchInOneFile.bind(undefined, _addSearchMatches); + return Async.doInParallel(fileListResult, doSearch); + }) + .done(function () { + // Done searching all files: show results + _showSearchResults(); + StatusBar.hideBusyIndicator(); + PerfUtils.addMeasurement(perfTimer); + $(DocumentModule).on("documentChange.findInFiles", _documentChangeHandler); + }) + .fail(function (err) { + console.log("find in files failed: ", err); + StatusBar.hideBusyIndicator(); + PerfUtils.finalizeMeasurement(perfTimer); }); } @@ -877,49 +891,49 @@ define(function (require, exports, module) { * Handle a FileSystem "change" event * @param {$.Event} event * @param {FileSystemEntry} entry + * @param {Array.=} added Added children + * @param {Array.=} removed Removed children */ - function _fileSystemChangeHandler(event, entry) { + _fileSystemChangeHandler = function (event, entry, added, removed) { if (entry && entry.isDirectory) { var resultsChanged = false; - // This is a temporary watcher implementation that needs to be updated - // once we have our final watcher API. Specifically, we will be adding - // 'added' and 'removed' parameters to this function to easily determine - // which files/folders have been added or removed. - // - // In the meantime, do a quick check for directory changed events to see - // if any of the search results files have been deleted. - if (searchResultsPanel.isVisible()) { - entry.getContents(function (err, contents) { - if (!err) { - var _includesPath = function (fullPath) { - return _.some(contents, function (item) { - return item.fullPath === fullPath; - }); - }; - - // Update the search results - _.forEach(searchResults, function (item, fullPath) { - if (fullPath.lastIndexOf("/") === entry.fullPath.length - 1) { - // The changed directory includes this entry. Make sure the file still exits. - if (!_includesPath(fullPath)) { - delete searchResults[fullPath]; - resultsChanged = true; - } - } - }); - - // Restore the results if needed - if (resultsChanged) { - _restoreSearchResults(); - } + if (removed && removed.length > 0) { + var _includesPath = function (fullPath) { + return _.some(removed, function (item) { + return item.fullPath === fullPath; + }); + }; + + // Clear removed entries from the search results + _.forEach(searchResults, function (item, fullPath) { + if (fullPath.indexOf(entry.fullPath) === 0 && _includesPath(fullPath)) { + // The changed directory includes this entry and it was not removed. + delete searchResults[fullPath]; + resultsChanged = true; } }); } + + var addPromise; + if (added && added.length > 0) { + var doSearch = _doSearchInOneFile.bind(undefined, function () { + var resultsAdded = _addSearchMatches.apply(undefined, arguments); + resultsChanged = resultsChanged || resultsAdded; + }); + addPromise = Async.doInParallel(added, doSearch); + } else { + addPromise = $.Deferred().resolve().promise(); + } + + addPromise.done(function () { + // Restore the results if needed + if (resultsChanged) { + _restoreSearchResults(); + } + }); } - } - - + }; // Initialize items dependent on HTML DOM AppInit.htmlReady(function () { @@ -935,8 +949,6 @@ define(function (require, exports, module) { $(DocumentManager).on("fileNameChange", _fileNameChangeHandler); $(ProjectManager).on("beforeProjectClose", _hideSearchResults); - FileSystem.on("change", _fileSystemChangeHandler); - // Initialize: command handlers CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, _doFindInFiles); CommandManager.register(Strings.CMD_FIND_IN_SUBTREE, Commands.EDIT_FIND_IN_SUBTREE, _doFindInSubtree); From f56542039856411fe1b6e9d0cd1f34638f9dd4e5 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 25 Nov 2013 16:15:36 -0800 Subject: [PATCH 13/94] Add unified recursive or explicit watch and unwatch support --- src/filesystem/FileIndex.js | 2 - src/filesystem/FileSystem.js | 116 ++++++++++-------- .../impls/appshell/AppshellFileSystem.js | 2 +- 3 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/filesystem/FileIndex.js b/src/filesystem/FileIndex.js index 06630e5500a..e6af676c600 100644 --- a/src/filesystem/FileIndex.js +++ b/src/filesystem/FileIndex.js @@ -96,8 +96,6 @@ define(function (require, exports, module) { } } - entry._clearCachedData(); - delete this._index[path]; for (property in entry) { diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index ab162080f61..ac756919ed3 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -221,61 +221,83 @@ define(function (require, exports, module) { * or unwatched (false). */ FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { - var watchPaths = [], - allChildren = []; + var commandName = shouldWatch ? "watchPath" : "unwatchPath", + watchOrUnwatch = this._impl[commandName].bind(this, entry.fullPath), + visitor; + + var genericProcessChild; + if (shouldWatch) { + genericProcessChild = function (child) { + child._setWatched(); + }; + } else { + genericProcessChild = function (child) { + child._clearCachedData(); + this._index.removeEntry(child.fullPath); + }; + } - var visitor = function (child) { + var genericVisitor = function (processChild, child) { if (watchedRoot.filter(child.name)) { - if (child.isDirectory || child === watchedRoot.entry) { - watchPaths.push(child.fullPath); - } + processChild.call(this, child); - allChildren.push(child); - return true; } return false; - }.bind(this); - - entry.visit(visitor, function (err) { - if (err) { - callback(err); - return; - } + }; + + if (this._impl.recursiveWatch) { + // The impl will handle finding all subdirectories to watch. Here we + // just need to find all entries in order to either mark them as + // watched or to remove them from the index. + this._enqueueWatchRequest(function (callback) { + watchOrUnwatch(function (err) { + if (err) { + console.warn("Watch error: ", entry.fullPath, err); + callback(err); + return; + } + + visitor = genericVisitor.bind(this, genericProcessChild); + entry.visit(visitor, callback); + }.bind(this)); + }.bind(this), callback); + } else { + // The impl can't handle recursive watch requests, so it's up to the + // filesystem to recursively watch or unwatch all subdirectories, as + // well as either marking all children as watched or removing them + // from the index. - // sort paths by max depth for a breadth-first traversal - var dirCount = {}; - watchPaths.forEach(function (path) { - dirCount[path] = path.split("/").length; - }); + var counter = 0; - watchPaths.sort(function (path1, path2) { - var dirCount1 = dirCount[path1], - dirCount2 = dirCount[path2]; - - return dirCount1 - dirCount2; - }); + var processChild = function (child) { + if (child.isDirectory || child === watchedRoot.entry) { + watchOrUnwatch(function (err) { + if (err) { + console.warn("Watch error: ", child.fullPath, err); + return; + } + + if (child.isDirectory) { + child.getContents(function (err, contents) { + if (err) { + return; + } + + contents.forEach(function (child) { + genericProcessChild.call(this, child); + }, this); + }.bind(this)); + } + }.bind(this)); + } + }; this._enqueueWatchRequest(function (callback) { - if (shouldWatch) { - watchPaths.forEach(function (path, index) { - this._impl.watchPath(path); - }, this); - allChildren.forEach(function (child) { - child._setWatched(); - }); - } else { - watchPaths.forEach(function (path, index) { - this._impl.unwatchPath(path); - }, this); - allChildren.forEach(function (child) { - this._index.removeEntry(child); - }, this); - } - - callback(null); + visitor = genericVisitor.bind(this, processChild); + entry.visit(visitor, callback); }.bind(this), callback); - }.bind(this)); + } }; /** @@ -725,15 +747,14 @@ define(function (require, exports, module) { return; } - this._watchedRoots[fullPath] = watchedRoot; - this._watchEntry(entry, watchedRoot, function (err) { if (err) { console.warn("Failed to watch root: ", entry.fullPath, err); callback(err); return; } - + + this._watchedRoots[fullPath] = watchedRoot; callback(null); }.bind(this)); }; @@ -759,7 +780,6 @@ define(function (require, exports, module) { } delete this._watchedRoots[fullPath]; - this._unwatchEntry(entry, watchedRoot, function (err) { if (err) { console.warn("Failed to unwatch root: ", entry.fullPath, err); diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 819edb65904..e19f27c037d 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -397,7 +397,7 @@ define(function (require, exports, module) { exports.unwatchAll = unwatchAll; // Node only supports recursive file watching on the Darwin - exports.recursiveWatch = appshell.platform === "mac"; + exports.recursiveWatch = false; // appshell.platform === "mac"; // Only perform UNC path normalization on Windows exports.normalizeUNCPaths = appshell.platform === "win"; From de3f16923e7ee1da2ab97195de9a91dc3290fcdb Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 26 Nov 2013 14:15:13 -0800 Subject: [PATCH 14/94] Save As should always allow blind writes --- src/document/DocumentCommandHandlers.js | 2 +- src/file/FileUtils.js | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 0489668e86e..42ec681cba9 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -654,7 +654,7 @@ define(function (require, exports, module) { // First, write document's current text to new file newFile = FileSystem.getFileForPath(path); - FileUtils.writeText(newFile, doc.getText()).done(function () { + FileUtils.writeText(newFile, doc.getText(), true).done(function () { // Add new file to project tree ProjectManager.refreshFileTree().done(function () { // If there were unsaved changes before Save As, they don't stay with the old diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 03e98f07e94..22caf38d34b 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -73,13 +73,19 @@ define(function (require, exports, module) { * Asynchronously writes a file as UTF-8 encoded text. * @param {!File} file File to write * @param {!string} text + * @param {boolean=} allowBlindWrite * @return {$.Promise} a jQuery promise that will be resolved when * file writing completes, or rejected with a FileSystemError. */ - function writeText(file, text) { - var result = new $.Deferred(); + function writeText(file, text, allowBlindWrite) { + var result = new $.Deferred(), + options = {}; + + if (allowBlindWrite) { + options.blind = true; + } - file.write(text, function (err) { + file.write(text, options, function (err) { if (!err) { result.resolve(); } else { From 94a23b8e48213bbdbf16131e9d906888bc5fbaf2 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 26 Nov 2013 14:36:08 -0800 Subject: [PATCH 15/94] Initial support for FSEvents-based change notifications --- src/filesystem/FileSystem.js | 33 +- .../impls/appshell/AppshellFileSystem.js | 4 +- .../impls/appshell/node/FileWatcherDomain.js | 40 +- .../node/node_modules/fsevents/CHANGELOG.md | 10 + .../node/node_modules/fsevents/LICENSE | 22 ++ .../node/node_modules/fsevents/Readme.md | 72 ++++ .../node/node_modules/fsevents/binding.gyp | 8 + .../node/node_modules/fsevents/build/Makefile | 350 ++++++++++++++++++ .../fsevents/build/Release/.deps/Makefile.d | 1 + .../Release/.deps/Release/fswatch.node.d | 1 + .../obj.target/fswatch/nodefsevents.o.d | 19 + .../fsevents/build/Release/fswatch.node | Bin 0 -> 25480 bytes .../fsevents/build/Release/linker.lock | 0 .../Release/obj.target/fswatch/nodefsevents.o | Bin 0 -> 103332 bytes .../fsevents/build/binding.Makefile | 6 + .../node_modules/fsevents/build/config.gypi | 36 ++ .../fsevents/build/fswatch.target.mk | 154 ++++++++ .../node_modules/fsevents/build/gyp-mac-tool | 265 +++++++++++++ .../node/node_modules/fsevents/fsevents.js | 69 ++++ .../node_modules/fsevents/nodefsevents.cc | 196 ++++++++++ .../node/node_modules/fsevents/package.json | 26 ++ 21 files changed, 1292 insertions(+), 20 deletions(-) create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/CHANGELOG.md create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/LICENSE create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/Readme.md create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/binding.gyp create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Makefile create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Makefile.d create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/fswatch.node.d create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/obj.target/fswatch/nodefsevents.o.d create mode 100755 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/fswatch.node create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/linker.lock create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/obj.target/fswatch/nodefsevents.o create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/binding.Makefile create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/config.gypi create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/build/fswatch.target.mk create mode 100755 src/filesystem/impls/appshell/node/node_modules/fsevents/build/gyp-mac-tool create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/fsevents.js create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/nodefsevents.cc create mode 100644 src/filesystem/impls/appshell/node/node_modules/fsevents/package.json diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index ac756919ed3..0d75b31c39d 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -221,6 +221,15 @@ define(function (require, exports, module) { * or unwatched (false). */ FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { + var recursiveWatch = this._impl.recursiveWatch; + + if (recursiveWatch && entry !== watchedRoot.entry) { + // Watch and unwatch calls to children of the watched root are + // no-ops if the impl supports recursiveWatch + callback(null); + return; + } + var commandName = shouldWatch ? "watchPath" : "unwatchPath", watchOrUnwatch = this._impl[commandName].bind(this, entry.fullPath), visitor; @@ -233,7 +242,7 @@ define(function (require, exports, module) { } else { genericProcessChild = function (child) { child._clearCachedData(); - this._index.removeEntry(child.fullPath); + this._index.removeEntry(child); }; } @@ -246,7 +255,7 @@ define(function (require, exports, module) { return false; }; - if (this._impl.recursiveWatch) { + if (recursiveWatch) { // The impl will handle finding all subdirectories to watch. Here we // just need to find all entries in order to either mark them as // watched or to remove them from the index. @@ -505,7 +514,7 @@ define(function (require, exports, module) { * with a FileSystemError string or with the entry for the provided path. */ FileSystem.prototype.resolve = function (path, callback) { - var normalizedPath = _normalizePath(path, false), + var normalizedPath = this._normalizePath(path, false), item = this._index.getEntry(normalizedPath); if (!item) { @@ -619,9 +628,19 @@ define(function (require, exports, module) { } path = this._normalizePath(path, false); - var entry = this._index.getEntry(path); + var entry = this._index.getEntry(path); if (entry) { + var watchedRoot = this._findWatchedRootForPath(entry.fullPath); + if (!watchedRoot) { + console.warn("Received change notification for unwatched path: ", path); + return; + } + + if (!watchedRoot.filter(entry.name)) { + return; + } + if (entry.isFile) { // Update stat and clear contents, but only if out of date if (!(stat && entry._stat && stat.mtime.getTime() === entry._stat.mtime.getTime())) { @@ -638,12 +657,6 @@ define(function (require, exports, module) { entry._clearCachedData(); entry._stat = stat; - var watchedRoot = this._findWatchedRootForPath(entry.fullPath); - if (!watchedRoot) { - console.warn("Received change notification for unwatched path: ", path); - return; - } - // Update changed entries entry.getContents(function (err, contents) { diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index e19f27c037d..3d4c2d4b950 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -77,8 +77,6 @@ define(function (require, exports, module) { function _fileWatcherChange(evt, path, event, filename) { var change; - console.log.bind(console, "Change!").apply(undefined, arguments); - if (event === "change") { // Only register change events if filename is passed if (filename) { @@ -397,7 +395,7 @@ define(function (require, exports, module) { exports.unwatchAll = unwatchAll; // Node only supports recursive file watching on the Darwin - exports.recursiveWatch = false; // appshell.platform === "mac"; + exports.recursiveWatch = appshell.platform === "mac"; // Only perform UNC path normalization on Windows exports.normalizeUNCPaths = appshell.platform === "win"; diff --git a/src/filesystem/impls/appshell/node/FileWatcherDomain.js b/src/filesystem/impls/appshell/node/FileWatcherDomain.js index cafc95442ed..b738c3d8f50 100644 --- a/src/filesystem/impls/appshell/node/FileWatcherDomain.js +++ b/src/filesystem/impls/appshell/node/FileWatcherDomain.js @@ -26,7 +26,12 @@ "use strict"; -var fs = require("fs"); +var fs = require("fs"), + fsevents; + +if (process.platform === "darwin") { + fsevents = require("fsevents"); +} var _domainManager, _watcherMap = {}; @@ -38,9 +43,15 @@ var _domainManager, function unwatchPath(path) { var watcher = _watcherMap[path]; + console.info("Unwatching: " + path); + if (watcher) { try { - watcher.close(); + if (fsevents) { + watcher.stop(); + } else { + watcher.close(); + } } catch (err) { console.warn("Failed to unwatch file " + path + ": " + (err && err.message)); } finally { @@ -58,11 +69,27 @@ function watchPath(path) { return; } + console.info("Watching: " + path); + try { - var watcher = fs.watch(path, {persistent: false}, function (event, filename) { - // File/directory changes are emitted as "change" events on the fileWatcher domain. - _domainManager.emitEvent("fileWatcher", "change", [path, event, filename]); - }); + var watcher; + + if (fsevents) { + watcher = fsevents(path); + watcher.on("change", function (filename, info) { + var lastIndex = filename.lastIndexOf("/") + 1, + parent = lastIndex && filename.substring(0, lastIndex), + name = lastIndex && filename.substring(lastIndex), + type = info.event === "modified" ? "change" : "rename"; + + _domainManager.emitEvent("fileWatcher", "change", [parent, type, name]); + }); + } else { + watcher = fs.watch(path, {persistent: false}, function (event, filename) { + // File/directory changes are emitted as "change" events on the fileWatcher domain. + _domainManager.emitEvent("fileWatcher", "change", [path, event, filename]); + }); + } _watcherMap[path] = watcher; @@ -142,4 +169,3 @@ function init(domainManager) { } exports.init = init; - diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/CHANGELOG.md b/src/filesystem/impls/appshell/node/node_modules/fsevents/CHANGELOG.md new file mode 100644 index 00000000000..6c4d8e4e09b --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/CHANGELOG.md @@ -0,0 +1,10 @@ +# Native Access to Mac OS-X FSEvents - Change-Log + +## Version 0.0.1 + + * Basic functionality + +## Version 0.1.2 + + * Finally made the Jump to node 0.8+ with this module. + * Much more Event-Details diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/LICENSE b/src/filesystem/impls/appshell/node/node_modules/fsevents/LICENSE new file mode 100644 index 00000000000..9cfd7eae0ef --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/LICENSE @@ -0,0 +1,22 @@ +MIT License +----------- + +Copyright (C) 2010-2013 Philipp Dunkel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/Readme.md b/src/filesystem/impls/appshell/node/node_modules/fsevents/Readme.md new file mode 100644 index 00000000000..fefaa55792e --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/Readme.md @@ -0,0 +1,72 @@ +# FSEvents [![NPM](https://nodei.co/npm/fsevents.png)](https://nodei.co/npm/fsevents/) +## Native Access to Mac OS-X FSEvents + + * [Node.js](http://nodejs.org/) + * [Github repo](https://github.com/phidelta/NodeJS-FSEvents.git) + * [Module Site](https://github.com/phidelta/NodeJS-FSEvents) + * [NPM Page](https://npmjs.org/package/fsevents) + +## Installation + + $ npm install -g node-gyp + $ git clone https://github.com/phidelta/NodeJS-FSEvents.git fsevents + $ cd fsevents + $ node-gyp configure build + +OR SIMPLY + + $ npm install fsevents + +## Usage + + var fsevents = require('fsevents'); + var watcher = fsevents(__dirname); + watcher.on('fsevent', function(path, flags, id) { }); // RAW Event as emitted by OS-X + watcher.on('change', function(path, info) {}); // Common Event for all changes + +### Events + + * *fsevent* - RAW Event as emitted by OS-X + * *change* - Common Event for all changes + * *created* - A File-System-Item has been created + * *deleted* - A File-System-Item has been deleted + * *modified* - A File-System-Item has been modified + * *moved-out* - A File-System-Item has been moved away from this location + * *moved-in* - A File-System-Item has been moved into this location + +All events except *fsevent* take an *info* object as the second parameter of the callback. The structure of this object is: + + { + "event": "", + "id": , + "path": "", + "type": "", + "changes": { + "inode": true, // Has the iNode Meta-Information changed + "finder": false, // Has the Finder Meta-Data changed + "access": false, // Have the access permissions changed + "xattrs": false // Have the xAttributes changed + } + } + +## MIT License + +Copyright (C) 2010-2013 Philipp Dunkel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/binding.gyp b/src/filesystem/impls/appshell/node/node_modules/fsevents/binding.gyp new file mode 100644 index 00000000000..d0349fa8e3f --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'fswatch', + 'sources': [ 'nodefsevents.cc' ], + } + ] +} \ No newline at end of file diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Makefile b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Makefile new file mode 100644 index 00000000000..250273e5ebf --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Makefile @@ -0,0 +1,350 @@ +# We borrow heavily from the kernel build setup, though we are simpler since +# we don't have Kconfig tweaking settings on us. + +# The implicit make rules have it looking for RCS files, among other things. +# We instead explicitly write all the rules we care about. +# It's even quicker (saves ~200ms) to pass -r on the command line. +MAKEFLAGS=-r + +# The source directory tree. +srcdir := .. +abs_srcdir := $(abspath $(srcdir)) + +# The name of the builddir. +builddir_name ?= . + +# The V=1 flag on command line makes us verbosely print command lines. +ifdef V + quiet= +else + quiet=quiet_ +endif + +# Specify BUILDTYPE=Release on the command line for a release build. +BUILDTYPE ?= Release + +# Directory all our build output goes into. +# Note that this must be two directories beneath src/ for unit tests to pass, +# as they reach into the src/ directory for data with relative paths. +builddir ?= $(builddir_name)/$(BUILDTYPE) +abs_builddir := $(abspath $(builddir)) +depsdir := $(builddir)/.deps + +# Object output directory. +obj := $(builddir)/obj +abs_obj := $(abspath $(obj)) + +# We build up a list of every single one of the targets so we can slurp in the +# generated dependency rule Makefiles in one pass. +all_deps := + + + +CC.target ?= $(CC) +CFLAGS.target ?= $(CFLAGS) +CXX.target ?= $(CXX) +CXXFLAGS.target ?= $(CXXFLAGS) +LINK.target ?= $(LINK) +LDFLAGS.target ?= $(LDFLAGS) +AR.target ?= $(AR) + +# C++ apps need to be linked with g++. +# +# Note: flock is used to seralize linking. Linking is a memory-intensive +# process so running parallel links can often lead to thrashing. To disable +# the serialization, override LINK via an envrionment variable as follows: +# +# export LINK=g++ +# +# This will allow make to invoke N linker processes as specified in -jN. +LINK ?= ./gyp-mac-tool flock $(builddir)/linker.lock $(CXX.target) + +# TODO(evan): move all cross-compilation logic to gyp-time so we don't need +# to replicate this environment fallback in make as well. +CC.host ?= gcc +CFLAGS.host ?= +CXX.host ?= g++ +CXXFLAGS.host ?= +LINK.host ?= $(CXX.host) +LDFLAGS.host ?= +AR.host ?= ar + +# Define a dir function that can handle spaces. +# http://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions +# "leading spaces cannot appear in the text of the first argument as written. +# These characters can be put into the argument value by variable substitution." +empty := +space := $(empty) $(empty) + +# http://stackoverflow.com/questions/1189781/using-make-dir-or-notdir-on-a-path-with-spaces +replace_spaces = $(subst $(space),?,$1) +unreplace_spaces = $(subst ?,$(space),$1) +dirx = $(call unreplace_spaces,$(dir $(call replace_spaces,$1))) + +# Flags to make gcc output dependency info. Note that you need to be +# careful here to use the flags that ccache and distcc can understand. +# We write to a dep file on the side first and then rename at the end +# so we can't end up with a broken dep file. +depfile = $(depsdir)/$(call replace_spaces,$@).d +DEPFLAGS = -MMD -MF $(depfile).raw + +# We have to fixup the deps output in a few ways. +# (1) the file output should mention the proper .o file. +# ccache or distcc lose the path to the target, so we convert a rule of +# the form: +# foobar.o: DEP1 DEP2 +# into +# path/to/foobar.o: DEP1 DEP2 +# (2) we want missing files not to cause us to fail to build. +# We want to rewrite +# foobar.o: DEP1 DEP2 \ +# DEP3 +# to +# DEP1: +# DEP2: +# DEP3: +# so if the files are missing, they're just considered phony rules. +# We have to do some pretty insane escaping to get those backslashes +# and dollar signs past make, the shell, and sed at the same time. +# Doesn't work with spaces, but that's fine: .d files have spaces in +# their names replaced with other characters. +define fixup_dep +# The depfile may not exist if the input file didn't have any #includes. +touch $(depfile).raw +# Fixup path as in (1). +sed -e "s|^$(notdir $@)|$@|" $(depfile).raw >> $(depfile) +# Add extra rules as in (2). +# We remove slashes and replace spaces with new lines; +# remove blank lines; +# delete the first line and append a colon to the remaining lines. +sed -e 's|\\||' -e 'y| |\n|' $(depfile).raw |\ + grep -v '^$$' |\ + sed -e 1d -e 's|$$|:|' \ + >> $(depfile) +rm $(depfile).raw +endef + +# Command definitions: +# - cmd_foo is the actual command to run; +# - quiet_cmd_foo is the brief-output summary of the command. + +quiet_cmd_cc = CC($(TOOLSET)) $@ +cmd_cc = $(CC.$(TOOLSET)) $(GYP_CFLAGS) $(DEPFLAGS) $(CFLAGS.$(TOOLSET)) -c -o $@ $< + +quiet_cmd_cxx = CXX($(TOOLSET)) $@ +cmd_cxx = $(CXX.$(TOOLSET)) $(GYP_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $< + +quiet_cmd_objc = CXX($(TOOLSET)) $@ +cmd_objc = $(CC.$(TOOLSET)) $(GYP_OBJCFLAGS) $(DEPFLAGS) -c -o $@ $< + +quiet_cmd_objcxx = CXX($(TOOLSET)) $@ +cmd_objcxx = $(CXX.$(TOOLSET)) $(GYP_OBJCXXFLAGS) $(DEPFLAGS) -c -o $@ $< + +# Commands for precompiled header files. +quiet_cmd_pch_c = CXX($(TOOLSET)) $@ +cmd_pch_c = $(CC.$(TOOLSET)) $(GYP_PCH_CFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $< +quiet_cmd_pch_cc = CXX($(TOOLSET)) $@ +cmd_pch_cc = $(CC.$(TOOLSET)) $(GYP_PCH_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $< +quiet_cmd_pch_m = CXX($(TOOLSET)) $@ +cmd_pch_m = $(CC.$(TOOLSET)) $(GYP_PCH_OBJCFLAGS) $(DEPFLAGS) -c -o $@ $< +quiet_cmd_pch_mm = CXX($(TOOLSET)) $@ +cmd_pch_mm = $(CC.$(TOOLSET)) $(GYP_PCH_OBJCXXFLAGS) $(DEPFLAGS) -c -o $@ $< + +# gyp-mac-tool is written next to the root Makefile by gyp. +# Use $(4) for the command, since $(2) and $(3) are used as flag by do_cmd +# already. +quiet_cmd_mac_tool = MACTOOL $(4) $< +cmd_mac_tool = ./gyp-mac-tool $(4) $< "$@" + +quiet_cmd_mac_package_framework = PACKAGE FRAMEWORK $@ +cmd_mac_package_framework = ./gyp-mac-tool package-framework "$@" $(4) + +quiet_cmd_infoplist = INFOPLIST $@ +cmd_infoplist = $(CC.$(TOOLSET)) -E -P -Wno-trigraphs -x c $(INFOPLIST_DEFINES) "$<" -o "$@" + +quiet_cmd_touch = TOUCH $@ +cmd_touch = touch $@ + +quiet_cmd_copy = COPY $@ +# send stderr to /dev/null to ignore messages when linking directories. +cmd_copy = rm -rf "$@" && cp -af "$<" "$@" + +quiet_cmd_alink = LIBTOOL-STATIC $@ +cmd_alink = rm -f $@ && ./gyp-mac-tool filter-libtool libtool $(GYP_LIBTOOLFLAGS) -static -o $@ $(filter %.o,$^) + +quiet_cmd_link = LINK($(TOOLSET)) $@ +cmd_link = $(LINK.$(TOOLSET)) $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o "$@" $(LD_INPUTS) $(LIBS) + +quiet_cmd_solink = SOLINK($(TOOLSET)) $@ +cmd_solink = $(LINK.$(TOOLSET)) -shared $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o "$@" $(LD_INPUTS) $(LIBS) + +quiet_cmd_solink_module = SOLINK_MODULE($(TOOLSET)) $@ +cmd_solink_module = $(LINK.$(TOOLSET)) -bundle $(GYP_LDFLAGS) $(LDFLAGS.$(TOOLSET)) -o $@ $(filter-out FORCE_DO_CMD, $^) $(LIBS) + + +# Define an escape_quotes function to escape single quotes. +# This allows us to handle quotes properly as long as we always use +# use single quotes and escape_quotes. +escape_quotes = $(subst ','\'',$(1)) +# This comment is here just to include a ' to unconfuse syntax highlighting. +# Define an escape_vars function to escape '$' variable syntax. +# This allows us to read/write command lines with shell variables (e.g. +# $LD_LIBRARY_PATH), without triggering make substitution. +escape_vars = $(subst $$,$$$$,$(1)) +# Helper that expands to a shell command to echo a string exactly as it is in +# make. This uses printf instead of echo because printf's behaviour with respect +# to escape sequences is more portable than echo's across different shells +# (e.g., dash, bash). +exact_echo = printf '%s\n' '$(call escape_quotes,$(1))' + +# Helper to compare the command we're about to run against the command +# we logged the last time we ran the command. Produces an empty +# string (false) when the commands match. +# Tricky point: Make has no string-equality test function. +# The kernel uses the following, but it seems like it would have false +# positives, where one string reordered its arguments. +# arg_check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \ +# $(filter-out $(cmd_$@), $(cmd_$(1)))) +# We instead substitute each for the empty string into the other, and +# say they're equal if both substitutions produce the empty string. +# .d files contain ? instead of spaces, take that into account. +command_changed = $(or $(subst $(cmd_$(1)),,$(cmd_$(call replace_spaces,$@))),\ + $(subst $(cmd_$(call replace_spaces,$@)),,$(cmd_$(1)))) + +# Helper that is non-empty when a prerequisite changes. +# Normally make does this implicitly, but we force rules to always run +# so we can check their command lines. +# $? -- new prerequisites +# $| -- order-only dependencies +prereq_changed = $(filter-out FORCE_DO_CMD,$(filter-out $|,$?)) + +# Helper that executes all postbuilds until one fails. +define do_postbuilds + @E=0;\ + for p in $(POSTBUILDS); do\ + eval $$p;\ + E=$$?;\ + if [ $$E -ne 0 ]; then\ + break;\ + fi;\ + done;\ + if [ $$E -ne 0 ]; then\ + rm -rf "$@";\ + exit $$E;\ + fi +endef + +# do_cmd: run a command via the above cmd_foo names, if necessary. +# Should always run for a given target to handle command-line changes. +# Second argument, if non-zero, makes it do asm/C/C++ dependency munging. +# Third argument, if non-zero, makes it do POSTBUILDS processing. +# Note: We intentionally do NOT call dirx for depfile, since it contains ? for +# spaces already and dirx strips the ? characters. +define do_cmd +$(if $(or $(command_changed),$(prereq_changed)), + @$(call exact_echo, $($(quiet)cmd_$(1))) + @mkdir -p "$(call dirx,$@)" "$(dir $(depfile))" + $(if $(findstring flock,$(word 2,$(cmd_$1))), + @$(cmd_$(1)) + @echo " $(quiet_cmd_$(1)): Finished", + @$(cmd_$(1)) + ) + @$(call exact_echo,$(call escape_vars,cmd_$(call replace_spaces,$@) := $(cmd_$(1)))) > $(depfile) + @$(if $(2),$(fixup_dep)) + $(if $(and $(3), $(POSTBUILDS)), + $(call do_postbuilds) + ) +) +endef + +# Declare the "all" target first so it is the default, +# even though we don't have the deps yet. +.PHONY: all +all: + +# make looks for ways to re-generate included makefiles, but in our case, we +# don't have a direct way. Explicitly telling make that it has nothing to do +# for them makes it go faster. +%.d: ; + +# Use FORCE_DO_CMD to force a target to run. Should be coupled with +# do_cmd. +.PHONY: FORCE_DO_CMD +FORCE_DO_CMD: + +TOOLSET := target +# Suffix rules, putting all outputs into $(obj). +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.c FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cpp FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cxx FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.m FORCE_DO_CMD + @$(call do_cmd,objc,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.mm FORCE_DO_CMD + @$(call do_cmd,objcxx,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.S FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(srcdir)/%.s FORCE_DO_CMD + @$(call do_cmd,cc,1) + +# Try building from generated source, too. +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.c FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cpp FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.cxx FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.m FORCE_DO_CMD + @$(call do_cmd,objc,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.mm FORCE_DO_CMD + @$(call do_cmd,objcxx,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.S FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(obj).$(TOOLSET)/%.s FORCE_DO_CMD + @$(call do_cmd,cc,1) + +$(obj).$(TOOLSET)/%.o: $(obj)/%.c FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.cc FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.cpp FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.cxx FORCE_DO_CMD + @$(call do_cmd,cxx,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.m FORCE_DO_CMD + @$(call do_cmd,objc,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.mm FORCE_DO_CMD + @$(call do_cmd,objcxx,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.S FORCE_DO_CMD + @$(call do_cmd,cc,1) +$(obj).$(TOOLSET)/%.o: $(obj)/%.s FORCE_DO_CMD + @$(call do_cmd,cc,1) + + +ifeq ($(strip $(foreach prefix,$(NO_LOAD),\ + $(findstring $(join ^,$(prefix)),\ + $(join ^,fswatch.target.mk)))),) + include fswatch.target.mk +endif + +quiet_cmd_regen_makefile = ACTION Regenerating $@ +cmd_regen_makefile = cd $(srcdir); /usr/local/lib/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "--toplevel-dir=." -I/Users/wehrman/Source/edge-code/src/filesystem/impls/appshell/node/node_modules/fsevents/build/config.gypi -I/usr/local/lib/node_modules/node-gyp/addon.gypi -I/Users/wehrman/.node-gyp/0.10.22/common.gypi "--depth=." "-Goutput_dir=." "--generator-output=build" "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/wehrman/.node-gyp/0.10.22" "-Dmodule_root_dir=/Users/wehrman/Source/edge-code/src/filesystem/impls/appshell/node/node_modules/fsevents" binding.gyp +Makefile: $(srcdir)/../../../../../../../../../../../usr/local/lib/node_modules/node-gyp/addon.gypi $(srcdir)/build/config.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../../../../.node-gyp/0.10.22/common.gypi + $(call do_cmd,regen_makefile) + +# "all" is a concatenation of the "all" targets from all the included +# sub-makefiles. This is just here to clarify. +all: + +# Add in dependency-tracking rules. $(all_deps) is the list of every single +# target in our tree. Only consider the ones with .d (dependency) info: +d_files := $(wildcard $(foreach f,$(all_deps),$(depsdir)/$(f).d)) +ifneq ($(d_files),) + include $(d_files) +endif diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Makefile.d b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Makefile.d new file mode 100644 index 00000000000..362a9358a2f --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Makefile.d @@ -0,0 +1 @@ +cmd_Makefile := cd ..; /usr/local/lib/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "--toplevel-dir=." -I/Users/wehrman/Source/edge-code/src/filesystem/impls/appshell/node/node_modules/fsevents/build/config.gypi -I/usr/local/lib/node_modules/node-gyp/addon.gypi -I/Users/wehrman/.node-gyp/0.10.22/common.gypi "--depth=." "-Goutput_dir=." "--generator-output=build" "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/wehrman/.node-gyp/0.10.22" "-Dmodule_root_dir=/Users/wehrman/Source/edge-code/src/filesystem/impls/appshell/node/node_modules/fsevents" binding.gyp diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/fswatch.node.d b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/fswatch.node.d new file mode 100644 index 00000000000..cae48227a94 --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/fswatch.node.d @@ -0,0 +1 @@ +cmd_Release/fswatch.node := ./gyp-mac-tool flock ./Release/linker.lock c++ -bundle -Wl,-search_paths_first -mmacosx-version-min=10.5 -arch i386 -L./Release -o Release/fswatch.node Release/obj.target/fswatch/nodefsevents.o -undefined dynamic_lookup diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/obj.target/fswatch/nodefsevents.o.d b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/obj.target/fswatch/nodefsevents.o.d new file mode 100644 index 00000000000..8621681fbc2 --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/.deps/Release/obj.target/fswatch/nodefsevents.o.d @@ -0,0 +1,19 @@ +cmd_Release/obj.target/fswatch/nodefsevents.o := c++ '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DBUILDING_NODE_EXTENSION' -I/Users/wehrman/.node-gyp/0.10.22/src -I/Users/wehrman/.node-gyp/0.10.22/deps/uv/include -I/Users/wehrman/.node-gyp/0.10.22/deps/v8/include -Os -gdwarf-2 -mmacosx-version-min=10.5 -arch i386 -Wall -Wendif-labels -W -Wno-unused-parameter -fno-rtti -fno-exceptions -fno-threadsafe-statics -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/fswatch/nodefsevents.o.d.raw -c -o Release/obj.target/fswatch/nodefsevents.o ../nodefsevents.cc +Release/obj.target/fswatch/nodefsevents.o: ../nodefsevents.cc \ + /Users/wehrman/.node-gyp/0.10.22/src/node.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/uv-unix.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/ngx-queue.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/uv-darwin.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/v8/include/v8.h \ + /Users/wehrman/.node-gyp/0.10.22/deps/v8/include/v8stdint.h \ + /Users/wehrman/.node-gyp/0.10.22/src/node_object_wrap.h +../nodefsevents.cc: +/Users/wehrman/.node-gyp/0.10.22/src/node.h: +/Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv.h: +/Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/uv-unix.h: +/Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/ngx-queue.h: +/Users/wehrman/.node-gyp/0.10.22/deps/uv/include/uv-private/uv-darwin.h: +/Users/wehrman/.node-gyp/0.10.22/deps/v8/include/v8.h: +/Users/wehrman/.node-gyp/0.10.22/deps/v8/include/v8stdint.h: +/Users/wehrman/.node-gyp/0.10.22/src/node_object_wrap.h: diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/fswatch.node b/src/filesystem/impls/appshell/node/node_modules/fsevents/build/Release/fswatch.node new file mode 100755 index 0000000000000000000000000000000000000000..5655c86170b24d33860642ed158340135dd9f98b GIT binary patch literal 25480 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%0Aeq2#frY>q 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 z${"): + lines[i] = None + lines[i - 1] = None + lines = '\n'.join(filter(lambda x: x is not None, lines)) + + # Write out the file with variables replaced. + fd = open(dest, 'w') + fd.write(lines) + fd.close() + + # Now write out PkgInfo file now that the Info.plist file has been + # "compiled". + self._WritePkgInfo(dest) + + def _WritePkgInfo(self, info_plist): + """This writes the PkgInfo file from the data stored in Info.plist.""" + plist = plistlib.readPlist(info_plist) + if not plist: + return + + # Only create PkgInfo for executable types. + package_type = plist['CFBundlePackageType'] + if package_type != 'APPL': + return + + # The format of PkgInfo is eight characters, representing the bundle type + # and bundle signature, each four characters. If that is missing, four + # '?' characters are used instead. + signature_code = plist.get('CFBundleSignature', '????') + if len(signature_code) != 4: # Wrong length resets everything, too. + signature_code = '?' * 4 + + dest = os.path.join(os.path.dirname(info_plist), 'PkgInfo') + fp = open(dest, 'w') + fp.write('%s%s' % (package_type, signature_code)) + fp.close() + + def ExecFlock(self, lockfile, *cmd_list): + """Emulates the most basic behavior of Linux's flock(1).""" + # Rely on exception handling to report errors. + fd = os.open(lockfile, os.O_RDONLY|os.O_NOCTTY|os.O_CREAT, 0o666) + fcntl.flock(fd, fcntl.LOCK_EX) + return subprocess.call(cmd_list) + + def ExecFilterLibtool(self, *cmd_list): + """Calls libtool and filters out '/path/to/libtool: file: foo.o has no + symbols'.""" + libtool_re = re.compile(r'^.*libtool: file: .* has no symbols$') + libtoolout = subprocess.Popen(cmd_list, stderr=subprocess.PIPE) + _, err = libtoolout.communicate() + for line in err.splitlines(): + if not libtool_re.match(line): + print >>sys.stderr, line + return libtoolout.returncode + + def ExecPackageFramework(self, framework, version): + """Takes a path to Something.framework and the Current version of that and + sets up all the symlinks.""" + # Find the name of the binary based on the part before the ".framework". + binary = os.path.basename(framework).split('.')[0] + + CURRENT = 'Current' + RESOURCES = 'Resources' + VERSIONS = 'Versions' + + if not os.path.exists(os.path.join(framework, VERSIONS, version, binary)): + # Binary-less frameworks don't seem to contain symlinks (see e.g. + # chromium's out/Debug/org.chromium.Chromium.manifest/ bundle). + return + + # Move into the framework directory to set the symlinks correctly. + pwd = os.getcwd() + os.chdir(framework) + + # Set up the Current version. + self._Relink(version, os.path.join(VERSIONS, CURRENT)) + + # Set up the root symlinks. + self._Relink(os.path.join(VERSIONS, CURRENT, binary), binary) + self._Relink(os.path.join(VERSIONS, CURRENT, RESOURCES), RESOURCES) + + # Back to where we were before! + os.chdir(pwd) + + def _Relink(self, dest, link): + """Creates a symlink to |dest| named |link|. If |link| already exists, + it is overwritten.""" + if os.path.lexists(link): + os.remove(link) + os.symlink(dest, link) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/fsevents.js b/src/filesystem/impls/appshell/node/node_modules/fsevents/fsevents.js new file mode 100644 index 00000000000..67dc5939901 --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/fsevents.js @@ -0,0 +1,69 @@ +/* +** © 2013 by Philipp Dunkel . Licensed under MIT License. +*/ + +var util = require('util'); +var events = require('events'); +var binding; +//try { + binding = require('./build/Release/fswatch'); +//} catch(ex) { +// binding = require('./build/Debug/fswatch'); +//} + +var Fs = require('fs'); + +module.exports = function(path) { + var fsevents = new FSEvents(path); + fsevents.on('fsevent', function(path, flags, id) { + var info = { + event:'unknown', + id:id, + path: path, + type: fileType(flags), + changes: fileChanges(flags), + }; + if (FSEvents.kFSEventStreamEventFlagItemCreated & flags) { + info.event = 'created'; + } else if (FSEvents.kFSEventStreamEventFlagItemRemoved & flags) { + info.event = 'deleted'; + } else if (FSEvents.kFSEventStreamEventFlagItemRenamed & flags) { + info.event = 'moved'; + } else if (FSEvents.kFSEventStreamEventFlagItemModified & flags) { + info.event = 'modified'; + } + + if (info.event == 'moved') { + Fs.stat(info.path, function(err, stat) { + if (err || !stat) { + info.event = 'moved-out'; + } else { + info.event = 'moved-in'; + } + fsevents.emit('change', path, info); + fsevents.emit(info.event, path, info); + }); + } else { + fsevents.emit('change', path, info); + if (info.event !== 'unknown') fsevents.emit(info.event, path, info); + } + }); + return fsevents; +}; +var FSEvents = binding.FSEvents; +util.inherits(FSEvents, events.EventEmitter); + +function fileType(flags) { + if (FSEvents.kFSEventStreamEventFlagItemIsFile & flags) return 'file'; + if (FSEvents.kFSEventStreamEventFlagItemIsDir & flags) return 'directory'; + if (FSEvents.kFSEventStreamEventFlagItemIsSymlink & flags) return 'symlink'; +} + +function fileChanges(flags) { + var res = {}; + res.inode = !!(FSEvents.kFSEventStreamEventFlagItemInodeMetaMod & flags); + res.finder = !!(FSEvents.kFSEventStreamEventFlagItemFinderInfoMod & flags); + res.access = !!(FSEvents.kFSEventStreamEventFlagItemChangeOwner & flags); + res.xattrs = !!(FSEvents.kFSEventStreamEventFlagItemXattrMod & flags); + return res; +} diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/nodefsevents.cc b/src/filesystem/impls/appshell/node/node_modules/fsevents/nodefsevents.cc new file mode 100644 index 00000000000..cb167482125 --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/nodefsevents.cc @@ -0,0 +1,196 @@ +/* +** © 2013 by Philipp Dunkel . Licensed under MIT License. +*/ + +#include +#include +#include + +#include +#include +#include + +#include + +#define MAXPATH 1024 + +typedef struct s_evt *p_evt; +struct s_evt { + FSEventStreamEventFlags flags; + FSEventStreamEventId evtid; + char path[MAXPATH + 1]; + p_evt next; +}; +static v8::Persistent constructor_template; +namespace node_fsevents { + using namespace v8; + using namespace node; + + static Persistent emit_sym; + static Persistent change_sym; + class NodeFSEvents : node::ObjectWrap { + public: + static void Initialize(v8::Handle target) { + HandleScope scope; + emit_sym = NODE_PSYMBOL("emit"); + change_sym = NODE_PSYMBOL("fsevent"); + Local t = FunctionTemplate::New(NodeFSEvents::New); + constructor_template = Persistent::New(t); + constructor_template->InstanceTemplate()->SetInternalFieldCount(1); + constructor_template->SetClassName(String::NewSymbol("FSEvents")); + Local constructor = constructor_template->GetFunction(); + + constructor->Set(String::New("kFSEventStreamEventFlagNone"), Integer::New(0x00000000)); + constructor->Set(String::New("kFSEventStreamEventFlagMustScanSubDirs"), Integer::New(0x00000001)); + constructor->Set(String::New("kFSEventStreamEventFlagUserDropped"), Integer::New(0x00000002)); + constructor->Set(String::New("kFSEventStreamEventFlagKernelDropped"), Integer::New(0x00000004)); + constructor->Set(String::New("kFSEventStreamEventFlagEventIdsWrapped"), Integer::New(0x00000008)); + constructor->Set(String::New("kFSEventStreamEventFlagHistoryDone"), Integer::New(0x00000010)); + constructor->Set(String::New("kFSEventStreamEventFlagRootChanged"), Integer::New(0x00000020)); + constructor->Set(String::New("kFSEventStreamEventFlagMount"), Integer::New(0x00000040)); + constructor->Set(String::New("kFSEventStreamEventFlagUnmount"), Integer::New(0x00000080)); + constructor->Set(String::New("kFSEventStreamEventFlagItemCreated"), Integer::New(0x00000100)); + constructor->Set(String::New("kFSEventStreamEventFlagItemRemoved"), Integer::New(0x00000200)); + constructor->Set(String::New("kFSEventStreamEventFlagItemInodeMetaMod"), Integer::New(0x00000400)); + constructor->Set(String::New("kFSEventStreamEventFlagItemRenamed"), Integer::New(0x00000800)); + constructor->Set(String::New("kFSEventStreamEventFlagItemModified"), Integer::New(0x00001000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemFinderInfoMod"), Integer::New(0x00002000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemChangeOwner"), Integer::New(0x00004000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemXattrMod"), Integer::New(0x00008000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemIsFile"), Integer::New(0x00010000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemIsDir"), Integer::New(0x00020000)); + constructor->Set(String::New("kFSEventStreamEventFlagItemIsSymlink"), Integer::New(0x00040000)); + + target->Set(String::NewSymbol("FSEvents"), constructor); + } + static v8::Handle Shutdown(const v8::Arguments& args) { + HandleScope scope; + NodeFSEvents *native = node::ObjectWrap::Unwrap(args.This()); + native->Shutdown(); + return Undefined(); + } + static v8::Handle New(const v8::Arguments& args) { + HandleScope scope; + + if (args.Length() != 1 || !args[0]->IsString()) { + return ThrowException(String::New("Bad arguments")); + } + + String::Utf8Value pathname(args[0]->ToString()); + + NodeFSEvents *nativeobj = new NodeFSEvents(*pathname); + nativeobj->Wrap(args.Holder()); + NODE_SET_METHOD(args.Holder(), "stop", NodeFSEvents::Shutdown); + return args.This(); + } + + + NodeFSEvents(const char *path) : ObjectWrap() { + running=1; + first = NULL; + last = NULL; + strncpy(pathname, path ? path : "/", MAXPATH); + pthread_mutex_init(&mutex, NULL); + uv_async_init(uv_default_loop(), &watcher, NodeFSEvents::Callback); + watcher.data = this; + pthread_create(&thread, NULL, &NodeFSEvents::Run, this); + } + + ~NodeFSEvents() { + this->Shutdown(); + } + void Shutdown() { + if (running) { + CFRunLoopStop(runLoop); + pthread_join(thread, NULL); + pthread_mutex_destroy(&mutex); + uv_close((uv_handle_t*) &watcher, NULL); + } + running = 0; + } + static void *Run(void *data) { + NodeFSEvents *This = (NodeFSEvents *)data; + CFStringRef dir_names[1]; + dir_names[0] = CFStringCreateWithCString(NULL, This->pathname, kCFStringEncodingUTF8); + CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&dir_names, 1, NULL); + FSEventStreamContext context = { 0, data, NULL, NULL, NULL }; + FSEventStreamRef stream = FSEventStreamCreate(NULL, &NodeFSEvents::Event, &context, pathsToWatch, kFSEventStreamEventIdSinceNow, (CFAbsoluteTime) 0.1, kFSEventStreamCreateFlagNone | kFSEventStreamCreateFlagWatchRoot | kFSEventStreamCreateFlagFileEvents); + This->runLoop = CFRunLoopGetCurrent(); + FSEventStreamScheduleWithRunLoop(stream, This->runLoop, kCFRunLoopDefaultMode); + FSEventStreamStart(stream); + CFRunLoopRun(); + FSEventStreamStop(stream); + FSEventStreamUnscheduleFromRunLoop(stream, This->runLoop, kCFRunLoopDefaultMode); + FSEventStreamInvalidate(stream); + FSEventStreamRelease(stream); + pthread_exit(NULL); + } + static void Event(ConstFSEventStreamRef streamRef, void *userData, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { + NodeFSEvents *This = static_cast(userData); + char **paths = static_cast(eventPaths); + size_t idx; + p_evt item; + pthread_mutex_lock(&(This->mutex)); + for (idx=0; idx < numEvents; idx++) { + item = (p_evt)malloc(sizeof(struct s_evt)); + if (!This->first) { + This->first = item; + This->last = item; + } else { + This->last->next = item; + This->last = item; + } + item->next = NULL; + strncpy(item->path, paths[idx],MAXPATH); + item->flags = eventFlags[idx]; + item->evtid = eventIds[idx]; + } + pthread_mutex_unlock(&(This->mutex)); + uv_async_send(&(This->watcher)); + } + static void Callback(uv_async_t *handle, int status) { + NodeFSEvents *This = static_cast(handle->data); + HandleScope scope; + TryCatch try_catch; + Local callback_v = This->handle_->Get(emit_sym); + Local callback = Local::Cast(callback_v); + p_evt item; + pthread_mutex_lock(&(This->mutex)); + + v8::Handle args[4]; + args[0] = change_sym; + This->Ref(); + item = This->first; + while (item) { + This->first = item->next; + if (!try_catch.HasCaught()) { + args[1] = v8::String::New(item->path ? item->path : ""); + args[2] = v8::Integer::New(item->flags); + args[3] = v8::Integer::New(item->evtid); + callback->Call(This->handle_, 4, args); + } + free(item); + item = This->first; + } + This->first = NULL; + This->last = NULL; + This->Ref(); + pthread_mutex_unlock(&(This->mutex)); + if (try_catch.HasCaught()) try_catch.ReThrow(); + } + + int running; + char pathname[MAXPATH + 1]; + CFRunLoopRef runLoop; + p_evt first; + p_evt last; + uv_async_t watcher; + pthread_t thread; + pthread_mutex_t mutex; + }; + extern "C" void init(v8::Handle target) { + node_fsevents::NodeFSEvents::Initialize(target); + } +} + +NODE_MODULE(fswatch, node_fsevents::init) diff --git a/src/filesystem/impls/appshell/node/node_modules/fsevents/package.json b/src/filesystem/impls/appshell/node/node_modules/fsevents/package.json new file mode 100644 index 00000000000..1f7e4666a7e --- /dev/null +++ b/src/filesystem/impls/appshell/node/node_modules/fsevents/package.json @@ -0,0 +1,26 @@ +{ + "name": "fsevents", + "description": "Native Access to Mac OS-X FSEvents", + "homepage": "https://github.com/phidelta/NodeJS-FSEvents", + "version": "0.1.5", + "maintainers": [ + "Philipp Dunkel " + ], + "contributors": [ + "Philipp Dunkel " + ], + "bugs": { + "url": "https://github.com/phidelta/fsevents" + }, + "licenses": [ + { + "type": "MIT" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/phidelta/fsevents.git" + }, + "main": "./fsevents.js", + "engines": {"node": ">=0.8"} +} From fdc6e5fa8dffb92935b8724fdac7f730e3f38e89 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 26 Nov 2013 15:13:36 -0800 Subject: [PATCH 16/94] Fix unit tests that broke with the watcher changes --- test/spec/FileSystem-test.js | 4 ---- test/spec/MockFileSystemImpl.js | 9 ++++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index d790786f19b..523e7ef3013 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -977,7 +977,6 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { expect(file._isWatched).toBe(true); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1021,7 +1020,6 @@ define(function (require, exports, module) { // confirm empty cached data and then write blindly runs(function () { expect(file._isWatched).toBe(true); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(writeCalls).toBe(0); @@ -1066,7 +1064,6 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { expect(file._isWatched).toBe(true); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1116,7 +1113,6 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { expect(file._isWatched).toBe(true); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index 5ec041d6554..5feed3aa4fb 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -327,13 +327,16 @@ define(function (require, exports, module) { _watcherCallback = callback; } - function watchPath(path) { + function watchPath(path, callback) { + callback(null); } - function unwatchPath(path) { + function unwatchPath(path, callback) { + callback(null); } - function unwatchAll() { + function unwatchAll(callback) { + callback(null); } From 7413a2d872989dda643ed2e8860c34d3163b9214 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 26 Nov 2013 16:01:51 -0800 Subject: [PATCH 17/94] Add an offline callback for impl to call when all watchers should be considered dead --- src/filesystem/FileSystem.js | 18 ++++++++++++++++-- .../impls/appshell/AppshellFileSystem.js | 10 ++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 0d75b31c39d..6ff82d0982b 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -346,8 +346,12 @@ define(function (require, exports, module) { FileSystem.prototype.init = function (impl) { console.assert(!this._impl, "This FileSystem has already been initialized!"); + var changeCallback = this._enqueueWatchResult.bind(this), + offlineCallback = this._unwatchAll.bind(this); + + this._impl = impl; - this._impl.initWatchers(this._enqueueWatchResult.bind(this)); + this._impl.initWatchers(changeCallback, offlineCallback); }; /** @@ -720,7 +724,7 @@ define(function (require, exports, module) { } } }; - + /** * Start watching a filesystem root entry. * @@ -804,6 +808,16 @@ define(function (require, exports, module) { }.bind(this)); }; + /** + * Unwatch all watched roots. Calls unwatch on the underlying impl for each + * watched root and ignores errors. + * @private + */ + FileSystem.prototype._unwatchAll = function () { + Object.keys(this._watchedRoots).forEach(this.unwatch, this); + }; + + // The singleton instance var _instance; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 3d4c2d4b950..0a3596b6c8d 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -36,6 +36,7 @@ define(function (require, exports, module) { var FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes var _changeCallback, // Callback to notify FileSystem of watcher changes + _offlineCallback, // Callback to notify FileSystem that watchers are offline _changeTimeout, // Timeout used to batch up file watcher changes _pendingChanges = {}; // Pending file watcher changes @@ -112,6 +113,10 @@ define(function (require, exports, module) { $(_nodeConnection).on("close", function (event, promise) { _domainsLoaded = false; _nodeConnectionPromise = promise.then(_reloadDomains); + + if (_offlineCallback) { + _offlineCallback(); + } }); function _execWhenConnected(name, args, callback, errback) { @@ -349,8 +354,9 @@ define(function (require, exports, module) { }); } - function initWatchers(callback) { - _changeCallback = callback; + function initWatchers(changeCallback, offlineCallback) { + _changeCallback = changeCallback; + _offlineCallback = offlineCallback; } function watchPath(path, callback) { From f6285553f23ed465840f72677f3d9e7b9a957141 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 09:58:42 -0800 Subject: [PATCH 18/94] Explicitly filter files using shouldShow in findAllFiles in case the project is not currently watched --- src/project/ProjectManager.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index e51dee12e5f..aa54427b522 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1639,11 +1639,14 @@ define(function (require, exports, module) { function visitor(entry) { - if (entry.isFile && !isBinaryFile(entry.name)) { - result.push(entry); + if (shouldShow(entry)) { + if (entry.isFile && !isBinaryFile(entry.name)) { + result.push(entry); + } + return true; } - return true; + return false; } // First gather all files in project proper From 842656b6085603455554dc37fccaddfe26a7f8f9 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 11:12:06 -0800 Subject: [PATCH 19/94] Stub out the _fileSystemChange function for @jasonsanjose --- src/project/ProjectManager.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index aa54427b522..c70356bdea7 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1686,9 +1686,12 @@ define(function (require, exports, module) { * @private * Respond to a FileSystem change event. */ - _fileSystemChange = function (event, item) { - // TODO: Refresh file tree too - once watchers are precise enough to notify only - // when real changes occur, instead of on every window focus! + _fileSystemChange = function (event, entry, added, removed) { + if (entry) { + // Use the added and removed sets to update the project tree here. + } else { + refreshFileTree(); + } FileSyncManager.syncOpenDocuments(); }; From e0508266fd66737184e7ab4964eb17725083ac89 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 12:28:36 -0800 Subject: [PATCH 20/94] Cache stats and directory contents (but not file contents, for space reasons) speculatively --- src/filesystem/Directory.js | 27 +++++++++------------------ src/filesystem/File.js | 19 +++++++++++-------- src/filesystem/FileSystemEntry.js | 9 ++++----- test/spec/FileSystem-test.js | 3 --- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 9cdf1afbf88..8eb1d0a3439 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -120,12 +120,9 @@ define(function (require, exports, module) { this._contentsCallbacks.push(callback); return; } - - if (this._contents) { - // Return cached contents - // Watchers aren't guaranteed to fire immediately, so it's possible this will be somewhat stale. But - // unlike file contents, we're willing to tolerate directory contents being stale. It should at least - // be up-to-date with respect to changes made internally (by this filesystem). + + // Return cached contents if the directory is watched + if (this._contents && this._isWatched) { callback(null, this._contents, this._contentsStats, this._contentsStatsErrors); return; } @@ -168,9 +165,7 @@ define(function (require, exports, module) { entry = this._fileSystem.getDirectoryForPath(entryPath); } - if (entry._isWatched) { - entry._stat = entryStats; - } + entry._stat = entryStats; contents.push(entry); contentsStats.push(entryStats); @@ -178,12 +173,10 @@ define(function (require, exports, module) { } }, this); - - if (this._isWatched) { - this._contents = contents; - this._contentsStats = contentsStats; - this._contentsStatsErrors = contentsStatsErrors; - } + + this._contents = contents; + this._contentsStats = contentsStats; + this._contentsStatsErrors = contentsStatsErrors; } // Reset the callback list before we begin calling back so that @@ -213,9 +206,7 @@ define(function (require, exports, module) { return; } - if (this._isWatched) { - this._stat = stat; - } + this._stat = stat; try { callback(null, stat); diff --git a/src/filesystem/File.js b/src/filesystem/File.js index a33e0a73985..85630e33e9b 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -87,6 +87,8 @@ define(function (require, exports, module) { options = {}; } + // We don't need to check isWatched here because contents are only saved + // for watched files if (this._contents && this._stat) { callback(null, this._contents, this._stat); return; @@ -99,11 +101,11 @@ define(function (require, exports, module) { return; } + this._stat = stat; this._hash = stat._hash; - // Only cache the stats for and contents of watched files + // Only cache the contents of watched files if (this._isWatched) { - this._stat = stat; this._contents = data; } @@ -126,11 +128,9 @@ define(function (require, exports, module) { } callback = callback || function () {}; - - // Hashes are only saved for watched files - var watched = this._isWatched; // Request a consistency check if the file is watched and the write is not blind + var watched = this._isWatched; if (watched && !options.blind) { options.hash = this._hash; } @@ -142,7 +142,6 @@ define(function (require, exports, module) { try { if (err) { this._clearCachedData(); - callback(err); return; } @@ -161,9 +160,13 @@ define(function (require, exports, module) { this._fileSystem._handleWatchResult(this._path, stat); } - // Update cached stats and contents if the file is watched + // Wait until AFTER the synthetic change has been processed + // to update the cached stats so the change handler recognizes + // it is a non-duplicate change event. + this._stat = stat; + + // Only cache the contents of watched files if (watched) { - this._stat = stat; this._contents = data; } } diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 69eea8e4aff..0af9cc91a58 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -216,7 +216,7 @@ define(function (require, exports, module) { * FileSystemError string or FileSystemStats object. */ FileSystemEntry.prototype.stat = function (callback) { - if (this._stat) { + if (this._stat && this._isWatched) { callback(null, this._stat); return; } @@ -228,9 +228,7 @@ define(function (require, exports, module) { return; } - if (this._isWatched) { - this._stat = stat; - } + this._stat = stat; callback(null, stat); }.bind(this)); @@ -297,11 +295,12 @@ define(function (require, exports, module) { * string parameter. */ FileSystemEntry.prototype.moveToTrash = function (callback) { - callback = callback || function () {}; if (!this._impl.moveToTrash) { this.unlink(callback); return; } + + callback = callback || function () {}; this._clearCachedData(); diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 523e7ef3013..ae7c8f04c23 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1183,7 +1183,6 @@ define(function (require, exports, module) { file = fileSystem.getFileForPath(filename); expect(file._isWatched).toBe(false); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1196,7 +1195,6 @@ define(function (require, exports, module) { runs(function () { expect(cb1.error).toBeFalsy(); expect(file._isWatched).toBe(false); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeTruthy(); expect(readCalls).toBe(1); @@ -1211,7 +1209,6 @@ define(function (require, exports, module) { runs(function () { expect(cb2.error).toBeFalsy(); expect(file._isWatched).toBe(false); - expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBe(savedHash); expect(readCalls).toBe(2); From 9a9e63b5fa5b74e17a78dafa3a178f9c4aaf4d6b Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 12:29:32 -0800 Subject: [PATCH 21/94] Update the dualWrite tests to reflect the fact that synthetic change events are fired immediately --- test/spec/FileSystem-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index ae7c8f04c23..14e6b071ad7 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -868,9 +868,9 @@ define(function (require, exports, module) { }); $(fileSystem).on("change", function (evt, entry) { - // this is the important check: both callbacks should have already run! - expect(write1Done).toBe(true); - expect(write2Done).toBe(true); + // change for file N should not precede write callback for write to N + expect(write1Done || entry.fullPath !== "/file1.txt").toBe(true); + expect(write2Done || entry.fullPath !== "/file2.txt").toBe(true); expect(entry.fullPath === "/file1.txt" || entry.fullPath === "/file2.txt").toBe(true); if (entry.fullPath === "/file1.txt") { From 7b296e664e1e5cdf733fd8852bee9c81d634a4b1 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 16:52:41 -0800 Subject: [PATCH 22/94] Fire a wholesale change event if watchers go offline --- src/filesystem/FileSystem.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 6ff82d0982b..6136890bb7f 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -814,7 +814,13 @@ define(function (require, exports, module) { * @private */ FileSystem.prototype._unwatchAll = function () { + console.warn("File watchers went offline!"); + Object.keys(this._watchedRoots).forEach(this.unwatch, this); + + // Fire a wholesale change event because all previously watched entries + // have been removed from the index and should no longer be referenced + this._handleWatchResult(null); }; From 75ad54f665aeebde05626c6889155e2f22aa3896 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 16:53:55 -0800 Subject: [PATCH 23/94] Set an inactive watched root before watching and activate it when watchers are online for more consistent filtering --- src/filesystem/FileSystem.js | 16 +++++++++++----- src/filesystem/FileSystemEntry.js | 4 +++- src/project/ProjectManager.js | 10 +++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 6136890bb7f..69dfea2861b 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -348,7 +348,6 @@ define(function (require, exports, module) { var changeCallback = this._enqueueWatchResult.bind(this), offlineCallback = this._unwatchAll.bind(this); - this._impl = impl; this._impl.initWatchers(changeCallback, offlineCallback); @@ -740,8 +739,9 @@ define(function (require, exports, module) { FileSystem.prototype.watch = function (entry, filter, callback) { var fullPath = entry.fullPath, watchedRoot = { - entry: entry, - filter: filter + entry : entry, + filter : filter, + active : false }; callback = callback || function () {}; @@ -764,6 +764,8 @@ define(function (require, exports, module) { return; } + this._watchedRoots[fullPath] = watchedRoot; + this._watchEntry(entry, watchedRoot, function (err) { if (err) { console.warn("Failed to watch root: ", entry.fullPath, err); @@ -771,7 +773,8 @@ define(function (require, exports, module) { return; } - this._watchedRoots[fullPath] = watchedRoot; + watchedRoot.active = true; + callback(null); }.bind(this)); }; @@ -796,7 +799,8 @@ define(function (require, exports, module) { return; } - delete this._watchedRoots[fullPath]; + watchedRoot.active = false; + this._unwatchEntry(entry, watchedRoot, function (err) { if (err) { console.warn("Failed to unwatch root: ", entry.fullPath, err); @@ -804,6 +808,8 @@ define(function (require, exports, module) { return; } + delete this._watchedRoots[fullPath]; + callback(null); }.bind(this)); }; diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 0af9cc91a58..b5625c03b61 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -50,7 +50,9 @@ define(function (require, exports, module) { this._setPath(path); this._fileSystem = fileSystem; this._id = nextId++; - this._watched = !!fileSystem._findWatchedRootForPath(path); + + var watchedRoot = fileSystem._findWatchedRootForPath(path); + this._watched = !!(watchedRoot && watchedRoot.active); } // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index c70356bdea7..878d814f6c8 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1639,14 +1639,10 @@ define(function (require, exports, module) { function visitor(entry) { - if (shouldShow(entry)) { - if (entry.isFile && !isBinaryFile(entry.name)) { - result.push(entry); - } - return true; + if (entry.isFile && !isBinaryFile(entry.name)) { + result.push(entry); } - - return false; + return true; } // First gather all files in project proper From 491e3bfd21e191223ff863e9e3ba0503d8810f91 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 27 Nov 2013 21:37:13 -0800 Subject: [PATCH 24/94] Remove an unnecessary bind --- src/filesystem/impls/appshell/AppshellFileSystem.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 0a3596b6c8d..b3febe659e4 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -364,7 +364,7 @@ define(function (require, exports, module) { _execWhenConnected("watchPath", [path], callback.bind(undefined, null), - callback.bind(undefined)); + callback); } function unwatchPath(path, callback) { @@ -372,7 +372,7 @@ define(function (require, exports, module) { _execWhenConnected("unwatchPath", [path], callback.bind(undefined, null), - callback.bind(undefined)); + callback); } function unwatchAll(callback) { @@ -380,7 +380,7 @@ define(function (require, exports, module) { _execWhenConnected("watchPath", [], callback.bind(undefined, null), - callback.bind(undefined)); + callback); } // Export public API From 0fc024eae8dd37f0d79c9d40bfbc07424a9565eb Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 29 Nov 2013 16:22:22 -0800 Subject: [PATCH 25/94] Remove some dead code --- .../impls/appshell/AppshellFileSystem.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index b3febe659e4..d439f6e113f 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -100,19 +100,13 @@ define(function (require, exports, module) { }); } - function _reloadDomains() { - return _loadDomains().done(function () { - // call back into the filesystem here to restore any previously watched paths. - }); - } - var _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); $(_nodeConnection).on("fileWatcher.change", _fileWatcherChange); $(_nodeConnection).on("close", function (event, promise) { _domainsLoaded = false; - _nodeConnectionPromise = promise.then(_reloadDomains); + _nodeConnectionPromise = promise.then(_loadDomains); if (_offlineCallback) { _offlineCallback(); @@ -162,15 +156,6 @@ define(function (require, exports, module) { return FileSystemError.UNKNOWN; } - /** Returns the path of the item's containing directory (item may be a file or a directory) */ - function _parentPath(path) { - var lastSlash = path.lastIndexOf("/"); - if (lastSlash === path.length - 1) { - lastSlash = path.lastIndexOf("/", lastSlash - 1); - } - return path.substr(0, lastSlash + 1); - } - function _wrap(cb) { return function (err) { var args = Array.prototype.slice.call(arguments); From 5ee0387b943c167a9b97b15a6a4b11b7f1943735 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 11:01:22 -0800 Subject: [PATCH 26/94] Add a slightly more useful error message for CONTENTS_MODIFIED --- src/file/FileUtils.js | 2 ++ src/nls/root/strings.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 22caf38d34b..66d29340ba5 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -155,6 +155,8 @@ define(function (require, exports, module) { result = Strings.NOT_READABLE_ERR; } else if (name === FileSystemError.NOT_WRITABLE) { result = Strings.NO_MODIFICATION_ALLOWED_ERR_FILE; + } else if (name === FileSystemError.CONTENTS_MODIFIED) { + result = Strings.CONTENTS_MODIFIED_ERR; } else { result = StringUtils.format(Strings.GENERIC_ERROR, name); } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 2a29e5c3f14..abb53b46e7e 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -36,6 +36,7 @@ define({ "NOT_READABLE_ERR" : "The file could not be read.", "NO_MODIFICATION_ALLOWED_ERR" : "The target directory cannot be modified.", "NO_MODIFICATION_ALLOWED_ERR_FILE" : "The permissions do not allow you to make modifications.", + "CONTENTS_MODIFIED_ERR" : "The contents of the file were modified outside of the editor.", "FILE_EXISTS_ERR" : "The file or directory already exists.", "FILE" : "file", "DIRECTORY" : "directory", From b63df53f3ffadad8e6d265b207cf8bb2af85810b Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 12:27:42 -0800 Subject: [PATCH 27/94] Disable watchers for network drives; document private AppshellFileSystem functions --- src/filesystem/FileSystem.js | 11 +- .../impls/appshell/AppshellFileSystem.js | 174 +++++++++++++----- 2 files changed, 131 insertions(+), 54 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 69dfea2861b..64a942fe868 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -747,7 +747,7 @@ define(function (require, exports, module) { callback = callback || function () {}; var watchingParentRoot = this._findWatchedRootForPath(fullPath); - if (watchingParentRoot) { + if (watchingParentRoot && watchingParentRoot.active) { callback("A parent of this root is already watched"); return; } @@ -759,7 +759,7 @@ define(function (require, exports, module) { return watchedPath.indexOf(fullPath) === 0; }, this); - if (watchingChildRoot) { + if (watchingChildRoot && watchingChildRoot.active) { callback("A child of this root is already watched"); return; } @@ -769,6 +769,7 @@ define(function (require, exports, module) { this._watchEntry(entry, watchedRoot, function (err) { if (err) { console.warn("Failed to watch root: ", entry.fullPath, err); + delete this._watchedRoots[fullPath]; callback(err); return; } @@ -802,14 +803,14 @@ define(function (require, exports, module) { watchedRoot.active = false; this._unwatchEntry(entry, watchedRoot, function (err) { + delete this._watchedRoots[fullPath]; + if (err) { console.warn("Failed to unwatch root: ", entry.fullPath, err); callback(err); return; } - - delete this._watchedRoots[fullPath]; - + callback(null); }.bind(this)); }; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index d439f6e113f..2d0a822d5f0 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -45,10 +45,53 @@ define(function (require, exports, module) { _nodePath = "node/FileWatcherDomain", _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), _nodeConnection = new NodeConnection(), - _domainsLoaded = false; + _domainLoaded = false; // Whether the fileWatcher domain has been loaded - function _enqueueChange(change, needsStats) { - _pendingChanges[change] = _pendingChanges[change] || needsStats; + /** + * A promise that resolves when the NodeConnection object is connected and + * the fileWatcher domain has been loaded. + * + * @type {?jQuery.Promise} + */ + var _nodeConnectionPromise; + + /** + * Load the fileWatcher domain on the assumed-open NodeConnection object + * + * @private + */ + function _loadDomains() { + return _nodeConnection + .loadDomains(_domainPath, true) + .done(function () { + _domainLoaded = true; + _nodeConnectionPromise = null; + }); + } + + // Initialize the connection and connection promise + _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); + + // Setup the close handler. Re-initializes the connection promise and + // notifies the FileSystem that watchers have gone offline. + $(_nodeConnection).on("close", function (event, promise) { + _domainLoaded = false; + _nodeConnectionPromise = promise.then(_loadDomains); + + if (_offlineCallback) { + _offlineCallback(); + } + }); + + /** + * Enqueue a file change event for eventual reporting back to the FileSystem. + * + * @param {string} changedPath The path that was changed + * @param {boolean} needsStats Whether or not the eventual change event should include stats + * @private + */ + function _enqueueChange(changedPath, needsStats) { + _pendingChanges[changedPath] = _pendingChanges[changedPath] || needsStats; if (!_changeTimeout) { _changeTimeout = window.setTimeout(function () { @@ -75,6 +118,15 @@ define(function (require, exports, module) { } } + /** + * Event handler for the Node fileWatcher domain's change event. + * + * @param {jQuery.Event} The underlying change event + * @param {string} path The path that is reported to have changed + * @param {string} event The type of the event: either "change" or "rename" + * @param {string=} filename The name of the file that changed. + * @private + */ function _fileWatcherChange(evt, path, event, filename) { var change; @@ -91,47 +143,48 @@ define(function (require, exports, module) { _enqueueChange(change, false); } } - - function _loadDomains() { - return _nodeConnection - .loadDomains(_domainPath, true) - .done(function () { - _domainsLoaded = true; - }); - } - - var _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); - + + // Setup the change handler. This only needs to happen once. $(_nodeConnection).on("fileWatcher.change", _fileWatcherChange); - $(_nodeConnection).on("close", function (event, promise) { - _domainsLoaded = false; - _nodeConnectionPromise = promise.then(_loadDomains); + /** + * Execute the named function from the fileWatcher domain when the + * NodeConnection is connected and the domain has been loaded. Additional + * parameters are passed as arguments to the command. + * + * @param {string} name The name of the command to execute + * @return {jQuery.Promise} Resolves with the results of the command. + * @private + */ + function _execWhenConnected(name) { + var params = Array.prototype.slice.call(arguments, 1); - if (_offlineCallback) { - _offlineCallback(); - } - }); - - function _execWhenConnected(name, args, callback, errback) { function execConnected() { - var domain = _nodeConnection.domains.fileWatcher, - fn = domain[name]; - - return fn.apply(domain, args) - .done(callback) - .fail(errback); + var domains = _nodeConnection.domains, + domain = domains && domains.fileWatcher, + fn = domain && domain[name]; + + if (fn) { + return fn.apply(domain, params); + } else { + return $.Deferred().reject().promise(); + } } - if (_domainsLoaded && _nodeConnection.connected()) { - execConnected(); + if (_domainLoaded && _nodeConnection.connected()) { + return execConnected(); } else { - _nodeConnectionPromise - .done(execConnected) - .fail(errback); + return _nodeConnectionPromise.then(execConnected); } } + /** + * Convert appshell error codes to FileSystemError values. + * + * @param {?number} err An appshell error code + * @return {?string} A FileSystemError string, or null if there was no error code. + * @private + */ function _mapError(err) { if (!err) { return null; @@ -156,6 +209,14 @@ define(function (require, exports, module) { return FileSystemError.UNKNOWN; } + /** + * Convert a callback to one that transforms its first parameter from an + * appshell error code to a FileSystemError string. + * + * @param {function(?number)} cb A callback that expects an appshell error code + * @return {function(?string)} A callback that expects a FileSystemError string + * @private + */ function _wrap(cb) { return function (err) { var args = Array.prototype.slice.call(arguments); @@ -345,27 +406,42 @@ define(function (require, exports, module) { } function watchPath(path, callback) { - callback = callback || function () {}; - - _execWhenConnected("watchPath", [path], - callback.bind(undefined, null), - callback); + appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + if (err || isNetworkDrive) { + callback(FileSystemError.UNKNOWN); + return; + } + + _execWhenConnected("watchPath", path) + .done(callback.bind(undefined, null)) + .fail(callback); + }); } function unwatchPath(path, callback) { - callback = callback || function () {}; - - _execWhenConnected("unwatchPath", [path], - callback.bind(undefined, null), - callback); + appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + if (err || isNetworkDrive) { + callback(FileSystemError.UNKNOWN); + return; + } + + _execWhenConnected("unwatchPath", path) + .done(callback.bind(undefined, null)) + .fail(callback); + }); } function unwatchAll(callback) { - callback = callback || function () {}; - - _execWhenConnected("watchPath", [], - callback.bind(undefined, null), - callback); + appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + if (err || isNetworkDrive) { + callback(FileSystemError.UNKNOWN); + return; + } + + _execWhenConnected("unwatchAll") + .done(callback.bind(undefined, null)) + .fail(callback); + }); } // Export public API From 0581be8a81e459dcc45639376e495769f13bd027 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 13:10:49 -0800 Subject: [PATCH 28/94] Apply both the entry's name and the parent path to the watchedRoot filter function --- src/filesystem/FileSystem.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 64a942fe868..b23c919b679 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -247,7 +247,7 @@ define(function (require, exports, module) { } var genericVisitor = function (processChild, child) { - if (watchedRoot.filter(child.name)) { + if (watchedRoot.filter(child.name, child.parentPath)) { processChild.call(this, child); return true; @@ -276,9 +276,6 @@ define(function (require, exports, module) { // filesystem to recursively watch or unwatch all subdirectories, as // well as either marking all children as watched or removing them // from the index. - - var counter = 0; - var processChild = function (child) { if (child.isDirectory || child === watchedRoot.entry) { watchOrUnwatch(function (err) { @@ -373,7 +370,7 @@ define(function (require, exports, module) { var parentRoot = this._findWatchedRootForPath(path); if (parentRoot) { - return parentRoot.filter(name); + return parentRoot.filter(name, path); } // It might seem more sensible to return false (exclude) for files outside the watch roots, but @@ -640,7 +637,7 @@ define(function (require, exports, module) { return; } - if (!watchedRoot.filter(entry.name)) { + if (!watchedRoot.filter(entry.name, entry.parentPath)) { return; } From ba1d5283a326adf2eb913f091ba8e11cb40e632b Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 13:31:17 -0800 Subject: [PATCH 29/94] Only mark new entries as watched if they are beneath active watched roots AND if they pass the watchedRoot's filter function --- src/filesystem/FileSystem.js | 29 +++++++++++++++++-- src/filesystem/FileSystemEntry.js | 3 -- .../impls/appshell/AppshellFileSystem.js | 6 ++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index b23c919b679..bc4cccb0b58 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -207,6 +207,22 @@ define(function (require, exports, module) { return watchedRoot; }; + /** + * Indicates whether the given FileSystemEntry is watched. + * + * @param {FileSystemEntry} entry A FileSystemEntry that may or may not be watched. + * @return {boolean} True iff the path is watched. + */ + FileSystem.prototype._isEntryWatched = function (entry) { + var watchedRoot = this._findWatchedRootForPath(entry.fullPath); + + if (watchedRoot && watchedRoot.active) { + return watchedRoot.filter(entry.name, entry.parentPath); + } + + return false; + }; + /** * Helper function to watch or unwatch a filesystem entry beneath a given * watchedRoot. @@ -481,6 +497,11 @@ define(function (require, exports, module) { if (!file) { file = new File(path, this); + + if (this._isEntryWatched(file)) { + file._setWatched(); + } + this._index.addEntry(file); } @@ -500,6 +521,11 @@ define(function (require, exports, module) { if (!directory) { directory = new Directory(path, this); + + if (this._isEntryWatched(directory)) { + directory._setWatched(); + } + this._index.addEntry(directory); } @@ -852,9 +878,6 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; - // Private methods - exports._findWatchedRootForPath = _wrap(FileSystem.prototype._findWatchedRootForPath); - // Export "on" and "off" methods diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index b5625c03b61..b2b08929fde 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -50,9 +50,6 @@ define(function (require, exports, module) { this._setPath(path); this._fileSystem = fileSystem; this._id = nextId++; - - var watchedRoot = fileSystem._findWatchedRootForPath(path); - this._watched = !!(watchedRoot && watchedRoot.active); } // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 2d0a822d5f0..f53683243a9 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -406,7 +406,7 @@ define(function (require, exports, module) { } function watchPath(path, callback) { - appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { if (err || isNetworkDrive) { callback(FileSystemError.UNKNOWN); return; @@ -419,7 +419,7 @@ define(function (require, exports, module) { } function unwatchPath(path, callback) { - appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { if (err || isNetworkDrive) { callback(FileSystemError.UNKNOWN); return; @@ -432,7 +432,7 @@ define(function (require, exports, module) { } function unwatchAll(callback) { - appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { + appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { if (err || isNetworkDrive) { callback(FileSystemError.UNKNOWN); return; From 1ddf3932b9b96a39d027aec541c2a912e80b8a8c Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 16:43:06 -0800 Subject: [PATCH 30/94] Always filter getAllFiles using shouldShow in case the project is not watched --- src/project/ProjectManager.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 878d814f6c8..25684bc2694 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1639,10 +1639,13 @@ define(function (require, exports, module) { function visitor(entry) { - if (entry.isFile && !isBinaryFile(entry.name)) { - result.push(entry); + if (shouldShow(entry)) { + if (entry.isFile && !isBinaryFile(entry.name)) { + result.push(entry); + } + return true; } - return true; + return false; } // First gather all files in project proper From 6200ec1dddf001ab8b8d0a89f143abbaa7879206 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 2 Dec 2013 16:52:59 -0800 Subject: [PATCH 31/94] Handle a potential race in which the socket has disconnected but the reconnect has not yet begun. --- src/filesystem/impls/appshell/AppshellFileSystem.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index f53683243a9..7768a4caf07 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -173,8 +173,10 @@ define(function (require, exports, module) { if (_domainLoaded && _nodeConnection.connected()) { return execConnected(); - } else { + } else if (_nodeConnectionPromise) { return _nodeConnectionPromise.then(execConnected); + } else { + return $.Deferred().reject().promise(); } } From d32b26a35d3d89d6de712f9f987985b0cc26ae45 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 3 Dec 2013 09:53:03 -0800 Subject: [PATCH 32/94] unwatchPath bugfix --- src/filesystem/impls/appshell/AppshellFileSystem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 7768a4caf07..6af9332cf23 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -176,7 +176,7 @@ define(function (require, exports, module) { } else if (_nodeConnectionPromise) { return _nodeConnectionPromise.then(execConnected); } else { - return $.Deferred().reject().promise(); + return $.Deferred().reject().promise(); } } @@ -434,7 +434,7 @@ define(function (require, exports, module) { } function unwatchAll(callback) { - appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { + appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { if (err || isNetworkDrive) { callback(FileSystemError.UNKNOWN); return; From 3d400571fddd21ade962d1621b413fb56ef9aa25 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 3 Dec 2013 15:08:46 -0800 Subject: [PATCH 33/94] Typo in Directory.create --- src/filesystem/Directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 8eb1d0a3439..4a0adfa76cc 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -211,7 +211,7 @@ define(function (require, exports, module) { try { callback(null, stat); } finally { - this._fileSystem._handleWatchResult(this.parent, stat); + this._fileSystem._handleWatchResult(this.parentPath, stat); } }.bind(this)); }; From abe72c58387c560fa2d15326881e090f98b019c4 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 3 Dec 2013 15:09:21 -0800 Subject: [PATCH 34/94] Block external changes around the execution of unlink and moveToTrash --- src/filesystem/FileSystemEntry.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index b2b08929fde..523862007f5 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -276,16 +276,20 @@ define(function (require, exports, module) { this._clearCachedData(); + // Block external change events until after the write has finished + this._fileSystem._beginWrite(); + this._impl.unlink(this._path, function (err) { try { callback(err); } finally { this._fileSystem._handleWatchResult(this._parentPath); this._fileSystem._index.removeEntry(this); + this._fileSystem._endWrite(); } }.bind(this)); }; - + /** * Move this entry to the trash. If the underlying file system doesn't support move * to trash, the item is permanently deleted. @@ -302,6 +306,9 @@ define(function (require, exports, module) { callback = callback || function () {}; this._clearCachedData(); + + // Block external change events until after the write has finished + this._fileSystem._beginWrite(); this._impl.moveToTrash(this._path, function (err) { try { @@ -309,6 +316,7 @@ define(function (require, exports, module) { } finally { this._fileSystem._handleWatchResult(this._parentPath); this._fileSystem._index.removeEntry(this); + this._fileSystem._endWrite(); } }.bind(this)); }; From a6939e6159b5804479612b038d0cd9ab957cf868 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 3 Dec 2013 17:51:23 -0800 Subject: [PATCH 35/94] First attempt at extracting filesystem state updates from change notifications --- src/filesystem/Directory.js | 8 +- src/filesystem/File.js | 28 +++-- src/filesystem/FileSystem.js | 180 ++++++++++++++++-------------- src/filesystem/FileSystemEntry.js | 41 +++++-- src/search/FindInFiles.js | 9 +- 5 files changed, 162 insertions(+), 104 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 4a0adfa76cc..2b846726913 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -199,6 +199,7 @@ define(function (require, exports, module) { */ Directory.prototype.create = function (callback) { callback = callback || function () {}; + this._impl.mkdir(this._path, function (err, stat) { if (err) { this._clearCachedData(); @@ -206,12 +207,17 @@ define(function (require, exports, module) { return; } + // Update internal filesystem state this._stat = stat; + var parent = this._fileSystem.getDirectoryForPath(this.parentPath), + oldContents = parent._contents; + + parent._clearCachedData(); try { callback(null, stat); } finally { - this._fileSystem._handleWatchResult(this.parentPath, stat); + this._fileSystem._fireChangeEvent(parent, oldContents); } }.bind(this)); }; diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 85630e33e9b..3eaf1657162 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -146,28 +146,32 @@ define(function (require, exports, module) { return; } + // Update internal filesystem state this._hash = stat._hash; + this._stat = stat; + + // Only cache the contents of watched files + if (watched) { + this._contents = data; + } + + var parent, oldContents; + if (created) { + parent = this._fileSystem.getDirectoryForPath(this.parentPath); + oldContents = parent._contents; + } try { + // Notify the caller callback(null, stat); } finally { // If the write succeeded, fire a synthetic change event if (created) { // new file created - this._fileSystem._handleWatchResult(this._parentPath); + this._fileSystem._fireChangeEvent(parent, oldContents); } else { // existing file modified - this._fileSystem._handleWatchResult(this._path, stat); - } - - // Wait until AFTER the synthetic change has been processed - // to update the cached stats so the change handler recognizes - // it is a non-duplicate change event. - this._stat = stat; - - // Only cache the contents of watched files - if (watched) { - this._contents = data; + this._fileSystem._fireChangeEvent(this); } } } finally { diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index bc4cccb0b58..da9730a45f4 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -548,7 +548,7 @@ define(function (require, exports, module) { item = this._index.getEntry(normalizedPath); } - if (item && item._stat) { + if (item && item._stat && item._isWatched) { callback(null, item, item._stat); return; } @@ -577,9 +577,12 @@ define(function (require, exports, module) { * @param {string} newName * @param {boolean} isDirectory */ - FileSystem.prototype._entryRenamed = function (oldName, newName, isDirectory) { + FileSystem.prototype._handleRename = function (oldName, newName, isDirectory) { // Update all affected entries in the index this._index.entryRenamed(oldName, newName, isDirectory); + }; + + FileSystem.prototype._fireRenameEvent = function (oldName, newName) { $(this).trigger("rename", [oldName, newName]); }; @@ -626,6 +629,94 @@ define(function (require, exports, module) { this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback); }; + FileSystem.prototype._fireChangeEvent = function (entry, oldContents) { + + var fireChangeEvent = function (entry, added, removed) { + // Trigger a change event + $(this).trigger("change", [entry, added, removed]); + }.bind(this); + + if (!entry) { + fireChangeEvent(null); + return; + } + + var watchedRoot = this._findWatchedRootForPath(entry.fullPath); + if (!watchedRoot) { + console.warn("Received change notification for unwatched path: ", entry.fullPath); + return; + } + + if (!watchedRoot.filter(entry.name, entry.parentPath)) { + return; + } + + if (entry.isFile) { + fireChangeEvent(entry); + } else { + oldContents = oldContents || []; + // Update changed entries + entry.getContents(function (err, contents) { + + var addNewEntries = function (callback) { + // Check for added directories and scan to add to index + // Re-scan this directory to add any new contents + var entriesToAdd = contents.filter(function (entry) { + return oldContents.indexOf(entry) === -1; + }); + + var addCounter = entriesToAdd.length; + + if (addCounter === 0) { + callback([]); + } else { + entriesToAdd.forEach(function (entry) { + this._watchEntry(entry, watchedRoot, function (err) { + if (--addCounter === 0) { + callback(entriesToAdd); + } + }); + }, this); + } + }.bind(this); + + var removeOldEntries = function (callback) { + var entriesToRemove = oldContents.filter(function (entry) { + return contents.indexOf(entry) === -1; + }); + + var removeCounter = entriesToRemove.length; + + if (removeCounter === 0) { + callback([]); + } else { + entriesToRemove.forEach(function (entry) { + this._unwatchEntry(entry, watchedRoot, function (err) { + if (--removeCounter === 0) { + callback(entriesToRemove); + } + }); + }, this); + } + }.bind(this); + + if (err) { + console.warn("Unable to get contents of changed directory: ", entry.fullPath, err); + } else { + removeOldEntries(function (removed) { + addNewEntries(function (added) { + if (added.length > 0 || removed.length > 0) { + fireChangeEvent(entry, added, removed); + } else { + console.info("Detected duplicate directory change event: ", entry.fullPath); + } + }); + }); + } + }.bind(this)); + } + }; + /** * @private * Processes a result from the file/directory watchers. Watch results are sent from the low-level implementation @@ -636,11 +727,6 @@ define(function (require, exports, module) { * passed. */ FileSystem.prototype._handleWatchResult = function (path, stat) { - - var fireChangeEvent = function (entry, added, removed) { - // Trigger a change event - $(this).trigger("change", [entry, added, removed]); - }.bind(this); if (!path) { // This is a "wholesale" change event @@ -649,7 +735,7 @@ define(function (require, exports, module) { entry._clearCachedData(); }); - fireChangeEvent(null); + this._fireChangeEvent(null); return; } @@ -657,92 +743,21 @@ define(function (require, exports, module) { var entry = this._index.getEntry(path); if (entry) { - var watchedRoot = this._findWatchedRootForPath(entry.fullPath); - if (!watchedRoot) { - console.warn("Received change notification for unwatched path: ", path); - return; - } - - if (!watchedRoot.filter(entry.name, entry.parentPath)) { - return; - } - if (entry.isFile) { // Update stat and clear contents, but only if out of date if (!(stat && entry._stat && stat.mtime.getTime() === entry._stat.mtime.getTime())) { entry._clearCachedData(); entry._stat = stat; - fireChangeEvent(entry); + this._fireChangeEvent(entry); } else { console.info("Detected duplicate file change event: ", path); } } else { - var oldContents = entry._contents || []; - - // Clear out old contents + var oldContents = entry._contents; entry._clearCachedData(); entry._stat = stat; - // Update changed entries - entry.getContents(function (err, contents) { - - var addNewEntries = function (callback) { - // Check for added directories and scan to add to index - // Re-scan this directory to add any new contents - var entriesToAdd = contents.filter(function (entry) { - return oldContents.indexOf(entry) === -1; - }); - - var addCounter = entriesToAdd.length; - - if (addCounter === 0) { - callback([]); - } else { - entriesToAdd.forEach(function (entry) { - this._watchEntry(entry, watchedRoot, function (err) { - if (--addCounter === 0) { - callback(entriesToAdd); - } - }); - }, this); - } - }.bind(this); - - var removeOldEntries = function (callback) { - var entriesToRemove = oldContents.filter(function (entry) { - return contents.indexOf(entry) === -1; - }); - - var removeCounter = entriesToRemove.length; - - if (removeCounter === 0) { - callback([]); - } else { - entriesToRemove.forEach(function (entry) { - this._unwatchEntry(entry, watchedRoot, function (err) { - if (--removeCounter === 0) { - callback(entriesToRemove); - } - }); - }, this); - } - }.bind(this); - - if (err) { - console.warn("Unable to get contents of changed directory: ", path, err); - } else { - removeOldEntries(function (removed) { - addNewEntries(function (added) { - if (added.length > 0 || removed.length > 0) { - fireChangeEvent(entry, added, removed); - } else { - console.info("Detected duplicate directory change event: ", path); - } - }); - }); - } - - }.bind(this)); + this._fireChangeEvent(entry, oldContents); } } }; @@ -878,7 +893,6 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; - // Export "on" and "off" methods /** diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 523862007f5..db27daea44a 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -242,7 +242,10 @@ define(function (require, exports, module) { */ FileSystemEntry.prototype.rename = function (newFullPath, callback) { callback = callback || function () {}; + + // Block external change events until after the write has finished this._fileSystem._beginWrite(); + this._impl.rename(this._path, newFullPath, function (err) { try { if (err) { @@ -251,11 +254,15 @@ define(function (require, exports, module) { return; } + // Update internal filesystem state + this._fileSystem._handleRename(this._path, newFullPath, this.isDirectory); + try { - callback(null); // notify caller + // Notify the caller + callback(null); } finally { - // Notify the file system of the name change - this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); + // Notify rename listeners + this._fileSystem._fireRenameEvent(this._path, newFullPath); } } finally { // Unblock external change events @@ -280,11 +287,21 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); this._impl.unlink(this._path, function (err) { + // Update internal filesystem state + this._fileSystem._index.removeEntry(this); + var parent = this._fileSystem.getDirectoryForPath(this.parentPath), + oldContents = parent._contents; + + parent._clearCachedData(); + try { + // Notify the caller callback(err); } finally { - this._fileSystem._handleWatchResult(this._parentPath); - this._fileSystem._index.removeEntry(this); + // Notify change listeners + this._fileSystem._fireChangeEvent(parent, oldContents); + + // Unblock external change events this._fileSystem._endWrite(); } }.bind(this)); @@ -311,11 +328,21 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); this._impl.moveToTrash(this._path, function (err) { + // Update internal filesystem state + this._fileSystem._index.removeEntry(this); + var parent = this._fileSystem.getDirectoryForPath(this.parentPath), + oldContents = parent._contents; + + parent._clearCachedData(); + try { + // Notify the caller callback(err); } finally { - this._fileSystem._handleWatchResult(this._parentPath); - this._fileSystem._index.removeEntry(this); + // Notify change listeners + this._fileSystem._fireChangeEvent(parent, oldContents); + + // Unblock external change events this._fileSystem._endWrite(); } }.bind(this)); diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 0e9113c524e..beaea905bf3 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -916,11 +916,18 @@ define(function (require, exports, module) { } var addPromise; - if (added && added.length > 0) { + + added = added || []; + added = added.filter(function (entry) { + return entry.isFile; + }); + + if (added.length > 0) { var doSearch = _doSearchInOneFile.bind(undefined, function () { var resultsAdded = _addSearchMatches.apply(undefined, arguments); resultsChanged = resultsChanged || resultsAdded; }); + addPromise = Async.doInParallel(added, doSearch); } else { addPromise = $.Deferred().resolve().promise(); From 63ceeb0bbd6c4ac7cd023574f1ad8929d1e44c83 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 4 Dec 2013 13:40:15 -0800 Subject: [PATCH 36/94] When updating search results as a result of filesystem changes, include results from all included files and not just those directly added --- src/search/FindInFiles.js | 54 ++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index beaea905bf3..e275783578f 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -901,7 +901,7 @@ define(function (require, exports, module) { if (removed && removed.length > 0) { var _includesPath = function (fullPath) { return _.some(removed, function (item) { - return item.fullPath === fullPath; + return fullPath.indexOf(item.fullPath) === 0; }); }; @@ -916,19 +916,53 @@ define(function (require, exports, module) { } var addPromise; - - added = added || []; - added = added.filter(function (entry) { - return entry.isFile; - }); - - if (added.length > 0) { + if (added && added.length > 0) { var doSearch = _doSearchInOneFile.bind(undefined, function () { var resultsAdded = _addSearchMatches.apply(undefined, arguments); resultsChanged = resultsChanged || resultsAdded; }); - addPromise = Async.doInParallel(added, doSearch); + var addedFiles = [], + addedDirectories = []; + + // sort added entries into files and directories + added.forEach(function (entry) { + if (entry.isFile) { + addedFiles.push(entry); + } else { + addedDirectories.push(entry); + } + }); + + // visit added directories and add their included files + var visitor = function (child) { + if (ProjectManager.shouldShow(child)) { + if (child.isFile) { + addedFiles.push(child); + } + return true; + } + }; + + var visitPromise = Async.doInParallel(addedDirectories, function (directory) { + var deferred = $.Deferred(); + + directory.visit(visitor, function (err) { + if (err) { + deferred.reject(err); + return; + } + + deferred.resolve(); + }); + + return deferred.promise(); + }); + + // find additional matches in all added files + addPromise = visitPromise.then(function () { + return Async.doInParallel(addedFiles, doSearch); + }); } else { addPromise = $.Deferred().resolve().promise(); } @@ -938,6 +972,8 @@ define(function (require, exports, module) { if (resultsChanged) { _restoreSearchResults(); } + }).fail(function (err) { + console.warn("Failed to update FindInFiles results: ", err); }); } }; From 6dbcea2dc88fc62295042950470f4481d422c6e8 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 4 Dec 2013 17:17:47 -0800 Subject: [PATCH 37/94] Improved factoring of change handling and event firing --- src/filesystem/Directory.js | 42 +++++-- src/filesystem/File.js | 67 +++++----- src/filesystem/FileSystem.js | 199 +++++++++++++----------------- src/filesystem/FileSystemEntry.js | 66 +++++----- test/spec/FileSystem-test.js | 2 +- 5 files changed, 186 insertions(+), 190 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 2b846726913..b13541c28c0 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -76,8 +76,15 @@ define(function (require, exports, module) { * Clear any cached data for this directory * @private */ - Directory.prototype._clearCachedData = function () { + Directory.prototype._clearCachedData = function (stopRecursing) { this.parentClass._clearCachedData.apply(this); + + if (!stopRecursing && this._contents) { + this._contents.forEach(function (child) { + child._clearCachedData(true); + }); + } + this._contents = undefined; this._contentsStats = undefined; this._contentsStatsErrors = undefined; @@ -200,25 +207,34 @@ define(function (require, exports, module) { Directory.prototype.create = function (callback) { callback = callback || function () {}; + // Block external change events until after the write has finished + this._fileSystem._beginWrite(); + this._impl.mkdir(this._path, function (err, stat) { if (err) { this._clearCachedData(); - callback(err); - return; + try { + callback(err); + return; + } finally { + // Unblock external change events + this._fileSystem._endWrite(); + } } + + var parent = this._fileSystem.getDirectoryForPath(this.parentPath); // Update internal filesystem state this._stat = stat; - var parent = this._fileSystem.getDirectoryForPath(this.parentPath), - oldContents = parent._contents; - - parent._clearCachedData(); - - try { - callback(null, stat); - } finally { - this._fileSystem._fireChangeEvent(parent, oldContents); - } + this._fileSystem._handleDirectoryChange(parent, function (added, removed) { + try { + callback(null, stat); + } finally { + this._fileSystem._fireChangeEvent(parent, added, removed); + // Unblock external change events + this._fileSystem._endWrite(); + } + }.bind(this)); }.bind(this)); }; diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 3eaf1657162..9aaf646c4e1 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -139,44 +139,51 @@ define(function (require, exports, module) { this._fileSystem._beginWrite(); this._impl.writeFile(this._path, data, options, function (err, stat, created) { - try { - if (err) { - this._clearCachedData(); + if (err) { + this._clearCachedData(); + try { callback(err); return; + } finally { + // Always unblock external change events + this._fileSystem._endWrite(); } - - // Update internal filesystem state - this._hash = stat._hash; - this._stat = stat; - - // Only cache the contents of watched files - if (watched) { - this._contents = data; - } - - var parent, oldContents; - if (created) { - parent = this._fileSystem.getDirectoryForPath(this.parentPath); - oldContents = parent._contents; - } - + } + + // Update internal filesystem state + this._hash = stat._hash; + this._stat = stat; + + // Only cache the contents of watched files + if (watched) { + this._contents = data; + } + + if (created) { + var parent = this._fileSystem.getDirectoryForPath(this.parentPath); + this._fileSystem._handleDirectoryChange(parent, function (added, removed) { + try { + // Notify the caller + callback(null, stat); + } finally { + // If the write succeeded, fire a synthetic change event + this._fileSystem._fireChangeEvent(parent, added, removed); + + // Always unblock external change events + this._fileSystem._endWrite(); + } + }.bind(this)); + } else { try { // Notify the caller callback(null, stat); } finally { - // If the write succeeded, fire a synthetic change event - if (created) { - // new file created - this._fileSystem._fireChangeEvent(parent, oldContents); - } else { - // existing file modified - this._fileSystem._fireChangeEvent(this); - } + // existing file modified + this._fileSystem._fireChangeEvent(this); + + // Always unblock external change events + this._fileSystem._endWrite(); } - } finally { - // Always unblock external change events - this._fileSystem._endWrite(); } }.bind(this)); }; diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index da9730a45f4..9be1356c8ec 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -119,10 +119,10 @@ define(function (require, exports, module) { */ FileSystem.prototype._watchResults = null; - /** Process all queued watcher results, by calling _handleWatchResult() on each */ + /** Process all queued watcher results, by calling _handleExternalChange() on each */ FileSystem.prototype._triggerWatchCallbacksNow = function () { this._watchResults.forEach(function (info) { - this._handleWatchResult(info.path, info.stat); + this._handleExternalChange(info.path, info.stat); }, this); this._watchResults.length = 0; }; @@ -258,7 +258,6 @@ define(function (require, exports, module) { } else { genericProcessChild = function (child) { child._clearCachedData(); - this._index.removeEntry(child); }; } @@ -347,7 +346,15 @@ define(function (require, exports, module) { * watch is complete, possibly with a FileSystemError string. */ FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) { - this._watchOrUnwatchEntry(entry, watchedRoot, callback, false); + this._watchOrUnwatchEntry(entry, watchedRoot, function (err) { + this._index.visitAll(function (child) { + if (child.fullPath.indexOf(entry.fullPath) === 0) { + this._index.removeEntry(child); + } + }.bind(this)); + + callback(err); + }.bind(this), false); }; /** @@ -569,23 +576,6 @@ define(function (require, exports, module) { }.bind(this)); }; - /** - * @private - * Notify the system when an entry name has changed. - * - * @param {string} oldName - * @param {string} newName - * @param {boolean} isDirectory - */ - FileSystem.prototype._handleRename = function (oldName, newName, isDirectory) { - // Update all affected entries in the index - this._index.entryRenamed(oldName, newName, isDirectory); - }; - - FileSystem.prototype._fireRenameEvent = function (oldName, newName) { - $(this).trigger("rename", [oldName, newName]); - }; - /** * Show an "Open" dialog and return the file(s)/directories selected by the user. * @@ -628,93 +618,77 @@ define(function (require, exports, module) { FileSystem.prototype.showSaveDialog = function (title, initialPath, proposedNewFilename, callback) { this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback); }; - - FileSystem.prototype._fireChangeEvent = function (entry, oldContents) { - - var fireChangeEvent = function (entry, added, removed) { - // Trigger a change event - $(this).trigger("change", [entry, added, removed]); - }.bind(this); - - if (!entry) { - fireChangeEvent(null); - return; - } - var watchedRoot = this._findWatchedRootForPath(entry.fullPath); - if (!watchedRoot) { - console.warn("Received change notification for unwatched path: ", entry.fullPath); - return; - } + FileSystem.prototype._fireRenameEvent = function (oldName, newName) { + $(this).trigger("rename", [oldName, newName]); + }; + + FileSystem.prototype._fireChangeEvent = function (entry, added, removed) { + $(this).trigger("change", [entry, added, removed]); + }; + + /** + * @private + * Notify the system when an entry name has changed. + * + * @param {string} oldName + * @param {string} newName + * @param {boolean} isDirectory + */ + FileSystem.prototype._handleRename = function (oldName, newName, isDirectory) { + // Update all affected entries in the index + this._index.entryRenamed(oldName, newName, isDirectory); + }; + + FileSystem.prototype._handleDirectoryChange = function (directory, callback) { + var oldContents = directory._contents || []; - if (!watchedRoot.filter(entry.name, entry.parentPath)) { - return; - } + directory._clearCachedData(); + directory.getContents(function (err, contents) { + var addedEntries = contents.filter(function (entry) { + return oldContents.indexOf(entry) === -1; + }); + + var removedEntries = oldContents.filter(function (entry) { + return contents.indexOf(entry) === -1; + }); - if (entry.isFile) { - fireChangeEvent(entry); - } else { - oldContents = oldContents || []; - // Update changed entries - entry.getContents(function (err, contents) { - - var addNewEntries = function (callback) { - // Check for added directories and scan to add to index - // Re-scan this directory to add any new contents - var entriesToAdd = contents.filter(function (entry) { - return oldContents.indexOf(entry) === -1; - }); - - var addCounter = entriesToAdd.length; - - if (addCounter === 0) { - callback([]); - } else { - entriesToAdd.forEach(function (entry) { - this._watchEntry(entry, watchedRoot, function (err) { - if (--addCounter === 0) { - callback(entriesToAdd); - } - }); - }, this); - } - }.bind(this); + // If directory is not watched, clear the cache the children of removed + // entries manually. Otherwise, this is handled by the unwatch call. + var watchedRoot = this._findWatchedRootForPath(directory.fullPath); + if (!watchedRoot || !watchedRoot.filter(directory.name, directory.parentPath)) { + removedEntries.forEach(function (removed) { + this._index.visitAll(function (entry) { + if (entry.fullPath.indexOf(removed.fullPath) === 0) { + entry._clearCachedData(); + } + }.bind(this)); + }, this); - var removeOldEntries = function (callback) { - var entriesToRemove = oldContents.filter(function (entry) { - return contents.indexOf(entry) === -1; - }); - - var removeCounter = entriesToRemove.length; - - if (removeCounter === 0) { - callback([]); - } else { - entriesToRemove.forEach(function (entry) { - this._unwatchEntry(entry, watchedRoot, function (err) { - if (--removeCounter === 0) { - callback(entriesToRemove); - } - }); - }, this); - } - }.bind(this); - - if (err) { - console.warn("Unable to get contents of changed directory: ", entry.fullPath, err); - } else { - removeOldEntries(function (removed) { - addNewEntries(function (added) { - if (added.length > 0 || removed.length > 0) { - fireChangeEvent(entry, added, removed); - } else { - console.info("Detected duplicate directory change event: ", entry.fullPath); - } - }); - }); + callback(addedEntries, removedEntries); + return; + } + + var counter = addedEntries.length + removedEntries.length; + if (counter === 0) { + callback(directory, addedEntries, removedEntries); + return; + } + + var watchOrUnwatchCallback = function (err) { + if (--counter === 0) { + callback(directory, addedEntries, removedEntries); } - }.bind(this)); - } + }; + + addedEntries.forEach(function (entry) { + this._watchEntry(entry, watchedRoot, watchOrUnwatchCallback); + }, this); + + removedEntries.forEach(function (entry) { + this._unwatchEntry(entry, watchedRoot, watchOrUnwatchCallback); + }, this); + }.bind(this)); }; /** @@ -726,7 +700,7 @@ define(function (require, exports, module) { * @param {FileSystemStats=} stat Optional stat for the item that changed. This param is not always * passed. */ - FileSystem.prototype._handleWatchResult = function (path, stat) { + FileSystem.prototype._handleExternalChange = function (path, stat) { if (!path) { // This is a "wholesale" change event @@ -753,11 +727,16 @@ define(function (require, exports, module) { console.info("Detected duplicate file change event: ", path); } } else { - var oldContents = entry._contents; - entry._clearCachedData(); - entry._stat = stat; - - this._fireChangeEvent(entry, oldContents); + this._handleDirectoryChange(entry, function (added, removed) { + entry._stat = stat; + + if (added && added.length === 0 && removed && removed.length === 0) { + console.info("Detected duplicate directory change event: ", entry.fullPath); + return; + } + + this._fireChangeEvent(entry, added, removed); + }.bind(this)); } } }; @@ -865,7 +844,7 @@ define(function (require, exports, module) { // Fire a wholesale change event because all previously watched entries // have been removed from the index and should no longer be referenced - this._handleWatchResult(null); + this._handleExternalChange(null); }; diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index db27daea44a..494b07824b4 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -281,29 +281,26 @@ define(function (require, exports, module) { FileSystemEntry.prototype.unlink = function (callback) { callback = callback || function () {}; - this._clearCachedData(); - // Block external change events until after the write has finished this._fileSystem._beginWrite(); + this._clearCachedData(); this._impl.unlink(this._path, function (err) { - // Update internal filesystem state - this._fileSystem._index.removeEntry(this); - var parent = this._fileSystem.getDirectoryForPath(this.parentPath), - oldContents = parent._contents; + var parent = this._fileSystem.getDirectoryForPath(this.parentPath); - parent._clearCachedData(); - - try { - // Notify the caller - callback(err); - } finally { - // Notify change listeners - this._fileSystem._fireChangeEvent(parent, oldContents); - - // Unblock external change events - this._fileSystem._endWrite(); - } + // Update internal filesystem state + this._fileSystem._handleDirectoryChange(parent, function (added, removed) { + try { + // Notify the caller + callback(err); + } finally { + // Notify change listeners + this._fileSystem._fireChangeEvent(parent, added, removed); + + // Unblock external change events + this._fileSystem._endWrite(); + } + }.bind(this)); }.bind(this)); }; @@ -321,30 +318,27 @@ define(function (require, exports, module) { } callback = callback || function () {}; - - this._clearCachedData(); // Block external change events until after the write has finished this._fileSystem._beginWrite(); + this._clearCachedData(); this._impl.moveToTrash(this._path, function (err) { - // Update internal filesystem state - this._fileSystem._index.removeEntry(this); - var parent = this._fileSystem.getDirectoryForPath(this.parentPath), - oldContents = parent._contents; + var parent = this._fileSystem.getDirectoryForPath(this.parentPath); - parent._clearCachedData(); - - try { - // Notify the caller - callback(err); - } finally { - // Notify change listeners - this._fileSystem._fireChangeEvent(parent, oldContents); - - // Unblock external change events - this._fileSystem._endWrite(); - } + // Update internal filesystem state + this._fileSystem._handleDirectoryChange(parent, function (added, removed) { + try { + // Notify the caller + callback(err); + } finally { + // Notify change listeners + this._fileSystem._fireChangeEvent(parent, added, removed); + + // Unblock external change events + this._fileSystem._endWrite(); + } + }.bind(this)); }.bind(this)); }; diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 14e6b071ad7..af03230aa8b 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1137,7 +1137,7 @@ define(function (require, exports, module) { }); // Fire a whole-sale change event - fileSystem._handleWatchResult(null); + fileSystem._handleExternalChange(null); }); waitsFor(function () { return fileChanged; }); From a8941dae2e1f00ddb2c5974002edc76b57a08153 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Thu, 5 Dec 2013 14:28:22 -0800 Subject: [PATCH 38/94] wire up the project tree to FileSystem change events --- src/project/ProjectManager.js | 238 +++++++++++++++++++++++----------- 1 file changed, 163 insertions(+), 75 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 25684bc2694..a642ad6d29b 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -667,6 +667,39 @@ define(function (require, exports, module) { function isBinaryFile(fileName) { return fileName.match(_binaryExclusionListRegEx); } + + /** + * @private + * Create JSON object for a jstree node. Insert mapping from full path to + * jstree node ID. + * + * For more info on jsTree's JSON format see: http://www.jstree.com/documentation/json_data + * @param {!FileSystemEntry} entry + * @return {data: string, attr: {id: string}, metadata: {entry: FileSystemEntry}, children: Array., state: string} + */ + function _entryToJSON(entry) { + if (!shouldShow(entry)) { + return null; + } + + var jsonEntry = { + data: entry.name, + attr: { id: "node" + _projectInitialLoad.id++ }, + metadata: { entry: entry } + }; + + if (entry.isDirectory) { + jsonEntry.children = []; + jsonEntry.state = "closed"; + } else { + jsonEntry.data = ViewUtils.getFileEntryDisplay(entry); + } + + // Map path to ID to initialize loaded and opened states + _projectInitialLoad.fullPathToIdMap[entry.fullPath] = jsonEntry.attr.id; + + return jsonEntry; + } /** * @private @@ -687,29 +720,9 @@ define(function (require, exports, module) { jsonEntry; for (entryI = 0; entryI < entries.length; entryI++) { - entry = entries[entryI]; - - if (shouldShow(entry)) { - jsonEntry = { - data: entry.name, - attr: { id: "node" + _projectInitialLoad.id++ }, - metadata: { entry: entry } - }; - - if (entry.isDirectory) { - jsonEntry.children = []; - jsonEntry.state = "closed"; - } else { - jsonEntry.data = ViewUtils.getFileEntryDisplay(entry); - } - - // For more info on jsTree's JSON format see: http://www.jstree.com/documentation/json_data - jsonEntryList.push(jsonEntry); - - // Map path to ID to initialize loaded and opened states - _projectInitialLoad.fullPathToIdMap[entry.fullPath] = jsonEntry.attr.id; - } + jsonEntryList.push(_entryToJSON(entries[entryI])); } + return jsonEntryList; } @@ -1046,15 +1059,20 @@ define(function (require, exports, module) { * outside the project, or if it doesn't exist). * * @param {!(File|Directory)} entry File or Directory to find + * @param {boolean} shallowSearch Flag to return no result if the node is not loaded * @return {$.Promise} Resolved with jQ obj for the jsTree tree node; or rejected if not found */ - function _findTreeNode(entry) { + function _findTreeNode(entry, shallowSearch) { var result = new $.Deferred(); - // If path not within project, ignore var projRelativePath = makeProjectRelativeIfPossible(entry.fullPath); + if (projRelativePath === entry.fullPath) { + // If path not within project, ignore return result.reject().promise(); + } else if (entry === getProjectRoot()) { + // If path is the project root, return the tree itself + return result.resolve($projectTreeList).promise(); } var treeAPI = $.jstree._reference(_projectTree); @@ -1083,7 +1101,7 @@ define(function (require, exports, module) { var subChildren = treeAPI._get_children($node); if (subChildren.length > 0) { findInSubtree(subChildren, segmentI + 1); - } else { + } else if (!shallowSearch) { // Subtree not loaded yet: force async load & try again treeAPI.load_node($node, function (data) { subChildren = treeAPI._get_children($node); @@ -1091,6 +1109,9 @@ define(function (require, exports, module) { }, function (err) { result.reject(); // includes case where folder is empty }); + } else { + // Stop searching + result.resolve(null); } } } @@ -1119,8 +1140,8 @@ define(function (require, exports, module) { return _loadProject(getProjectRoot().fullPath, true) .done(function () { if (selectedEntry) { - _findTreeNode(selectedEntry).done(function (node) { - _forceSelection(null, node); + _findTreeNode(selectedEntry).done(function ($node) { + _forceSelection(null, $node); }); } }); @@ -1226,6 +1247,24 @@ define(function (require, exports, module) { return true; } + /** + * @private + * Add a new node (existing FileSystemEntry or untitled file) to the project tree + * + * @param {?jQueryObject} $target Parent or sibling node + * @param {?number|string} position Position to insert + * @param {!Object} data + * @param {!boolean} skipRename + */ + function _createNode($target, position, data, skipRename) { + if (typeof data === "string") { + data = { data: data }; + } + + // Create the node and open the editor + _projectTree.jstree("create", $target, position || 0, data, null, skipRename); + } + /** * Create a new item in the project tree. * @@ -1238,7 +1277,7 @@ define(function (require, exports, module) { * filename. */ function createNewItem(baseDir, initialName, skipRename, isFolder) { - var node = null, + var $node = null, selection = _projectTree.jstree("get_selected"), selectionEntry = null, position = "inside", @@ -1402,12 +1441,14 @@ define(function (require, exports, module) { // TODO (issue #115): Need API to get tree node for baseDir. // In the meantime, pass null for node so new item is placed // relative to the selection - node = selection; - - function createNode() { - // Create the node and open the editor - _projectTree.jstree("create", node, position, {data: initialName}, null, skipRename); - + $node = selection; + + // There is a race condition in jstree if "open_node" and "create" are called in rapid + // succession and the node was not yet loaded. To avoid it, first open the node and wait + // for the open_node event before trying to create the new one. See #2085 for more details. + if (wasNodeOpen) { + _createNode($node, position, { data: initialName }, skipRename); + if (!skipRename) { var $renameInput = _projectTree.find(".jstree-rename-input"); @@ -1421,18 +1462,13 @@ define(function (require, exports, module) { ViewUtils.scrollElementIntoView(_projectTree, $renameInput, true); } - } - - // There is a race condition in jstree if "open_node" and "create" are called in rapid - // succession and the node was not yet loaded. To avoid it, first open the node and wait - // for the open_node event before trying to create the new one. See #2085 for more details. - if (wasNodeOpen) { - createNode(); } else { - _projectTree.one("open_node.jstree", createNode); + _projectTree.one("open_node.jstree", function () { + _createNode($node, position, { data: initialName }, skipRename); + }); // Open the node before creating the new child - _projectTree.jstree("open_node", node); + _projectTree.jstree("open_node", $node); } return result.promise(); @@ -1553,6 +1589,56 @@ define(function (require, exports, module) { }); // No fail handler: silently no-op if file doesn't exist in tree } + + /** + * @private + * Deletes a node from jstree. Does not make assumptions on file existence. + * + * @param {FileSystemEntry} + * @return {$.Promise} Promise that is always resolved + */ + function _deleteTreeNode(entry) { + var deferred = new $.Deferred(); + + _findTreeNode(entry, true).done(function ($node) { + if (!$node) { + return; + } + + _projectTree.one("delete_node.jstree", function () { + // When a node is deleted, the previous node is automatically selected. + // This works fine as long as the previous node is a file, but doesn't + // work so well if the node is a folder + var sel = _projectTree.jstree("get_selected"), + entry = sel ? sel.data("entry") : null; + + if (entry && entry.isDirectory) { + // Make sure it didn't turn into a leaf node. This happens if + // the only file in the directory was deleted + if (sel.hasClass("jstree-leaf")) { + sel.removeClass("jstree-leaf jstree-open"); + sel.addClass("jstree-closed"); + } + } + }); + + var oldSuppressToggleOpen = suppressToggleOpen; + suppressToggleOpen = true; + _projectTree.jstree("delete_node", $node); + suppressToggleOpen = oldSuppressToggleOpen; + }).always(function () { + _redraw(true); + deferred.resolve(); + }); + + if (DocumentManager.getCurrentDocument()) { + DocumentManager.notifyPathDeleted(entry.fullPath); + } else { + EditorManager.notifyPathDeleted(entry.fullPath); + } + + return deferred.promise(); + } /** * Delete file or directore from project @@ -1563,37 +1649,7 @@ define(function (require, exports, module) { entry.moveToTrash(function (err) { if (!err) { - _findTreeNode(entry).done(function ($node) { - _projectTree.one("delete_node.jstree", function () { - // When a node is deleted, the previous node is automatically selected. - // This works fine as long as the previous node is a file, but doesn't - // work so well if the node is a folder - var sel = _projectTree.jstree("get_selected"), - entry = sel ? sel.data("entry") : null; - - if (entry && entry.isDirectory) { - // Make sure it didn't turn into a leaf node. This happens if - // the only file in the directory was deleted - if (sel.hasClass("jstree-leaf")) { - sel.removeClass("jstree-leaf jstree-open"); - sel.addClass("jstree-closed"); - } - } - }); - var oldSuppressToggleOpen = suppressToggleOpen; - suppressToggleOpen = true; - _projectTree.jstree("delete_node", $node); - suppressToggleOpen = oldSuppressToggleOpen; - }); - - if (DocumentManager.getCurrentDocument()) { - DocumentManager.notifyPathDeleted(entry.fullPath); - } else { - EditorManager.notifyPathDeleted(entry.fullPath); - } - - _redraw(true); - result.resolve(); + _deleteTreeNode(entry).then(result.resolve, result.reject); } else { // Show an error alert Dialogs.showModalDialog( @@ -1684,10 +1740,42 @@ define(function (require, exports, module) { /** * @private * Respond to a FileSystem change event. + * @param {$.Event} event + * @param {File|Directory} entry File or Directory changed + * @param {Array.=} added If entry is a Directory, contains zero or more added children + * @param {Array.=} removed If entry is a Directory, contains zero or more removed children */ _fileSystemChange = function (event, entry, added, removed) { if (entry) { - // Use the added and removed sets to update the project tree here. + // Directory contents removed + if (removed && removed.length) { + removed.forEach(function (removedEntry) { + _deleteTreeNode(removedEntry); + }); + } + + // Directory contents added + if (added && added.length) { + // Find parent node to add to. Use shallowSearch=true to + // skip adding a child if it's parent is not visible + _findTreeNode(entry, true).done(function ($directoryNode) { + var json; + + added.forEach(function (addedEntry) { + json = _entryToJSON(addedEntry); + + // _entryToJSON returns null if the added file is filtered from view + if (json) { + // position is irrelevant due to sorting + _createNode($directoryNode, null, json, true); + } + }); + }); + } + + // FIXME (jasonsanjose): File rename is handled as a remove and add + // If the active editor is selected in the file tree (instead of + // the working set), how can we fix the selection? } else { refreshFileTree(); } From d1de0677a599390e76f961a96d54f754b84ff947 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 5 Dec 2013 22:17:09 -0800 Subject: [PATCH 39/94] Remove a superfluous argument applied to the _handleDirectoryChange callback --- src/filesystem/FileSystem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 9be1356c8ec..f0725b97987 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -671,13 +671,13 @@ define(function (require, exports, module) { var counter = addedEntries.length + removedEntries.length; if (counter === 0) { - callback(directory, addedEntries, removedEntries); + callback(addedEntries, removedEntries); return; } var watchOrUnwatchCallback = function (err) { if (--counter === 0) { - callback(directory, addedEntries, removedEntries); + callback(addedEntries, removedEntries); } }; From 9fb030ddf0563d107cfbd57e9d7c350ca4d2b781 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 6 Dec 2013 10:10:45 -0800 Subject: [PATCH 40/94] Only remove entries from the index after unwatch, not after _unwatchEntry. --- src/filesystem/FileSystem.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 29b8a89b697..e09c219744f 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -346,15 +346,7 @@ define(function (require, exports, module) { * watch is complete, possibly with a FileSystemError string. */ FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) { - this._watchOrUnwatchEntry(entry, watchedRoot, function (err) { - this._index.visitAll(function (child) { - if (child.fullPath.indexOf(entry.fullPath) === 0) { - this._index.removeEntry(child); - } - }.bind(this)); - - callback(err); - }.bind(this), false); + this._watchOrUnwatchEntry(entry, watchedRoot, callback, false); }; /** @@ -827,6 +819,12 @@ define(function (require, exports, module) { this._unwatchEntry(entry, watchedRoot, function (err) { delete this._watchedRoots[fullPath]; + this._index.visitAll(function (child) { + if (child.fullPath.indexOf(entry.fullPath) === 0) { + this._index.removeEntry(child); + } + }.bind(this)); + if (err) { console.warn("Failed to unwatch root: ", entry.fullPath, err); callback(err); From 28a655a7b751f2b84220691596c182f9a51082b1 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 6 Dec 2013 11:05:21 -0800 Subject: [PATCH 41/94] Fix the rename unit test --- test/spec/FileSystem-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index af03230aa8b..fc01f821b98 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -790,7 +790,7 @@ define(function (require, exports, module) { describe("Event timing", function () { - it("should notify rename callback before 'change' event", function () { + it("should apply rename callback before firing the 'rename' event", function () { var origFilePath = "/file1.txt", origFile = fileSystem.getFileForPath(origFilePath), renamedFilePath = "/file1_renamed.txt"; @@ -803,7 +803,7 @@ define(function (require, exports, module) { callback: delay(250) }); - $(fileSystem).on("change", function (evt, entry) { + $(fileSystem).on("rename", function (evt, entry) { expect(renameDone).toBe(true); // this is the important check: callback should have already run! changeDone = true; }); @@ -822,7 +822,7 @@ define(function (require, exports, module) { }); - it("should notify write callback before 'change' event", function () { + it("should apply write callback before firing the 'change' event", function () { var testFilePath = "/file1.txt", testFile = fileSystem.getFileForPath(testFilePath); From ec57b00d7a7ff849cb12d75d8f81692a3f11c9b6 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Sun, 8 Dec 2013 15:32:04 -0800 Subject: [PATCH 42/94] Do not swallow directory change events if there are no added or removed sets --- src/filesystem/FileSystem.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index e09c219744f..2533eb3c588 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -727,11 +727,6 @@ define(function (require, exports, module) { this._handleDirectoryChange(entry, function (added, removed) { entry._stat = stat; - if (added && added.length === 0 && removed && removed.length === 0) { - console.info("Detected duplicate directory change event: ", entry.fullPath); - return; - } - this._fireChangeEvent(entry, added, removed); }.bind(this)); } From b7a1f6be00e5f75ce7e81ec6bdbb15b4d021b22f Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Sun, 8 Dec 2013 15:37:22 -0800 Subject: [PATCH 43/94] Do not insert new tree nodes when the search for the parent node returns null --- src/project/ProjectManager.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index a642ad6d29b..f3707d40877 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1739,7 +1739,10 @@ define(function (require, exports, module) { /** * @private - * Respond to a FileSystem change event. + * Respond to a FileSystem change event. Note that if renames are initiated + * externally, they may be reported as a separate removal and addition. In + * this case, the editor state isn't currently preserved. + * * @param {$.Event} event * @param {File|Directory} entry File or Directory changed * @param {Array.=} added If entry is a Directory, contains zero or more added children @@ -1759,23 +1762,20 @@ define(function (require, exports, module) { // Find parent node to add to. Use shallowSearch=true to // skip adding a child if it's parent is not visible _findTreeNode(entry, true).done(function ($directoryNode) { - var json; - - added.forEach(function (addedEntry) { - json = _entryToJSON(addedEntry); - - // _entryToJSON returns null if the added file is filtered from view - if (json) { - // position is irrelevant due to sorting - _createNode($directoryNode, null, json, true); - } - }); + if ($directoryNode) { + added.forEach(function (addedEntry) { + var json = _entryToJSON(addedEntry); + + // _entryToJSON returns null if the added file is filtered from view + if (json) { + console.log("Adding: ", addedEntry.fullPath); + // position is irrelevant due to sorting + _createNode($directoryNode, null, json, true); + } + }); + } }); } - - // FIXME (jasonsanjose): File rename is handled as a remove and add - // If the active editor is selected in the file tree (instead of - // the working set), how can we fix the selection? } else { refreshFileTree(); } From f4e5517b7a56eb0763292ce7daf24f8302749819 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Sun, 8 Dec 2013 22:08:39 -0800 Subject: [PATCH 44/94] Serialize changes to the file tree; don't expand nodes to add children --- src/project/ProjectManager.js | 62 ++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index f3707d40877..060e972397f 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -83,6 +83,14 @@ define(function (require, exports, module) { var _fileSystemChange, _fileSystemRename; + /** + * @private + * @type {Async.PromiseQueue} + * Used to serialize changes to the file tree + */ + var _fileTreeChangeQueue = new Async.PromiseQueue(); + + /** * @private * File and folder names which are not displayed or searched @@ -1255,14 +1263,20 @@ define(function (require, exports, module) { * @param {?number|string} position Position to insert * @param {!Object} data * @param {!boolean} skipRename + * @return {jQuery.Promise} Resolves once the node has been created. */ function _createNode($target, position, data, skipRename) { + var deferred = new $.Deferred(); + if (typeof data === "string") { data = { data: data }; } // Create the node and open the editor + _projectTree.one("create.jstree", deferred.resolve); _projectTree.jstree("create", $target, position || 0, data, null, skipRename); + + return deferred.promise(); } /** @@ -1602,6 +1616,7 @@ define(function (require, exports, module) { _findTreeNode(entry, true).done(function ($node) { if (!$node) { + deferred.resolve(); return; } @@ -1620,15 +1635,17 @@ define(function (require, exports, module) { sel.addClass("jstree-closed"); } } + deferred.resolve(); }); var oldSuppressToggleOpen = suppressToggleOpen; suppressToggleOpen = true; _projectTree.jstree("delete_node", $node); suppressToggleOpen = oldSuppressToggleOpen; + }).fail(function () { + deferred.resolve(); }).always(function () { _redraw(true); - deferred.resolve(); }); if (DocumentManager.getCurrentDocument()) { @@ -1736,7 +1753,7 @@ define(function (require, exports, module) { return (LanguageManager.getLanguageForPath(file.fullPath).getId() === languageId); }; } - + /** * @private * Respond to a FileSystem change event. Note that if renames are initiated @@ -1752,28 +1769,35 @@ define(function (require, exports, module) { if (entry) { // Directory contents removed if (removed && removed.length) { - removed.forEach(function (removedEntry) { - _deleteTreeNode(removedEntry); + _fileTreeChangeQueue.add(function () { + return Async.doSequentially(removed, function (removedEntry) { + return _deleteTreeNode(removedEntry); + }, false); }); } // Directory contents added if (added && added.length) { - // Find parent node to add to. Use shallowSearch=true to - // skip adding a child if it's parent is not visible - _findTreeNode(entry, true).done(function ($directoryNode) { - if ($directoryNode) { - added.forEach(function (addedEntry) { - var json = _entryToJSON(addedEntry); - - // _entryToJSON returns null if the added file is filtered from view - if (json) { - console.log("Adding: ", addedEntry.fullPath); - // position is irrelevant due to sorting - _createNode($directoryNode, null, json, true); - } - }); - } + _fileTreeChangeQueue.add(function () { + // Find parent node to add to. Use shallowSearch=true to + // skip adding a child if it's parent is not visible + return _findTreeNode(entry, true).then(function ($directoryNode) { + if ($directoryNode && !$directoryNode.hasClass("jstree-closed")) { + return Async.doSequentially(added, function (addedEntry) { + var json = _entryToJSON(addedEntry); + + // _entryToJSON returns null if the added file is filtered from view + if (json) { + // position is irrelevant due to sorting + return _createNode($directoryNode, null, json, true); + } else { + return new $.Deferred().resolve(); + } + }, false); + } else { + return new $.Deferred().resolve(); + } + }); }); } } else { From 77a35d84931e7ba765c0bdf224d277bd7708a531 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Sun, 8 Dec 2013 22:09:13 -0800 Subject: [PATCH 45/94] Remove no-longer-useful logging statements --- src/filesystem/impls/appshell/node/FileWatcherDomain.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/filesystem/impls/appshell/node/FileWatcherDomain.js b/src/filesystem/impls/appshell/node/FileWatcherDomain.js index b738c3d8f50..1576240a3eb 100644 --- a/src/filesystem/impls/appshell/node/FileWatcherDomain.js +++ b/src/filesystem/impls/appshell/node/FileWatcherDomain.js @@ -42,9 +42,7 @@ var _domainManager, */ function unwatchPath(path) { var watcher = _watcherMap[path]; - - console.info("Unwatching: " + path); - + if (watcher) { try { if (fsevents) { @@ -68,9 +66,7 @@ function watchPath(path) { if (_watcherMap.hasOwnProperty(path)) { return; } - - console.info("Watching: " + path); - + try { var watcher; From c6967bd715319978cde49a3d33e266d7f1ff850e Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 9 Dec 2013 22:13:48 -0800 Subject: [PATCH 46/94] Added documentation to FileSystemEntry and AppshellFileSystem. --- src/filesystem/Directory.js | 2 +- src/filesystem/FileSystemEntry.js | 45 +++++ .../impls/appshell/AppshellFileSystem.js | 180 +++++++++++++++++- 3 files changed, 219 insertions(+), 8 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index d6357873ed7..48f6365eb10 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -28,7 +28,7 @@ define(function (require, exports, module) { "use strict"; - var FileSystemEntry = require("filesystem/FileSystemEntry"); + var FileSystemEntry = require("filesystem/FileSystemEntry"); /* * @constructor diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index ac14d879fbd..d244318c8e5 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -21,6 +21,45 @@ * */ +/* + * To ensure cache coherence, current and future asynchronous state-changing + * operations of FileSystemEntry and its subclasses should implement the + * following high-level sequence of steps: + * + * 1. Block external filesystem change events; + * 2. Execute the low-level state-changing operation; + * 3. Update the internal filesystem state, including caches; + * 4. Apply the callback; + * 5. Fire an appropriate internal change notification; and + * 6. Unblock external change events. + * + * Note that because internal filesystem state is updated first, both the original + * caller and the change notification listeners observe filesystem state that is + * current w.r.t. the operation. Furthermore, because external change events are + * blocked before the operation begins, listeners will only receive the internal + * change event for the operation and not additional (or possibly inconsistent) + * external change events. + * + * State-changing operations that block external filesystem change events must + * take care to always subsequently unblock the external change events in all + * control paths. It is safe to assume, however, that the underlying impl will + * always apply the callback with some value. + + * Caches should be conservative. Consequently, the entry's cached data should + * always be cleared if the underlying impl's operation fails. This is the case + * event for read-only operations because an unexpected failure implies that the + * system is in an unknown state. The entry should communicate this by failing + * where appropriate, and should not use the cache to hide failure. + * + * Only watched entries should make use of cached data because change events are + * only expected for such entries, and change events are used to granularly + * invalidate out-of-date caches. + * + * By convention, callbacks are optional for asynchronous, state-changing + * operations, but required for read-only operations. The first argument to the + * callback should always be a nullable error string from FileSystemError. + */ + /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define */ @@ -414,6 +453,12 @@ define(function (require, exports, module) { /** * Visit this entry and its descendents with the supplied visitor function. + * Correctly handles symbolic link cycles and options can be provided to limit + * search depth and total number of entries visited. No particular traversal + * order is guaranteed; instead of relying on such an order, it is preferable + * to use the visit function to build a list of visited entries, sort those + * entries as desired, and then process them. Whenever possible, deep + * filesystem traversals should use this method. * * @param {function(FileSystemEntry): boolean} visitor - A visitor function, which is * applied to this entry and all descendent FileSystemEntry objects. If the function returns diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 6af9332cf23..bc8f5bd21a8 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -227,14 +227,44 @@ define(function (require, exports, module) { }; } + /** + * Display an open-files dialog to the user and call back asynchronously with + * either a FileSystmError string or an array of path strings, which indicate + * the entry or entries selected. + * + * @param {boolean} allowMultipleSelection + * @param {boolean} chooseDirectories + * @param {string} title + * @param {string} initialPath + * @param {Array.=} fileTypes + * @param {function(?string, Array.=)} callback + */ function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { appshell.fs.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, _wrap(callback)); } + /** + * Display a save-file dialog and call back asynchronously with either a + * FileSystemError string or the path to which the user has chosen to save + * the file. If the dialog is cancelled, the path string will be empty. + * + * @param {string} title + * @param {string} initialPath + * @param {string} proposedNewFilename + * @param {function(?string, string=)} callback + */ function showSaveDialog(title, initialPath, proposedNewFilename, callback) { appshell.fs.showSaveDialog(title, initialPath, proposedNewFilename, _wrap(callback)); } + /** + * Stat the file or directory at the given path, calling back + * asynchronously with either a FileSystemError string or the entry's + * associated FileSystemStats object. + * + * @param {string} path + * @param {function(?string, FileSystemStats=)} callback + */ function stat(path, callback) { appshell.fs.stat(path, function (err, stats) { if (err) { @@ -255,6 +285,16 @@ define(function (require, exports, module) { }); } + /** + * Determine whether a file or directory exists at the given path by calling + * back asynchronously with either a FileSystemError string or a boolean, + * which is true if the file exists and false otherwise. The error will never + * be FileSystemError.NOT_FOUND; in that case, there will be no error and the + * boolean parameter will be false. + * + * @param {string} path + * @param {function(?string, boolean)} callback + */ function exists(path, callback) { stat(path, function (err) { if (err) { @@ -270,6 +310,17 @@ define(function (require, exports, module) { }); } + /** + * Read the contents of the directory at the given path, calling back + * asynchronously either with a FileSystemError string or an array of + * FileSystemEntry objects along with another consistent array, each index + * of which either contains a FileSystemStats object for the corresponding + * FileSystemEntry object in the second parameter or a FileSystemError + * string describing a stat error. + * + * @param {string} path + * @param {function(?string, Array.=, Array.=)} callback + */ function readdir(path, callback) { appshell.fs.readdir(path, function (err, contents) { if (err) { @@ -296,6 +347,16 @@ define(function (require, exports, module) { }); } + /** + * Create a directory at the given path, and call back asynchronously with + * either a FileSystemError string or a stats object for the newly created + * directory. The octal mode parameter is optional; if unspecified, the mode + * of the created directory is implementation dependent. + * + * @param {string} path + * @param {number=} mode The base-eight mode of the newly created directory. + * @param {function(?string, FileSystemStats=)=} callback + */ function mkdir(path, mode, callback) { if (typeof mode === "function") { callback = mode; @@ -312,20 +373,38 @@ define(function (require, exports, module) { }); } + /** + * Rename the file or directory at oldPath to newPath, and call back + * asynchronously with a possibly null FileSystemError string. + * + * @param {string} oldPath + * @param {string} newPath + * @param {function(?string)=} callback + */ function rename(oldPath, newPath, callback) { appshell.fs.rename(oldPath, newPath, _wrap(callback)); - // No need to fake a file-watcher result here: FileSystem already updates index on rename() } - /* + /** + * Read the contents of the file at the given path, calling back + * asynchronously with either a FileSystemError string, or with the data and + * the FileSystemStats object associated with the read file. The (optional) + * options parameter can be used to specify an encoding (default "utf8"). + * * Note: if either the read or the stat call fails then neither the read data - * or stat will be passed back, and the call should be considered to have failed. + * nor stat will be passed back, and the call should be considered to have failed. * If both calls fail, the error from the read call is passed back. + * + * @param {string} path + * @param {{encoding : string=}=} options + * @param {function(?string, string=, FileSystemStats=)} callback */ function readFile(path, options, callback) { var encoding = options.encoding || "utf8"; - // Execute the read and stat calls in parallel + // Execute the read and stat calls in parallel. Callback early if the + // read call completes first with an error; otherwise wait for both + // to finish. var done = false, data, stat, err; appshell.fs.readFile(path, encoding, function (_err, _data) { @@ -353,6 +432,22 @@ define(function (require, exports, module) { }); } + /** + * Write data to the file at the given path, calling back asynchronously with + * either a FileSystemError string or the FileSystemStats object associated + * with the written file. If no file exists at the given path, a new file will + * be created. The (optional) options parameter can be used to specify an + * encoding (default "utf8"), an octal mode (default unspecified and + * implementation dependent), and a consistency hash, which is used to the + * current state of the file before overwriting it. If a consistency hash is + * provided but does not match the hash of the file on disk, a + * FileSystemError.CONTENTS_MODIFIED error is passed to the callback. + * + * @param {string} path + * @param {string} data + * @param {{encoding : string=, mode : number=, hash : string=}=} options + * @param {function(?string, FileSystemStats=)} callback + */ function writeFile(path, data, options, callback) { var encoding = options.encoding || "utf8"; @@ -390,23 +485,66 @@ define(function (require, exports, module) { }); } + /** + * Unlink (i.e., permanently delete) the file or directory at the given path, + * calling back asynchronously with a possibly null FileSystemError string. + * Directories will be unlinked even when non-empty. + * + * @param {string} path + * @param {function(string)=} callback + */ function unlink(path, callback) { appshell.fs.unlink(path, function (err) { callback(_mapError(err)); }); } - + + /** + * Move the file or directory at the given path to a system dependent trash + * location, calling back asynchronously with a possibly null FileSystemError + * string. Directories will be moved even when non-empty. + * + * @param {string} path + * @param {function(string)=} callback + */ function moveToTrash(path, callback) { appshell.fs.moveToTrash(path, function (err) { callback(_mapError(err)); }); } + /** + * Initialize file watching for this filesystem, using the supplied + * changeCallback to provide change notifications. The first parameter of + * changeCallback specifies the changed path (either a file or a directory); + * if this parameter is null, it indicates that the implementation cannot + * specify a particular changed path, and so the callers should consider all + * paths to have changed and to update their state accordingly. The second + * parameter to changeCallback is an optional FileSystemStats object that + * may be provided in case the changed path already exists and stats are + * readily available. The offlineCallback will be called in case watchers + * are no longer expected to function properly. All watched paths are + * cleared when the offlineCallback is called. + * + * @param {function(?string, FileSystemStats=)} changeCallback + * @param {function()=} callback + */ function initWatchers(changeCallback, offlineCallback) { _changeCallback = changeCallback; _offlineCallback = offlineCallback; } + /** + * Start providing change notifications for the file or directory at the + * given path, calling back asynchronously with a possibly null FileSystemError + * string when the initialization is complete. Notifications are provided + * using the changeCallback function provided by the initWatchers method. + * Note that change notifications are only provided recursively for directories + * when the recursiveWatch property of this module is true. + * + * @param {string} path + * @param {function(?string)=} callback + */ function watchPath(path, callback) { appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { if (err || isNetworkDrive) { @@ -420,6 +558,14 @@ define(function (require, exports, module) { }); } + /** + * Stop providing change notifications for the file or directory at the + * given path, calling back asynchronously with a possibly null FileSystemError + * string when the operation is complete. + * + * @param {string} path + * @param {function(?string)=} callback + */ function unwatchPath(path, callback) { appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { if (err || isNetworkDrive) { @@ -433,6 +579,13 @@ define(function (require, exports, module) { }); } + /** + * Stop providing change notifications for all previously watched files and + * directories, optionally calling back asynchronously with a possibly null + * FileSystemError string when the operation is complete. + * + * @param {function(?string)=} callback + */ function unwatchAll(callback) { appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { if (err || isNetworkDrive) { @@ -463,9 +616,22 @@ define(function (require, exports, module) { exports.unwatchPath = unwatchPath; exports.unwatchAll = unwatchAll; - // Node only supports recursive file watching on the Darwin + /** + * Indicates whether or not recursive watching notifications are supported + * by the watchPath call. Currently, only Darwin supports recursive watching. + * + * @type {boolean} + */ exports.recursiveWatch = appshell.platform === "mac"; - // Only perform UNC path normalization on Windows + // + /** + * Indicates whether or not the filesystem should expect and normalize UNC + * paths. If set, then //server/directory/ is a normalized path; otherwise the + * filesystem will normalize it to /server/directory. Currently, UNC path + * normalization only occurs on Windows. + * + * @type {boolean} + */ exports.normalizeUNCPaths = appshell.platform === "win"; }); From b636495c6e6b76e3c62f82bb9947b2e81fe1fb09 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 11 Dec 2013 21:58:32 -0800 Subject: [PATCH 47/94] Try again to squash the duplicate-tree-nodes bug caused by ProjectManager._fileSystemChange --- src/project/ProjectManager.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 060e972397f..9f99a40233d 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1788,8 +1788,22 @@ define(function (require, exports, module) { // _entryToJSON returns null if the added file is filtered from view if (json) { - // position is irrelevant due to sorting - return _createNode($directoryNode, null, json, true); + + // Before creating a new node, make sure it doesn't already exist. + // TODO: Improve the efficiency of this search! + return _findTreeNode(addedEntry).then(function ($childNode) { + if ($childNode) { + // the node already exists; do nothing; + return new $.Deferred().resolve(); + } else { + // The node wasn't found; create it. + // Position is irrelevant due to sorting + return _createNode($directoryNode, null, json, true); + } + }, function () { + // The node doesn't exist; create it. + return _createNode($directoryNode, null, json, true); + }); } else { return new $.Deferred().resolve(); } From 998317d107d7b4116c5b9c4981dd78d9a17a2579 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 13 Dec 2013 15:05:28 -0800 Subject: [PATCH 48/94] Add a rough FILE_SAVE dialog customized for CONTENTS_MODIFIED errors --- src/document/DocumentCommandHandlers.js | 52 +++++++++++++++++++++++-- src/nls/root/strings.js | 3 ++ src/widgets/Dialogs.js | 2 + 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index b513787361c..b116a51e439 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -522,13 +522,16 @@ define(function (require, exports, module) { ); } + + /** * Saves a document to its existing path. Does NOT support untitled documents. * @param {!Document} docToSave + * @param {boolean=} force Ignore CONTENTS_MODIFIED errors from the FileSystem * @return {$.Promise} a promise that is resolved with the File of docToSave (to mirror * the API of _doSaveAs()). Rejected in case of IO error (after error dialog dismissed). */ - function doSave(docToSave) { + function doSave(docToSave, force) { var result = new $.Deferred(), file = docToSave.file; @@ -538,18 +541,61 @@ define(function (require, exports, module) { result.reject(error); }); } + + function handleContentsModified() { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.EXT_MODIFIED_TITLE, + StringUtils.format( + Strings.EXT_MODIFIED_WARNING, + StringUtils.breakableUrl(docToSave.file.name) + ), + [ + { + className : Dialogs.DIALOG_BTN_CLASS_LEFT, + id : Dialogs.DIALOG_BTN_SAVE_AS, + text : Strings.SAVE_AS + }, + { + className : Dialogs.DIALOG_BTN_CLASS_NORMAL, + id : Dialogs.DIALOG_BTN_CANCEL, + text : Strings.CANCEL + }, + { + className : Dialogs.DIALOG_BTN_CLASS_PRIMARY, + id : Dialogs.DIALOG_BTN_OK, + text : Strings.SAVE_AND_OVERWRITE + } + ] + ) + .done(function (id) { + if (id === Dialogs.DIALOG_BTN_CANCEL) { + result.reject(); + } else if (id === Dialogs.DIALOG_BTN_OK) { + // Re-do the save, ignoring any CONTENTS_MODIFIED errors + doSave(docToSave, true).then(result.resolve, result.reject); + } else if (id === Dialogs.DIALOG_BTN_SAVE_AS) { + // Let the user choose a different path at which to write the file + exports.handleFileSaveAs({doc: docToSave}).then(result.resolve, result.reject); + } + }); + } if (docToSave.isDirty) { var writeError = false; // We don't want normalized line endings, so it's important to pass true to getText() - FileUtils.writeText(file, docToSave.getText(true)) + FileUtils.writeText(file, docToSave.getText(true), force) .done(function () { docToSave.notifySaved(); result.resolve(file); }) .fail(function (err) { - handleError(err); + if (err === FileSystemError.CONTENTS_MODIFIED) { + handleContentsModified(); + } else { + handleError(err); + } }); } else { result.resolve(file); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index abb53b46e7e..96e3413c0b3 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -108,6 +108,7 @@ define({ "CONFIRM_FOLDER_DELETE_TITLE" : "Confirm Delete", "CONFIRM_FOLDER_DELETE" : "Are you sure you want to delete the folder {0}?", "FILE_DELETED_TITLE" : "File Deleted", + "EXT_MODIFIED_WARNING" : "{0} has been modified on disk.

Do you want to save the file and overwrite those changes?", "EXT_MODIFIED_MESSAGE" : "{0} has been modified on disk, but also has unsaved changes in {APP_NAME}.

Which version do you want to keep?", "EXT_DELETED_MESSAGE" : "{0} has been deleted on disk, but has unsaved changes in {APP_NAME}.

Do you want to keep your changes?", @@ -311,6 +312,8 @@ define({ "OK" : "OK", "DONT_SAVE" : "Don't Save", "SAVE" : "Save", + "SAVE_AS" : "Save As\u2026", + "SAVE_AND_OVERWRITE" : "Overwrite", "CANCEL" : "Cancel", "DELETE" : "Delete", "RELOAD_FROM_DISK" : "Reload from Disk", diff --git a/src/widgets/Dialogs.js b/src/widgets/Dialogs.js index 3adcaa54842..87e4a075d19 100644 --- a/src/widgets/Dialogs.js +++ b/src/widgets/Dialogs.js @@ -46,6 +46,7 @@ define(function (require, exports, module) { var DIALOG_BTN_CANCEL = "cancel", DIALOG_BTN_OK = "ok", DIALOG_BTN_DONTSAVE = "dontsave", + DIALOG_BTN_SAVE_AS = "save_as", DIALOG_CANCELED = "_canceled", DIALOG_BTN_DOWNLOAD = "download"; @@ -345,6 +346,7 @@ define(function (require, exports, module) { exports.DIALOG_BTN_CANCEL = DIALOG_BTN_CANCEL; exports.DIALOG_BTN_OK = DIALOG_BTN_OK; exports.DIALOG_BTN_DONTSAVE = DIALOG_BTN_DONTSAVE; + exports.DIALOG_BTN_SAVE_AS = DIALOG_BTN_SAVE_AS; exports.DIALOG_CANCELED = DIALOG_CANCELED; exports.DIALOG_BTN_DOWNLOAD = DIALOG_BTN_DOWNLOAD; From be0645ba08d586c7cb8e1a23a55061068df15353 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 16 Dec 2013 10:25:00 -0800 Subject: [PATCH 49/94] Allow FileSystemEntry objects to become unwatched. (Previously, unwatching coincided with removal from the index, so the transition was unnecessary.) --- src/filesystem/FileSystem.js | 62 ++++++++++++++++--------------- src/filesystem/FileSystemEntry.js | 15 +++++--- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 2533eb3c588..06f389cae61 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -253,11 +253,11 @@ define(function (require, exports, module) { var genericProcessChild; if (shouldWatch) { genericProcessChild = function (child) { - child._setWatched(); + child._setWatched(true); }; } else { genericProcessChild = function (child) { - child._clearCachedData(); + child._setWatched(false); }; } @@ -485,6 +485,31 @@ define(function (require, exports, module) { return path; }; + + /** + * Return a (strict subclass of a) FileSystemEntry object for the specified + * path using the provided constuctor. For now, the provided constructor + * should be either File or Directory. + * + * @private + * @param {function(string, FileSystem)} EntryConstructor Constructor with + * which to initialize new FileSystemEntry objects. + * @param {string} path Absolute path of file. + * @return {File|Directory} The File or Directory object. This file may not + * yet exist on disk. + */ + FileSystem.prototype._getEntryForPath = function (EntryConstructor, path) { + var isDirectory = EntryConstructor === Directory; + path = this._normalizePath(path, isDirectory); + var entry = this._index.getEntry(path); + + if (!entry) { + entry = new EntryConstructor(path, this); + this._index.addEntry(entry); + } + + return entry; + }; /** * Return a File object for the specified path. @@ -494,20 +519,7 @@ define(function (require, exports, module) { * @return {File} The File object. This file may not yet exist on disk. */ FileSystem.prototype.getFileForPath = function (path) { - path = this._normalizePath(path, false); - var file = this._index.getEntry(path); - - if (!file) { - file = new File(path, this); - - if (this._isEntryWatched(file)) { - file._setWatched(); - } - - this._index.addEntry(file); - } - - return file; + return this._getEntryForPath(File, path); }; /** @@ -518,20 +530,7 @@ define(function (require, exports, module) { * @return {Directory} The Directory object. This directory may not yet exist on disk. */ FileSystem.prototype.getDirectoryForPath = function (path) { - path = this._normalizePath(path, true); - var directory = this._index.getEntry(path); - - if (!directory) { - directory = new Directory(path, this); - - if (this._isEntryWatched(directory)) { - directory._setWatched(); - } - - this._index.addEntry(directory); - } - - return directory; + return this._getEntryForPath(Directory, path); }; /** @@ -870,6 +869,9 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; + // Private helper methods used by internal filesystem modules + exports._isEntryWatched = _wrap(FileSystem.prototype._isEntryWatched); + // Export "on" and "off" methods /** diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index d244318c8e5..fe31f74527a 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -89,6 +89,7 @@ define(function (require, exports, module) { this._setPath(path); this._fileSystem = fileSystem; this._id = nextId++; + this._setWatched(fileSystem._isEntryWatched(this)); } // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters @@ -195,13 +196,17 @@ define(function (require, exports, module) { }; /** - * Mark this entry as being watched after construction. There is no way to - * set an entry as being unwatched after construction because entries should - * be discarded upon being unwatched. + * Mark this entry as being watched or unwatched. Setting an entry as being + * unwatched will cause its cached data to be cleared. * @private + * @param watched Whether or not the entry is watched */ - FileSystemEntry.prototype._setWatched = function () { - this._isWatched = true; + FileSystemEntry.prototype._setWatched = function (watched) { + this._isWatched = watched; + + if (!watched) { + this._clearCachedData(); + } }; /** From e37614d3ea8c889298c8b275ae77ddf097231a6d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 16 Dec 2013 10:49:42 -0800 Subject: [PATCH 50/94] Fix a problem with the InMemoryFile prototypical inhertance chain --- src/document/InMemoryFile.js | 8 ++++++++ src/filesystem/Directory.js | 2 +- src/filesystem/File.js | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/document/InMemoryFile.js b/src/document/InMemoryFile.js index 955c5d3cb30..fecbc25cfa9 100644 --- a/src/document/InMemoryFile.js +++ b/src/document/InMemoryFile.js @@ -52,6 +52,14 @@ define(function (require, exports, module) { InMemoryFile.prototype.parentClass = File.prototype; + /** + * Clear any cached data for this file. + * @private + */ + InMemoryFile.prototype._clearCachedData = function () { + File.prototype._clearCachedData.apply(this); + }; + // Stub out invalid calls inherited from File /** diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 48f6365eb10..a442f350afe 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -77,7 +77,7 @@ define(function (require, exports, module) { * @private */ Directory.prototype._clearCachedData = function (stopRecursing) { - this.parentClass._clearCachedData.apply(this); + FileSystemEntry.prototype._clearCachedData.apply(this); if (!stopRecursing && this._contents) { this._contents.forEach(function (child) { diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 70343f13ee4..b4b6ec6b2bf 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -70,7 +70,7 @@ define(function (require, exports, module) { * @private */ File.prototype._clearCachedData = function () { - this.parentClass._clearCachedData.apply(this); + FileSystemEntry.prototype._clearCachedData.apply(this); this._contents = undefined; }; From a7241162886da9c1c6efad8b1e00ab0802c18e78 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 16 Dec 2013 15:02:02 -0800 Subject: [PATCH 51/94] Add additional event ordering tests --- test/spec/FileSystem-test.js | 86 ++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index fc01f821b98..78966362398 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -788,33 +788,42 @@ define(function (require, exports, module) { }); }); - describe("Event timing", function () { + describe("Event ordering", function () { - it("should apply rename callback before firing the 'rename' event", function () { - var origFilePath = "/file1.txt", - origFile = fileSystem.getFileForPath(origFilePath), - renamedFilePath = "/file1_renamed.txt"; + function eventOrderingTest(eventName, implOpName, entry, methodName) { + var params = Array.prototype.slice.call(arguments, 4); runs(function () { - var renameDone = false, changeDone = false; + var opDone = false, eventDone = false; // Delay impl callback to happen after impl watcher notification - MockFileSystemImpl.when("rename", origFilePath, { + MockFileSystemImpl.when(implOpName, entry.fullPath, { callback: delay(250) }); - $(fileSystem).on("rename", function (evt, entry) { - expect(renameDone).toBe(true); // this is the important check: callback should have already run! - changeDone = true; + $(fileSystem).on(eventName, function (evt, entry) { + expect(opDone).toBe(true); // this is the important check: callback should have already run! + eventDone = true; }); - origFile.rename(renamedFilePath, function (err) { + params.push(function (err) { expect(err).toBeFalsy(); - renameDone = true; + expect(eventDone).toBe(false); + opDone = true; }); - waitsFor(function () { return changeDone && renameDone; }); + entry[methodName].apply(entry, params); + + waitsFor(function () { return opDone && eventDone; }); }); + } + + it("should apply rename callback before firing the 'rename' event", function () { + var origFilePath = "/file1.txt", + origFile = fileSystem.getFileForPath(origFilePath), + renamedFilePath = "/file1_renamed.txt"; + + eventOrderingTest("rename", "rename", origFile, "rename", renamedFilePath); runs(function () { expect(origFile.fullPath).toBe(renamedFilePath); @@ -826,26 +835,28 @@ define(function (require, exports, module) { var testFilePath = "/file1.txt", testFile = fileSystem.getFileForPath(testFilePath); - runs(function () { - var writeDone = false, changeDone = false; - - // Delay impl callback to happen after impl watcher notification - MockFileSystemImpl.when("writeFile", testFilePath, { - callback: delay(250) - }); - - $(fileSystem).on("change", function (evt, entry) { - expect(writeDone).toBe(true); // this is the important check: callback should have already run! - changeDone = true; - }); - - testFile.write("Foobar", { blind: true }, function (err) { - expect(err).toBeFalsy(); - writeDone = true; - }); - - waitsFor(function () { return changeDone && writeDone; }); - }); + eventOrderingTest("change", "writeFile", testFile, "write", "Foobar", { blind: true }); + }); + + it("should apply unlink callback before firing the 'change' event", function () { + var testFilePath = "/file1.txt", + testFile = fileSystem.getFileForPath(testFilePath); + + eventOrderingTest("change", "unlink", testFile, "unlink"); + }); + + it("should apply moveToTrash callback before firing the 'change' event", function () { + var testFilePath = "/file1.txt", + testFile = fileSystem.getFileForPath(testFilePath); + + eventOrderingTest("change", "moveToTrash", testFile, "moveToTrash"); + }); + + it("should apply create callback before firing the 'change' event", function () { + var testDirPath = "/a/new/directory.txt", + testDir = fileSystem.getDirectoryForPath(testDirPath); + + eventOrderingTest("change", "create", testDir, "create"); }); // Used for various tests below where two write operations (to two different files) overlap in various ways @@ -1169,6 +1180,15 @@ define(function (require, exports, module) { cb1 = readCallback(), cb2 = readCallback(), savedHash; + + // confirm watched and empty cached data + runs(function () { + file = fileSystem.getFileForPath(filename); + + expect(file._isWatched).toBe(true); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + }); // unwatch root directory runs(function () { From 188ac33a088202be34864b5f307e5067dba4c6ce Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 17 Dec 2013 10:29:44 -0800 Subject: [PATCH 52/94] Rename watchResults/writeCount to externalChanges/externalChangeCount; improve documentation; assert \!externalChangeCount before and after each test --- src/filesystem/FileSystem.js | 121 ++++++++++++++++++++++------------- test/spec/FileSystem-test.js | 7 +- 2 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 06f389cae61..9180d31da42 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -85,7 +85,8 @@ define(function (require, exports, module) { // Initialize the watch/unwatch request queue this._watchRequests = []; - this._watchResults = []; + // Initialize the queue of pending external changes + this._externalChanges = []; } /** @@ -98,47 +99,53 @@ define(function (require, exports, module) { * The FileIndex used by this object. This is initialized in the constructor. */ FileSystem.prototype._index = null; - - /** - * Refcount of any pending write operations. Used to guarantee file-watcher callbacks don't - * run until after operation-specific callbacks & index fixups complete (this is important for - * distinguishing rename from an unrelated delete-add pair). - * @type {number} - */ - FileSystem.prototype._writeCount = 0; /** - * The queue of pending watch/unwatch requests. - * @type {Array.<{fn: function(), cb: function()}>} + * Refcount of any pending filesystem mutation operations (e.g., writes, + * unlinks, etc.). Used to ensure that external change events aren't processed + * until after index fixups, operation-specific callbacks, and internal change + * events are complete. (This is important for distinguishing rename from + * an unrelated delete-add pair). + * @type {number} */ - FileSystem.prototype._watchRequests = null; + FileSystem.prototype._activeChangeCount = 0; /** - * Queue of arguments to invoke _handleWatchResult() with; triggered once _writeCount drops to zero - * @type {!Array.<{path:string, stat:Object}>} + * Queue of arguments with which to invoke _handleExternalChanges(); triggered + * once _activeChangeCount drops to zero. + * @type {!Array.<{path:?string, stat:FileSystemStats=}>} */ - FileSystem.prototype._watchResults = null; + FileSystem.prototype._externalChanges = null; /** Process all queued watcher results, by calling _handleExternalChange() on each */ - FileSystem.prototype._triggerWatchCallbacksNow = function () { - this._watchResults.forEach(function (info) { + FileSystem.prototype._triggerExternalChangesNow = function () { + this._externalChanges.forEach(function (info) { this._handleExternalChange(info.path, info.stat); }, this); - this._watchResults.length = 0; + this._externalChanges.length = 0; }; /** - * Receives a result from the impl's watcher callback, and either processes it immediately (if - * _writeCount is 0) or stores it for later processing (if _writeCount > 0). + * Receives a result from the impl's watcher callback, and either processes it + * immediately (if _activeChangeCount is 0) or otherwise stores it for later + * processing. + * @param {?string} path The fullPath of the changed entry + * @param {FileSystemStats=} stat An optional stat object for the changed entry */ - FileSystem.prototype._enqueueWatchResult = function (path, stat) { - this._watchResults.push({path: path, stat: stat}); - if (!this._writeCount) { - this._triggerWatchCallbacksNow(); + FileSystem.prototype._enqueueExternalChange = function (path, stat) { + this._externalChanges.push({path: path, stat: stat}); + if (!this._activeChangeCount) { + this._triggerExternalChangesNow(); } }; + /** + * The queue of pending watch/unwatch requests. + * @type {Array.<{fn: function(), cb: function()}>} + */ + FileSystem.prototype._watchRequests = null; + /** * Dequeue and process all pending watch/unwatch requests */ @@ -250,16 +257,9 @@ define(function (require, exports, module) { watchOrUnwatch = this._impl[commandName].bind(this, entry.fullPath), visitor; - var genericProcessChild; - if (shouldWatch) { - genericProcessChild = function (child) { - child._setWatched(true); - }; - } else { - genericProcessChild = function (child) { - child._setWatched(false); - }; - } + var genericProcessChild = function (child) { + child._setWatched(shouldWatch); + }; var genericVisitor = function (processChild, child) { if (watchedRoot.filter(child.name, child.parentPath)) { @@ -315,6 +315,7 @@ define(function (require, exports, module) { }; this._enqueueWatchRequest(function (callback) { + genericProcessChild(entry); visitor = genericVisitor.bind(this, processChild); entry.visit(visitor, callback); }.bind(this), callback); @@ -358,7 +359,7 @@ define(function (require, exports, module) { FileSystem.prototype.init = function (impl) { console.assert(!this._impl, "This FileSystem has already been initialized!"); - var changeCallback = this._enqueueWatchResult.bind(this), + var changeCallback = this._enqueueExternalChange.bind(this), offlineCallback = this._unwatchAll.bind(this); this._impl = impl; @@ -398,20 +399,20 @@ define(function (require, exports, module) { }; FileSystem.prototype._beginWrite = function () { - this._writeCount++; - //console.log("> beginWrite -> " + this._writeCount); + this._activeChangeCount++; + //console.log("> beginWrite -> " + this._activeChangeCount); }; FileSystem.prototype._endWrite = function () { - this._writeCount--; - //console.log("< endWrite -> " + this._writeCount); + this._activeChangeCount--; + //console.log("< endWrite -> " + this._activeChangeCount); if (this._writeCount < 0) { - console.error("FileSystem _writeCount has fallen below zero!"); + console.error("FileSystem _activeChangeCount has fallen below zero!"); } - if (!this._writeCount) { - this._triggerWatchCallbacksNow(); + if (!this._activeChangeCount) { + this._triggerExternalChangesNow(); } }; @@ -615,10 +616,25 @@ define(function (require, exports, module) { this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback); }; - FileSystem.prototype._fireRenameEvent = function (oldName, newName) { - $(this).trigger("rename", [oldName, newName]); + /** + * Fire a rename event. Clients listen for these events using FileSystem.on. + * + * @param {string} oldPath The entry's previous fullPath + * @param {string} newPath The entry's current fullPath + */ + FileSystem.prototype._fireRenameEvent = function (oldPath, newPath) { + $(this).trigger("rename", [oldPath, newPath]); }; - + + /** + * Fire a change event. Clients listen for these events using FileSystem.on. + * + * @param {File|Directory} entry The entry that has changed + * @param {Array=} added If the entry is a directory, this + * is a set of new entries in the directory. + * @param {Array=} removed If the entry is a directory, this + * is a set of removed entries from the directory. + */ FileSystem.prototype._fireChangeEvent = function (entry, added, removed) { $(this).trigger("change", [entry, added, removed]); }; @@ -636,6 +652,18 @@ define(function (require, exports, module) { this._index.entryRenamed(oldName, newName, isDirectory); }; + /** + * Notify the filesystem that the given directory has changed. Updates the filesystem's + * internal state as a result of the change, and calls back with the set of added and + * removed entries. Mutating FileSystemEntry operations should call this method before + * applying the operation's callback, and pass along the resulting change sets in the + * internal change event. + * + * @param {Directory} directory The directory that has changed. + * @param {function(Array=, Array=)} callback + * The callback that will be applied to a set of added and a set of removed + * FileSystemEntry objects. + */ FileSystem.prototype._handleDirectoryChange = function (directory, callback) { var oldContents = directory._contents || []; @@ -872,7 +900,8 @@ define(function (require, exports, module) { // Private helper methods used by internal filesystem modules exports._isEntryWatched = _wrap(FileSystem.prototype._isEntryWatched); - // Export "on" and "off" methods + // For testing only + exports._activeChangeCount = _wrap(FileSystem.prototype._activeChangeCount); /** * Add an event listener for a FileSystem event. diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 78966362398..43c13b76e15 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -35,7 +35,7 @@ define(function (require, exports, module) { MockFileSystemImpl = require("./MockFileSystemImpl"); describe("FileSystem", function () { - + // Callback factories function resolveCallback() { var callback = function (err, entry) { @@ -117,9 +117,14 @@ define(function (require, exports, module) { waitsFor(function () { return cb.wasCalled; }); runs(function () { expect(cb.error).toBeFalsy(); + expect(fileSystem._activeChangeCount).toBe(0); }); }); + afterEach(function () { + expect(fileSystem._activeChangeCount).toBe(0); + }); + describe("Path normalization", function () { // Auto-prepended to both origPath & normPath in all the test helpers below var prefix = ""; From 5f16cd99b9eb5149eeb613e8cb234a9498f06223 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 17 Dec 2013 12:38:04 -0800 Subject: [PATCH 53/94] Add stub MockFileSystemImpl.recursiveWatch as a reminder to implement more sophisticated watching/change event infrastructure to the MockImpl --- test/spec/MockFileSystemImpl.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index 5feed3aa4fb..deecfbcc464 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -67,6 +67,9 @@ define(function (require, exports, module) { // Indicates whether, by default, the FS should perform UNC Path normalization var _normalizeUNCPathsDefault = false; + + // Indicates whether, by default, the FS should perform watch and unwatch recursively + var _recursiveWatchDefault = true; // "Live" data for this instance of the file system. Use reset() to // initialize with _initialData @@ -365,6 +368,7 @@ define(function (require, exports, module) { _hooks = {}; exports.normalizeUNCPaths = _normalizeUNCPathsDefault; + exports.recursiveWatch = _recursiveWatchDefault; }; /** From 9fbbc358593f6e168abd611c10aa93eabcaf069c Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 17 Dec 2013 17:17:53 -0800 Subject: [PATCH 54/94] Filter out image files in the FindInFiles search --- src/search/FindInFiles.js | 15 ++++++++++++++- test/spec/FileSystem-test.js | 34 +++++++++++++++++----------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 7bb4d22e465..50763fe04e2 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -57,6 +57,7 @@ define(function (require, exports, module) { FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), FileViewController = require("project/FileViewController"), + LanguageManager = require("language/LanguageManager"), FindReplace = require("search/FindReplace"), PerfUtils = require("utils/PerfUtils"), InMemoryFile = require("document/InMemoryFile"), @@ -673,6 +674,18 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Used to filter out image files when building a list of file in which to + * search. Ideally this would filter out ALL binary files. + * @private + * @param {FileSystemEntry} entry The entry to test + * @return {boolean} Whether or not the entry's contents should be searched + */ + function _findInFilesFilter(entry) { + var language = LanguageManager.getLanguageForPath(entry.fullPath); + return language.getId() !== "image"; + } + /** * @private * Executes the Find in Files search inside the 'currentScope' @@ -691,7 +704,7 @@ define(function (require, exports, module) { var scopeName = currentScope ? currentScope.fullPath : ProjectManager.getProjectRoot().fullPath, perfTimer = PerfUtils.markStart("FindIn: " + scopeName + " - " + query); - ProjectManager.getAllFiles(true) + ProjectManager.getAllFiles(_findInFilesFilter, true) .then(function (fileListResult) { var doSearch = _doSearchInOneFile.bind(undefined, _addSearchMatches); return Async.doInParallel(fileListResult, doSearch); diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 43c13b76e15..5d7ce85ec96 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -992,7 +992,7 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1004,7 +1004,7 @@ define(function (require, exports, module) { // confirm impl read and cached data and then read again runs(function () { expect(cb1.error).toBeFalsy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb1.stat); expect(file._contents).toBe(cb1.data); expect(file._hash).toBeTruthy(); @@ -1019,7 +1019,7 @@ define(function (require, exports, module) { expect(cb2.error).toBeFalsy(); expect(cb2.stat).toBe(cb1.stat); expect(cb2.data).toBe(cb1.data); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb2.stat); expect(file._contents).toBe(cb2.data); expect(file._hash).toBeTruthy(); @@ -1035,7 +1035,7 @@ define(function (require, exports, module) { // confirm empty cached data and then write blindly runs(function () { - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(writeCalls).toBe(0); @@ -1048,7 +1048,7 @@ define(function (require, exports, module) { runs(function () { expect(cb1.error).toBe(FileSystemError.CONTENTS_MODIFIED); expect(cb1.stat).toBeFalsy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); @@ -1062,7 +1062,7 @@ define(function (require, exports, module) { runs(function () { expect(cb2.error).toBeFalsy(); expect(cb2.stat).toBeTruthy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb2.stat); expect(file._contents).toBe(newFileContent); expect(file._hash).toBeTruthy(); @@ -1079,7 +1079,7 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1092,7 +1092,7 @@ define(function (require, exports, module) { // confirm impl read and cached data and then write runs(function () { expect(cb1.error).toBeFalsy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb1.stat); expect(file._contents).toBe(cb1.data); expect(file._hash).toBeTruthy(); @@ -1109,7 +1109,7 @@ define(function (require, exports, module) { expect(cb2.error).toBeFalsy(); expect(cb2.stat).not.toBe(cb1.stat); expect(cb2.stat).toBeTruthy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb2.stat); expect(file._contents).toBe(newFileContent); expect(file._hash).not.toBe(savedHash); @@ -1128,7 +1128,7 @@ define(function (require, exports, module) { // confirm empty cached data and then read runs(function () { - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1140,7 +1140,7 @@ define(function (require, exports, module) { // confirm impl read and cached data and then fire a synthetic change event runs(function () { expect(cb1.error).toBeFalsy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb1.stat); expect(file._contents).toBe(cb1.data); expect(file._hash).toBeTruthy(); @@ -1159,7 +1159,7 @@ define(function (require, exports, module) { // confirm now-empty cached data and then read runs(function () { - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBeFalsy(); expect(file._contents).toBeFalsy(); // contents and stat should be cleared expect(file._hash).toBe(savedHash); // but hash should not be cleared @@ -1171,7 +1171,7 @@ define(function (require, exports, module) { // confirm impl read and new cached data runs(function () { expect(cb2.error).toBeFalsy(); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._stat).toBe(cb2.stat); expect(file._contents).toBe(cb2.data); expect(file._hash).toBeTruthy(); @@ -1190,7 +1190,7 @@ define(function (require, exports, module) { runs(function () { file = fileSystem.getFileForPath(filename); - expect(file._isWatched).toBe(true); + expect(file._isWatched()).toBe(true); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); }); @@ -1207,7 +1207,7 @@ define(function (require, exports, module) { file = fileSystem.getFileForPath(filename); - expect(file._isWatched).toBe(false); + expect(file._isWatched()).toBe(false); expect(file._contents).toBeFalsy(); expect(file._hash).toBeFalsy(); expect(readCalls).toBe(0); @@ -1219,7 +1219,7 @@ define(function (require, exports, module) { // confirm impl read, empty cached data and then read again runs(function () { expect(cb1.error).toBeFalsy(); - expect(file._isWatched).toBe(false); + expect(file._isWatched()).toBe(false); expect(file._contents).toBeFalsy(); expect(file._hash).toBeTruthy(); expect(readCalls).toBe(1); @@ -1233,7 +1233,7 @@ define(function (require, exports, module) { // confirm impl read and empty cached data runs(function () { expect(cb2.error).toBeFalsy(); - expect(file._isWatched).toBe(false); + expect(file._isWatched()).toBe(false); expect(file._contents).toBeFalsy(); expect(file._hash).toBe(savedHash); expect(readCalls).toBe(2); From 464934186c438e5357d40aad2237e17472490998 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 17 Dec 2013 17:27:31 -0800 Subject: [PATCH 55/94] Replace the _isWatched flag with an _isWatched() method --- src/filesystem/Directory.js | 12 +- src/filesystem/File.js | 22 ++-- src/filesystem/FileSystem.js | 184 +++++++++++++----------------- src/filesystem/FileSystemEntry.js | 79 +++++++++---- 4 files changed, 148 insertions(+), 149 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index a442f350afe..c43a6762dcc 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -129,7 +129,7 @@ define(function (require, exports, module) { } // Return cached contents if the directory is watched - if (this._contents && this._isWatched) { + if (this._contents && this._isWatched()) { callback(null, this._contents, this._contentsStats, this._contentsStatsErrors); return; } @@ -162,12 +162,6 @@ define(function (require, exports, module) { // entryStats is a FileSystemStats object if (entryStats.isFile) { entry = this._fileSystem.getFileForPath(entryPath); - - // If file already existed, its cache may now be invalid (a change - // to file content may be messaged EITHER as a watcher change - // directly on that file, OR as a watcher change to its parent dir) - // TODO: move this to FileSystem._handleWatchResult()? - entry._clearCachedData(); } else { entry = this._fileSystem.getDirectoryForPath(entryPath); } @@ -208,7 +202,7 @@ define(function (require, exports, module) { callback = callback || function () {}; // Block external change events until after the write has finished - this._fileSystem._beginWrite(); + this._fileSystem._beginChange(); this._impl.mkdir(this._path, function (err, stat) { if (err) { @@ -232,7 +226,7 @@ define(function (require, exports, module) { } finally { this._fileSystem._fireChangeEvent(parent, added, removed); // Unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } }.bind(this)); }.bind(this)); diff --git a/src/filesystem/File.js b/src/filesystem/File.js index b4b6ec6b2bf..a4bf964a3ec 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -71,7 +71,7 @@ define(function (require, exports, module) { */ File.prototype._clearCachedData = function () { FileSystemEntry.prototype._clearCachedData.apply(this); - this._contents = undefined; + this._contents = null; }; /** @@ -87,9 +87,11 @@ define(function (require, exports, module) { options = {}; } - // We don't need to check isWatched here because contents are only saved - // for watched files - if (this._contents && this._stat) { + // We don't need to check isWatched() here because contents are only saved + // for watched files. Note that we need to explicitly test this._contents + // for a default value; otherwise it could be the empty string, which is + // falsey. + if (this._contents !== null && this._stat) { callback(null, this._contents, this._stat); return; } @@ -105,7 +107,7 @@ define(function (require, exports, module) { this._hash = stat._hash; // Only cache the contents of watched files - if (this._isWatched) { + if (this._isWatched()) { this._contents = data; } @@ -130,13 +132,13 @@ define(function (require, exports, module) { callback = callback || function () {}; // Request a consistency check if the file is watched and the write is not blind - var watched = this._isWatched; + var watched = this._isWatched(); if (watched && !options.blind) { options.hash = this._hash; } // Block external change events until after the write has finished - this._fileSystem._beginWrite(); + this._fileSystem._beginChange(); this._impl.writeFile(this._path, data, options, function (err, stat, created) { if (err) { @@ -146,7 +148,7 @@ define(function (require, exports, module) { return; } finally { // Always unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } } @@ -170,7 +172,7 @@ define(function (require, exports, module) { this._fileSystem._fireChangeEvent(parent, added, removed); // Always unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } }.bind(this)); } else { @@ -182,7 +184,7 @@ define(function (require, exports, module) { this._fileSystem._fireChangeEvent(this); // Always unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } } }.bind(this)); diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 9180d31da42..8eb4f37dd22 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -214,22 +214,6 @@ define(function (require, exports, module) { return watchedRoot; }; - /** - * Indicates whether the given FileSystemEntry is watched. - * - * @param {FileSystemEntry} entry A FileSystemEntry that may or may not be watched. - * @return {boolean} True iff the path is watched. - */ - FileSystem.prototype._isEntryWatched = function (entry) { - var watchedRoot = this._findWatchedRootForPath(entry.fullPath); - - if (watchedRoot && watchedRoot.active) { - return watchedRoot.filter(entry.name, entry.parentPath); - } - - return false; - }; - /** * Helper function to watch or unwatch a filesystem entry beneath a given * watchedRoot. @@ -244,80 +228,63 @@ define(function (require, exports, module) { * or unwatched (false). */ FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { - var recursiveWatch = this._impl.recursiveWatch; - - if (recursiveWatch && entry !== watchedRoot.entry) { - // Watch and unwatch calls to children of the watched root are - // no-ops if the impl supports recursiveWatch - callback(null); - return; - } - - var commandName = shouldWatch ? "watchPath" : "unwatchPath", - watchOrUnwatch = this._impl[commandName].bind(this, entry.fullPath), - visitor; - - var genericProcessChild = function (child) { - child._setWatched(shouldWatch); - }; - - var genericVisitor = function (processChild, child) { - if (watchedRoot.filter(child.name, child.parentPath)) { - processChild.call(this, child); - - return true; - } - return false; - }; + var recursiveWatch = this._impl.recursiveWatch, + commandName = shouldWatch ? "watchPath" : "unwatchPath", + watchOrUnwatch = this._impl[commandName].bind(this); if (recursiveWatch) { - // The impl will handle finding all subdirectories to watch. Here we - // just need to find all entries in order to either mark them as - // watched or to remove them from the index. - this._enqueueWatchRequest(function (callback) { - watchOrUnwatch(function (err) { + if (entry !== watchedRoot.entry) { + // Watch and unwatch calls to children of the watched root are + // no-ops if the impl supports recursiveWatch + callback(null); + } else { + // The impl will handle finding all subdirectories to watch. Here we + // just need to find all entries in order to either mark them as + // watched or to remove them from the index. + this._enqueueWatchRequest(function (requestCb) { + watchOrUnwatch(entry.fullPath, requestCb); + }.bind(this), callback); + } + } else { + // The impl can't handle recursive watch requests, so it's up to the + // filesystem to recursively watch or unwatch all subdirectories. + this._enqueueWatchRequest(function (requestCb) { + // First construct a list of entries to watch or unwatch + var entriesToWatchOrUnwatch = []; + + var visitor = function (child) { + if (watchedRoot.filter(child.name, child.parentPath)) { + if (child.isDirectory || child === watchedRoot.entry) { + entriesToWatchOrUnwatch.push(child); + } + return true; + } + return false; + }; + + entry.visit(visitor, function (err) { if (err) { - console.warn("Watch error: ", entry.fullPath, err); - callback(err); + requestCb(err); return; } - visitor = genericVisitor.bind(this, genericProcessChild); - entry.visit(visitor, callback); - }.bind(this)); - }.bind(this), callback); - } else { - // The impl can't handle recursive watch requests, so it's up to the - // filesystem to recursively watch or unwatch all subdirectories, as - // well as either marking all children as watched or removing them - // from the index. - var processChild = function (child) { - if (child.isDirectory || child === watchedRoot.entry) { - watchOrUnwatch(function (err) { - if (err) { - console.warn("Watch error: ", child.fullPath, err); - return; - } - - if (child.isDirectory) { - child.getContents(function (err, contents) { - if (err) { - return; - } - - contents.forEach(function (child) { - genericProcessChild.call(this, child); - }, this); - }.bind(this)); + // Then watch or unwatched all these entries + var count = entriesToWatchOrUnwatch.length; + if (count === 0) { + requestCb(null); + return; + } + + var watchOrUnwatchCallback = function () { + if (--count === 0) { + requestCb(null); } - }.bind(this)); - } - }; - - this._enqueueWatchRequest(function (callback) { - genericProcessChild(entry); - visitor = genericVisitor.bind(this, processChild); - entry.visit(visitor, callback); + }; + + entriesToWatchOrUnwatch.forEach(function (entry) { + watchOrUnwatch(entry.fullPath, watchOrUnwatchCallback); + }); + }.bind(this)); }.bind(this), callback); } }; @@ -398,12 +365,12 @@ define(function (require, exports, module) { return true; }; - FileSystem.prototype._beginWrite = function () { + FileSystem.prototype._beginChange = function () { this._activeChangeCount++; //console.log("> beginWrite -> " + this._activeChangeCount); }; - FileSystem.prototype._endWrite = function () { + FileSystem.prototype._endChange = function () { this._activeChangeCount--; //console.log("< endWrite -> " + this._activeChangeCount); @@ -550,25 +517,33 @@ define(function (require, exports, module) { item = this._index.getEntry(normalizedPath); } - if (item && item._stat && item._isWatched) { - callback(null, item, item._stat); - return; + if (item) { + item.stat(function (err, stat) { + if (err) { + callback(err); + return; + } + + callback(null, item, stat); + }); + } else { + this._impl.stat(path, function (err, stat) { + if (err) { + callback(err); + return; + } + + if (stat.isFile) { + item = this.getFileForPath(path); + } else { + item = this.getDirectoryForPath(path); + } + + item._stat = stat; + + callback(null, item, stat); + }.bind(this)); } - - this._impl.stat(path, function (err, stat) { - if (err) { - callback(err); - return; - } - - if (stat.isFile) { - item = this.getFileForPath(path); - } else { - item = this.getDirectoryForPath(path); - } - - callback(null, item, stat); - }.bind(this)); }; /** @@ -896,9 +871,6 @@ define(function (require, exports, module) { // Static public utility methods exports.isAbsolutePath = FileSystem.isAbsolutePath; - - // Private helper methods used by internal filesystem modules - exports._isEntryWatched = _wrap(FileSystem.prototype._isEntryWatched); // For testing only exports._activeChangeCount = _wrap(FileSystem.prototype._activeChangeCount); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index fe31f74527a..4da985da973 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -89,7 +89,6 @@ define(function (require, exports, module) { this._setPath(path); this._fileSystem = fileSystem; this._id = nextId++; - this._setWatched(fileSystem._isEntryWatched(this)); } // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters @@ -167,10 +166,50 @@ define(function (require, exports, module) { FileSystemEntry.prototype._isDirectory = false; /** - * Whether or not the entry is watched. + * Cached copy of this entry's watched root + * @type {WatchedRoot} + */ + FileSystemEntry.prototype._watchedRoot = null; + + /** + * Cached result of _watchedRoot.filter(this.name, this.parentPath). * @type {boolean} */ - FileSystemEntry.prototype._isWatched = false; + FileSystemEntry.prototype._watchedRootFilterResult = false; + + /** + * Determines whether or not the entry is watched. + * @return {boolean} + */ + FileSystemEntry.prototype._isWatched = function () { + var watchedRoot = this._watchedRoot, + filterResult = this._watchedRootFilterResult; + + if (!watchedRoot) { + watchedRoot = this._fileSystem._findWatchedRootForPath(this._path); + + if (watchedRoot) { + this._watchedRoot = watchedRoot; + filterResult = watchedRoot.filter(this._name, this._parentPath); + this._watchedRootFilterResult = filterResult; + } + } + + if (watchedRoot) { + if (watchedRoot.active) { + if (!filterResult) { + console.warn("Not watched (inactive): ", this._path); + } + return filterResult; + } else { + // We had a watched root, but it's no longer active, so it must now be invalid. + this._watchedRoot = undefined; + this._watchedRootFilterResult = false; + } + } + console.warn("Not watched (root): ", this._path); + return false; + }; /** * Update the path for this entry @@ -192,21 +231,13 @@ define(function (require, exports, module) { this._parentPath = null; } - this._path = newPath; - }; - - /** - * Mark this entry as being watched or unwatched. Setting an entry as being - * unwatched will cause its cached data to be cleared. - * @private - * @param watched Whether or not the entry is watched - */ - FileSystemEntry.prototype._setWatched = function (watched) { - this._isWatched = watched; - - if (!watched) { - this._clearCachedData(); + // Update watchedRootFilterResult + var watchedRoot = this._watchedRoot; + if (watchedRoot) { + this._watchedRootFilterResult = watchedRoot.filter(this._name, this._parentPath); } + + this._path = newPath; }; /** @@ -259,7 +290,7 @@ define(function (require, exports, module) { * FileSystemError string or FileSystemStats object. */ FileSystemEntry.prototype.stat = function (callback) { - if (this._stat && this._isWatched) { + if (this._stat && this._isWatched()) { callback(null, this._stat); return; } @@ -288,7 +319,7 @@ define(function (require, exports, module) { callback = callback || function () {}; // Block external change events until after the write has finished - this._fileSystem._beginWrite(); + this._fileSystem._beginChange(); this._impl.rename(this._path, newFullPath, function (err) { try { @@ -310,7 +341,7 @@ define(function (require, exports, module) { } } finally { // Unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } }.bind(this)); }; @@ -326,7 +357,7 @@ define(function (require, exports, module) { callback = callback || function () {}; // Block external change events until after the write has finished - this._fileSystem._beginWrite(); + this._fileSystem._beginChange(); this._clearCachedData(); this._impl.unlink(this._path, function (err) { @@ -342,7 +373,7 @@ define(function (require, exports, module) { this._fileSystem._fireChangeEvent(parent, added, removed); // Unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } }.bind(this)); }.bind(this)); @@ -364,7 +395,7 @@ define(function (require, exports, module) { callback = callback || function () {}; // Block external change events until after the write has finished - this._fileSystem._beginWrite(); + this._fileSystem._beginChange(); this._clearCachedData(); this._impl.moveToTrash(this._path, function (err) { @@ -380,7 +411,7 @@ define(function (require, exports, module) { this._fileSystem._fireChangeEvent(parent, added, removed); // Unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } }.bind(this)); }.bind(this)); From afaed1fb4183948aadc65b67230b351315e1839d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 17 Dec 2013 22:46:33 -0800 Subject: [PATCH 56/94] Avoid redundant stat calls during readFile when cached stats are available --- src/filesystem/File.js | 7 +++++- .../impls/appshell/AppshellFileSystem.js | 25 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index a4bf964a3ec..01ed7f7bde0 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -96,6 +96,11 @@ define(function (require, exports, module) { return; } + var isWatched = this._isWatched(); + if (isWatched) { + options.stat = this._stat; + } + this._impl.readFile(this._path, options, function (err, data, stat) { if (err) { this._clearCachedData(); @@ -107,7 +112,7 @@ define(function (require, exports, module) { this._hash = stat._hash; // Only cache the contents of watched files - if (this._isWatched()) { + if (isWatched) { this._contents = data; } diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index bc8f5bd21a8..e7cde5f32ce 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -406,6 +406,21 @@ define(function (require, exports, module) { // read call completes first with an error; otherwise wait for both // to finish. var done = false, data, stat, err; + + if (options.stat) { + done = true; + stat = options.stat; + } else { + exports.stat(path, function (_err, _stat) { + if (done) { + callback(_err, _err ? null : data, _stat); + } else { + done = true; + stat = _stat; + err = _err; + } + }); + } appshell.fs.readFile(path, encoding, function (_err, _data) { if (_err) { @@ -420,16 +435,6 @@ define(function (require, exports, module) { data = _data; } }); - - exports.stat(path, function (_err, _stat) { - if (done) { - callback(_err, _err ? null : data, _stat); - } else { - done = true; - stat = _stat; - err = _err; - } - }); } /** From 228aa0b9da41d31d62ac2beb0a8df8e2d7912693 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 09:54:51 -0800 Subject: [PATCH 57/94] Only store cached data for watched files; always return cached data when it exists --- src/filesystem/Directory.js | 28 ++++++++++++++++++---------- src/filesystem/File.js | 21 +++++++++------------ src/filesystem/FileSystem.js | 4 +++- src/filesystem/FileSystemEntry.js | 11 +++++------ 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index c43a6762dcc..c4067749add 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -129,7 +129,7 @@ define(function (require, exports, module) { } // Return cached contents if the directory is watched - if (this._contents && this._isWatched()) { + if (this._contents) { callback(null, this._contents, this._contentsStats, this._contentsStatsErrors); return; } @@ -144,12 +144,14 @@ define(function (require, exports, module) { if (err) { this._clearCachedData(); } else { + var watched = this._isWatched(); + entries.forEach(function (name, index) { - var entryPath = this.fullPath + name, - entry; + var entryPath = this.fullPath + name; if (this._fileSystem._indexFilter(entryPath, name)) { - var entryStats = stats[index]; + var entryStats = stats[index], + entry; // Note: not all entries necessarily have associated stats. if (typeof entryStats === "string") { @@ -166,18 +168,21 @@ define(function (require, exports, module) { entry = this._fileSystem.getDirectoryForPath(entryPath); } - entry._stat = entryStats; + if (watched) { + entry._stat = entryStats; + } contents.push(entry); contentsStats.push(entryStats); } - } }, this); - this._contents = contents; - this._contentsStats = contentsStats; - this._contentsStatsErrors = contentsStatsErrors; + if (watched) { + this._contents = contents; + this._contentsStats = contentsStats; + this._contentsStatsErrors = contentsStatsErrors; + } } // Reset the callback list before we begin calling back so that @@ -219,7 +224,10 @@ define(function (require, exports, module) { var parent = this._fileSystem.getDirectoryForPath(this.parentPath); // Update internal filesystem state - this._stat = stat; + if (this._isWatched()) { + this._stat = stat; + } + this._fileSystem._handleDirectoryChange(parent, function (added, removed) { try { callback(null, stat); diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 01ed7f7bde0..24e4f618ec3 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -96,8 +96,8 @@ define(function (require, exports, module) { return; } - var isWatched = this._isWatched(); - if (isWatched) { + var watched = this._isWatched(); + if (watched) { options.stat = this._stat; } @@ -107,12 +107,11 @@ define(function (require, exports, module) { callback(err); return; } - - this._stat = stat; - this._hash = stat._hash; - // Only cache the contents of watched files - if (isWatched) { + // Only cache data for watched files + if (watched) { + this._stat = stat; + this._hash = stat._hash; this._contents = data; } @@ -157,12 +156,10 @@ define(function (require, exports, module) { } } - // Update internal filesystem state - this._hash = stat._hash; - this._stat = stat; - - // Only cache the contents of watched files + // Only cache data for watched files if (watched) { + this._stat = stat; + this._hash = stat._hash; this._contents = data; } diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 8eb4f37dd22..9d6cf3672c2 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -539,7 +539,9 @@ define(function (require, exports, module) { item = this.getDirectoryForPath(path); } - item._stat = stat; + if (item._isWatched()) { + item._stat = stat; + } callback(null, item, stat); }.bind(this)); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 4da985da973..ff47c2b9d52 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -197,17 +197,14 @@ define(function (require, exports, module) { if (watchedRoot) { if (watchedRoot.active) { - if (!filterResult) { - console.warn("Not watched (inactive): ", this._path); - } return filterResult; } else { // We had a watched root, but it's no longer active, so it must now be invalid. this._watchedRoot = undefined; this._watchedRootFilterResult = false; + this._clearCachedData(); } } - console.warn("Not watched (root): ", this._path); return false; }; @@ -290,7 +287,7 @@ define(function (require, exports, module) { * FileSystemError string or FileSystemStats object. */ FileSystemEntry.prototype.stat = function (callback) { - if (this._stat && this._isWatched()) { + if (this._stat) { callback(null, this._stat); return; } @@ -302,7 +299,9 @@ define(function (require, exports, module) { return; } - this._stat = stat; + if (this._isWatched()) { + this._stat = stat; + } callback(null, stat); }.bind(this)); From b844177779a47197b8caea51bc84305535c84863 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 10:08:48 -0800 Subject: [PATCH 58/94] Always save the file hash, even when not watched. --- src/filesystem/File.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 24e4f618ec3..79191b461d1 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -108,10 +108,12 @@ define(function (require, exports, module) { return; } + // Always store the hash + this._hash = stat._hash; + // Only cache data for watched files if (watched) { this._stat = stat; - this._hash = stat._hash; this._contents = data; } @@ -156,10 +158,12 @@ define(function (require, exports, module) { } } + // Always store the hash + this._hash = stat._hash; + // Only cache data for watched files if (watched) { this._stat = stat; - this._hash = stat._hash; this._contents = data; } From e91be351854227949bb69937128b179e018fe770 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 10:09:04 -0800 Subject: [PATCH 59/94] Show the fullPath in the contents modified error dialog --- src/document/DocumentCommandHandlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index b116a51e439..bf0d5368968 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -548,7 +548,7 @@ define(function (require, exports, module) { Strings.EXT_MODIFIED_TITLE, StringUtils.format( Strings.EXT_MODIFIED_WARNING, - StringUtils.breakableUrl(docToSave.file.name) + StringUtils.breakableUrl(docToSave.file.fullPath) ), [ { From f4fd368963c9e5e3f0716acc516175735d55601d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 10:12:00 -0800 Subject: [PATCH 60/94] Fix a typo and remove a superfluous console.info statement --- src/filesystem/FileSystem.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 9d6cf3672c2..955ff537e68 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -367,14 +367,14 @@ define(function (require, exports, module) { FileSystem.prototype._beginChange = function () { this._activeChangeCount++; - //console.log("> beginWrite -> " + this._activeChangeCount); + //console.log("> beginChange -> " + this._activeChangeCount); }; FileSystem.prototype._endChange = function () { this._activeChangeCount--; - //console.log("< endWrite -> " + this._activeChangeCount); + //console.log("< endChange -> " + this._activeChangeCount); - if (this._writeCount < 0) { + if (this._activeChangeCount < 0) { console.error("FileSystem _activeChangeCount has fallen below zero!"); } @@ -724,8 +724,6 @@ define(function (require, exports, module) { entry._clearCachedData(); entry._stat = stat; this._fireChangeEvent(entry); - } else { - console.info("Detected duplicate file change event: ", path); } } else { this._handleDirectoryChange(entry, function (added, removed) { From 8634380479724740e52fc6571fe76c945ddd7f25 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 11:13:29 -0800 Subject: [PATCH 61/94] Make the options parameter to readFile and writeFile mandatory --- .../impls/appshell/AppshellFileSystem.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index e7cde5f32ce..0f12cb0d58c 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -388,15 +388,15 @@ define(function (require, exports, module) { /** * Read the contents of the file at the given path, calling back * asynchronously with either a FileSystemError string, or with the data and - * the FileSystemStats object associated with the read file. The (optional) - * options parameter can be used to specify an encoding (default "utf8"). + * the FileSystemStats object associated with the read file. The options + * parameter can be used to specify an encoding (default "utf8"). * * Note: if either the read or the stat call fails then neither the read data * nor stat will be passed back, and the call should be considered to have failed. * If both calls fail, the error from the read call is passed back. * * @param {string} path - * @param {{encoding : string=}=} options + * @param {{encoding : string=}} options * @param {function(?string, string=, FileSystemStats=)} callback */ function readFile(path, options, callback) { @@ -441,16 +441,16 @@ define(function (require, exports, module) { * Write data to the file at the given path, calling back asynchronously with * either a FileSystemError string or the FileSystemStats object associated * with the written file. If no file exists at the given path, a new file will - * be created. The (optional) options parameter can be used to specify an - * encoding (default "utf8"), an octal mode (default unspecified and - * implementation dependent), and a consistency hash, which is used to the - * current state of the file before overwriting it. If a consistency hash is - * provided but does not match the hash of the file on disk, a + * be created. The options parameter can be used to specify an encoding + * (default "utf8"), an octal mode (default unspecified and implementation + * dependent), and a consistency hash, which is used to the current state + * of the file before overwriting it. If a consistency hash is provided but + * does not match the hash of the file on disk, a * FileSystemError.CONTENTS_MODIFIED error is passed to the callback. * * @param {string} path * @param {string} data - * @param {{encoding : string=, mode : number=, hash : string=}=} options + * @param {{encoding : string=, mode : number=, hash : string=}} options * @param {function(?string, FileSystemStats=)} callback */ function writeFile(path, data, options, callback) { From 4132060e8dcccede7a78fcbe49409b130a072f68 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 11:32:27 -0800 Subject: [PATCH 62/94] Replace NodeConnection with NodeDomain --- .../impls/appshell/AppshellFileSystem.js | 88 +++---------------- 1 file changed, 11 insertions(+), 77 deletions(-) diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 0f12cb0d58c..deda3c2cedd 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -31,7 +31,7 @@ define(function (require, exports, module) { var FileUtils = require("file/FileUtils"), FileSystemStats = require("filesystem/FileSystemStats"), FileSystemError = require("filesystem/FileSystemError"), - NodeConnection = require("utils/NodeConnection"); + NodeDomain = require("utils/NodeDomain"); var FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes @@ -44,40 +44,10 @@ define(function (require, exports, module) { _modulePath = FileUtils.getNativeModuleDirectoryPath(module), _nodePath = "node/FileWatcherDomain", _domainPath = [_bracketsPath, _modulePath, _nodePath].join("/"), - _nodeConnection = new NodeConnection(), - _domainLoaded = false; // Whether the fileWatcher domain has been loaded + _nodeDomain = new NodeDomain("fileWatcher", _domainPath); - /** - * A promise that resolves when the NodeConnection object is connected and - * the fileWatcher domain has been loaded. - * - * @type {?jQuery.Promise} - */ - var _nodeConnectionPromise; - - /** - * Load the fileWatcher domain on the assumed-open NodeConnection object - * - * @private - */ - function _loadDomains() { - return _nodeConnection - .loadDomains(_domainPath, true) - .done(function () { - _domainLoaded = true; - _nodeConnectionPromise = null; - }); - } - - // Initialize the connection and connection promise - _nodeConnectionPromise = _nodeConnection.connect(true).then(_loadDomains); - - // Setup the close handler. Re-initializes the connection promise and - // notifies the FileSystem that watchers have gone offline. - $(_nodeConnection).on("close", function (event, promise) { - _domainLoaded = false; - _nodeConnectionPromise = promise.then(_loadDomains); - + // If the connection closes, notify the FileSystem that watchers have gone offline. + $(_nodeDomain.connection).on("close", function (event, promise) { if (_offlineCallback) { _offlineCallback(); } @@ -145,40 +115,7 @@ define(function (require, exports, module) { } // Setup the change handler. This only needs to happen once. - $(_nodeConnection).on("fileWatcher.change", _fileWatcherChange); - - /** - * Execute the named function from the fileWatcher domain when the - * NodeConnection is connected and the domain has been loaded. Additional - * parameters are passed as arguments to the command. - * - * @param {string} name The name of the command to execute - * @return {jQuery.Promise} Resolves with the results of the command. - * @private - */ - function _execWhenConnected(name) { - var params = Array.prototype.slice.call(arguments, 1); - - function execConnected() { - var domains = _nodeConnection.domains, - domain = domains && domains.fileWatcher, - fn = domain && domain[name]; - - if (fn) { - return fn.apply(domain, params); - } else { - return $.Deferred().reject().promise(); - } - } - - if (_domainLoaded && _nodeConnection.connected()) { - return execConnected(); - } else if (_nodeConnectionPromise) { - return _nodeConnectionPromise.then(execConnected); - } else { - return $.Deferred().reject().promise(); - } - } + $(_nodeDomain).on("change", _fileWatcherChange); /** * Convert appshell error codes to FileSystemError values. @@ -557,9 +494,8 @@ define(function (require, exports, module) { return; } - _execWhenConnected("watchPath", path) - .done(callback.bind(undefined, null)) - .fail(callback); + _nodeDomain.exec("watchPath", path) + .then(callback, callback); }); } @@ -578,9 +514,8 @@ define(function (require, exports, module) { return; } - _execWhenConnected("unwatchPath", path) - .done(callback.bind(undefined, null)) - .fail(callback); + _nodeDomain.exec("unwatchPath", path) + .then(callback, callback); }); } @@ -598,9 +533,8 @@ define(function (require, exports, module) { return; } - _execWhenConnected("unwatchAll") - .done(callback.bind(undefined, null)) - .fail(callback); + _nodeDomain.exec("unwatchAll") + .then(callback, callback); }); } From de6e962adf9f3880fc5c4ebb8379c7af9c86aed4 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 11:33:27 -0800 Subject: [PATCH 63/94] Type error in _unwatchAll: unwatch takes a FileSystemEntry, not a path --- src/filesystem/FileSystem.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 955ff537e68..56a8b3167d2 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -840,7 +840,13 @@ define(function (require, exports, module) { FileSystem.prototype._unwatchAll = function () { console.warn("File watchers went offline!"); - Object.keys(this._watchedRoots).forEach(this.unwatch, this); + Object.keys(this._watchedRoots).forEach(function (path) { + var entry = this._index.getEntry(path); + + if (entry) { + this.unwatch(entry); + } + }, this); // Fire a wholesale change event because all previously watched entries // have been removed from the index and should no longer be referenced From 2856bb0b6c2674db806c04c9e3bf2265d19bb612 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Wed, 18 Dec 2013 12:04:07 -0800 Subject: [PATCH 64/94] include FileWatcherDomain in build --- Gruntfile.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index fae2674010d..77bad586666 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -64,13 +64,24 @@ module.exports = function (grunt) { 'LiveDevelopment/launch.html' ] }, + /* node domains are not minified and must be copied to dist */ + { + expand: true, + dest: 'dist/', + cwd: 'src/', + src: [ + 'extensibility/node/**', + '!extensibility/node/spec/**', + 'filesystem/impls/appshell/node/**', + '!filesystem/impls/appshell/node/spec/**' + ] + }, /* extensions and CodeMirror modes */ { expand: true, dest: 'dist/', cwd: 'src/', src: [ - 'extensibility/**/*', '!extensions/default/*/unittest-files/**/*', '!extensions/default/*/unittests.js', 'extensions/default/*/**/*', From cafec669bcdea5820927c69514b07ead3172e9ed Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 15:56:25 -0800 Subject: [PATCH 65/94] Address a first round of review comments --- src/filesystem/FileSystemEntry.js | 2 +- .../impls/appshell/AppshellFileSystem.js | 24 ++++++++++--------- .../impls/appshell/node/FileWatcherDomain.js | 8 +++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index ff47c2b9d52..a4cbd3e8b5d 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -167,7 +167,7 @@ define(function (require, exports, module) { /** * Cached copy of this entry's watched root - * @type {WatchedRoot} + * @type {entry: File|Directory, filter: function(FileSystemEntry):boolean, active: boolean} */ FileSystemEntry.prototype._watchedRoot = null; diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index deda3c2cedd..0adf017aa49 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -326,14 +326,16 @@ define(function (require, exports, module) { * Read the contents of the file at the given path, calling back * asynchronously with either a FileSystemError string, or with the data and * the FileSystemStats object associated with the read file. The options - * parameter can be used to specify an encoding (default "utf8"). + * parameter can be used to specify an encoding (default "utf8"), and also + * a cached stats object that the implementation is free to use in order + * to avoid an additional stat call. * * Note: if either the read or the stat call fails then neither the read data * nor stat will be passed back, and the call should be considered to have failed. * If both calls fail, the error from the read call is passed back. * * @param {string} path - * @param {{encoding : string=}} options + * @param {{encoding: string=, stat: FileSystemStats=}} options * @param {function(?string, string=, FileSystemStats=)} callback */ function readFile(path, options, callback) { @@ -377,18 +379,19 @@ define(function (require, exports, module) { /** * Write data to the file at the given path, calling back asynchronously with * either a FileSystemError string or the FileSystemStats object associated - * with the written file. If no file exists at the given path, a new file will - * be created. The options parameter can be used to specify an encoding - * (default "utf8"), an octal mode (default unspecified and implementation - * dependent), and a consistency hash, which is used to the current state - * of the file before overwriting it. If a consistency hash is provided but - * does not match the hash of the file on disk, a - * FileSystemError.CONTENTS_MODIFIED error is passed to the callback. + * with the written file and a boolean that indicates whether the file was + * created by the write (true) or not (false). If no file exists at the + * given path, a new file will be created. The options parameter can be used + * to specify an encoding (default "utf8"), an octal mode (default + * unspecified and implementation dependent), and a consistency hash, which + * is used to the current state of the file before overwriting it. If a + * consistency hash is provided but does not match the hash of the file on + * disk, a FileSystemError.CONTENTS_MODIFIED error is passed to the callback. * * @param {string} path * @param {string} data * @param {{encoding : string=, mode : number=, hash : string=}} options - * @param {function(?string, FileSystemStats=)} callback + * @param {function(?string, FileSystemStats=, boolean)} callback */ function writeFile(path, data, options, callback) { var encoding = options.encoding || "utf8"; @@ -563,7 +566,6 @@ define(function (require, exports, module) { */ exports.recursiveWatch = appshell.platform === "mac"; - // /** * Indicates whether or not the filesystem should expect and normalize UNC * paths. If set, then //server/directory/ is a normalized path; otherwise the diff --git a/src/filesystem/impls/appshell/node/FileWatcherDomain.js b/src/filesystem/impls/appshell/node/FileWatcherDomain.js index 1576240a3eb..b718affd3c2 100644 --- a/src/filesystem/impls/appshell/node/FileWatcherDomain.js +++ b/src/filesystem/impls/appshell/node/FileWatcherDomain.js @@ -26,7 +26,8 @@ "use strict"; -var fs = require("fs"), +var fspath = require("path"), + fs = require("fs"), fsevents; if (process.platform === "darwin") { @@ -73,9 +74,8 @@ function watchPath(path) { if (fsevents) { watcher = fsevents(path); watcher.on("change", function (filename, info) { - var lastIndex = filename.lastIndexOf("/") + 1, - parent = lastIndex && filename.substring(0, lastIndex), - name = lastIndex && filename.substring(lastIndex), + var parent = filename && (fspath.dirname(filename) + "/"), + name = filename && fspath.basename(filename), type = info.event === "modified" ? "change" : "rename"; _domainManager.emitEvent("fileWatcher", "change", [parent, type, name]); From 62e307aade9a759897b76b75a0f5cf5c29f54bae Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 16:36:43 -0800 Subject: [PATCH 66/94] Fix the watchPath/unwatchPath bind inside watchOrUnwatchEntry --- src/filesystem/FileSystem.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 56a8b3167d2..515287630b5 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -228,9 +228,9 @@ define(function (require, exports, module) { * or unwatched (false). */ FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { - var recursiveWatch = this._impl.recursiveWatch, - commandName = shouldWatch ? "watchPath" : "unwatchPath", - watchOrUnwatch = this._impl[commandName].bind(this); + var impl = this._impl, + recursiveWatch = impl.recursiveWatch, + commandName = shouldWatch ? "watchPath" : "unwatchPath"; if (recursiveWatch) { if (entry !== watchedRoot.entry) { @@ -242,7 +242,7 @@ define(function (require, exports, module) { // just need to find all entries in order to either mark them as // watched or to remove them from the index. this._enqueueWatchRequest(function (requestCb) { - watchOrUnwatch(entry.fullPath, requestCb); + impl[commandName].call(impl, entry.fullPath, requestCb); }.bind(this), callback); } } else { @@ -250,7 +250,8 @@ define(function (require, exports, module) { // filesystem to recursively watch or unwatch all subdirectories. this._enqueueWatchRequest(function (requestCb) { // First construct a list of entries to watch or unwatch - var entriesToWatchOrUnwatch = []; + var entriesToWatchOrUnwatch = [], + watchOrUnwatch = impl[commandName].bind(impl); var visitor = function (child) { if (watchedRoot.filter(child.name, child.parentPath)) { @@ -282,7 +283,7 @@ define(function (require, exports, module) { }; entriesToWatchOrUnwatch.forEach(function (entry) { - watchOrUnwatch(entry.fullPath, watchOrUnwatchCallback); + watchOrUnwatch(impl, entry.fullPath, watchOrUnwatchCallback); }); }.bind(this)); }.bind(this), callback); From 49f99b2615757f2b1f2194b39b340d747b51b8cb Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 18 Dec 2013 17:04:36 -0800 Subject: [PATCH 67/94] Add documentation about our use of fsevents versus fs.watch for file watching --- .../impls/appshell/node/FileWatcherDomain.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/filesystem/impls/appshell/node/FileWatcherDomain.js b/src/filesystem/impls/appshell/node/FileWatcherDomain.js index b718affd3c2..ec22a88be0a 100644 --- a/src/filesystem/impls/appshell/node/FileWatcherDomain.js +++ b/src/filesystem/impls/appshell/node/FileWatcherDomain.js @@ -30,6 +30,31 @@ var fspath = require("path"), fs = require("fs"), fsevents; +/* + * NOTE: The fsevents package is a temporary solution for file-watching on darwin. + * Node's native fs.watch call would be preferable, but currently has a hard limit + * of 451 watched directories and fails silently after that! In the next stable + * version of Node (0.12), fs.watch on darwin will support a new "recursive" option + * that will allow us to only request a single watched path per directory structure. + * When that is stable, we should switch back to fs.watch for all platforms, and + * we should use the recursive option where available. As of January 2014, the + * current experimental Node branch (0.11) only supports the recursive option for + * darwin. + * + * In the meantime, the fsevents package makes direct use of the Mac OS fsevents + * API to provide file watching capabilities. Its behavior is also recursive + * (like fs.watch with the recursive option), but the events it emits are not + * exactly the same as those emitted by Node watchers. Consequently, we require, + * for now, dual implementations of the FileWatcher domain. + * + * ALSO NOTE: the fsevents package as installed by NPM is not suitable for + * distribution with Brackets! The problem is that the native code embedded in + * the fsevents module is compiled by default for x86-64, but the Brackets-node + * process is compiled for x86-32. Consequently, the fsevents module must be + * compiled manually, which is why it is checked into Brackets source control + * and not managed by NPM. Changing compilation from 64- to 32-bit just requires + * changing a couple of definitions in the .gyp file used to build fsevents. + */ if (process.platform === "darwin") { fsevents = require("fsevents"); } From 71d27e48dd613e4d01d43e6477509e94a0b6b762 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 19 Dec 2013 10:10:04 -0800 Subject: [PATCH 68/94] Always make sure to clear cached data after unwatch; add a test to confirm this --- src/filesystem/FileSystem.js | 12 ++++++++- test/spec/FileSystem-test.js | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 515287630b5..4719eb798e2 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -315,7 +315,17 @@ define(function (require, exports, module) { * watch is complete, possibly with a FileSystemError string. */ FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) { - this._watchOrUnwatchEntry(entry, watchedRoot, callback, false); + this._watchOrUnwatchEntry(entry, watchedRoot, function (err) { + // Make sure to clear cached data for all unwatched entries because + // entries always return cached data if it exists! + this._index.visitAll(function (child) { + if (child.fullPath.indexOf(entry.fullPath) === 0) { + child._clearCachedData(); + } + }.bind(this)); + + callback(err); + }.bind(this), false); }; /** diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 5d7ce85ec96..15584a1c80a 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1240,6 +1240,55 @@ define(function (require, exports, module) { }); }); + it("should invalidate cached data after unwatch", function () { + var file, + cb0 = readCallback(), + cb1 = errorCallback(), + cb2 = readCallback(), + savedHash; + + // confirm watched and empty cached data + runs(function () { + file = fileSystem.getFileForPath(filename); + + expect(file._isWatched()).toBe(true); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + + file.read(cb0); + }); + waitsFor(function () { return cb0.wasCalled; }); + + // confirm impl read and cached data, and then unwatch root directory + runs(function () { + expect(file._isWatched()).toBe(true); + expect(file._stat).toBeTruthy(); + expect(file._contents).toBe(cb0.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + + fileSystem.unwatch(fileSystem.getDirectoryForPath("/"), cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // read again + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(file._hash).toBeTruthy(); + + file.read(cb2); + }); + waitsFor(function () { return cb2.wasCalled; }); + + // confirm impl read and empty cached data + runs(function () { + expect(cb2.error).toBeFalsy(); + expect(cb2.data).toBe(cb0.data); + expect(file._isWatched()).toBe(false); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(2); + }); + }); }); }); }); From 8c390365a4e6c3a6056110843140f54cda98dc4e Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 19 Dec 2013 11:28:04 -0800 Subject: [PATCH 69/94] Do not remove entries from the index when watchers go offline; test that reads are not cached after going offline --- src/filesystem/FileSystem.js | 12 +++++---- test/spec/FileSystem-test.js | 46 +++++++++++++++++++++++++++++++++ test/spec/MockFileSystemImpl.js | 17 +++++++++--- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 4719eb798e2..4c99a64ac98 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -852,11 +852,13 @@ define(function (require, exports, module) { console.warn("File watchers went offline!"); Object.keys(this._watchedRoots).forEach(function (path) { - var entry = this._index.getEntry(path); - - if (entry) { - this.unwatch(entry); - } + var watchedRoot = this._watchedRoots[path]; + + watchedRoot.active = false; + delete this._watchedRoots[path]; + this._unwatchEntry(watchedRoot.entry, watchedRoot, function () { + console.warn("Watching disabled for", watchedRoot.entry.fullPath); + }); }, this); // Fire a wholesale change event because all previously watched entries diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 15584a1c80a..e651ffaf10c 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1289,6 +1289,52 @@ define(function (require, exports, module) { expect(readCalls).toBe(2); }); }); + + it("should unwatch when watchers go offline", function () { + var file, + cb0 = readCallback(), + cb1 = readCallback(), + savedHash; + + // confirm watched and empty cached data + runs(function () { + file = fileSystem.getFileForPath(filename); + + expect(file._isWatched()).toBe(true); + expect(file._contents).toBeFalsy(); + expect(file._hash).toBeFalsy(); + + file.read(cb0); + }); + waitsFor(function () { return cb0.wasCalled; }); + + // confirm impl read and cached data, and then unwatch root directory + runs(function () { + expect(file._isWatched()).toBe(true); + expect(file._stat).toBeTruthy(); + expect(file._contents).toBe(cb0.data); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(1); + + MockFileSystemImpl.goOffline(); + }); + waits(500); + + // read again + runs(function () { + file.read(cb1); + }); + waitsFor(function () { return cb1.wasCalled; }); + + // confirm impl read and empty cached data + runs(function () { + expect(cb1.error).toBeFalsy(); + expect(cb1.data).toBe(cb0.data); + expect(file._isWatched()).toBe(false); + expect(file._hash).toBeTruthy(); + expect(readCalls).toBe(2); + }); + }); }); }); }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index deecfbcc464..ea11b4cacd9 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -30,9 +30,12 @@ define(function (require, exports, module) { var FileSystemError = require("filesystem/FileSystemError"), FileSystemStats = require("filesystem/FileSystemStats"); - // Watcher callback function + // Watcher change callback function var _watcherCallback; + // Watcher offline callback function + var _offlineCallback; + // Initial file system data. var _initialData = { "/": { @@ -326,8 +329,9 @@ define(function (require, exports, module) { } } - function initWatchers(callback) { - _watcherCallback = callback; + function initWatchers(changeCallback, offlineCallback) { + _watcherCallback = changeCallback; + _offlineCallback = offlineCallback; } function watchPath(path, callback) { @@ -371,6 +375,13 @@ define(function (require, exports, module) { exports.recursiveWatch = _recursiveWatchDefault; }; + // Simulate file watchers going offline + exports.goOffline = function () { + if (_offlineCallback) { + _offlineCallback(); + } + }; + /** * Add a callback and notification hooks to be used when specific * methods are called with a specific path. From 52cabdd3b64974e97793f34725cbc68e6c83bac8 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 19 Dec 2013 11:40:11 -0800 Subject: [PATCH 70/94] More fixes from the review: remove extra argument to watchOrUnwatch, remove superfluous binds, fix endWrite typo --- src/filesystem/Directory.js | 2 +- src/filesystem/FileSystem.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index c4067749add..a85707c8a4d 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -217,7 +217,7 @@ define(function (require, exports, module) { return; } finally { // Unblock external change events - this._fileSystem._endWrite(); + this._fileSystem._endChange(); } } diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 4c99a64ac98..7604e6c880f 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -283,10 +283,10 @@ define(function (require, exports, module) { }; entriesToWatchOrUnwatch.forEach(function (entry) { - watchOrUnwatch(impl, entry.fullPath, watchOrUnwatchCallback); + watchOrUnwatch(entry.fullPath, watchOrUnwatchCallback); }); - }.bind(this)); - }.bind(this), callback); + }); + }, callback); } }; From 896188f7eb4ba05bb46a1dc2a01ffee4c4d72a34 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 19 Dec 2013 16:38:46 -0800 Subject: [PATCH 71/94] Factor out a MockFileSystemModel from the MockFileSystemImpl to make it possible to test external change events (as distinct from internal change events fired from within the FileSystem); remove some redundant dualWrite unit tests that were written with the assumption that the impl itself was firing all change events --- test/spec/FileSystem-test.js | 107 +++++------- test/spec/MockFileSystemImpl.js | 285 ++++++++----------------------- test/spec/MockFileSystemModel.js | 218 +++++++++++++++++++++++ 3 files changed, 329 insertions(+), 281 deletions(-) create mode 100644 test/spec/MockFileSystemModel.js diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index e651ffaf10c..462c273e92e 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -457,7 +457,7 @@ define(function (require, exports, module) { }; } - MockFileSystemImpl.when("readdir", "/subdir/", {callback: delayedCallback}); + MockFileSystemImpl.when("readdir", "/subdir/", delayedCallback); // Fire off 2 getContents() calls in rapid succession runs(function () { @@ -738,7 +738,7 @@ define(function (require, exports, module) { function addSymbolicLink(dir, name, target) { // Add the symbolic link to the base directory - MockFileSystemImpl.when("readdir", dir, { callback: function (cb) { + MockFileSystemImpl.when("readdir", dir, function (cb) { return function (err, contents, contentsStats, contentsStatsErrors) { contents.push("/" + name); contentsStats.push(new FileSystemStats({ @@ -749,14 +749,14 @@ define(function (require, exports, module) { cb(err, contents, contentsStats, contentsStatsErrors); }; - }}); + }); // use the target's contents when listing the contents of the link - MockFileSystemImpl.when("readdir", dir + name + "/", { callback: function (cb) { + MockFileSystemImpl.when("readdir", dir + name + "/", function (cb) { return function (err, contents, contentsStats, contentsStatsErrors) { MockFileSystemImpl.readdir(target, cb); }; - }}); + }); // clear cached data for the base directory so readdir will be called fileSystem.getDirectoryForPath(dir)._clearCachedData(); @@ -802,9 +802,7 @@ define(function (require, exports, module) { var opDone = false, eventDone = false; // Delay impl callback to happen after impl watcher notification - MockFileSystemImpl.when(implOpName, entry.fullPath, { - callback: delay(250) - }); + MockFileSystemImpl.when(implOpName, entry.fullPath, delay(250)); $(fileSystem).on(eventName, function (evt, entry) { expect(opDone).toBe(true); // this is the important check: callback should have already run! @@ -865,7 +863,7 @@ define(function (require, exports, module) { }); // Used for various tests below where two write operations (to two different files) overlap in various ways - function dualWrite(cb1Delay, watcher1Delay, cb2Delay, watcher2Delay) { + function dualWrite(cb1Delay, cb2Delay) { var testFile1 = fileSystem.getFileForPath("/file1.txt"), testFile2 = fileSystem.getFileForPath("/file2.txt"); @@ -874,14 +872,8 @@ define(function (require, exports, module) { var write2Done = false, change2Done = false; // Delay impl callback to happen after impl watcher notification - MockFileSystemImpl.when("writeFile", "/file1.txt", { - callback: delay(cb1Delay), - notify: delay(watcher1Delay) - }); - MockFileSystemImpl.when("writeFile", "/file2.txt", { - callback: delay(cb2Delay), - notify: delay(watcher2Delay) - }); + MockFileSystemImpl.when("writeFile", "/file1.txt", delay(cb1Delay)); + MockFileSystemImpl.when("writeFile", "/file2.txt", delay(cb2Delay)); $(fileSystem).on("change", function (evt, entry) { // change for file N should not precede write callback for write to N @@ -890,8 +882,10 @@ define(function (require, exports, module) { expect(entry.fullPath === "/file1.txt" || entry.fullPath === "/file2.txt").toBe(true); if (entry.fullPath === "/file1.txt") { + expect(change1Done).toBe(false); // we do NOT expect to receive duplicate change events change1Done = true; } else { + expect(change2Done).toBe(false); change2Done = true; } }); @@ -910,48 +904,27 @@ define(function (require, exports, module) { waitsFor(function () { return change1Done && write1Done && change2Done && write2Done; }); }); } - - it("should handle overlapping writes to different files - 2nd file finishes much faster", function () { - dualWrite(100, 200, 0, 0); - }); - it("should handle overlapping writes to different files - 2nd file finishes much faster, 1st file watcher runs early", function () { - dualWrite(200, 100, 0, 0); - }); - it("should handle overlapping writes to different files - 1st file finishes much faster", function () { - dualWrite(0, 0, 100, 200); - }); - it("should handle overlapping writes to different files - 1st file finishes much faster, 2nd file watcher runs early", function () { - dualWrite(0, 0, 200, 100); - }); - it("should handle overlapping writes to different files - both watchers run early", function () { - dualWrite(100, 0, 200, 0); - }); - it("should handle overlapping writes to different files - both watchers run early, reversed", function () { - dualWrite(100, 50, 200, 0); - }); - it("should handle overlapping writes to different files - 2nd file finishes faster, both watchers run early", function () { - dualWrite(200, 0, 100, 0); - }); - it("should handle overlapping writes to different files - 2nd file finishes faster, both watchers run early, reversed", function () { - dualWrite(200, 50, 100, 0); + + it("should handle overlapping writes to different files", function () { + dualWrite(0, 0); }); - it("should handle overlapping writes to different files - watchers run in order", function () { - dualWrite(0, 100, 0, 200); + it("should handle overlapping writes to different files - 2nd file finishes faster", function () { + dualWrite(100, 0); }); - it("should handle overlapping writes to different files - watchers reversed", function () { - dualWrite(0, 200, 0, 100); + it("should handle overlapping writes to different files - 2nd file finishes much faster", function () { + dualWrite(200, 0); }); - it("should handle overlapping writes to different files - nonoverlapping in order", function () { - dualWrite(0, 50, 100, 200); + it("should handle overlapping writes to different files - 1st file finishes faster", function () { + dualWrite(0, 100); }); - it("should handle overlapping writes to different files - nonoverlapping reversed", function () { - dualWrite(100, 200, 0, 50); + it("should handle overlapping writes to different files - 1st file finishes much faster", function () { + dualWrite(0, 200); }); - it("should handle overlapping writes to different files - overlapped in order", function () { - dualWrite(0, 100, 50, 200); + it("should handle overlapping writes to different files - 1st file finishes less slowly", function () { + dualWrite(100, 200); }); - it("should handle overlapping writes to different files - overlapped reversed", function () { - dualWrite(50, 200, 0, 100); + it("should handle overlapping writes to different files - 2nd file finishes less slowly", function () { + dualWrite(200, 100); }); }); @@ -964,24 +937,20 @@ define(function (require, exports, module) { readCalls = 0; writeCalls = 0; - MockFileSystemImpl.when("readFile", filename, { - callback: function (cb) { - return function () { - var args = arguments; - readCalls++; - cb.apply(undefined, args); - }; - } + MockFileSystemImpl.when("readFile", filename, function (cb) { + return function () { + var args = arguments; + readCalls++; + cb.apply(undefined, args); + }; }); - MockFileSystemImpl.when("writeFile", filename, { - callback: function (cb) { - return function () { - var args = arguments; - writeCalls++; - cb.apply(undefined, args); - }; - } + MockFileSystemImpl.when("writeFile", filename, function (cb) { + return function () { + var args = arguments; + writeCalls++; + cb.apply(undefined, args); + }; }); }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index ea11b4cacd9..6fea0910c68 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -28,56 +28,24 @@ define(function (require, exports, module) { "use strict"; var FileSystemError = require("filesystem/FileSystemError"), - FileSystemStats = require("filesystem/FileSystemStats"); + FileSystemStats = require("filesystem/FileSystemStats"), + MockFileSystemModel = require("./MockFileSystemModel"); + + // A sychronous model of a file system + var _model; // Watcher change callback function - var _watcherCallback; + var _changeCallback; // Watcher offline callback function var _offlineCallback; - // Initial file system data. - var _initialData = { - "/": { - isFile: false, - mtime: new Date() - }, - "/file1.txt": { - isFile: true, - mtime: new Date(), - contents: "File 1 Contents" - }, - "/file2.txt": { - isFile: true, - mtime: new Date(), - contents: "File 2 Contents" - }, - "/subdir/": { - isFile: false, - mtime: new Date() - }, - "/subdir/file3.txt": { - isFile: true, - mtime: new Date(), - contents: "File 3 Contents" - }, - "/subdir/file4.txt": { - isFile: true, - mtime: new Date(), - contents: "File 4 Contents" - } - }; - // Indicates whether, by default, the FS should perform UNC Path normalization var _normalizeUNCPathsDefault = false; // Indicates whether, by default, the FS should perform watch and unwatch recursively var _recursiveWatchDefault = true; - // "Live" data for this instance of the file system. Use reset() to - // initialize with _initialData - var _data; - // Callback hooks, set in when(). See when() for more details. var _hooks; @@ -87,60 +55,13 @@ define(function (require, exports, module) { function _getCallback(method, path, cb) { var entry = _getHookEntry(method, path), - result = entry && entry.callback && entry.callback(cb); - - if (!result) { - result = cb; - } - return result; - } - - function _getNotification(method, path, cb) { - var entry = _getHookEntry(method, path), - result = entry && entry.notify && entry.notify(cb); + result = entry && entry(cb); if (!result) { result = cb; } return result; } - - function _getStat(path) { - var entry = _data[path], - stat = null; - - if (entry) { - stat = new FileSystemStats({ - isFile: entry.isFile, - mtime: entry.mtime, - size: entry.contents ? entry.contents.length : 0, - hash: entry.mtime.getTime() - }); - } - - return stat; - } - - function _sendWatcherNotification(path, stats) { - if (_watcherCallback) { - _watcherCallback(path, stats); - } - } - - function _sendDirectoryWatcherNotification(path) { - // Path may be a file or a directory. If it's a file, - // strip the file name off - if (path[path.length - 1] !== "/") { - path = path.substr(0, path.lastIndexOf("/") + 1); - } - _sendWatcherNotification(path); - } - - function init(callback) { - if (callback) { - callback(); - } - } function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { // Not implemented @@ -154,36 +75,24 @@ define(function (require, exports, module) { function exists(path, callback) { var cb = _getCallback("exists", path, callback); - cb(null, !!_data[path]); + cb(null, _model.exists(path)); } function readdir(path, callback) { - var cb = _getCallback("readdir", path, callback), - entry, - contents = [], - stats = []; + var cb = _getCallback("readdir", path, callback); - if (!_data[path]) { + if (!_model.exists(path)) { cb(FileSystemError.NOT_FOUND); return; } - for (entry in _data) { - if (_data.hasOwnProperty(entry)) { - var isDir = false; - if (entry[entry.length - 1] === "/") { - entry = entry.substr(0, entry.length - 1); - isDir = true; - } - if (entry !== path && - entry.indexOf(path) === 0 && - entry.lastIndexOf("/") === path.lastIndexOf("/")) { - contents.push(entry.substr(entry.lastIndexOf("/")) + (isDir ? "/" : "")); - stats.push(_getStat(entry + (isDir ? "/" : ""))); - } - } - } - cb(null, contents, stats); + var contents = _model.readdir(path), + trimmedPath = path.substring(0, path.length - 1), + stats = contents.map(function (name) { + return _model.stat(trimmedPath + name); + }); + + cb(null, contents, stats, []); } function mkdir(path, mode, callback) { @@ -191,74 +100,37 @@ define(function (require, exports, module) { callback = mode; mode = null; } - var cb = _getCallback("mkdir", path, callback), - notify = _getNotification("mkdir", path, _sendDirectoryWatcherNotification); - if (_data[path]) { + var cb = _getCallback("mkdir", path, callback); + + if (_model.exists(path)) { cb(FileSystemError.ALREADY_EXISTS); } else { - var entry = { - isFile: false, - mtime: new Date() - }; - _data[path] = entry; - cb(null, _getStat(path)); - - // Strip the trailing slash off the directory name so the - // notification gets sent to the parent - var notifyPath = path.substr(0, path.length - 1); - notify(notifyPath); + _model.mkdir(path); + cb(null, _model.stat(path)); } } function rename(oldPath, newPath, callback) { - var cb = _getCallback("rename", oldPath, callback), - notify = _getNotification("rename", oldPath, _sendDirectoryWatcherNotification); + var cb = _getCallback("rename", oldPath, callback); - if (_data[newPath]) { + if (_model.exists(newPath)) { cb(FileSystemError.ALREADY_EXISTS); - } else if (!_data[oldPath]) { + } else if (!_model.exists(oldPath)) { cb(FileSystemError.NOT_FOUND); } else { - _data[newPath] = _data[oldPath]; - delete _data[oldPath]; - if (!_data[newPath].isFile) { - var entry, i, - toDelete = []; - - for (entry in _data) { - if (_data.hasOwnProperty(entry)) { - if (entry.indexOf(oldPath) === 0) { - _data[newPath + entry.substr(oldPath.length)] = _data[entry]; - toDelete.push(entry); - } - } - } - for (i = toDelete.length; i; i--) { - delete _data[toDelete.pop()]; - } - } + _model.rename(oldPath, newPath); cb(null); - - // If renaming a Directory, remove the slash from the notification - // name so the *parent* directory is notified of the change - var notifyPath; - - if (oldPath[oldPath.length - 1] === "/") { - notifyPath = oldPath.substr(0, oldPath.length - 1); - } else { - notifyPath = oldPath; - } - notify(notifyPath); } } function stat(path, callback) { var cb = _getCallback("stat", path, callback); - if (!_data[path]) { + + if (!_model.exists(path)) { cb(FileSystemError.NOT_FOUND); } else { - cb(null, _getStat(path)); + cb(null, _model.stat(path)); } } @@ -270,10 +142,10 @@ define(function (require, exports, module) { var cb = _getCallback("readFile", path, callback); - if (!_data[path]) { + if (!_model.exists(path)) { cb(FileSystemError.NOT_FOUND); } else { - cb(null, _data[path].contents, _getStat(path)); + cb(null, _model.readFile(path), _model.stat(path)); } } @@ -283,71 +155,53 @@ define(function (require, exports, module) { options = null; } - stat(path, function (err, stats) { - var cb = _getCallback("writeFile", path, callback); - - if (err && err !== FileSystemError.NOT_FOUND) { - cb(err); - return; - } - - var exists = !!stats; - if (exists && options.hasOwnProperty("hash") && options.hash !== stats._hash) { - cb(FileSystemError.CONTENTS_MODIFIED); - return; - } - - var notification = exists ? _sendWatcherNotification : _sendDirectoryWatcherNotification, - notify = _getNotification("writeFile", path, notification); - - if (!exists) { - if (!_data[path]) { - _data[path] = { - isFile: true - }; - } - } - - _data[path].contents = data; - _data[path].mtime = new Date(); - var newStat = _getStat(path); - cb(null, newStat); - notify(path, newStat); - }); + var cb = _getCallback("writeFile", path, callback); + + if (_model.exists(path) && options.hasOwnProperty("hash") && options.hash !== _model.stat(path)._hash) { + cb(FileSystemError.CONTENTS_MODIFIED); + return; + } + + _model.writeFile(path, data); + cb(null, _model.stat(path)); } function unlink(path, callback) { - var cb = _getCallback("unlink", path, callback), - notify = _getNotification("unlink", path, _sendDirectoryWatcherNotification); + var cb = _getCallback("unlink", path, callback); - if (!_data[path]) { + if (!_model.exists(path)) { cb(FileSystemError.NOT_FOUND); } else { - delete _data[path]; + _model.unlink(path); cb(null); - notify(path); } } function initWatchers(changeCallback, offlineCallback) { - _watcherCallback = changeCallback; + _changeCallback = changeCallback; _offlineCallback = offlineCallback; } function watchPath(path, callback) { - callback(null); + var cb = _getCallback("watchPath", path, callback); + + _model.watchPath(path); + cb(null); } function unwatchPath(path, callback) { - callback(null); + var cb = _getCallback("unwatchPath", path, callback); + _model.unwatchPath(path); + cb(null); } function unwatchAll(callback) { - callback(null); + var cb = _getCallback("unwatchAll", null, callback); + _model.unwatchAll(); + cb(null); } - exports.init = init; exports.showOpenDialog = showOpenDialog; exports.showSaveDialog = showSaveDialog; exports.exists = exists; @@ -364,12 +218,20 @@ define(function (require, exports, module) { exports.unwatchAll = unwatchAll; exports.normalizeUNCPaths = _normalizeUNCPathsDefault; + exports.recursiveWatch = _recursiveWatchDefault; // Test methods exports.reset = function () { - _data = {}; - $.extend(_data, _initialData); + _model = new MockFileSystemModel(); _hooks = {}; + _changeCallback = null; + _offlineCallback = null; + + $(_model).on("change", function (event, path) { + if (_changeCallback) { + _changeCallback(path, _model.stat(path)); + } + }); exports.normalizeUNCPaths = _normalizeUNCPathsDefault; exports.recursiveWatch = _recursiveWatchDefault; @@ -383,17 +245,16 @@ define(function (require, exports, module) { }; /** - * Add a callback and notification hooks to be used when specific - * methods are called with a specific path. + * Add callback hooks to be used when specific methods are called with a + * specific path. * * @param {string} method The name of the method * @param {string} path The path that must be matched - * @param {object} callbacks Object with optional 'callback' and 'notify' - * fields. These are functions that have one parameter and - * must return a function. + * @param {function} getCallback A function that has one parameter and + * must return a callback function. * - * Here is an example that delays the callback and change notifications by 300ms when - * writing a file named "/foo.txt". + * Here is an example that delays the callback by 300ms when writing a file + * named "/foo.txt". * * function delayedCallback(cb) { * return function () { @@ -404,12 +265,12 @@ define(function (require, exports, module) { * }; * } * - * MockFileSystem.when("writeFile", "/foo.txt", {callback: delayedCallback, notify: delayedCallback}); + * MockFileSystem.when("writeFile", "/foo.txt", delayedCallback); */ - exports.when = function (method, path, callbacks) { + exports.when = function (method, path, getCallback) { if (!_hooks[method]) { _hooks[method] = {}; } - _hooks[method][path] = callbacks; + _hooks[method][path] = getCallback; }; }); diff --git a/test/spec/MockFileSystemModel.js b/test/spec/MockFileSystemModel.js new file mode 100644 index 00000000000..4dd89969a4c --- /dev/null +++ b/test/spec/MockFileSystemModel.js @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemStats = require("filesystem/FileSystemStats"); + + // Initial file system data. + var _initialData = { + "/": { + isFile: false, + mtime: new Date() + }, + "/file1.txt": { + isFile: true, + mtime: new Date(), + contents: "File 1 Contents" + }, + "/file2.txt": { + isFile: true, + mtime: new Date(), + contents: "File 2 Contents" + }, + "/subdir/": { + isFile: false, + mtime: new Date() + }, + "/subdir/file3.txt": { + isFile: true, + mtime: new Date(), + contents: "File 3 Contents" + }, + "/subdir/file4.txt": { + isFile: true, + mtime: new Date(), + contents: "File 4 Contents" + } + }; + + function _parentPath(path) { + // trim off the trailing slash if necessary + if (path[path.length - 1] === "/") { + path = path.substring(0, path.length - 1); + } + + if (path[path.length - 1] !== "/") { + path = path.substr(0, path.lastIndexOf("/") + 1); + } + + return path; + } + + function MockFileSystemModel() { + this._data = {}; + $.extend(this._data, _initialData); + this._watchedPaths = {}; + } + + MockFileSystemModel.prototype.stat = function (path) { + var entry = this._data[path]; + + if (entry) { + return new FileSystemStats({ + isFile: entry.isFile, + mtime: entry.mtime, + size: entry.contents ? entry.contents.length : 0, + hash: entry.mtime.getTime() + }); + } else { + return null; + } + }; + + MockFileSystemModel.prototype.exists = function (path) { + return !!this._data[path]; + }; + + MockFileSystemModel.prototype._isPathWatched = function (path) { + return Object.keys(this._watchedPaths).some(function (watchedPath) { + return path.indexOf(watchedPath) === 0; + }); + }; + + MockFileSystemModel.prototype._sendWatcherNotification = function (path) { + if (this._isPathWatched(path)) { + $(this).triggerHandler("change", path); + } + }; + + MockFileSystemModel.prototype._sendDirectoryWatcherNotification = function (path) { + this._sendWatcherNotification(_parentPath(path)); + }; + + MockFileSystemModel.prototype.mkdir = function (path) { + this._data[path] = { + isFile: false, + mtime: new Date() + }; + + this._sendDirectoryWatcherNotification(path); + }; + + MockFileSystemModel.prototype.readFile = function (path) { + return this.exists(path) ? this._data[path].contents : null; + }; + + MockFileSystemModel.prototype.writeFile = function (path, contents) { + var exists = this.exists(path); + + this._data[path] = { + isFile: true, + contents: contents, + mtime: new Date() + }; + + if (exists) { + this._sendWatcherNotification(path); + } else { + this._sendDirectoryWatcherNotification(path); + } + }; + + MockFileSystemModel.prototype.unlink = function (path) { + var entry; + for (entry in this._data) { + if (this._data.hasOwnProperty(entry)) { + if (entry.indexOf(path) === 0) { + delete this._data[entry]; + } + } + } + + this._sendDirectoryWatcherNotification(path); + }; + + MockFileSystemModel.prototype.rename = function (oldPath, newPath) { + this._data[newPath] = this._data[oldPath]; + delete this._data[oldPath]; + if (!this._data[newPath].isFile) { + var entry, i, + toDelete = []; + + for (entry in this._data) { + if (this._data.hasOwnProperty(entry)) { + if (entry.indexOf(oldPath) === 0) { + this._data[newPath + entry.substr(oldPath.length)] = this._data[entry]; + toDelete.push(entry); + } + } + } + for (i = toDelete.length; i; i--) { + delete this._data[toDelete.pop()]; + } + } + + this._sendDirectoryWatcherNotification(oldPath); + }; + + MockFileSystemModel.prototype.readdir = function (path) { + var entry, + contents = []; + + for (entry in this._data) { + if (this._data.hasOwnProperty(entry)) { + var isDir = false; + if (entry[entry.length - 1] === "/") { + entry = entry.substr(0, entry.length - 1); + isDir = true; + } + if (entry !== path && + entry.indexOf(path) === 0 && + entry.lastIndexOf("/") === path.lastIndexOf("/")) { + contents.push(entry.substr(entry.lastIndexOf("/")) + (isDir ? "/" : "")); + } + } + } + + return contents; + }; + + MockFileSystemModel.prototype.watchPath = function (path) { + this._watchedPaths[path] = true; + }; + + MockFileSystemModel.prototype.unwatchPath = function (path) { + delete this._watchedPaths[path]; + }; + + MockFileSystemModel.prototype.unwatchAll = function () { + this._watchedPaths = {}; + }; + + module.exports = MockFileSystemModel; +}); From ae7f28b045a39f0a693f41bccc495edf6c41f69d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 20 Dec 2013 10:02:34 -0800 Subject: [PATCH 72/94] Add some basic tests for external change events --- test/spec/FileSystem-test.js | 70 +++++++++++++++++++++++++++++++++ test/spec/MockFileSystemImpl.js | 4 ++ 2 files changed, 74 insertions(+) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 462c273e92e..3e3b13b0aba 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1305,5 +1305,75 @@ define(function (require, exports, module) { }); }); }); + describe("External change events", function () { + var _model, + changedEntry, + addedEntries, + removedEntries, + changeDone; + + beforeEach(function () { + _model = MockFileSystemImpl._model; + + changedEntry = null; + addedEntries = null; + removedEntries = null; + changeDone = false; + + runs(function () { + $(fileSystem).on("change", function (event, entry, added, removed) { + changedEntry = entry; + addedEntries = added; + removedEntries = removed; + changeDone = true; + }); + + }); + }); + + it("should forward external change events on file creation", function () { + var dirname = "/subdir/", + newfilename = "/subdir/file.that.does.not.exist", + dir, + newfile; + + runs(function () { + dir = fileSystem.getDirectoryForPath(dirname); + newfile = fileSystem.getFileForPath(newfilename); + + _model.writeFile(newfilename, "a lost spacecraft, a collapsed building"); + }); + waitsFor(function () { return changeDone; }, "external change event"); + + runs(function () { + var newfileAdded = addedEntries.some(function (entry) { + return entry === newfile; + }); + + expect(changedEntry).toBe(dir); + expect(addedEntries.length).toBeGreaterThan(0); + expect(newfileAdded).toBe(true); + expect(removedEntries.length).toBe(0); + }); + }); + + it("should forward external change events on file update", function () { + var oldfilename = "/subdir/file3.txt", + oldfile; + + runs(function () { + oldfile = fileSystem.getFileForPath(oldfilename); + + _model.writeFile(oldfilename, "a crashed aeroplane, or a world war"); + }); + waitsFor(function () { return changeDone; }, "external change event"); + + runs(function () { + expect(changedEntry).toBe(oldfile); + expect(addedEntries).toBeFalsy(); + expect(removedEntries).toBeFalsy(); + }); + }); + }); }); }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index 6fea0910c68..b0740db7c72 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -235,6 +235,10 @@ define(function (require, exports, module) { exports.normalizeUNCPaths = _normalizeUNCPathsDefault; exports.recursiveWatch = _recursiveWatchDefault; + + // Allows unit tests to manipulate the filesystem directly in order to + // simulate external change events + exports._model = _model; }; // Simulate file watchers going offline From 54cec576d3977481c74d5b2da2bf3f6432513bbd Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 20 Dec 2013 15:35:55 -0800 Subject: [PATCH 73/94] Address more review comments --- src/document/DocumentCommandHandlers.js | 5 + src/document/InMemoryFile.js | 9 -- src/file/FileUtils.js | 4 +- src/filesystem/Directory.js | 16 +- src/filesystem/File.js | 19 ++- src/filesystem/FileSystem.js | 117 +++++++++----- src/filesystem/FileSystemEntry.js | 20 ++- src/filesystem/FileSystemStats.js | 8 +- .../impls/appshell/AppshellFileSystem.js | 28 +--- src/nls/root/strings.js | 2 +- src/project/ProjectManager.js | 99 ++++++------ src/search/FindInFiles.js | 153 +++++++++--------- test/spec/FileSystem-test.js | 20 ++- test/spec/MockFileSystemImpl.js | 2 +- 14 files changed, 278 insertions(+), 224 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index bf0d5368968..0162ba85e9d 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -697,6 +697,11 @@ define(function (require, exports, module) { // First, write document's current text to new file newFile = FileSystem.getFileForPath(path); + + // Save as warns you when you're about to overwrite a file, so we + // explictly allow "blind" writes to the filesystem in this case, + // ignoring warnings about the contents being modified outside of + // the editor. FileUtils.writeText(newFile, doc.getText(), true).done(function () { // Add new file to project tree ProjectManager.refreshFileTree().done(function () { diff --git a/src/document/InMemoryFile.js b/src/document/InMemoryFile.js index fecbc25cfa9..a62f3a8a763 100644 --- a/src/document/InMemoryFile.js +++ b/src/document/InMemoryFile.js @@ -51,15 +51,6 @@ define(function (require, exports, module) { InMemoryFile.prototype.constructor = InMemoryFile; InMemoryFile.prototype.parentClass = File.prototype; - - /** - * Clear any cached data for this file. - * @private - */ - InMemoryFile.prototype._clearCachedData = function () { - File.prototype._clearCachedData.apply(this); - }; - // Stub out invalid calls inherited from File /** diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 66d29340ba5..31386b87bef 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -73,7 +73,9 @@ define(function (require, exports, module) { * Asynchronously writes a file as UTF-8 encoded text. * @param {!File} file File to write * @param {!string} text - * @param {boolean=} allowBlindWrite + * @param {boolean=} allowBlindWrite Indicates whether or not CONTENTS_MODIFIED + * errors---which can be triggered if the actual file contents differ from + * the FileSystem's last-known contents---should be ignored. * @return {$.Promise} a jQuery promise that will be resolved when * file writing completes, or rejected with a FileSystemError. */ diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index a85707c8a4d..21cec32779a 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -73,13 +73,19 @@ define(function (require, exports, module) { Directory.prototype._contentsStatsErrors = null; /** - * Clear any cached data for this directory + * Clear any cached data for this directory. By default, we clear the contents + * of immediate children as well, because in some cases file watchers fail + * provide precise change notifications. (Sometimes, like after a "git + * checkout", they just report that some directory has changed when in fact + * many of the file within the directory have changed. + * * @private + * @param {boolean=} preserveImmediateChildren */ - Directory.prototype._clearCachedData = function (stopRecursing) { + Directory.prototype._clearCachedData = function (preserveImmediateChildren) { FileSystemEntry.prototype._clearCachedData.apply(this); - if (!stopRecursing && this._contents) { + if (!preserveImmediateChildren && this._contents) { this._contents.forEach(function (child) { child._clearCachedData(true); }); @@ -136,7 +142,7 @@ define(function (require, exports, module) { this._contentsCallbacks = [callback]; - this._impl.readdir(this.fullPath, function (err, entries, stats) { + this._impl.readdir(this.fullPath, function (err, names, stats) { var contents = [], contentsStats = [], contentsStatsErrors; @@ -146,7 +152,7 @@ define(function (require, exports, module) { } else { var watched = this._isWatched(); - entries.forEach(function (name, index) { + names.forEach(function (name, index) { var entryPath = this.fullPath + name; if (this._fileSystem._indexFilter(entryPath, name)) { diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 79191b461d1..736e6151ae0 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -54,13 +54,19 @@ define(function (require, exports, module) { File.prototype.parentClass = FileSystemEntry.prototype; /** - * Contents of this file. + * Cached contents of this file. This value is nullable but should NOT be undefined. + * @private + * @type {?string} */ File.prototype._contents = null; /** * Consistency hash for this file. Reads and writes update this value, and - * writes confirm the hash before overwriting existing files. + * writes confirm the hash before overwriting existing files. The type of + * this object is dependent on the FileSystemImpl; the only constraint is + * that === can be used as an equality relation on hashes. + * @private + * @type {?object} */ File.prototype._hash = null; @@ -137,10 +143,9 @@ define(function (require, exports, module) { callback = callback || function () {}; - // Request a consistency check if the file is watched and the write is not blind - var watched = this._isWatched(); - if (watched && !options.blind) { - options.hash = this._hash; + // Request a consistency check if the write is not blind + if (!options.blind) { + options.expectedHash = this._hash; } // Block external change events until after the write has finished @@ -162,7 +167,7 @@ define(function (require, exports, module) { this._hash = stat._hash; // Only cache data for watched files - if (watched) { + if (this._isWatched()) { this._stat = stat; this._contents = data; } diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 7604e6c880f..8796128c73d 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -40,7 +40,9 @@ * * FileSystem dispatches the following events: * change - Sent whenever there is a change in the file system. The handler - * is passed one argument -- entry. This argument can be... + * is passed up to three arguments: the changed entry and, if that changed entry + * is a Directory, a list of entries added to the directory and a list of entries + * removed from the Directory. The entry argument can be: * * a File - the contents of the file have changed, and should be reloaded. * * a Directory - an immediate child of the directory has been added, removed, * or renamed/moved. Not triggered for "grandchildren". @@ -51,7 +53,9 @@ * rename - Sent whenever a File or Directory is renamed. All affected File and Directory * objects have been updated to reflect the new path by the time this event is dispatched. * This event should be used to trigger any UI updates that may need to occur when a path - * has changed. + * has changed. Note that these events will only be sent for rename operations that happen + * within the filesystem. If a file is renamed externally, a change event on the parent + * directory will be sent instead. * * FileSystem may perform caching. But it guarantees: * * File contents & metadata - reads are guaranteed to be up to date (cached data is not used @@ -110,6 +114,11 @@ define(function (require, exports, module) { */ FileSystem.prototype._activeChangeCount = 0; + // For unit testing only + FileSystem.prototype._getActiveChangeCount = function () { + return this._activeChangeCount; + }; + /** * Queue of arguments with which to invoke _handleExternalChanges(); triggered * once _activeChangeCount drops to zero. @@ -186,10 +195,13 @@ define(function (require, exports, module) { /** * The set of watched roots, encoded as a mapping from full paths to objects - * which contain a file entry, filter function, and change handler function. + * which contain a file entry, filter function, and an indication of whether + * the watched root is full active (instead of, e.g., in the process of + * starting up). * * @type{Object.} + * filter: function(string): boolean, + * active: boolean} >} */ FileSystem.prototype._watchedRoots = null; @@ -238,9 +250,7 @@ define(function (require, exports, module) { // no-ops if the impl supports recursiveWatch callback(null); } else { - // The impl will handle finding all subdirectories to watch. Here we - // just need to find all entries in order to either mark them as - // watched or to remove them from the index. + // The impl will handle finding all subdirectories to watch. this._enqueueWatchRequest(function (requestCb) { impl[commandName].call(impl, entry.fullPath, requestCb); }.bind(this), callback); @@ -376,11 +386,29 @@ define(function (require, exports, module) { return true; }; + /** + * Indicates that a filesystem-mutating operation has begun. As long as there + * are changes taking place, change events from the external watchers are + * blocked and queued, to be handled once changes have finished. This is done + * because for mutating operations that originate from within the filesystem, + * synthetic change events are fired that do not depend on external file + * watchers, and we prefer the former over the latter for the following + * reasons: 1) there is no delay; and 2) they may have higher fidelity --- + * e.g., a rename operation can be detected as such, instead of as a nearly + * simultaneous addition and deletion. + * + * All operations that mutate the file system MUST begin with a call to + * _beginChange and must end with a call to _endChange. + */ FileSystem.prototype._beginChange = function () { this._activeChangeCount++; //console.log("> beginChange -> " + this._activeChangeCount); }; + /** + * Indicates that a filesystem-mutating operation has completed. See + * FileSystem._beginChange above. + */ FileSystem.prototype._endChange = function () { this._activeChangeCount--; //console.log("< endChange -> " + this._activeChangeCount); @@ -404,7 +432,7 @@ define(function (require, exports, module) { return (fullPath[0] === "/" || fullPath[1] === ":"); }; - function _appendTrailingSlash(path) { + function _ensureTrailingSlash(path) { if (path[path.length - 1] !== "/") { path += "/"; } @@ -454,7 +482,7 @@ define(function (require, exports, module) { if (isDirectory) { // Make sure path DOES include trailing slash - path = _appendTrailingSlash(path); + path = _ensureTrailingSlash(path); } if (isUNCPath) { @@ -524,7 +552,7 @@ define(function (require, exports, module) { item = this._index.getEntry(normalizedPath); if (!item) { - normalizedPath = _appendTrailingSlash(normalizedPath); + normalizedPath = _ensureTrailingSlash(normalizedPath); item = this._index.getEntry(normalizedPath); } @@ -631,13 +659,13 @@ define(function (require, exports, module) { * @private * Notify the system when an entry name has changed. * - * @param {string} oldName - * @param {string} newName + * @param {string} oldFullPath + * @param {string} newFullPath * @param {boolean} isDirectory */ - FileSystem.prototype._handleRename = function (oldName, newName, isDirectory) { + FileSystem.prototype._handleRename = function (oldFullPath, newFullPath, isDirectory) { // Update all affected entries in the index - this._index.entryRenamed(oldName, newName, isDirectory); + this._index.entryRenamed(oldFullPath, newFullPath, isDirectory); }; /** @@ -653,35 +681,35 @@ define(function (require, exports, module) { * FileSystemEntry objects. */ FileSystem.prototype._handleDirectoryChange = function (directory, callback) { - var oldContents = directory._contents || []; + var oldContents = directory._contents; directory._clearCachedData(); directory.getContents(function (err, contents) { - var addedEntries = contents.filter(function (entry) { + var addedEntries = oldContents && contents.filter(function (entry) { return oldContents.indexOf(entry) === -1; }); - var removedEntries = oldContents.filter(function (entry) { + var removedEntries = oldContents && oldContents.filter(function (entry) { return contents.indexOf(entry) === -1; }); - // If directory is not watched, clear the cache the children of removed - // entries manually. Otherwise, this is handled by the unwatch call. + // If directory is not watched, clear children's caches manually. var watchedRoot = this._findWatchedRootForPath(directory.fullPath); if (!watchedRoot || !watchedRoot.filter(directory.name, directory.parentPath)) { - removedEntries.forEach(function (removed) { - this._index.visitAll(function (entry) { - if (entry.fullPath.indexOf(removed.fullPath) === 0) { - entry._clearCachedData(); - } - }.bind(this)); - }, this); + this._index.visitAll(function (entry) { + if (entry.fullPath.indexOf(directory.fullPath) === 0) { + entry._clearCachedData(); + } + }.bind(this)); callback(addedEntries, removedEntries); return; } + + var addedCounter = addedEntries ? addedEntries.length : 0, + removedCounter = removedEntries ? removedEntries.length : 0, + counter = addedCounter + removedCounter; - var counter = addedEntries.length + removedEntries.length; if (counter === 0) { callback(addedEntries, removedEntries); return; @@ -693,13 +721,17 @@ define(function (require, exports, module) { } }; - addedEntries.forEach(function (entry) { - this._watchEntry(entry, watchedRoot, watchOrUnwatchCallback); - }, this); + if (addedEntries) { + addedEntries.forEach(function (entry) { + this._watchEntry(entry, watchedRoot, watchOrUnwatchCallback); + }, this); + } - removedEntries.forEach(function (entry) { - this._unwatchEntry(entry, watchedRoot, watchOrUnwatchCallback); - }, this); + if (removedEntries) { + removedEntries.forEach(function (entry) { + this._unwatchEntry(entry, watchedRoot, watchOrUnwatchCallback); + }, this); + } }.bind(this)); }; @@ -715,8 +747,7 @@ define(function (require, exports, module) { FileSystem.prototype._handleExternalChange = function (path, stat) { if (!path) { - // This is a "wholesale" change event - // Clear all caches (at least those that won't do a stat() double-check before getting used) + // This is a "wholesale" change event; clear all caches this._index.visitAll(function (entry) { entry._clearCachedData(); }); @@ -729,9 +760,10 @@ define(function (require, exports, module) { var entry = this._index.getEntry(path); if (entry) { + var oldStat = entry._stat; if (entry.isFile) { // Update stat and clear contents, but only if out of date - if (!(stat && entry._stat && stat.mtime.getTime() === entry._stat.mtime.getTime())) { + if (!(stat && oldStat && stat.mtime.getTime() === oldStat.mtime.getTime())) { entry._clearCachedData(); entry._stat = stat; this._fireChangeEvent(entry); @@ -739,8 +771,9 @@ define(function (require, exports, module) { } else { this._handleDirectoryChange(entry, function (added, removed) { entry._stat = stat; - - this._fireChangeEvent(entry, added, removed); + if (!(added && added.length === 0 && removed && removed.length === 0)) { + this._fireChangeEvent(entry, added, removed); + } }.bind(this)); } } @@ -822,6 +855,8 @@ define(function (require, exports, module) { return; } + // Mark this as inactive, but don't delete the entry until the unwatch is complete. + // This is useful for making sure we don't try to concurrently watch overlapping roots. watchedRoot.active = false; this._unwatchEntry(entry, watchedRoot, function (err) { @@ -861,8 +896,8 @@ define(function (require, exports, module) { }); }, this); - // Fire a wholesale change event because all previously watched entries - // have been removed from the index and should no longer be referenced + // Fire a wholesale change event, clearing all caches and request that + // clients manually update their state. this._handleExternalChange(null); }; @@ -892,7 +927,7 @@ define(function (require, exports, module) { exports.isAbsolutePath = FileSystem.isAbsolutePath; // For testing only - exports._activeChangeCount = _wrap(FileSystem.prototype._activeChangeCount); + exports._getActiveChangeCount = _wrap(FileSystem.prototype._getActiveChangeCount); /** * Add an event listener for a FileSystem event. diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index a4cbd3e8b5d..923767fc899 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -227,14 +227,20 @@ define(function (require, exports, module) { // root directories have no parent path this._parentPath = null; } - - // Update watchedRootFilterResult + + this._path = newPath; + var watchedRoot = this._watchedRoot; if (watchedRoot) { - this._watchedRootFilterResult = watchedRoot.filter(this._name, this._parentPath); + if (watchedRoot.entry.fullPath.indexOf(newPath) === 0) { + // Update watchedRootFilterResult + this._watchedRootFilterResult = watchedRoot.filter(this._name, this._parentPath); + } else { + // The entry was moved outside of the watched root + this._watchedRoot = null; + this._watchedRootFilterResult = false; + } } - - this._path = newPath; }; /** @@ -276,6 +282,10 @@ define(function (require, exports, module) { return; } + if (!exists) { + this._clearCachedData(); + } + callback(null, exists); }.bind(this)); }; diff --git a/src/filesystem/FileSystemStats.js b/src/filesystem/FileSystemStats.js index 13131f26a0c..cabb709cab5 100644 --- a/src/filesystem/FileSystemStats.js +++ b/src/filesystem/FileSystemStats.js @@ -33,7 +33,7 @@ define(function (require, exports, module) { /** * @constructor - * @param {{isFile: boolean, mtime: Date, size: Number, realPath: ?string}} options + * @param {{isFile: boolean, mtime: Date, size: Number, realPath: ?string, hash: object}} options */ function FileSystemStats(options) { var isFile = options.isFile; @@ -101,6 +101,12 @@ define(function (require, exports, module) { * @type {Number} */ FileSystemStats.prototype._size = null; + + /** + * Consistency hash for a file + * @type {object} + */ + FileSystemStats.prototype._hash = null; /** * The canonical path of this file or directory ONLY if it is a symbolic link, diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js index 0adf017aa49..fb9fde7a716 100644 --- a/src/filesystem/impls/appshell/AppshellFileSystem.js +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -390,7 +390,7 @@ define(function (require, exports, module) { * * @param {string} path * @param {string} data - * @param {{encoding : string=, mode : number=, hash : string=}} options + * @param {{encoding : string=, mode : number=, expectedHash : object=}} options * @param {function(?string, FileSystemStats=, boolean)} callback */ function writeFile(path, data, options, callback) { @@ -420,8 +420,8 @@ define(function (require, exports, module) { return; } - if (options.hasOwnProperty("hash") && options.hash !== stats._hash) { - console.warn("Blind write attempted: ", path, stats._hash, options.hash); + if (options.hasOwnProperty("expectedHash") && options.expectedHash !== stats._hash) { + console.warn("Blind write attempted: ", path, stats._hash, options.expectedHash); callback(FileSystemError.CONTENTS_MODIFIED); return; } @@ -511,15 +511,8 @@ define(function (require, exports, module) { * @param {function(?string)=} callback */ function unwatchPath(path, callback) { - appshell.fs.isNetworkDrive(path, function (err, isNetworkDrive) { - if (err || isNetworkDrive) { - callback(FileSystemError.UNKNOWN); - return; - } - - _nodeDomain.exec("unwatchPath", path) - .then(callback, callback); - }); + _nodeDomain.exec("unwatchPath", path) + .then(callback, callback); } /** @@ -530,15 +523,8 @@ define(function (require, exports, module) { * @param {function(?string)=} callback */ function unwatchAll(callback) { - appshell.fs.isNetworkDrive(function (err, isNetworkDrive) { - if (err || isNetworkDrive) { - callback(FileSystemError.UNKNOWN); - return; - } - - _nodeDomain.exec("unwatchAll") - .then(callback, callback); - }); + _nodeDomain.exec("unwatchAll") + .then(callback, callback); } // Export public API diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index a713958d40e..1c74eb733d8 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -36,7 +36,7 @@ define({ "NOT_READABLE_ERR" : "The file could not be read.", "NO_MODIFICATION_ALLOWED_ERR" : "The target directory cannot be modified.", "NO_MODIFICATION_ALLOWED_ERR_FILE" : "The permissions do not allow you to make modifications.", - "CONTENTS_MODIFIED_ERR" : "The contents of the file were modified outside of the editor.", + "CONTENTS_MODIFIED_ERR" : "The file has been modified outside of {APP_NAME}.", "FILE_EXISTS_ERR" : "The file or directory already exists.", "FILE" : "file", "DIRECTORY" : "directory", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 9f99a40233d..fa23220148c 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1761,64 +1761,69 @@ define(function (require, exports, module) { * this case, the editor state isn't currently preserved. * * @param {$.Event} event - * @param {File|Directory} entry File or Directory changed + * @param {?(File|Directory)} entry File or Directory changed * @param {Array.=} added If entry is a Directory, contains zero or more added children * @param {Array.=} removed If entry is a Directory, contains zero or more removed children */ _fileSystemChange = function (event, entry, added, removed) { - if (entry) { - // Directory contents removed - if (removed && removed.length) { - _fileTreeChangeQueue.add(function () { - return Async.doSequentially(removed, function (removedEntry) { - return _deleteTreeNode(removedEntry); - }, false); - }); + FileSyncManager.syncOpenDocuments(); + + if (!entry) { + refreshFileTree(); + return; + } + + if (entry.isDirectory) { + if (!added || !removed) { + // TODO: just update children of entry in this case. + refreshFileTree(); + return; } + // Directory contents removed + _fileTreeChangeQueue.add(function () { + return Async.doSequentially(removed, function (removedEntry) { + return _deleteTreeNode(removedEntry); + }, false); + }); + // Directory contents added - if (added && added.length) { - _fileTreeChangeQueue.add(function () { - // Find parent node to add to. Use shallowSearch=true to - // skip adding a child if it's parent is not visible - return _findTreeNode(entry, true).then(function ($directoryNode) { - if ($directoryNode && !$directoryNode.hasClass("jstree-closed")) { - return Async.doSequentially(added, function (addedEntry) { - var json = _entryToJSON(addedEntry); + _fileTreeChangeQueue.add(function () { + // Find parent node to add to. Use shallowSearch=true to + // skip adding a child if it's parent is not visible + return _findTreeNode(entry, true).then(function ($directoryNode) { + if ($directoryNode && !$directoryNode.hasClass("jstree-closed")) { + return Async.doSequentially(added, function (addedEntry) { + var json = _entryToJSON(addedEntry); + + // _entryToJSON returns null if the added file is filtered from view + if (json) { - // _entryToJSON returns null if the added file is filtered from view - if (json) { - - // Before creating a new node, make sure it doesn't already exist. - // TODO: Improve the efficiency of this search! - return _findTreeNode(addedEntry).then(function ($childNode) { - if ($childNode) { - // the node already exists; do nothing; - return new $.Deferred().resolve(); - } else { - // The node wasn't found; create it. - // Position is irrelevant due to sorting - return _createNode($directoryNode, null, json, true); - } - }, function () { - // The node doesn't exist; create it. + // Before creating a new node, make sure it doesn't already exist. + // TODO: Improve the efficiency of this search! + return _findTreeNode(addedEntry).then(function ($childNode) { + if ($childNode) { + // the node already exists; do nothing; + return new $.Deferred().resolve(); + } else { + // The node wasn't found; create it. + // Position is irrelevant due to sorting return _createNode($directoryNode, null, json, true); - }); - } else { - return new $.Deferred().resolve(); - } - }, false); - } else { - return new $.Deferred().resolve(); - } - }); + } + }, function () { + // The node doesn't exist; create it. + return _createNode($directoryNode, null, json, true); + }); + } else { + return new $.Deferred().resolve(); + } + }, false); + } else { + return new $.Deferred().resolve(); + } }); - } - } else { - refreshFileTree(); + }); } - - FileSyncManager.syncOpenDocuments(); }; /** diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 50763fe04e2..54ffdc392a5 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -663,9 +663,8 @@ define(function (require, exports, module) { DocumentManager.getDocumentText(file) .done(function (text) { addMatches(file.fullPath, text, currentQueryExpr); - result.resolve(); }) - .fail(function (error) { + .always(function () { // Always resolve. If there is an error, this file // is skipped and we move on to the next file. result.resolve(); @@ -924,87 +923,93 @@ define(function (require, exports, module) { * @param {Array.=} removed Removed children */ _fileSystemChangeHandler = function (event, entry, added, removed) { - if (entry && entry.isDirectory) { - var resultsChanged = false; + var resultsChanged = false; + + /* + * Remove existing search results that match the given entry's path + * @param {File|Directory} + */ + function _removeSearchResultsForEntry(entry) { + Object.keys(searchResults).forEach(function (fullPath) { + if (fullPath.indexOf(entry.fullPath) === 0) { + delete searchResults[fullPath]; + resultsChanged = true; + } + }); + } + + /* + * Add new search results for this entry and all of its children + * @param {File|Directory} + * @param {jQuery.Promise} Resolves when the results have been added + */ + function _addSearchResultsForEntry(entry) { + var addedFiles = [], + deferred = new $.Deferred(); - if (removed && removed.length > 0) { - var _includesPath = function (fullPath) { - return _.some(removed, function (item) { - return fullPath.indexOf(item.fullPath) === 0; - }); - }; - - // Clear removed entries from the search results - _.forEach(searchResults, function (item, fullPath) { - if (fullPath.indexOf(entry.fullPath) === 0 && _includesPath(fullPath)) { - // The changed directory includes this entry and it was not removed. - delete searchResults[fullPath]; - resultsChanged = true; - } - }); - } + var doSearch = _doSearchInOneFile.bind(undefined, function () { + if (_addSearchMatches.apply(undefined, arguments)) { + resultsChanged = true; + } + }); - var addPromise; - if (added && added.length > 0) { - var doSearch = _doSearchInOneFile.bind(undefined, function () { - var resultsAdded = _addSearchMatches.apply(undefined, arguments); - resultsChanged = resultsChanged || resultsAdded; - }); - - var addedFiles = [], - addedDirectories = []; - - // sort added entries into files and directories - added.forEach(function (entry) { - if (entry.isFile) { - addedFiles.push(entry); - } else { - addedDirectories.push(entry); + // gather up added files + var visitor = function (child) { + if (ProjectManager.shouldShow(child)) { + if (child.isFile) { + addedFiles.push(child); } - }); + return true; + } + return false; + }; + + entry.visit(visitor, function (err) { + if (err) { + deferred.reject(err); + return; + } - // visit added directories and add their included files - var visitor = function (child) { - if (ProjectManager.shouldShow(child)) { - if (child.isFile) { - addedFiles.push(child); - } - return true; - } - }; - - var visitPromise = Async.doInParallel(addedDirectories, function (directory) { - var deferred = $.Deferred(); - - directory.visit(visitor, function (err) { - if (err) { - deferred.reject(err); - return; - } - - deferred.resolve(); - }); - - return deferred.promise(); - }); - // find additional matches in all added files - addPromise = visitPromise.then(function () { - return Async.doInParallel(addedFiles, doSearch); + Async.doInParallel(addedFiles, doSearch).always(deferred.resolve); + }); + + return deferred.promise(); + } + + if (!entry) { + // TODO: re-execute the search completely? + return; + } + + var addPromise; + if (entry.isDirectory) { + if (!added || !removed) { + // If the added or removed sets are null, we should redo the + // search for the entire directory + _removeSearchResultsForEntry(entry); + + var deferred = $.Deferred(); + addPromise = deferred.promise(); + entry.getContents(function (err, entries) { + Async.doInParallel(entries, _addSearchResultsForEntry).always(deferred.resolve); }); } else { - addPromise = $.Deferred().resolve().promise(); + removed.forEach(_removeSearchResultsForEntry); + addPromise = Async.doInParallel(added, _addSearchResultsForEntry); } - - addPromise.done(function () { - // Restore the results if needed - if (resultsChanged) { - _restoreSearchResults(); - } - }).fail(function (err) { - console.warn("Failed to update FindInFiles results: ", err); - }); + } else { // entry.isFile + _removeSearchResultsForEntry(entry); + addPromise = _addSearchResultsForEntry(entry); } + + addPromise.always(function () { + // Restore the results if needed + if (resultsChanged) { + _restoreSearchResults(); + } + }); + }; // Initialize items dependent on HTML DOM diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 3e3b13b0aba..421011b2f4d 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -117,12 +117,12 @@ define(function (require, exports, module) { waitsFor(function () { return cb.wasCalled; }); runs(function () { expect(cb.error).toBeFalsy(); - expect(fileSystem._activeChangeCount).toBe(0); + expect(fileSystem._getActiveChangeCount()).toBe(0); }); }); afterEach(function () { - expect(fileSystem._activeChangeCount).toBe(0); + expect(fileSystem._getActiveChangeCount()).toBe(0); }); describe("Path normalization", function () { @@ -1331,7 +1331,7 @@ define(function (require, exports, module) { }); }); - it("should forward external change events on file creation", function () { + it("should fire change event on external file creation", function () { var dirname = "/subdir/", newfilename = "/subdir/file.that.does.not.exist", dir, @@ -1341,23 +1341,21 @@ define(function (require, exports, module) { dir = fileSystem.getDirectoryForPath(dirname); newfile = fileSystem.getFileForPath(newfilename); - _model.writeFile(newfilename, "a lost spacecraft, a collapsed building"); + dir.getContents(function () { + _model.writeFile(newfilename, "a lost spacecraft, a collapsed building"); + }); }); waitsFor(function () { return changeDone; }, "external change event"); runs(function () { - var newfileAdded = addedEntries.some(function (entry) { - return entry === newfile; - }); - expect(changedEntry).toBe(dir); - expect(addedEntries.length).toBeGreaterThan(0); - expect(newfileAdded).toBe(true); + expect(addedEntries.length).toBe(1); + expect(addedEntries[0]).toBe(newfile); expect(removedEntries.length).toBe(0); }); }); - it("should forward external change events on file update", function () { + it("should fire change event on external file update", function () { var oldfilename = "/subdir/file3.txt", oldfile; diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js index b0740db7c72..55b7e8f37d8 100644 --- a/test/spec/MockFileSystemImpl.js +++ b/test/spec/MockFileSystemImpl.js @@ -157,7 +157,7 @@ define(function (require, exports, module) { var cb = _getCallback("writeFile", path, callback); - if (_model.exists(path) && options.hasOwnProperty("hash") && options.hash !== _model.stat(path)._hash) { + if (_model.exists(path) && options.hasOwnProperty("expectedHash") && options.expectedHash !== _model.stat(path)._hash) { cb(FileSystemError.CONTENTS_MODIFIED); return; } From 45ffc1c5b027abebcbceee46417f0aac69ea523c Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Wed, 25 Dec 2013 14:11:06 -0600 Subject: [PATCH 74/94] Additional external change event unit tests --- test/spec/FileSystem-test.js | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js index 421011b2f4d..bd059746e4a 100644 --- a/test/spec/FileSystem-test.js +++ b/test/spec/FileSystem-test.js @@ -1372,6 +1372,82 @@ define(function (require, exports, module) { expect(removedEntries).toBeFalsy(); }); }); + + it("should fire change event on external directory creation", function () { + var dirname = "/subdir/", + newdirname = "/subdir/dir.that.does.not.exist/", + dir, + newdir; + + runs(function () { + dir = fileSystem.getDirectoryForPath(dirname); + newdir = fileSystem.getDirectoryForPath(newdirname); + + dir.getContents(function () { + _model.mkdir(newdirname); + }); + }); + waitsFor(function () { return changeDone; }, "external change event"); + + runs(function () { + expect(changedEntry).toBe(dir); + expect(addedEntries.length).toBe(1); + expect(addedEntries[0]).toBe(newdir); + expect(removedEntries.length).toBe(0); + }); + }); + + it("should fire change event on external unlink", function () { + var dirname = "/", + olddirname = "/subdir/", + dir, + olddir; + + runs(function () { + dir = fileSystem.getDirectoryForPath(dirname); + olddir = fileSystem.getFileForPath(olddirname); + + dir.getContents(function () { + _model.unlink(olddirname); + }); + }); + waitsFor(function () { return changeDone; }, "external change event"); + + runs(function () { + expect(changedEntry).toBe(dir); + expect(addedEntries.length).toBe(0); + expect(removedEntries.length).toBe(1); + expect(removedEntries[0]).toBe(olddir); + }); + }); + + it("should fire change event on external file rename", function () { + var dirname = "/subdir/", + oldfilename = "/subdir/file3.txt", + newfilename = "/subdir/file3.new.txt", + oldfile, + newfile, + dir; + + runs(function () { + oldfile = fileSystem.getFileForPath(oldfilename); + dir = fileSystem.getDirectoryForPath(dirname); + + dir.getContents(function () { + _model.rename(oldfilename, newfilename); + newfile = fileSystem.getFileForPath(newfilename); + }); + }); + waitsFor(function () { return changeDone; }, "external change event"); + + runs(function () { + expect(changedEntry).toBe(dir); + expect(addedEntries.length).toBe(1); + expect(removedEntries.length).toBe(1); + expect(addedEntries[0]).toBe(newfile); + expect(removedEntries[0]).toBe(oldfile); + }); + }); }); }); }); From b6fbdeba6870c094c72b1a586018c06591560b05 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 26 Dec 2013 13:32:40 -0600 Subject: [PATCH 75/94] Exclude all binary files from FindInFiles, not just images --- src/language/LanguageManager.js | 22 ++++++++++++++++++++++ src/language/languages.json | 6 ++++++ src/search/FindInFiles.js | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/language/LanguageManager.js b/src/language/LanguageManager.js index c029eadfe08..d8123fc7d40 100644 --- a/src/language/LanguageManager.js +++ b/src/language/LanguageManager.js @@ -325,6 +325,9 @@ define(function (require, exports, module) { /** @type {{ prefix: string, suffix: string }} Block comment syntax */ Language.prototype._blockCommentSyntax = null; + /** @type {boolean} Whether or not the language is binary */ + Language.prototype._isBinary = false; + /** * Returns the identifier for this language. * @return {string} The identifier @@ -647,6 +650,22 @@ define(function (require, exports, module) { } }; + /** + * Indicates whether or not the language is binary (e.g., image or audio). + * @return {boolean} + */ + Language.prototype.isBinary = function () { + return this._isBinary; + }; + + /** + * Sets whether or not the language is binary + * @param {!boolean} isBinary + */ + Language.prototype._setBinary = function (isBinary) { + this._isBinary = isBinary; + }; + /** * Defines a language. * @@ -695,6 +714,9 @@ define(function (require, exports, module) { language.addFileName(fileNames[i]); } } + + language._setBinary(!!definition.isBinary); + // store language to language map _languages[language.getId()] = language; } diff --git a/src/language/languages.json b/src/language/languages.json index fbd8a4c7463..54c089b0af9 100644 --- a/src/language/languages.json +++ b/src/language/languages.json @@ -225,5 +225,11 @@ "name": "Image", "fileExtensions": ["gif", "png", "jpe", "jpeg", "jpg"], "isBinary": true + }, + + "audio": { + "name": "Audio", + "fileExtensions": ["mp3", "wav", "aif", "aiff", "ogg"], + "isBinary": true } } diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 54ffdc392a5..7b6aec57046 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -682,7 +682,7 @@ define(function (require, exports, module) { */ function _findInFilesFilter(entry) { var language = LanguageManager.getLanguageForPath(entry.fullPath); - return language.getId() !== "image"; + return !language.isBinary(); } /** From f30a4eb2e40b75661f14c51a53585245ad0830f6 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Thu, 2 Jan 2014 13:41:33 -0800 Subject: [PATCH 76/94] Error handling for missing Entry during change event --- src/project/ProjectManager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index fa23220148c..bc3dba44a8a 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1094,7 +1094,9 @@ define(function (require, exports, module) { function findInSubtree($nodes, segmentI) { var seg = pathSegments[segmentI]; var match = _.findIndex($nodes, function (node, i) { - var nodeName = $(node).data("entry").name; + var entry = $(node).data("entry"), + nodeName = entry ? entry.name : null; + return nodeName === seg; }); From e42f3dbd99d2345ce8b314059e46e06cb43698d4 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 2 Jan 2014 15:29:43 -0800 Subject: [PATCH 77/94] Hack around a file tree race condition in the ProjectManager integration tests --- test/spec/ProjectManager-test.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/spec/ProjectManager-test.js b/test/spec/ProjectManager-test.js index 44af7a2c8fa..4d72ae21136 100644 --- a/test/spec/ProjectManager-test.js +++ b/test/spec/ProjectManager-test.js @@ -23,7 +23,7 @@ /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ -/*global $, define, require, describe, it, expect, beforeEach, afterEach, waitsFor, runs, waitsForDone, beforeFirst, afterLast */ +/*global $, define, require, describe, it, expect, beforeEach, afterEach, waits, waitsFor, runs, waitsForDone, beforeFirst, afterLast */ define(function (require, exports, module) { "use strict"; @@ -43,6 +43,7 @@ define(function (require, exports, module) { this.category = "integration"; var testPath = SpecRunnerUtils.getTestPath("/spec/ProjectManager-test-files"), + changeEventFired, testWindow, brackets; @@ -57,6 +58,10 @@ define(function (require, exports, module) { FileSystem = testWindow.brackets.test.FileSystem; SpecRunnerUtils.loadProjectInTestWindow(testPath); + + FileSystem.on("change", function () { + changeEventFired = true; + }); }); }); @@ -273,12 +278,17 @@ define(function (require, exports, module) { // Delete the selected file. runs(function () { complete = false; + changeEventFired = false; + // delete the new file ProjectManager.deleteItem(selectedFile) .always(function () { complete = true; }); }); - waitsFor(function () { return complete; }, "ProjectManager.deleteItem() timeout", 1000); + waitsFor(function () { return complete && changeEventFired; }, "ProjectManager.deleteItem() timeout", 1000); + + // Wait for the call to refreshFileTree, which is triggered by the change event, to complete. + waits(500); // Verify that file no longer exists. runs(function () { @@ -399,11 +409,15 @@ define(function (require, exports, module) { // Delete the root folder and all files/folders in it. runs(function () { complete = false; - + changeEventFired = false; + ProjectManager.deleteItem(rootFolderEntry) .always(function () { complete = true; }); }); - waitsFor(function () { return complete; }, "ProjectManager.deleteItem() timeout", 1000); + waitsFor(function () { return complete && changeEventFired; }, "ProjectManager.deleteItem() timeout", 1000); + + // Wait for the call to refreshFileTree, which is triggered by the change event, to complete. + waits(500); // Verify that the root folder no longer exists. runs(function () { From a7a551d4f72092ac69b4aa2003a629072f2d0b03 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Thu, 2 Jan 2014 16:05:07 -0800 Subject: [PATCH 78/94] Add an allFiles cache, used by getAllFiles, to ensure that there is at most one uncached directory traversal happening at once before watchers come up --- src/project/ProjectManager.js | 77 +++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index bc3dba44a8a..f343ce9c764 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -178,6 +178,14 @@ define(function (require, exports, module) { var suppressToggleOpen = false; + /** + * @private + * @type {?jQuery.Promise.>} + * A promise that is resolved with an array of all project files. Used by + * ProjectManager.getAllFiles(). + */ + var _allFilesCachePromise = null; + /** * @private */ @@ -920,6 +928,9 @@ define(function (require, exports, module) { console.error("Error watching project root: ", rootPath, err); } }); + + // Reset allFiles cache + _allFilesCachePromise = null; } @@ -937,6 +948,9 @@ define(function (require, exports, module) { console.error("Error unwatching project root: ", _projectRoot.fullPath, err); } }); + + // Reset allFiles cache + _allFilesCachePromise = null; } } @@ -1688,6 +1702,46 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Returns a promise that resolves with a cached copy of all project files. + * Used by ProjectManager.getAllFiles(). Ensures that at most one un-cached + * directory traversal is active at a time, which is useful at project load + * time when watchers (and hence filesystem-level caching) has not finished + * starting up. The cache is cleared on every filesystem change event, and + * also on project load and unload. + * + * @private + * @return {jQuery.Promise.>} + */ + function _getAllFilesCache() { + if (!_allFilesCachePromise) { + var deferred = new $.Deferred(), + allFiles = [], + allFilesVisitor = function (entry) { + if (shouldShow(entry)) { + if (entry.isFile && !isBinaryFile(entry.name)) { + allFiles.push(entry); + } + return true; + } + return false; + }; + + _allFilesCachePromise = deferred.promise(); + + getProjectRoot().visit(allFilesVisitor, function (err) { + if (err) { + deferred.reject(); + _allFilesCachePromise = null; + } else { + deferred.resolve(allFiles); + } + }); + } + + return _allFilesCachePromise; + } + /** * Returns an Array of all files for this project, optionally including * files in the working set that are *not* under the project root. Files filtered @@ -1701,9 +1755,6 @@ define(function (require, exports, module) { * @return {$.Promise} Promise that is resolved with an Array of File objects. */ function getAllFiles(filter, includeWorkingSet) { - var deferred = new $.Deferred(), - result = []; - // The filter and includeWorkingSet params are both optional. // Handle the case where filter is omitted but includeWorkingSet is // specified. @@ -1712,19 +1763,8 @@ define(function (require, exports, module) { filter = null; } - - function visitor(entry) { - if (shouldShow(entry)) { - if (entry.isFile && !isBinaryFile(entry.name)) { - result.push(entry); - } - return true; - } - return false; - } - // First gather all files in project proper - getProjectRoot().visit(visitor, function (err) { + return _getAllFilesCache().then(function (result) { // Add working set entries, if requested if (includeWorkingSet) { DocumentManager.getWorkingSet().forEach(function (file) { @@ -1739,10 +1779,8 @@ define(function (require, exports, module) { result = result.filter(filter); } - deferred.resolve(result); + return result; }); - - return deferred.promise(); } /** @@ -1769,6 +1807,9 @@ define(function (require, exports, module) { */ _fileSystemChange = function (event, entry, added, removed) { FileSyncManager.syncOpenDocuments(); + + // Reset allFiles cache + _allFilesCachePromise = null; if (!entry) { refreshFileTree(); From febbfe6d36b744884d67beac428fdafca2a3aadd Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 3 Jan 2014 11:59:26 -0800 Subject: [PATCH 79/94] Allow Directory objects to cache their contents even while watchers are starting up; factor out WatchedRoot into its own module. --- src/filesystem/Directory.js | 4 +- src/filesystem/FileSystem.js | 47 ++++++++++---------- src/filesystem/FileSystemEntry.js | 11 +++-- src/filesystem/WatchedRoot.js | 73 +++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 src/filesystem/WatchedRoot.js diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js index 21cec32779a..49ef09c1a05 100644 --- a/src/filesystem/Directory.js +++ b/src/filesystem/Directory.js @@ -150,7 +150,9 @@ define(function (require, exports, module) { if (err) { this._clearCachedData(); } else { - var watched = this._isWatched(); + // Use the "relaxed" parameter to _isWatched because it's OK to + // cache data even while watchers are still starting up + var watched = this._isWatched(true); names.forEach(function (name, index) { var entryPath = this.fullPath + name; diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index 8796128c73d..993cd50ece2 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -73,7 +73,8 @@ define(function (require, exports, module) { var Directory = require("filesystem/Directory"), File = require("filesystem/File"), - FileIndex = require("filesystem/FileIndex"); + FileIndex = require("filesystem/FileIndex"), + WatchedRoot = require("filesystem/WatchedRoot"); /** * @constructor @@ -194,14 +195,11 @@ define(function (require, exports, module) { }; /** - * The set of watched roots, encoded as a mapping from full paths to objects - * which contain a file entry, filter function, and an indication of whether - * the watched root is full active (instead of, e.g., in the process of - * starting up). + * The set of watched roots, encoded as a mapping from full paths to WatchedRoot + * objects which contain a file entry, filter function, and an indication of + * whether the watched root is inactive, starting up or fully active. * - * @type{Object.} + * @type{Object.} */ FileSystem.prototype._watchedRoots = null; @@ -233,7 +231,7 @@ define(function (require, exports, module) { * @private * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a * non-strict descendent of watchedRoot.entry. - * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots. * @param {function(?string)} callback - A function that is called once the * watch is complete, possibly with a FileSystemError string. * @param {boolean} shouldWatch - Whether the entry should be watched (true) @@ -306,7 +304,7 @@ define(function (require, exports, module) { * @private * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a * non-strict descendent of watchedRoot.entry. - * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots. * @param {function(?string)} callback - A function that is called once the * watch is complete, possibly with a FileSystemError string. */ @@ -320,7 +318,7 @@ define(function (require, exports, module) { * @private * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a * non-strict descendent of watchedRoot.entry. - * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots. * @param {function(?string)} callback - A function that is called once the * watch is complete, possibly with a FileSystemError string. */ @@ -792,17 +790,14 @@ define(function (require, exports, module) { * string parametr. */ FileSystem.prototype.watch = function (entry, filter, callback) { - var fullPath = entry.fullPath, - watchedRoot = { - entry : entry, - filter : filter, - active : false - }; + var fullPath = entry.fullPath; callback = callback || function () {}; var watchingParentRoot = this._findWatchedRootForPath(fullPath); - if (watchingParentRoot && watchingParentRoot.active) { + if (watchingParentRoot && + (watchingParentRoot.status === WatchedRoot.STARTING || + watchingParentRoot.status === WatchedRoot.ACTIVE)) { callback("A parent of this root is already watched"); return; } @@ -814,12 +809,20 @@ define(function (require, exports, module) { return watchedPath.indexOf(fullPath) === 0; }, this); - if (watchingChildRoot && watchingChildRoot.active) { + if (watchingChildRoot && + (watchingChildRoot.status === WatchedRoot.STARTING || + watchingChildRoot.status === WatchedRoot.ACTIVE)) { callback("A child of this root is already watched"); return; } + var watchedRoot = new WatchedRoot(entry, filter); + this._watchedRoots[fullPath] = watchedRoot; + + // Enter the STARTING state early to indiate that watched Directory + // objects may cache their contents. See FileSystemEntry._isWatched. + watchedRoot.status = WatchedRoot.STARTING; this._watchEntry(entry, watchedRoot, function (err) { if (err) { @@ -829,7 +832,7 @@ define(function (require, exports, module) { return; } - watchedRoot.active = true; + watchedRoot.status = WatchedRoot.ACTIVE; callback(null); }.bind(this)); @@ -857,7 +860,7 @@ define(function (require, exports, module) { // Mark this as inactive, but don't delete the entry until the unwatch is complete. // This is useful for making sure we don't try to concurrently watch overlapping roots. - watchedRoot.active = false; + watchedRoot.status = WatchedRoot.INACTIVE; this._unwatchEntry(entry, watchedRoot, function (err) { delete this._watchedRoots[fullPath]; @@ -889,7 +892,7 @@ define(function (require, exports, module) { Object.keys(this._watchedRoots).forEach(function (path) { var watchedRoot = this._watchedRoots[path]; - watchedRoot.active = false; + watchedRoot.status = WatchedRoot.INACTIVE; delete this._watchedRoots[path]; this._unwatchEntry(watchedRoot.entry, watchedRoot, function () { console.warn("Watching disabled for", watchedRoot.entry.fullPath); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index 923767fc899..df131f25d72 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -67,7 +67,8 @@ define(function (require, exports, module) { "use strict"; - var FileSystemError = require("filesystem/FileSystemError"); + var FileSystemError = require("filesystem/FileSystemError"), + WatchedRoot = require("filesystem/WatchedRoot"); var VISIT_DEFAULT_MAX_DEPTH = 100, VISIT_DEFAULT_MAX_ENTRIES = 30000; @@ -179,9 +180,12 @@ define(function (require, exports, module) { /** * Determines whether or not the entry is watched. + * @param {boolean=} relaxed If falsey, the method will only return true if + * the watched root is fully active. If true, the method will return + * true if the watched root is either starting up or fully active. * @return {boolean} */ - FileSystemEntry.prototype._isWatched = function () { + FileSystemEntry.prototype._isWatched = function (relaxed) { var watchedRoot = this._watchedRoot, filterResult = this._watchedRootFilterResult; @@ -196,7 +200,8 @@ define(function (require, exports, module) { } if (watchedRoot) { - if (watchedRoot.active) { + if (watchedRoot.status === WatchedRoot.ACTIVE || + (relaxed && watchedRoot.status === WatchedRoot.STARTING)) { return filterResult; } else { // We had a watched root, but it's no longer active, so it must now be invalid. diff --git a/src/filesystem/WatchedRoot.js b/src/filesystem/WatchedRoot.js new file mode 100644 index 00000000000..99718526991 --- /dev/null +++ b/src/filesystem/WatchedRoot.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +define(function (require, exports, module) { + "use strict"; + + /* + * @constructor + * Represents file or directory structure watched by the FileSystem. If the + * entry is a directory, all children (that pass the supplied filter function) + * are also watched. A WatchedRoot object begins and ends its life in the + * INACTIVE state. While in the process of starting up watchers, the WatchedRoot + * is in the STARTING state. When watchers are ready, the WatchedRoot enters + * the ACTIVE state. + * + * See the FileSystem class for more details. + * + * @param {File|Directory} entry + * @param {function(string, string):boolean} filter + */ + function WatchedRoot(entry, filter) { + this.entry = entry; + this.filter = filter; + } + + // Status constants + WatchedRoot.INACTIVE = 0; + WatchedRoot.STARTING = 1; + WatchedRoot.ACTIVE = 2; + + /** + * @type {File|Directory} + */ + WatchedRoot.prototype.entry = null; + + /** + * @type {function(string, string):boolean} + */ + WatchedRoot.prototype.filter = null; + + /** + * @type {number} + */ + WatchedRoot.prototype.status = WatchedRoot.INACTIVE; + + + // Export this class + module.exports = WatchedRoot; +}); From 68eef10535d4dac411f388fbd4f16aa495f3e876 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 3 Jan 2014 12:23:10 -0800 Subject: [PATCH 80/94] Improved optional parameter initialization in File.write and FileSystemEntry.visit --- src/filesystem/File.js | 12 ++++++++---- src/filesystem/FileSystemEntry.js | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/filesystem/File.js b/src/filesystem/File.js index 736e6151ae0..177aefbf9f8 100644 --- a/src/filesystem/File.js +++ b/src/filesystem/File.js @@ -132,17 +132,21 @@ define(function (require, exports, module) { * * @param {string} data Data to write. * @param {object=} options Currently unused. - * @param {!function (?string, FileSystemStats=)=} callback Callback that is passed the + * @param {function (?string, FileSystemStats=)=} callback Callback that is passed the * FileSystemError string or the file's new stats. */ File.prototype.write = function (data, options, callback) { - if (typeof (options) === "function") { + if (typeof options === "function") { callback = options; options = {}; + } else { + if (options === undefined) { + options = {}; + } + + callback = callback || function () {}; } - callback = callback || function () {}; - // Request a consistency check if the write is not blind if (!options.blind) { options.expectedHash = this._hash; diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js index df131f25d72..624c6708844 100644 --- a/src/filesystem/FileSystemEntry.js +++ b/src/filesystem/FileSystemEntry.js @@ -520,8 +520,12 @@ define(function (require, exports, module) { if (typeof options === "function") { callback = options; options = {}; - } else if (options === undefined) { - options = {}; + } else { + if (options === undefined) { + options = {}; + } + + callback = callback || function () {}; } if (options.maxDepth === undefined) { From e1a3614d324f4c232f80cabfdd17459e0b467e8f Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Fri, 3 Jan 2014 14:34:10 -0800 Subject: [PATCH 81/94] Make SpecRunnerUtils.createTextFile writes blind because overwriting existing, unread files is fine. --- test/spec/SpecRunnerUtils.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 842c04bf5ad..348170dbd98 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -743,9 +743,12 @@ define(function (require, exports, module) { */ function createTextFile(path, text, fileSystem) { var deferred = new $.Deferred(), - file = fileSystem.getFileForPath(path); + file = fileSystem.getFileForPath(path), + options = { + blind: true // overwriting previous files is OK + }; - file.write(text, function (err) { + file.write(text, options, function (err) { if (!err) { deferred.resolve(file); } else { From 2914b980af16547bbaaa0eca93603f6aff4ea593 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Mon, 6 Jan 2014 12:05:25 -0800 Subject: [PATCH 82/94] maintain selection in refreshFileTree --- src/project/ProjectManager.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index f343ce9c764..8a2c3752343 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1155,20 +1155,30 @@ define(function (require, exports, module) { * fails to reload. */ function refreshFileTree() { - var selectedEntry; + var selectedEntry, + deferred = new $.Deferred(); + if (_lastSelected) { selectedEntry = _lastSelected.data("entry"); + } else { + selectedEntry = getSelectedItem(); } _lastSelected = null; - return _loadProject(getProjectRoot().fullPath, true) - .done(function () { + _loadProject(getProjectRoot().fullPath, true) + .then(function () { if (selectedEntry) { - _findTreeNode(selectedEntry).done(function ($node) { - _forceSelection(null, $node); - }); + _findTreeNode(selectedEntry) + .then(function ($node) { + _forceSelection(null, $node); + deferred.resolve(); + }, deferred.reject); + } else { + deferred.resolve(); } - }); + }, deferred.reject); + + return deferred.promise(); } /** From 4f07182bb7d6cfd1a2661f23a711679441293f9b Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 6 Jan 2014 15:31:23 -0800 Subject: [PATCH 83/94] Delay the promise resolution until after we find a tree node withe selection --- src/project/ProjectManager.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 8a2c3752343..3a148d8a06a 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1155,8 +1155,7 @@ define(function (require, exports, module) { * fails to reload. */ function refreshFileTree() { - var selectedEntry, - deferred = new $.Deferred(); + var selectedEntry; if (_lastSelected) { selectedEntry = _lastSelected.data("entry"); @@ -1165,20 +1164,15 @@ define(function (require, exports, module) { } _lastSelected = null; - _loadProject(getProjectRoot().fullPath, true) + return _loadProject(getProjectRoot().fullPath, true) .then(function () { if (selectedEntry) { - _findTreeNode(selectedEntry) - .then(function ($node) { + return _findTreeNode(selectedEntry) + .done(function ($node) { _forceSelection(null, $node); - deferred.resolve(); - }, deferred.reject); - } else { - deferred.resolve(); + }); } - }, deferred.reject); - - return deferred.promise(); + }); } /** From 1d608b2a00dc2c2ecd2e4f3e29418a125ccf1229 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Mon, 6 Jan 2014 15:52:26 -0800 Subject: [PATCH 84/94] Always resolve refreshFileTree when _loadProject succeeds --- src/project/ProjectManager.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 3a148d8a06a..1807454f795 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1149,13 +1149,15 @@ define(function (require, exports, module) { } /** - * Reloads the project's file tree + * Reloads the project's file tree, maintaining the current selection. * @return {$.Promise} A promise object that will be resolved when the * project tree is reloaded, or rejected if the project path - * fails to reload. + * fails to reload. If the previous selected entry is not found, + * the promise is still resolved. */ function refreshFileTree() { - var selectedEntry; + var selectedEntry, + deferred = new $.Deferred(); if (_lastSelected) { selectedEntry = _lastSelected.data("entry"); @@ -1164,15 +1166,21 @@ define(function (require, exports, module) { } _lastSelected = null; - return _loadProject(getProjectRoot().fullPath, true) + _loadProject(getProjectRoot().fullPath, true) .then(function () { if (selectedEntry) { - return _findTreeNode(selectedEntry) + // restore selection, always resolve + _findTreeNode(selectedEntry) .done(function ($node) { _forceSelection(null, $node); - }); + }) + .always(deferred.resolve); + } else { + deferred.resolve(); } - }); + }, deferred.reject); + + return deferred.promise(); } /** From 3cc8955dc16d207518b7911c0b40c432ed760052 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Mon, 6 Jan 2014 16:49:12 -0800 Subject: [PATCH 85/94] Share temp project for ProjectManager-test. Code cleanup. --- .../toDelete1/file.js | 0 test/spec/ProjectManager-test.js | 204 ++++-------------- 2 files changed, 42 insertions(+), 162 deletions(-) create mode 100644 test/spec/ProjectManager-test-files/toDelete1/file.js diff --git a/test/spec/ProjectManager-test-files/toDelete1/file.js b/test/spec/ProjectManager-test-files/toDelete1/file.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/spec/ProjectManager-test.js b/test/spec/ProjectManager-test.js index 4d72ae21136..117291cc824 100644 --- a/test/spec/ProjectManager-test.js +++ b/test/spec/ProjectManager-test.js @@ -23,7 +23,7 @@ /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ -/*global $, define, require, describe, it, expect, beforeEach, afterEach, waits, waitsFor, runs, waitsForDone, beforeFirst, afterLast */ +/*global $, jasmine, define, require, describe, it, expect, beforeEach, afterEach, waits, waitsFor, runs, waitsForDone, beforeFirst, afterLast */ define(function (require, exports, module) { "use strict"; @@ -43,11 +43,18 @@ define(function (require, exports, module) { this.category = "integration"; var testPath = SpecRunnerUtils.getTestPath("/spec/ProjectManager-test-files"), - changeEventFired, + tempDir = SpecRunnerUtils.getTempDirectory(), testWindow, brackets; beforeFirst(function () { + SpecRunnerUtils.createTempDirectory(); + + // copy files to temp directory + runs(function () { + waitsForDone(SpecRunnerUtils.copy(testPath, tempDir), "copy temp files"); + }); + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { testWindow = w; @@ -57,11 +64,7 @@ define(function (require, exports, module) { CommandManager = testWindow.brackets.test.CommandManager; FileSystem = testWindow.brackets.test.FileSystem; - SpecRunnerUtils.loadProjectInTestWindow(testPath); - - FileSystem.on("change", function () { - changeEventFired = true; - }); + SpecRunnerUtils.loadProjectInTestWindow(tempDir); }); }); @@ -71,8 +74,12 @@ define(function (require, exports, module) { ProjectManager = null; CommandManager = null; SpecRunnerUtils.closeTestWindow(); + SpecRunnerUtils.removeTempDirectory(); }); + afterEach(function () { + testWindow.closeAllFiles(); + }); describe("createNewItem", function () { it("should create a new file with a given name", function () { @@ -80,14 +87,14 @@ define(function (require, exports, module) { runs(function () { // skip rename - ProjectManager.createNewItem(testPath, "Untitled.js", true) + ProjectManager.createNewItem(tempDir, "Untitled.js", true) .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 1000); var error, stat, complete = false; - var filePath = testPath + "/Untitled.js"; + var filePath = tempDir + "/Untitled.js"; var file = FileSystem.getFileForPath(filePath); runs(function () { @@ -100,25 +107,10 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, 1000); - var unlinkError = null; runs(function () { expect(error).toBeFalsy(); expect(stat.isFile).toBe(true); - - // delete the new file - complete = false; - file.unlink(function (err) { - unlinkError = err; - complete = true; - }); }); - waitsFor( - function () { - return complete && (unlinkError === null); - }, - "unlink() failed to cleanup Untitled.js, err=" + unlinkError, - 1000 - ); }); it("should fail when a file already exists", function () { @@ -126,7 +118,7 @@ define(function (require, exports, module) { runs(function () { // skip rename - ProjectManager.createNewItem(testPath, "file.js", true) + ProjectManager.createNewItem(tempDir, "file.js", true) .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); @@ -145,7 +137,7 @@ define(function (require, exports, module) { runs(function () { // skip rename - ProjectManager.createNewItem(testPath, "directory", true) + ProjectManager.createNewItem(tempDir, "directory", true) .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); @@ -167,7 +159,7 @@ define(function (require, exports, module) { function createFile() { // skip rename - ProjectManager.createNewItem(testPath, "file" + charAt + ".js", true) + ProjectManager.createNewItem(tempDir, "file" + charAt + ".js", true) .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); } @@ -193,6 +185,7 @@ define(function (require, exports, module) { runs(assertFile); } }); + it("should fail when file name is invalid", function () { var files = ['com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9', @@ -203,7 +196,7 @@ define(function (require, exports, module) { function createFile() { // skip rename - ProjectManager.createNewItem(testPath, fileAt, true) + ProjectManager.createNewItem(tempDir, fileAt, true) .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); } @@ -234,25 +227,15 @@ define(function (require, exports, module) { describe("deleteItem", function () { it("should delete the selected file in the project tree", function () { var complete = false, - newFile = FileSystem.getFileForPath(testPath + "/brackets_unittests_delete_me.js"), + newFile = FileSystem.getFileForPath(tempDir + "/brackets_unittests_delete_me.js"), selectedFile, error, stat; - // Make sure we don't have any test file left from previous failure - // by explicitly deleting the test file if it exists. - runs(function () { - complete = false; - newFile.unlink(function (err) { - complete = true; - }); - }); - waitsFor(function () { return complete; }, "clean up leftover files timeout", 1000); - // Create a file and select it in the project tree. runs(function () { complete = false; - ProjectManager.createNewItem(testPath, "brackets_unittests_delete_me.js", true) + ProjectManager.createNewItem(tempDir, "brackets_unittests_delete_me.js", true) .always(function () { complete = true; }); }); waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); @@ -272,24 +255,15 @@ define(function (require, exports, module) { expect(error).toBeFalsy(); expect(stat.isFile).toBe(true); selectedFile = ProjectManager.getSelectedItem(); - expect(selectedFile.fullPath).toBe(testPath + "/brackets_unittests_delete_me.js"); + expect(selectedFile.fullPath).toBe(tempDir + "/brackets_unittests_delete_me.js"); }); - // Delete the selected file. runs(function () { - complete = false; - changeEventFired = false; - // delete the new file - ProjectManager.deleteItem(selectedFile) - .always(function () { complete = true; }); + var promise = ProjectManager.deleteItem(selectedFile); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 1000); }); - - waitsFor(function () { return complete && changeEventFired; }, "ProjectManager.deleteItem() timeout", 1000); - // Wait for the call to refreshFileTree, which is triggered by the change event, to complete. - waits(500); - // Verify that file no longer exists. runs(function () { complete = false; @@ -311,113 +285,19 @@ define(function (require, exports, module) { }); it("should delete the selected folder and all items in it.", function () { - var complete = false, - newFolderName = testPath + "/brackets_unittests_toDelete/", - rootFolderName = newFolderName, - rootFolderEntry, + var complete = false, + rootFolderName = tempDir + "/toDelete1/", + rootFolderEntry = FileSystem.getDirectoryForPath(rootFolderName), error, - stat; - - // Make sure we don't have any test files/folders left from previous failure - // by explicitly deleting the root test folder if it exists. - runs(function () { - var rootFolder = FileSystem.getDirectoryForPath(rootFolderName); - complete = false; - rootFolder.moveToTrash(function (err) { - complete = true; - }); - }); - waitsFor(function () { return complete; }, "clean up leftover files timeout", 1000); - - // Create a folder - runs(function () { - complete = false; - ProjectManager.createNewItem(testPath, "brackets_unittests_toDelete", true, true) - .always(function () { complete = true; }); - }); - waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); - - runs(function () { - var newFolder = FileSystem.getDirectoryForPath(newFolderName); - complete = false; - newFolder.stat(function (err, _stat) { - error = err; - stat = _stat; - complete = true; - }); - }); - waitsFor(function () { return complete; }, 1000); - - runs(function () { - expect(error).toBeFalsy(); - expect(stat.isDirectory).toBe(true); - - rootFolderEntry = ProjectManager.getSelectedItem(); - expect(rootFolderEntry.fullPath).toBe(testPath + "/brackets_unittests_toDelete/"); - }); - - // Create a sub folder - runs(function () { - complete = false; - ProjectManager.createNewItem(newFolderName, "toDelete1", true, true) - .always(function () { complete = true; }); - }); - waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); - - runs(function () { - newFolderName += "toDelete1/"; - - var newFolder = FileSystem.getDirectoryForPath(newFolderName); - complete = false; - newFolder.stat(function (err, _stat) { - error = err; - stat = _stat; - complete = true; - }); - }); - waitsFor(function () { return complete; }, 1000); - - runs(function () { - expect(error).toBeFalsy(); - expect(stat.isDirectory).toBe(true); - }); - - // Create a file in the sub folder just created. - runs(function () { - complete = false; - ProjectManager.createNewItem(newFolderName, "toDelete2.txt", true) - .always(function () { complete = true; }); - }); - waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); - - runs(function () { - var file = FileSystem.getFileForPath(newFolderName + "/toDelete2.txt"); - complete = false; - file.stat(function (err, _stat) { - error = err; - stat = _stat; - complete = true; - }); - }); - waitsFor(function () { return complete; }, 1000); - - runs(function () { - expect(error).toBeFalsy(); - expect(stat.isFile).toBe(true); - }); + stat, + promise, + entry; // Delete the root folder and all files/folders in it. runs(function () { - complete = false; - changeEventFired = false; - - ProjectManager.deleteItem(rootFolderEntry) - .always(function () { complete = true; }); + promise = ProjectManager.deleteItem(rootFolderEntry); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 1000); }); - waitsFor(function () { return complete && changeEventFired; }, "ProjectManager.deleteItem() timeout", 1000); - - // Wait for the call to refreshFileTree, which is triggered by the change event, to complete. - waits(500); // Verify that the root folder no longer exists. runs(function () { @@ -457,8 +337,8 @@ define(function (require, exports, module) { it("should deselect after opening file not rendered in tree", function () { var promise, - exposedFile = testPath + "/file.js", - unexposedFile = testPath + "/directory/file.js"; + exposedFile = tempDir + "/file.js", + unexposedFile = tempDir + "/directory/file.js"; runs(function () { promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: exposedFile }); @@ -507,9 +387,9 @@ define(function (require, exports, module) { it("should reselect previously selected file when made visible again", function () { var promise, - initialFile = testPath + "/file.js", - folder = testPath + "/directory/", - fileInFolder = testPath + "/directory/file.js"; + initialFile = tempDir + "/file.js", + folder = tempDir + "/directory/", + fileInFolder = tempDir + "/directory/file.js"; runs(function () { promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: initialFile }); @@ -539,9 +419,9 @@ define(function (require, exports, module) { it("should deselect after opening file hidden in tree, but select when made visible again", function () { var promise, - initialFile = testPath + "/file.js", - folder = testPath + "/directory/", - fileInFolder = testPath + "/directory/file.js"; + initialFile = tempDir + "/file.js", + folder = tempDir + "/directory/", + fileInFolder = tempDir + "/directory/file.js"; runs(function () { promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: initialFile }); From 63896e6f79ceba7eb62019bb44c26877ad2a59bd Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 6 Jan 2014 16:51:20 -0800 Subject: [PATCH 86/94] Bump a few timeouts in the DCH tests to account for additional ProjectManager synchronization --- test/spec/DocumentCommandHandlers-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/DocumentCommandHandlers-test.js b/test/spec/DocumentCommandHandlers-test.js index 9376446d9e1..4f25305f59c 100644 --- a/test/spec/DocumentCommandHandlers-test.js +++ b/test/spec/DocumentCommandHandlers-test.js @@ -110,7 +110,7 @@ define(function (require, exports, module) { }); runs(function () { var promise = SpecRunnerUtils.deletePath(fullPath); - waitsForDone(promise, "Remove testfile " + fullPath); + waitsForDone(promise, "Remove testfile " + fullPath, 5000); }); } @@ -190,7 +190,7 @@ define(function (require, exports, module) { }); promise = CommandManager.execute(Commands.FILE_SAVE); - waitsForDone(promise, "Provide new filename"); + waitsForDone(promise, "Provide new filename", 5000); }); runs(function () { From af2b4eaf4e1d8642c05da6b024a26753b717b4e8 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 6 Jan 2014 17:08:08 -0800 Subject: [PATCH 87/94] Bump a few timeouts in the ProjectManager tests to account for additional synchronization --- test/spec/ProjectManager-test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/spec/ProjectManager-test.js b/test/spec/ProjectManager-test.js index 117291cc824..d5c5d63bc48 100644 --- a/test/spec/ProjectManager-test.js +++ b/test/spec/ProjectManager-test.js @@ -91,7 +91,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 5000); var error, stat, complete = false; var filePath = tempDir + "/Untitled.js"; @@ -122,7 +122,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 5000); runs(function () { expect(gotError).toBeTruthy(); @@ -141,7 +141,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 5000); runs(function () { expect(gotError).toBeTruthy(); @@ -181,7 +181,7 @@ define(function (require, exports, module) { charAt = chars.charAt(i); runs(createFile); - waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 5000); runs(assertFile); } }); @@ -218,7 +218,7 @@ define(function (require, exports, module) { fileAt = files[i]; runs(createFile); - waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 5000); runs(assertFile); } }); @@ -238,7 +238,7 @@ define(function (require, exports, module) { ProjectManager.createNewItem(tempDir, "brackets_unittests_delete_me.js", true) .always(function () { complete = true; }); }); - waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); + waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 5000); runs(function () { complete = false; @@ -261,7 +261,7 @@ define(function (require, exports, module) { runs(function () { // delete the new file var promise = ProjectManager.deleteItem(selectedFile); - waitsForDone(promise, "ProjectManager.deleteItem() timeout", 1000); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 5000); }); // Verify that file no longer exists. @@ -296,7 +296,7 @@ define(function (require, exports, module) { // Delete the root folder and all files/folders in it. runs(function () { promise = ProjectManager.deleteItem(rootFolderEntry); - waitsForDone(promise, "ProjectManager.deleteItem() timeout", 1000); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 5000); }); // Verify that the root folder no longer exists. From d0d5648d73afd59469c9a557d47473543e003f0c Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Mon, 6 Jan 2014 17:29:55 -0800 Subject: [PATCH 88/94] Really REALLY bump up the timeouts --- test/spec/ProjectManager-test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/spec/ProjectManager-test.js b/test/spec/ProjectManager-test.js index d5c5d63bc48..3f6ba3295cc 100644 --- a/test/spec/ProjectManager-test.js +++ b/test/spec/ProjectManager-test.js @@ -91,7 +91,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(function () { return didCreate && !gotError; }, "ProjectManager.createNewItem() timeout", 60000); var error, stat, complete = false; var filePath = tempDir + "/Untitled.js"; @@ -122,7 +122,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 60000); runs(function () { expect(gotError).toBeTruthy(); @@ -141,7 +141,7 @@ define(function (require, exports, module) { .done(function () { didCreate = true; }) .fail(function () { gotError = true; }); }); - waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(function () { return !didCreate && gotError; }, "ProjectManager.createNewItem() timeout", 60000); runs(function () { expect(gotError).toBeTruthy(); @@ -181,7 +181,7 @@ define(function (require, exports, module) { charAt = chars.charAt(i); runs(createFile); - waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 60000); runs(assertFile); } }); @@ -218,7 +218,7 @@ define(function (require, exports, module) { fileAt = files[i]; runs(createFile); - waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(waitForFileCreate, "ProjectManager.createNewItem() timeout", 60000); runs(assertFile); } }); @@ -238,7 +238,7 @@ define(function (require, exports, module) { ProjectManager.createNewItem(tempDir, "brackets_unittests_delete_me.js", true) .always(function () { complete = true; }); }); - waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 5000); + waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 60000); runs(function () { complete = false; @@ -261,7 +261,7 @@ define(function (require, exports, module) { runs(function () { // delete the new file var promise = ProjectManager.deleteItem(selectedFile); - waitsForDone(promise, "ProjectManager.deleteItem() timeout", 5000); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 60000); }); // Verify that file no longer exists. @@ -296,7 +296,7 @@ define(function (require, exports, module) { // Delete the root folder and all files/folders in it. runs(function () { promise = ProjectManager.deleteItem(rootFolderEntry); - waitsForDone(promise, "ProjectManager.deleteItem() timeout", 5000); + waitsForDone(promise, "ProjectManager.deleteItem() timeout", 60000); }); // Verify that the root folder no longer exists. From 7e517ee7c9163567af2868740039d4b55d06064d Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 7 Jan 2014 10:16:30 -0800 Subject: [PATCH 89/94] Bump up timeouts to account for additional Project Manager synchronization --- src/extensions/default/CloseOthers/unittests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extensions/default/CloseOthers/unittests.js b/src/extensions/default/CloseOthers/unittests.js index b855d519d9b..e2ab51c97fa 100644 --- a/src/extensions/default/CloseOthers/unittests.js +++ b/src/extensions/default/CloseOthers/unittests.js @@ -70,7 +70,7 @@ define(function (require, exports, module) { }); runs(function () { var promise = SpecRunnerUtils.deletePath(fullPath); - waitsForDone(promise, "Remove testfile " + fullPath); + waitsForDone(promise, "Remove testfile " + fullPath, 5000); }); } @@ -108,7 +108,7 @@ define(function (require, exports, module) { }); var promise = CommandManager.execute(Commands.FILE_SAVE_ALL); - waitsForDone(promise, "FILE_SAVE_ALL"); + waitsForDone(promise, "FILE_SAVE_ALL", 60000); }); }); From 26309a5dc6a6ce3809e0c5b0d3c7ce2d7a1dd4b3 Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Tue, 7 Jan 2014 13:57:28 -0800 Subject: [PATCH 90/94] Fix selection update after fs change event. Remove unnecessary updates when no added or removed files are present. --- src/project/ProjectManager.js | 84 +++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 1807454f795..8f2f6fa8862 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1301,7 +1301,11 @@ define(function (require, exports, module) { } // Create the node and open the editor - _projectTree.one("create.jstree", deferred.resolve); + _projectTree.one("create.jstree", function () { + // Redraw selection + _redraw(true); + deferred.resolve(); + }); _projectTree.jstree("create", $target, position || 0, data, null, skipRename); return deferred.promise(); @@ -1836,48 +1840,52 @@ define(function (require, exports, module) { } // Directory contents removed - _fileTreeChangeQueue.add(function () { - return Async.doSequentially(removed, function (removedEntry) { - return _deleteTreeNode(removedEntry); - }, false); - }); + if (removed) { + _fileTreeChangeQueue.add(function () { + return Async.doSequentially(removed, function (removedEntry) { + return _deleteTreeNode(removedEntry); + }, false); + }); + } // Directory contents added - _fileTreeChangeQueue.add(function () { - // Find parent node to add to. Use shallowSearch=true to - // skip adding a child if it's parent is not visible - return _findTreeNode(entry, true).then(function ($directoryNode) { - if ($directoryNode && !$directoryNode.hasClass("jstree-closed")) { - return Async.doSequentially(added, function (addedEntry) { - var json = _entryToJSON(addedEntry); - - // _entryToJSON returns null if the added file is filtered from view - if (json) { + if (added) { + _fileTreeChangeQueue.add(function () { + // Find parent node to add to. Use shallowSearch=true to + // skip adding a child if it's parent is not visible + return _findTreeNode(entry, true).then(function ($directoryNode) { + if ($directoryNode && !$directoryNode.hasClass("jstree-closed")) { + return Async.doSequentially(added, function (addedEntry) { + var json = _entryToJSON(addedEntry); - // Before creating a new node, make sure it doesn't already exist. - // TODO: Improve the efficiency of this search! - return _findTreeNode(addedEntry).then(function ($childNode) { - if ($childNode) { - // the node already exists; do nothing; - return new $.Deferred().resolve(); - } else { - // The node wasn't found; create it. - // Position is irrelevant due to sorting + // _entryToJSON returns null if the added file is filtered from view + if (json) { + + // Before creating a new node, make sure it doesn't already exist. + // TODO: Improve the efficiency of this search! + return _findTreeNode(addedEntry).then(function ($childNode) { + if ($childNode) { + // the node already exists; do nothing; + return new $.Deferred().resolve(); + } else { + // The node wasn't found; create it. + // Position is irrelevant due to sorting + return _createNode($directoryNode, null, json, true); + } + }, function () { + // The node doesn't exist; create it. return _createNode($directoryNode, null, json, true); - } - }, function () { - // The node doesn't exist; create it. - return _createNode($directoryNode, null, json, true); - }); - } else { - return new $.Deferred().resolve(); - } - }, false); - } else { - return new $.Deferred().resolve(); - } + }); + } else { + return new $.Deferred().resolve(); + } + }, false); + } else { + return new $.Deferred().resolve(); + } + }); }); - }); + } } }; From c2680b04dd83941af50413241440ef29a623224c Mon Sep 17 00:00:00 2001 From: Jason San Jose Date: Tue, 7 Jan 2014 14:43:17 -0800 Subject: [PATCH 91/94] fix check for added and removed files in fs change event handler --- src/project/ProjectManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 8f2f6fa8862..b247aba68e8 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1840,7 +1840,7 @@ define(function (require, exports, module) { } // Directory contents removed - if (removed) { + if (removed.length > 0) { _fileTreeChangeQueue.add(function () { return Async.doSequentially(removed, function (removedEntry) { return _deleteTreeNode(removedEntry); @@ -1849,7 +1849,7 @@ define(function (require, exports, module) { } // Directory contents added - if (added) { + if (added.length > 0) { _fileTreeChangeQueue.add(function () { // Find parent node to add to. Use shallowSearch=true to // skip adding a child if it's parent is not visible From 5a91580e457bac1986a0d2a14662c404b4b09365 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 7 Jan 2014 15:22:49 -0800 Subject: [PATCH 92/94] Don't refresh the file tree on change events for directories outside of the project root, but when we do refresh the file tree, also clear the change queue (using a new PromiseQueue.removeAll method) and use a debounced refreshFileTree call --- src/project/ProjectManager.js | 26 +++++++++++++++++++++++--- src/utils/Async.js | 7 +++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index b247aba68e8..fdd08d9f36a 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1183,6 +1183,17 @@ define(function (require, exports, module) { return deferred.promise(); } + /** + * A debounced version of refreshFileTree that executes on the leading edge + * and also on the trailing edge after a short timeout if called more than + * once. + * @private + */ + var _refreshFileTreeDebounced = _.debounce(refreshFileTree, 100, { + leading: true, + trailing: true + }); + /** * Expands tree nodes to show the given file or folder and selects it. Silently no-ops if the * path lies outside the project, or if it doesn't exist. @@ -1827,15 +1838,24 @@ define(function (require, exports, module) { // Reset allFiles cache _allFilesCachePromise = null; + // A whole-sale change event; refresh the entire file tree if (!entry) { - refreshFileTree(); + _fileTreeChangeQueue.removeAll(); + _refreshFileTreeDebounced(); return; } - + + // A change event for a different directory; ignore + if (!isWithinProject(entry.fullPath)) { + return; + } + if (entry.isDirectory) { + // A change event with unknown added and removed sets if (!added || !removed) { // TODO: just update children of entry in this case. - refreshFileTree(); + _fileTreeChangeQueue.removeAll(); + _refreshFileTreeDebounced(); return; } diff --git a/src/utils/Async.js b/src/utils/Async.js index 47dbc2d9e64..60ccb8c4a1d 100644 --- a/src/utils/Async.js +++ b/src/utils/Async.js @@ -404,6 +404,13 @@ define(function (require, exports, module) { } }; + /** + * Removes all pending promises from the queue. + */ + PromiseQueue.prototype.removeAll = function () { + this._queue = []; + }; + /** * @private * Pulls the next operation off the queue and executes it. From 3d821feccf20d40a9eb5674e79c680b13bdcecff Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 7 Jan 2014 16:08:05 -0800 Subject: [PATCH 93/94] Use Brackets shell for copying files to reduce the number of external watcher notifications and increase timeouts --- test/spec/LiveDevelopment-test.js | 40 +++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/spec/LiveDevelopment-test.js b/test/spec/LiveDevelopment-test.js index cb6084a37b3..4f1684f8570 100644 --- a/test/spec/LiveDevelopment-test.js +++ b/test/spec/LiveDevelopment-test.js @@ -79,7 +79,7 @@ define(function (require, exports, module) { function openLiveDevelopmentAndWait() { // start live dev runs(function () { - waitsForDone(LiveDevelopment.open(), "LiveDevelopment.open()", 15000); + waitsForDone(LiveDevelopment.open(), "LiveDevelopment.open() 1", 60000); }); } @@ -98,7 +98,7 @@ define(function (require, exports, module) { // save the file var fileSavePromise = CommandManager.execute(Commands.FILE_SAVE, {doc: doc}); - waitsForDone(fileSavePromise, "FILE_SAVE", 1000); + waitsForDone(fileSavePromise, "FILE_SAVE", 5000); // wrap with a timeout to indicate loadEventFired was not fired return Async.withTimeout(deferred.promise(), 2000); @@ -112,13 +112,13 @@ define(function (require, exports, module) { runs(function () { spyOn(Inspector.Page, "reload"); - waitsForDone(SpecRunnerUtils.openProjectFiles([htmlFile]), "SpecRunnerUtils.openProjectFiles " + htmlFile, 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles([htmlFile]), "SpecRunnerUtils.openProjectFiles " + htmlFile, 10000); }); openLiveDevelopmentAndWait(); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([cssFile]), "SpecRunnerUtils.openProjectFiles " + cssFile, 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles([cssFile]), "SpecRunnerUtils.openProjectFiles " + cssFile, 10000); }); runs(function () { @@ -378,7 +378,7 @@ define(function (require, exports, module) { }); }); - waitsFor(function () { return (fileContent !== null); }, "Load fileContent", 1000); + waitsFor(function () { return (fileContent !== null); }, "Load fileContent", 5000); } } @@ -547,7 +547,7 @@ define(function (require, exports, module) { // copy files to temp directory runs(function () { - waitsForDone(SpecRunnerUtils.copy(testPath, tempDir), "copy temp files"); + waitsForDone(SpecRunnerUtils.copyPath(testPath, tempDir), "copy temp files"); }); // open project @@ -569,7 +569,7 @@ define(function (require, exports, module) { it("should establish a browser connection for an opened html file", function () { //open a file runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); }); openLiveDevelopmentAndWait(); @@ -585,12 +585,12 @@ define(function (require, exports, module) { it("should should not start a browser connection for an opened css file", function () { //open a file runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); }); //start live dev runs(function () { - waitsForFail(LiveDevelopment.open(), "LiveDevelopment.open()", 10000); + waitsForFail(LiveDevelopment.open(), "LiveDevelopment.open() 2", 30000); }); runs(function () { @@ -617,7 +617,7 @@ define(function (require, exports, module) { browserText; runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); }); runs(function () { @@ -631,7 +631,7 @@ define(function (require, exports, module) { }); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); }); openLiveDevelopmentAndWait(); @@ -665,7 +665,7 @@ define(function (require, exports, module) { spyOn(Inspector.Page, "reload").andCallThrough(); enableAgent(LiveDevelopment, "dom"); - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); }); runs(function () { @@ -679,7 +679,7 @@ define(function (require, exports, module) { }); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); }); // Modify some text in test file before starting live dev @@ -742,7 +742,7 @@ define(function (require, exports, module) { function _openSimpleHTML() { runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); }); openLiveDevelopmentAndWait(); @@ -851,7 +851,7 @@ define(function (require, exports, module) { saveDeferred.resolve(); }); CommandManager.execute(Commands.FILE_SAVE, { doc: doc }); - waitsForDone(saveDeferred.promise(), "file finished saving"); + waitsForDone(saveDeferred.promise(), "file finished saving", 5000); }); runs(function () { @@ -874,7 +874,7 @@ define(function (require, exports, module) { spyOn(Inspector.Page, "reload").andCallThrough(); promise = SpecRunnerUtils.openProjectFiles(["simple1.html"]); - waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.html", 1000); + waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.html", 10000); }); openLiveDevelopmentAndWait(); @@ -885,7 +885,7 @@ define(function (require, exports, module) { jsdoc = openDocs["simple1.js"]; }); - waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.js", 1000); + waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.js", 10000); }); runs(function () { @@ -1016,7 +1016,7 @@ define(function (require, exports, module) { function loadFile(fileToLoadIntoEditor) { runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([fileToLoadIntoEditor]), "SpecRunnerUtils.openProjectFiles " + fileToLoadIntoEditor); + waitsForDone(SpecRunnerUtils.openProjectFiles([fileToLoadIntoEditor]), "SpecRunnerUtils.openProjectFiles " + fileToLoadIntoEditor, 10000); }); } @@ -1029,7 +1029,7 @@ define(function (require, exports, module) { SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-1"); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile); + waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile, 10000); }); runs(function () { @@ -1201,7 +1201,7 @@ define(function (require, exports, module) { SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-1"); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile); + waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile, 10000); }); runs(function () { From 6e0c28696750b97c53b17cbcb7fede40985a91a3 Mon Sep 17 00:00:00 2001 From: Ian Wehrman Date: Tue, 7 Jan 2014 16:16:52 -0800 Subject: [PATCH 94/94] Revert LiveDev timeout changes --- test/spec/LiveDevelopment-test.js | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/spec/LiveDevelopment-test.js b/test/spec/LiveDevelopment-test.js index 4f1684f8570..c11c760abe6 100644 --- a/test/spec/LiveDevelopment-test.js +++ b/test/spec/LiveDevelopment-test.js @@ -79,7 +79,7 @@ define(function (require, exports, module) { function openLiveDevelopmentAndWait() { // start live dev runs(function () { - waitsForDone(LiveDevelopment.open(), "LiveDevelopment.open() 1", 60000); + waitsForDone(LiveDevelopment.open(), "LiveDevelopment.open()", 15000); }); } @@ -98,7 +98,7 @@ define(function (require, exports, module) { // save the file var fileSavePromise = CommandManager.execute(Commands.FILE_SAVE, {doc: doc}); - waitsForDone(fileSavePromise, "FILE_SAVE", 5000); + waitsForDone(fileSavePromise, "FILE_SAVE", 1000); // wrap with a timeout to indicate loadEventFired was not fired return Async.withTimeout(deferred.promise(), 2000); @@ -112,13 +112,13 @@ define(function (require, exports, module) { runs(function () { spyOn(Inspector.Page, "reload"); - waitsForDone(SpecRunnerUtils.openProjectFiles([htmlFile]), "SpecRunnerUtils.openProjectFiles " + htmlFile, 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles([htmlFile]), "SpecRunnerUtils.openProjectFiles " + htmlFile, 1000); }); openLiveDevelopmentAndWait(); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([cssFile]), "SpecRunnerUtils.openProjectFiles " + cssFile, 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles([cssFile]), "SpecRunnerUtils.openProjectFiles " + cssFile, 1000); }); runs(function () { @@ -378,7 +378,7 @@ define(function (require, exports, module) { }); }); - waitsFor(function () { return (fileContent !== null); }, "Load fileContent", 5000); + waitsFor(function () { return (fileContent !== null); }, "Load fileContent", 1000); } } @@ -569,7 +569,7 @@ define(function (require, exports, module) { it("should establish a browser connection for an opened html file", function () { //open a file runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); }); openLiveDevelopmentAndWait(); @@ -585,12 +585,12 @@ define(function (require, exports, module) { it("should should not start a browser connection for an opened css file", function () { //open a file runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); }); //start live dev runs(function () { - waitsForFail(LiveDevelopment.open(), "LiveDevelopment.open() 2", 30000); + waitsForFail(LiveDevelopment.open(), "LiveDevelopment.open()", 10000); }); runs(function () { @@ -617,7 +617,7 @@ define(function (require, exports, module) { browserText; runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); }); runs(function () { @@ -631,7 +631,7 @@ define(function (require, exports, module) { }); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); }); openLiveDevelopmentAndWait(); @@ -665,7 +665,7 @@ define(function (require, exports, module) { spyOn(Inspector.Page, "reload").andCallThrough(); enableAgent(LiveDevelopment, "dom"); - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.css"]), "SpecRunnerUtils.openProjectFiles simple1.css", 1000); }); runs(function () { @@ -679,7 +679,7 @@ define(function (require, exports, module) { }); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); }); // Modify some text in test file before starting live dev @@ -742,7 +742,7 @@ define(function (require, exports, module) { function _openSimpleHTML() { runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles(["simple1.html"]), "SpecRunnerUtils.openProjectFiles simple1.html", 1000); }); openLiveDevelopmentAndWait(); @@ -851,7 +851,7 @@ define(function (require, exports, module) { saveDeferred.resolve(); }); CommandManager.execute(Commands.FILE_SAVE, { doc: doc }); - waitsForDone(saveDeferred.promise(), "file finished saving", 5000); + waitsForDone(saveDeferred.promise(), "file finished saving"); }); runs(function () { @@ -874,7 +874,7 @@ define(function (require, exports, module) { spyOn(Inspector.Page, "reload").andCallThrough(); promise = SpecRunnerUtils.openProjectFiles(["simple1.html"]); - waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.html", 10000); + waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.html", 1000); }); openLiveDevelopmentAndWait(); @@ -885,7 +885,7 @@ define(function (require, exports, module) { jsdoc = openDocs["simple1.js"]; }); - waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.js", 10000); + waitsForDone(promise, "SpecRunnerUtils.openProjectFiles simple1.js", 1000); }); runs(function () { @@ -1016,7 +1016,7 @@ define(function (require, exports, module) { function loadFile(fileToLoadIntoEditor) { runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([fileToLoadIntoEditor]), "SpecRunnerUtils.openProjectFiles " + fileToLoadIntoEditor, 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles([fileToLoadIntoEditor]), "SpecRunnerUtils.openProjectFiles " + fileToLoadIntoEditor); }); } @@ -1029,7 +1029,7 @@ define(function (require, exports, module) { SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-1"); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile, 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile); }); runs(function () { @@ -1201,7 +1201,7 @@ define(function (require, exports, module) { SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-1"); runs(function () { - waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile, 10000); + waitsForDone(SpecRunnerUtils.openProjectFiles([indexFile]), "SpecRunnerUtils.openProjectFiles " + indexFile); }); runs(function () {