diff --git a/dist/phoenix-fs.js b/dist/phoenix-fs.js index 3a156fb..ad2555d 100644 --- a/dist/phoenix-fs.js +++ b/dist/phoenix-fs.js @@ -46,6 +46,12 @@ function getWindowsDrives(callback) { }); } +function isSubpath(parent, subPathToCheck) { + const relative = path.relative(parent, subPathToCheck); + const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); + return isSubdir; +} + /** * * @param metadata {Object} Max size can be 4GB @@ -302,7 +308,7 @@ function _unlink(ws, metadata) { // eventEmitterID to watcher const watchersMap = {}; function _watch(ws, metadata) { - const fullPath = metadata.data.path, + const fullPathToWatch = metadata.data.path, // array of anymatch compatible path definition. Eg. ["**/{node_modules,bower_components}/**"]. full path is checked ignoredPaths = metadata.data.ignoredPaths, // contents of a gitIgnore file as text. The given path is used as the base path for gitIgnore @@ -315,11 +321,23 @@ function _watch(ws, metadata) { // Filter function to integrate with chokidar function isIgnored(pathToFilter) { + console.log(`Watching: ${fullPathToWatch} Filtering: ${pathToFilter}`); + if(fullPathToWatch === pathToFilter) { + // if we are watching a file directly given file name, we don't run it though gitignore. + // also we cant get relative path of gitignore with respect to a file as root. + return false; + } + if(!isSubpath(fullPathToWatch, pathToFilter)) { + // Do not watch if the path given is not a subpath of our watched path. + // if we are watching a file directly given file name, Then we get an isignored call for the parent + // dir from chokidar. + return true; + } + const relativePath = path.relative(fullPathToWatch, pathToFilter); if(anymatch(ignoredPaths, pathToFilter)){ debugMode && console.log("ignored watch path: ", pathToFilter, "rel: ",relativePath); return true; } - const relativePath = path.relative(fullPath, pathToFilter); if(relativePath && gitignore.ignores(relativePath)){ debugMode && console.log("ignored watch gitIgnore path: ", pathToFilter, "rel: ",relativePath); return true; @@ -329,9 +347,10 @@ function _watch(ws, metadata) { } let readySent = false; - const watcher = chokidar.watch(fullPath, { + const watcher = chokidar.watch(fullPathToWatch, { persistent, ignoreInitial, + ignorePermissionErrors: true, ignored: path => isIgnored(path) }); const eventEmitterID = generateRandomId(); @@ -350,7 +369,7 @@ function _watch(ws, metadata) { return; } readySent = true; - _reportError(ws, metadata, err, `Error while watching path ${fullPath}`); + _reportError(ws, metadata, err, `Error while watching path ${fullPathToWatch}`); }); let watchEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir']; for(let watchEvent of watchEvents){ @@ -359,7 +378,7 @@ function _watch(ws, metadata) { }); } } catch (err) { - _reportError(ws, metadata, err, `Failed to watch path ${fullPath}`); + _reportError(ws, metadata, err, `Failed to watch path ${fullPathToWatch}`); } } diff --git a/src-tauri/node-src/phoenix-fs.js b/src-tauri/node-src/phoenix-fs.js index 3a156fb..ad2555d 100644 --- a/src-tauri/node-src/phoenix-fs.js +++ b/src-tauri/node-src/phoenix-fs.js @@ -46,6 +46,12 @@ function getWindowsDrives(callback) { }); } +function isSubpath(parent, subPathToCheck) { + const relative = path.relative(parent, subPathToCheck); + const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); + return isSubdir; +} + /** * * @param metadata {Object} Max size can be 4GB @@ -302,7 +308,7 @@ function _unlink(ws, metadata) { // eventEmitterID to watcher const watchersMap = {}; function _watch(ws, metadata) { - const fullPath = metadata.data.path, + const fullPathToWatch = metadata.data.path, // array of anymatch compatible path definition. Eg. ["**/{node_modules,bower_components}/**"]. full path is checked ignoredPaths = metadata.data.ignoredPaths, // contents of a gitIgnore file as text. The given path is used as the base path for gitIgnore @@ -315,11 +321,23 @@ function _watch(ws, metadata) { // Filter function to integrate with chokidar function isIgnored(pathToFilter) { + console.log(`Watching: ${fullPathToWatch} Filtering: ${pathToFilter}`); + if(fullPathToWatch === pathToFilter) { + // if we are watching a file directly given file name, we don't run it though gitignore. + // also we cant get relative path of gitignore with respect to a file as root. + return false; + } + if(!isSubpath(fullPathToWatch, pathToFilter)) { + // Do not watch if the path given is not a subpath of our watched path. + // if we are watching a file directly given file name, Then we get an isignored call for the parent + // dir from chokidar. + return true; + } + const relativePath = path.relative(fullPathToWatch, pathToFilter); if(anymatch(ignoredPaths, pathToFilter)){ debugMode && console.log("ignored watch path: ", pathToFilter, "rel: ",relativePath); return true; } - const relativePath = path.relative(fullPath, pathToFilter); if(relativePath && gitignore.ignores(relativePath)){ debugMode && console.log("ignored watch gitIgnore path: ", pathToFilter, "rel: ",relativePath); return true; @@ -329,9 +347,10 @@ function _watch(ws, metadata) { } let readySent = false; - const watcher = chokidar.watch(fullPath, { + const watcher = chokidar.watch(fullPathToWatch, { persistent, ignoreInitial, + ignorePermissionErrors: true, ignored: path => isIgnored(path) }); const eventEmitterID = generateRandomId(); @@ -350,7 +369,7 @@ function _watch(ws, metadata) { return; } readySent = true; - _reportError(ws, metadata, err, `Error while watching path ${fullPath}`); + _reportError(ws, metadata, err, `Error while watching path ${fullPathToWatch}`); }); let watchEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir']; for(let watchEvent of watchEvents){ @@ -359,7 +378,7 @@ function _watch(ws, metadata) { }); } } catch (err) { - _reportError(ws, metadata, err, `Failed to watch path ${fullPath}`); + _reportError(ws, metadata, err, `Failed to watch path ${fullPathToWatch}`); } } diff --git a/test/test-watcher.browser.js b/test/test-watcher.browser.js index 288c3a7..d1fb9c1 100644 --- a/test/test-watcher.browser.js +++ b/test/test-watcher.browser.js @@ -1,12 +1,18 @@ /* global expect , Filer, fs, waitForTrue, TEST_TYPE_FS_ACCESS, TEST_TYPE_FILER, TEST_TYPE_TAURI, TEST_TYPE_TAURI_WS*/ function _setupTests(testType) { - let testPath, watcher; + let testPath, watcher, watcher2; function consoleLogToShell(message) { return window.__TAURI__.invoke("console_log", {message}); } + async function _waitForSomeTime(timeMS) { + return new Promise(resolve=>{ + setTimeout(resolve, timeMS); + }); + } + async function _validate_exists(path) { let resolveP; const promise = new Promise((resolve) => {resolveP = resolve;}); @@ -82,6 +88,44 @@ function _setupTests(testType) { await promise; } + const ADD_DIR = fs.WATCH_EVENTS.ADD_DIR, + UNLINK_DIR = fs.WATCH_EVENTS.UNLINK_DIR, + ADD_FILE = fs.WATCH_EVENTS.ADD_FILE, + UNLINK_FILE = fs.WATCH_EVENTS.UNLINK_FILE, + CHANGE = fs.WATCH_EVENTS.CHANGE; + + async function _createNewWatcher(watchPath, pathChangeArray) { + const newWatcher = await fs.watchAsync(watchPath); + const watchEvents = [ADD_DIR, UNLINK_DIR, ADD_FILE, UNLINK_FILE, CHANGE]; + for(let watchEvent of watchEvents) { + newWatcher.on(watchEvent, function ({path}) { + console.log(`Watcher: ${newWatcher.eventEmitterID}: path ${watchEvent}`, path); + pathChangeArray.push({path, watchEvent}); + }); + } + return newWatcher; + } + + async function initWatcher(watchPath, pathChangeArray) { + if(watcher) { + await fs.unwatchAsync(watcher); + watcher = null; + } + watcher = await _createNewWatcher(watchPath, pathChangeArray); + console.log("watcher init done: ", watcher.eventEmitterID); + return watcher; + } + + async function initWatcher2(watchPath, pathChangeArray) { + if(watcher2) { + await fs.unwatchAsync(watcher2); + watcher2 = null; + } + watcher2 = await _createNewWatcher(watchPath, pathChangeArray); + console.log("watcher 2 init done: ", watcher2.eventEmitterID); + return watcher2; + } + before(async function () { switch (testType) { case TEST_TYPE_FS_ACCESS: testPath = window.mountTestPath;break; @@ -118,6 +162,10 @@ function _setupTests(testType) { await fs.unwatchAsync(watcher); watcher = null; } + if(watcher2){ + await fs.unwatchAsync(watcher2); + watcher2 = null; + } await _clean(); }); @@ -190,6 +238,24 @@ function _setupTests(testType) { expect(addedPaths[0]).to.eql(pathCreated); }); + it(`Should phoenix ${testType} watch a file`, async function () { + const watchPath = `${testPath}/a.txt`, + fileInSameDirNotWatched = `${testPath}/b.txt`; + await _writeTestFile(watchPath); + + const pathChangeArray = []; + + await initWatcher(watchPath, pathChangeArray); + + await _writeTestFile(watchPath); + await _writeTestFile(fileInSameDirNotWatched); + + await waitForTrue(()=>{return pathChangeArray.length === 1;},10000); + await _waitForSomeTime(100); // maybe some more events might come in so wait for some time to be sure? + expect(pathChangeArray).to.deep.include({ path: watchPath, watchEvent: CHANGE}); + expect(pathChangeArray.length).to.eql(1); + }); + it(`Should phoenix ${testType} watch for file rename`, async function () { const watchPath = `${testPath}/watch`; await _creatDirAndValidate(watchPath); @@ -218,25 +284,46 @@ function _setupTests(testType) { expect(addedPaths[0]).to.eql(pathRenamed); }); - const ADD_DIR = fs.WATCH_EVENTS.ADD_DIR, - UNLINK_DIR = fs.WATCH_EVENTS.UNLINK_DIR, - ADD_FILE = fs.WATCH_EVENTS.ADD_FILE, - UNLINK_FILE = fs.WATCH_EVENTS.UNLINK_FILE, - CHANGE = fs.WATCH_EVENTS.CHANGE; - async function initWatchers(watchPath, pathChangeArray) { - if(watcher) { - await fs.unwatchAsync(watcher); - watcher = null; - } - watcher = await fs.watchAsync(watchPath); - const watchEvents = [ADD_DIR, UNLINK_DIR, ADD_FILE, UNLINK_FILE, CHANGE]; - for(let watchEvent of watchEvents) { - watcher.on(watchEvent, function ({path}) { - console.log(`path ${watchEvent}`, path); - pathChangeArray.push({path, watchEvent}); - }); - } - } + it(`Should phoenix ${testType} watch support multiple watchers concurrently on same dir`, async function () { + const watchPath = `${testPath}/watch`; + await _creatDirAndValidate(watchPath); + + const pathChangeArray = [], watcher2PathChangeArray = []; + let pathCreated = `${watchPath}/x`; + const nestedFile = `${watchPath}/x/a.txt`; + + await initWatcher(watchPath, pathChangeArray); + await initWatcher2(watchPath, watcher2PathChangeArray); + + await _creatDirAndValidate(pathCreated); + await _writeTestFile(nestedFile); + + await waitForTrue(()=>{return pathChangeArray.length === 2;},10000); + await waitForTrue(()=>{return watcher2PathChangeArray.length === 2;},10000); + expect(pathChangeArray).to.deep.include({ path: pathCreated, watchEvent: ADD_DIR}); + expect(pathChangeArray).to.deep.include({ path: nestedFile, watchEvent: ADD_FILE}); + expect(watcher2PathChangeArray).to.deep.include({ path: pathCreated, watchEvent: ADD_DIR}); + expect(watcher2PathChangeArray).to.deep.include({ path: nestedFile, watchEvent: ADD_FILE}); + }); + + it(`Should phoenix ${testType} watch support multiple watchers concurrently on same file`, async function () { + const watchPath = `${testPath}/a.txt`; + await _writeTestFile(watchPath); + + const pathChangeArray = [], watcher2PathChangeArray = []; + + await initWatcher(watchPath, pathChangeArray); + await initWatcher2(watchPath, watcher2PathChangeArray); + + await _writeTestFile(watchPath); + await _writeTestFile(watchPath); + + await waitForTrue(()=>{return pathChangeArray.length === 1;},10000); + await waitForTrue(()=>{return watcher2PathChangeArray.length === 1;},10000); + await _waitForSomeTime(100); // maybe some more events might come in so wait for some time to be sure? + expect(pathChangeArray).to.deep.include({ path: watchPath, watchEvent: CHANGE}); + expect(watcher2PathChangeArray).to.deep.include({ path: watchPath, watchEvent: CHANGE}); + }); it(`Should phoenix ${testType} watch for folder rename with nested contents`, async function () { const watchPath = `${testPath}/watch`; @@ -248,7 +335,7 @@ function _setupTests(testType) { await _creatDirAndValidate(pathCreated); await _writeTestFile(nestedFile); - await initWatchers(watchPath, pathChangeArray); + await initWatcher(watchPath, pathChangeArray); const pathRenamed = `${watchPath}/y`; await _rename(pathCreated, pathRenamed);