You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
785 lines
20 KiB
785 lines
20 KiB
/* |
|
MIT License http://www.opensource.org/licenses/mit-license.php |
|
Author Tobias Koppers @sokra |
|
*/ |
|
"use strict"; |
|
|
|
const EventEmitter = require("events").EventEmitter; |
|
const fs = require("graceful-fs"); |
|
const path = require("path"); |
|
|
|
const watchEventSource = require("./watchEventSource"); |
|
|
|
const EXISTANCE_ONLY_TIME_ENTRY = Object.freeze({}); |
|
|
|
let FS_ACCURACY = 1000; |
|
|
|
const IS_OSX = require("os").platform() === "darwin"; |
|
const WATCHPACK_POLLING = process.env.WATCHPACK_POLLING; |
|
const FORCE_POLLING = |
|
`${+WATCHPACK_POLLING}` === WATCHPACK_POLLING |
|
? +WATCHPACK_POLLING |
|
: !!WATCHPACK_POLLING && WATCHPACK_POLLING !== "false"; |
|
|
|
function withoutCase(str) { |
|
return str.toLowerCase(); |
|
} |
|
|
|
function needCalls(times, callback) { |
|
return function() { |
|
if (--times === 0) { |
|
return callback(); |
|
} |
|
}; |
|
} |
|
|
|
class Watcher extends EventEmitter { |
|
constructor(directoryWatcher, filePath, startTime) { |
|
super(); |
|
this.directoryWatcher = directoryWatcher; |
|
this.path = filePath; |
|
this.startTime = startTime && +startTime; |
|
} |
|
|
|
checkStartTime(mtime, initial) { |
|
const startTime = this.startTime; |
|
if (typeof startTime !== "number") return !initial; |
|
return startTime <= mtime; |
|
} |
|
|
|
close() { |
|
this.emit("closed"); |
|
} |
|
} |
|
|
|
class DirectoryWatcher extends EventEmitter { |
|
constructor(watcherManager, directoryPath, options) { |
|
super(); |
|
if (FORCE_POLLING) { |
|
options.poll = FORCE_POLLING; |
|
} |
|
this.watcherManager = watcherManager; |
|
this.options = options; |
|
this.path = directoryPath; |
|
// safeTime is the point in time after which reading is safe to be unchanged |
|
// timestamp is a value that should be compared with another timestamp (mtime) |
|
/** @type {Map<string, { safeTime: number, timestamp: number }} */ |
|
this.files = new Map(); |
|
/** @type {Map<string, number>} */ |
|
this.filesWithoutCase = new Map(); |
|
this.directories = new Map(); |
|
this.lastWatchEvent = 0; |
|
this.initialScan = true; |
|
this.ignored = options.ignored || (() => false); |
|
this.nestedWatching = false; |
|
this.polledWatching = |
|
typeof options.poll === "number" |
|
? options.poll |
|
: options.poll |
|
? 5007 |
|
: false; |
|
this.timeout = undefined; |
|
this.initialScanRemoved = new Set(); |
|
this.initialScanFinished = undefined; |
|
/** @type {Map<string, Set<Watcher>>} */ |
|
this.watchers = new Map(); |
|
this.parentWatcher = null; |
|
this.refs = 0; |
|
this._activeEvents = new Map(); |
|
this.closed = false; |
|
this.scanning = false; |
|
this.scanAgain = false; |
|
this.scanAgainInitial = false; |
|
|
|
this.createWatcher(); |
|
this.doScan(true); |
|
} |
|
|
|
createWatcher() { |
|
try { |
|
if (this.polledWatching) { |
|
this.watcher = { |
|
close: () => { |
|
if (this.timeout) { |
|
clearTimeout(this.timeout); |
|
this.timeout = undefined; |
|
} |
|
} |
|
}; |
|
} else { |
|
if (IS_OSX) { |
|
this.watchInParentDirectory(); |
|
} |
|
this.watcher = watchEventSource.watch(this.path); |
|
this.watcher.on("change", this.onWatchEvent.bind(this)); |
|
this.watcher.on("error", this.onWatcherError.bind(this)); |
|
} |
|
} catch (err) { |
|
this.onWatcherError(err); |
|
} |
|
} |
|
|
|
forEachWatcher(path, fn) { |
|
const watchers = this.watchers.get(withoutCase(path)); |
|
if (watchers !== undefined) { |
|
for (const w of watchers) { |
|
fn(w); |
|
} |
|
} |
|
} |
|
|
|
setMissing(itemPath, initial, type) { |
|
if (this.initialScan) { |
|
this.initialScanRemoved.add(itemPath); |
|
} |
|
|
|
const oldDirectory = this.directories.get(itemPath); |
|
if (oldDirectory) { |
|
if (this.nestedWatching) oldDirectory.close(); |
|
this.directories.delete(itemPath); |
|
|
|
this.forEachWatcher(itemPath, w => w.emit("remove", type)); |
|
if (!initial) { |
|
this.forEachWatcher(this.path, w => |
|
w.emit("change", itemPath, null, type, initial) |
|
); |
|
} |
|
} |
|
|
|
const oldFile = this.files.get(itemPath); |
|
if (oldFile) { |
|
this.files.delete(itemPath); |
|
const key = withoutCase(itemPath); |
|
const count = this.filesWithoutCase.get(key) - 1; |
|
if (count <= 0) { |
|
this.filesWithoutCase.delete(key); |
|
this.forEachWatcher(itemPath, w => w.emit("remove", type)); |
|
} else { |
|
this.filesWithoutCase.set(key, count); |
|
} |
|
|
|
if (!initial) { |
|
this.forEachWatcher(this.path, w => |
|
w.emit("change", itemPath, null, type, initial) |
|
); |
|
} |
|
} |
|
} |
|
|
|
setFileTime(filePath, mtime, initial, ignoreWhenEqual, type) { |
|
const now = Date.now(); |
|
|
|
if (this.ignored(filePath)) return; |
|
|
|
const old = this.files.get(filePath); |
|
|
|
let safeTime, accuracy; |
|
if (initial) { |
|
safeTime = Math.min(now, mtime) + FS_ACCURACY; |
|
accuracy = FS_ACCURACY; |
|
} else { |
|
safeTime = now; |
|
accuracy = 0; |
|
|
|
if (old && old.timestamp === mtime && mtime + FS_ACCURACY < now - 1000) { |
|
// We are sure that mtime is untouched |
|
// This can be caused by some file attribute change |
|
// e. g. when access time has been changed |
|
// but the file content is untouched |
|
return; |
|
} |
|
} |
|
|
|
if (ignoreWhenEqual && old && old.timestamp === mtime) return; |
|
|
|
this.files.set(filePath, { |
|
safeTime, |
|
accuracy, |
|
timestamp: mtime |
|
}); |
|
|
|
if (!old) { |
|
const key = withoutCase(filePath); |
|
const count = this.filesWithoutCase.get(key); |
|
this.filesWithoutCase.set(key, (count || 0) + 1); |
|
if (count !== undefined) { |
|
// There is already a file with case-insensitive-equal name |
|
// On a case-insensitive filesystem we may miss the renaming |
|
// when only casing is changed. |
|
// To be sure that our information is correct |
|
// we trigger a rescan here |
|
this.doScan(false); |
|
} |
|
|
|
this.forEachWatcher(filePath, w => { |
|
if (!initial || w.checkStartTime(safeTime, initial)) { |
|
w.emit("change", mtime, type); |
|
} |
|
}); |
|
} else if (!initial) { |
|
this.forEachWatcher(filePath, w => w.emit("change", mtime, type)); |
|
} |
|
this.forEachWatcher(this.path, w => { |
|
if (!initial || w.checkStartTime(safeTime, initial)) { |
|
w.emit("change", filePath, safeTime, type, initial); |
|
} |
|
}); |
|
} |
|
|
|
setDirectory(directoryPath, birthtime, initial, type) { |
|
if (this.ignored(directoryPath)) return; |
|
if (directoryPath === this.path) { |
|
if (!initial) { |
|
this.forEachWatcher(this.path, w => |
|
w.emit("change", directoryPath, birthtime, type, initial) |
|
); |
|
} |
|
} else { |
|
const old = this.directories.get(directoryPath); |
|
if (!old) { |
|
const now = Date.now(); |
|
|
|
if (this.nestedWatching) { |
|
this.createNestedWatcher(directoryPath); |
|
} else { |
|
this.directories.set(directoryPath, true); |
|
} |
|
|
|
let safeTime; |
|
if (initial) { |
|
safeTime = Math.min(now, birthtime) + FS_ACCURACY; |
|
} else { |
|
safeTime = now; |
|
} |
|
|
|
this.forEachWatcher(directoryPath, w => { |
|
if (!initial || w.checkStartTime(safeTime, false)) { |
|
w.emit("change", birthtime, type); |
|
} |
|
}); |
|
this.forEachWatcher(this.path, w => { |
|
if (!initial || w.checkStartTime(safeTime, initial)) { |
|
w.emit("change", directoryPath, safeTime, type, initial); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
createNestedWatcher(directoryPath) { |
|
const watcher = this.watcherManager.watchDirectory(directoryPath, 1); |
|
watcher.on("change", (filePath, mtime, type, initial) => { |
|
this.forEachWatcher(this.path, w => { |
|
if (!initial || w.checkStartTime(mtime, initial)) { |
|
w.emit("change", filePath, mtime, type, initial); |
|
} |
|
}); |
|
}); |
|
this.directories.set(directoryPath, watcher); |
|
} |
|
|
|
setNestedWatching(flag) { |
|
if (this.nestedWatching !== !!flag) { |
|
this.nestedWatching = !!flag; |
|
if (this.nestedWatching) { |
|
for (const directory of this.directories.keys()) { |
|
this.createNestedWatcher(directory); |
|
} |
|
} else { |
|
for (const [directory, watcher] of this.directories) { |
|
watcher.close(); |
|
this.directories.set(directory, true); |
|
} |
|
} |
|
} |
|
} |
|
|
|
watch(filePath, startTime) { |
|
const key = withoutCase(filePath); |
|
let watchers = this.watchers.get(key); |
|
if (watchers === undefined) { |
|
watchers = new Set(); |
|
this.watchers.set(key, watchers); |
|
} |
|
this.refs++; |
|
const watcher = new Watcher(this, filePath, startTime); |
|
watcher.on("closed", () => { |
|
if (--this.refs <= 0) { |
|
this.close(); |
|
return; |
|
} |
|
watchers.delete(watcher); |
|
if (watchers.size === 0) { |
|
this.watchers.delete(key); |
|
if (this.path === filePath) this.setNestedWatching(false); |
|
} |
|
}); |
|
watchers.add(watcher); |
|
let safeTime; |
|
if (filePath === this.path) { |
|
this.setNestedWatching(true); |
|
safeTime = this.lastWatchEvent; |
|
for (const entry of this.files.values()) { |
|
fixupEntryAccuracy(entry); |
|
safeTime = Math.max(safeTime, entry.safeTime); |
|
} |
|
} else { |
|
const entry = this.files.get(filePath); |
|
if (entry) { |
|
fixupEntryAccuracy(entry); |
|
safeTime = entry.safeTime; |
|
} else { |
|
safeTime = 0; |
|
} |
|
} |
|
if (safeTime) { |
|
if (safeTime >= startTime) { |
|
process.nextTick(() => { |
|
if (this.closed) return; |
|
if (filePath === this.path) { |
|
watcher.emit( |
|
"change", |
|
filePath, |
|
safeTime, |
|
"watch (outdated on attach)", |
|
true |
|
); |
|
} else { |
|
watcher.emit( |
|
"change", |
|
safeTime, |
|
"watch (outdated on attach)", |
|
true |
|
); |
|
} |
|
}); |
|
} |
|
} else if (this.initialScan) { |
|
if (this.initialScanRemoved.has(filePath)) { |
|
process.nextTick(() => { |
|
if (this.closed) return; |
|
watcher.emit("remove"); |
|
}); |
|
} |
|
} else if ( |
|
!this.directories.has(filePath) && |
|
watcher.checkStartTime(this.initialScanFinished, false) |
|
) { |
|
process.nextTick(() => { |
|
if (this.closed) return; |
|
watcher.emit("initial-missing", "watch (missing on attach)"); |
|
}); |
|
} |
|
return watcher; |
|
} |
|
|
|
onWatchEvent(eventType, filename) { |
|
if (this.closed) return; |
|
if (!filename) { |
|
// In some cases no filename is provided |
|
// This seem to happen on windows |
|
// So some event happened but we don't know which file is affected |
|
// We have to do a full scan of the directory |
|
this.doScan(false); |
|
return; |
|
} |
|
|
|
const filePath = path.join(this.path, filename); |
|
if (this.ignored(filePath)) return; |
|
|
|
if (this._activeEvents.get(filename) === undefined) { |
|
this._activeEvents.set(filename, false); |
|
const checkStats = () => { |
|
if (this.closed) return; |
|
this._activeEvents.set(filename, false); |
|
fs.lstat(filePath, (err, stats) => { |
|
if (this.closed) return; |
|
if (this._activeEvents.get(filename) === true) { |
|
process.nextTick(checkStats); |
|
return; |
|
} |
|
this._activeEvents.delete(filename); |
|
// ENOENT happens when the file/directory doesn't exist |
|
// EPERM happens when the containing directory doesn't exist |
|
if (err) { |
|
if ( |
|
err.code !== "ENOENT" && |
|
err.code !== "EPERM" && |
|
err.code !== "EBUSY" |
|
) { |
|
this.onStatsError(err); |
|
} else { |
|
if (filename === path.basename(this.path)) { |
|
// This may indicate that the directory itself was removed |
|
if (!fs.existsSync(this.path)) { |
|
this.onDirectoryRemoved("stat failed"); |
|
} |
|
} |
|
} |
|
} |
|
this.lastWatchEvent = Date.now(); |
|
if (!stats) { |
|
this.setMissing(filePath, false, eventType); |
|
} else if (stats.isDirectory()) { |
|
this.setDirectory( |
|
filePath, |
|
+stats.birthtime || 1, |
|
false, |
|
eventType |
|
); |
|
} else if (stats.isFile() || stats.isSymbolicLink()) { |
|
if (stats.mtime) { |
|
ensureFsAccuracy(stats.mtime); |
|
} |
|
this.setFileTime( |
|
filePath, |
|
+stats.mtime || +stats.ctime || 1, |
|
false, |
|
false, |
|
eventType |
|
); |
|
} |
|
}); |
|
}; |
|
process.nextTick(checkStats); |
|
} else { |
|
this._activeEvents.set(filename, true); |
|
} |
|
} |
|
|
|
onWatcherError(err) { |
|
if (this.closed) return; |
|
if (err) { |
|
if (err.code !== "EPERM" && err.code !== "ENOENT") { |
|
console.error("Watchpack Error (watcher): " + err); |
|
} |
|
this.onDirectoryRemoved("watch error"); |
|
} |
|
} |
|
|
|
onStatsError(err) { |
|
if (err) { |
|
console.error("Watchpack Error (stats): " + err); |
|
} |
|
} |
|
|
|
onScanError(err) { |
|
if (err) { |
|
console.error("Watchpack Error (initial scan): " + err); |
|
} |
|
this.onScanFinished(); |
|
} |
|
|
|
onScanFinished() { |
|
if (this.polledWatching) { |
|
this.timeout = setTimeout(() => { |
|
if (this.closed) return; |
|
this.doScan(false); |
|
}, this.polledWatching); |
|
} |
|
} |
|
|
|
onDirectoryRemoved(reason) { |
|
if (this.watcher) { |
|
this.watcher.close(); |
|
this.watcher = null; |
|
} |
|
this.watchInParentDirectory(); |
|
const type = `directory-removed (${reason})`; |
|
for (const directory of this.directories.keys()) { |
|
this.setMissing(directory, null, type); |
|
} |
|
for (const file of this.files.keys()) { |
|
this.setMissing(file, null, type); |
|
} |
|
} |
|
|
|
watchInParentDirectory() { |
|
if (!this.parentWatcher) { |
|
const parentDir = path.dirname(this.path); |
|
// avoid watching in the root directory |
|
// removing directories in the root directory is not supported |
|
if (path.dirname(parentDir) === parentDir) return; |
|
|
|
this.parentWatcher = this.watcherManager.watchFile(this.path, 1); |
|
this.parentWatcher.on("change", (mtime, type) => { |
|
if (this.closed) return; |
|
|
|
// On non-osx platforms we don't need this watcher to detect |
|
// directory removal, as an EPERM error indicates that |
|
if ((!IS_OSX || this.polledWatching) && this.parentWatcher) { |
|
this.parentWatcher.close(); |
|
this.parentWatcher = null; |
|
} |
|
// Try to create the watcher when parent directory is found |
|
if (!this.watcher) { |
|
this.createWatcher(); |
|
this.doScan(false); |
|
|
|
// directory was created so we emit an event |
|
this.forEachWatcher(this.path, w => |
|
w.emit("change", this.path, mtime, type, false) |
|
); |
|
} |
|
}); |
|
this.parentWatcher.on("remove", () => { |
|
this.onDirectoryRemoved("parent directory removed"); |
|
}); |
|
} |
|
} |
|
|
|
doScan(initial) { |
|
if (this.scanning) { |
|
if (this.scanAgain) { |
|
if (!initial) this.scanAgainInitial = false; |
|
} else { |
|
this.scanAgain = true; |
|
this.scanAgainInitial = initial; |
|
} |
|
return; |
|
} |
|
this.scanning = true; |
|
if (this.timeout) { |
|
clearTimeout(this.timeout); |
|
this.timeout = undefined; |
|
} |
|
process.nextTick(() => { |
|
if (this.closed) return; |
|
fs.readdir(this.path, (err, items) => { |
|
if (this.closed) return; |
|
if (err) { |
|
if (err.code === "ENOENT" || err.code === "EPERM") { |
|
this.onDirectoryRemoved("scan readdir failed"); |
|
} else { |
|
this.onScanError(err); |
|
} |
|
this.initialScan = false; |
|
this.initialScanFinished = Date.now(); |
|
if (initial) { |
|
for (const watchers of this.watchers.values()) { |
|
for (const watcher of watchers) { |
|
if (watcher.checkStartTime(this.initialScanFinished, false)) { |
|
watcher.emit( |
|
"initial-missing", |
|
"scan (parent directory missing in initial scan)" |
|
); |
|
} |
|
} |
|
} |
|
} |
|
if (this.scanAgain) { |
|
this.scanAgain = false; |
|
this.doScan(this.scanAgainInitial); |
|
} else { |
|
this.scanning = false; |
|
} |
|
return; |
|
} |
|
const itemPaths = new Set( |
|
items.map(item => path.join(this.path, item.normalize("NFC"))) |
|
); |
|
for (const file of this.files.keys()) { |
|
if (!itemPaths.has(file)) { |
|
this.setMissing(file, initial, "scan (missing)"); |
|
} |
|
} |
|
for (const directory of this.directories.keys()) { |
|
if (!itemPaths.has(directory)) { |
|
this.setMissing(directory, initial, "scan (missing)"); |
|
} |
|
} |
|
if (this.scanAgain) { |
|
// Early repeat of scan |
|
this.scanAgain = false; |
|
this.doScan(initial); |
|
return; |
|
} |
|
const itemFinished = needCalls(itemPaths.size + 1, () => { |
|
if (this.closed) return; |
|
this.initialScan = false; |
|
this.initialScanRemoved = null; |
|
this.initialScanFinished = Date.now(); |
|
if (initial) { |
|
const missingWatchers = new Map(this.watchers); |
|
missingWatchers.delete(withoutCase(this.path)); |
|
for (const item of itemPaths) { |
|
missingWatchers.delete(withoutCase(item)); |
|
} |
|
for (const watchers of missingWatchers.values()) { |
|
for (const watcher of watchers) { |
|
if (watcher.checkStartTime(this.initialScanFinished, false)) { |
|
watcher.emit( |
|
"initial-missing", |
|
"scan (missing in initial scan)" |
|
); |
|
} |
|
} |
|
} |
|
} |
|
if (this.scanAgain) { |
|
this.scanAgain = false; |
|
this.doScan(this.scanAgainInitial); |
|
} else { |
|
this.scanning = false; |
|
this.onScanFinished(); |
|
} |
|
}); |
|
for (const itemPath of itemPaths) { |
|
fs.lstat(itemPath, (err2, stats) => { |
|
if (this.closed) return; |
|
if (err2) { |
|
if ( |
|
err2.code === "ENOENT" || |
|
err2.code === "EPERM" || |
|
err2.code === "EACCES" || |
|
err2.code === "EBUSY" |
|
) { |
|
this.setMissing(itemPath, initial, "scan (" + err2.code + ")"); |
|
} else { |
|
this.onScanError(err2); |
|
} |
|
itemFinished(); |
|
return; |
|
} |
|
if (stats.isFile() || stats.isSymbolicLink()) { |
|
if (stats.mtime) { |
|
ensureFsAccuracy(stats.mtime); |
|
} |
|
this.setFileTime( |
|
itemPath, |
|
+stats.mtime || +stats.ctime || 1, |
|
initial, |
|
true, |
|
"scan (file)" |
|
); |
|
} else if (stats.isDirectory()) { |
|
if (!initial || !this.directories.has(itemPath)) |
|
this.setDirectory( |
|
itemPath, |
|
+stats.birthtime || 1, |
|
initial, |
|
"scan (dir)" |
|
); |
|
} |
|
itemFinished(); |
|
}); |
|
} |
|
itemFinished(); |
|
}); |
|
}); |
|
} |
|
|
|
getTimes() { |
|
const obj = Object.create(null); |
|
let safeTime = this.lastWatchEvent; |
|
for (const [file, entry] of this.files) { |
|
fixupEntryAccuracy(entry); |
|
safeTime = Math.max(safeTime, entry.safeTime); |
|
obj[file] = Math.max(entry.safeTime, entry.timestamp); |
|
} |
|
if (this.nestedWatching) { |
|
for (const w of this.directories.values()) { |
|
const times = w.directoryWatcher.getTimes(); |
|
for (const file of Object.keys(times)) { |
|
const time = times[file]; |
|
safeTime = Math.max(safeTime, time); |
|
obj[file] = time; |
|
} |
|
} |
|
obj[this.path] = safeTime; |
|
} |
|
if (!this.initialScan) { |
|
for (const watchers of this.watchers.values()) { |
|
for (const watcher of watchers) { |
|
const path = watcher.path; |
|
if (!Object.prototype.hasOwnProperty.call(obj, path)) { |
|
obj[path] = null; |
|
} |
|
} |
|
} |
|
} |
|
return obj; |
|
} |
|
|
|
collectTimeInfoEntries(fileTimestamps, directoryTimestamps) { |
|
let safeTime = this.lastWatchEvent; |
|
for (const [file, entry] of this.files) { |
|
fixupEntryAccuracy(entry); |
|
safeTime = Math.max(safeTime, entry.safeTime); |
|
fileTimestamps.set(file, entry); |
|
} |
|
if (this.nestedWatching) { |
|
for (const w of this.directories.values()) { |
|
safeTime = Math.max( |
|
safeTime, |
|
w.directoryWatcher.collectTimeInfoEntries( |
|
fileTimestamps, |
|
directoryTimestamps |
|
) |
|
); |
|
} |
|
fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY); |
|
directoryTimestamps.set(this.path, { |
|
safeTime |
|
}); |
|
} else { |
|
for (const dir of this.directories.keys()) { |
|
// No additional info about this directory |
|
// but maybe another DirectoryWatcher has info |
|
fileTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY); |
|
if (!directoryTimestamps.has(dir)) |
|
directoryTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY); |
|
} |
|
fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY); |
|
directoryTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY); |
|
} |
|
if (!this.initialScan) { |
|
for (const watchers of this.watchers.values()) { |
|
for (const watcher of watchers) { |
|
const path = watcher.path; |
|
if (!fileTimestamps.has(path)) { |
|
fileTimestamps.set(path, null); |
|
} |
|
} |
|
} |
|
} |
|
return safeTime; |
|
} |
|
|
|
close() { |
|
this.closed = true; |
|
this.initialScan = false; |
|
if (this.watcher) { |
|
this.watcher.close(); |
|
this.watcher = null; |
|
} |
|
if (this.nestedWatching) { |
|
for (const w of this.directories.values()) { |
|
w.close(); |
|
} |
|
this.directories.clear(); |
|
} |
|
if (this.parentWatcher) { |
|
this.parentWatcher.close(); |
|
this.parentWatcher = null; |
|
} |
|
this.emit("closed"); |
|
} |
|
} |
|
|
|
module.exports = DirectoryWatcher; |
|
module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY; |
|
|
|
function fixupEntryAccuracy(entry) { |
|
if (entry.accuracy > FS_ACCURACY) { |
|
entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY; |
|
entry.accuracy = FS_ACCURACY; |
|
} |
|
} |
|
|
|
function ensureFsAccuracy(mtime) { |
|
if (!mtime) return; |
|
if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1; |
|
else if (FS_ACCURACY > 10 && mtime % 10 !== 0) FS_ACCURACY = 10; |
|
else if (FS_ACCURACY > 100 && mtime % 100 !== 0) FS_ACCURACY = 100; |
|
}
|
|
|