mirror of
https://github.com/atom/atom.git
synced 2026-01-14 17:38:03 -05:00
Merge pull request #14853 from atom/aw-filewatcher
Filesystem watcher API
This commit is contained in:
@@ -7,6 +7,7 @@ import BufferedNodeProcess from '../src/buffered-node-process'
|
||||
import BufferedProcess from '../src/buffered-process'
|
||||
import GitRepository from '../src/git-repository'
|
||||
import Notification from '../src/notification'
|
||||
import {watchPath} from '../src/path-watcher'
|
||||
|
||||
const atomExport = {
|
||||
BufferedNodeProcess,
|
||||
@@ -20,7 +21,8 @@ const atomExport = {
|
||||
Directory,
|
||||
Emitter,
|
||||
Disposable,
|
||||
CompositeDisposable
|
||||
CompositeDisposable,
|
||||
watchPath
|
||||
}
|
||||
|
||||
// Shell integration is required by both Squirrel and Settings-View
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"mocha-multi-reporters": "^1.1.4",
|
||||
"mock-spawn": "^0.2.6",
|
||||
"normalize-package-data": "^2.0.0",
|
||||
"nsfw": "^1.0.15",
|
||||
"nslog": "^3",
|
||||
"oniguruma": "6.2.1",
|
||||
"pathwatcher": "7.1.0",
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = function (packagedAppPath) {
|
||||
relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'debug', 'node.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"electron-winstaller": "2.6.2",
|
||||
"fs-extra": "0.30.0",
|
||||
"glob": "7.0.3",
|
||||
"joanna": "0.0.8",
|
||||
"joanna": "0.0.9",
|
||||
"klaw-sync": "^1.1.2",
|
||||
"legal-eagle": "0.14.0",
|
||||
"lodash.template": "4.4.0",
|
||||
|
||||
@@ -20,6 +20,11 @@ export function afterEach (fn) {
|
||||
|
||||
['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
|
||||
module.exports[name] = function (description, fn) {
|
||||
if (fn === undefined) {
|
||||
global[name](description)
|
||||
return
|
||||
}
|
||||
|
||||
global[name](description, function () {
|
||||
const result = fn()
|
||||
if (result instanceof Promise) {
|
||||
@@ -29,7 +34,7 @@ export function afterEach (fn) {
|
||||
}
|
||||
})
|
||||
|
||||
export async function conditionPromise (condition) {
|
||||
export async function conditionPromise (condition) {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
@@ -40,7 +45,7 @@ export async function conditionPromise (condition) {
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > 5000) {
|
||||
throw new Error("Timed out waiting on condition")
|
||||
throw new Error('Timed out waiting on condition')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,3 +77,27 @@ export function emitterEventPromise (emitter, event, timeout = 15000) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function promisify (original) {
|
||||
return function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
args.push((err, ...results) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(...results)
|
||||
}
|
||||
})
|
||||
|
||||
return original(...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function promisifySome (obj, fnNames) {
|
||||
const result = {}
|
||||
for (const fnName of fnNames) {
|
||||
result[fnName] = promisify(obj[fnName])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ StorageFolder = require '../src/storage-folder'
|
||||
|
||||
describe "AtomEnvironment", ->
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'window sizing methods', ->
|
||||
describe '::getPosition and ::setPosition', ->
|
||||
|
||||
@@ -86,7 +86,11 @@ describe("AtomPaths", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.ATOM_HOME
|
||||
fs.removeSync(electronUserDataPath)
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
app.setPath('userData', defaultElectronUserDataPath)
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ describe "Babel transpiler support", ->
|
||||
|
||||
afterEach ->
|
||||
CompileCache.setCacheDirectory(originalCacheDir)
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'when a .js file starts with /** @babel */;', ->
|
||||
it "transpiles it using babel", ->
|
||||
|
||||
@@ -21,7 +21,8 @@ describe "CommandInstaller on #darwin", ->
|
||||
spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it "shows an error dialog when installing commands interactively fails", ->
|
||||
appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"])
|
||||
|
||||
@@ -23,7 +23,8 @@ describe 'CompileCache', ->
|
||||
afterEach ->
|
||||
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
|
||||
CSON.setCacheDir(CompileCache.getCacheDirectory())
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'addPathToCache(filePath, atomHome)', ->
|
||||
describe 'when the given file is plain javascript', ->
|
||||
|
||||
@@ -10,7 +10,8 @@ describe "DefaultDirectoryProvider", ->
|
||||
tmp = temp.mkdirSync('atom-spec-default-dir-provider')
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".directoryForURISync(uri)", ->
|
||||
it "returns a Directory with a path that matches the uri", ->
|
||||
|
||||
@@ -12,7 +12,8 @@ describe "GitRepositoryProvider", ->
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
describe "when specified a Directory with a Git repository", ->
|
||||
|
||||
@@ -24,7 +24,8 @@ describe "the `grammars` global", ->
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".selectGrammar(filePath)", ->
|
||||
it "always returns a grammar", ->
|
||||
|
||||
@@ -508,7 +508,8 @@ describe('AtomApplication', function () {
|
||||
}
|
||||
|
||||
function makeTempDir (name) {
|
||||
return fs.realpathSync(require('temp').mkdirSync(name))
|
||||
const temp = require('temp').track()
|
||||
return fs.realpathSync(temp.mkdirSync(name))
|
||||
}
|
||||
|
||||
let channelIdCounter = 0
|
||||
|
||||
@@ -16,7 +16,11 @@ describe("FileRecoveryService", () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
describe("when no crash happens during a save", () => {
|
||||
|
||||
@@ -9,7 +9,8 @@ describe 'ModuleCache', ->
|
||||
spyOn(Module, '_findPath').andCallThrough()
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it 'resolves Electron module paths without hitting the filesystem', ->
|
||||
builtins = ModuleCache.cache.builtins
|
||||
|
||||
362
spec/native-watcher-registry-spec.js
Normal file
362
spec/native-watcher-registry-spec.js
Normal file
@@ -0,0 +1,362 @@
|
||||
/** @babel */
|
||||
|
||||
import {it, beforeEach} from './async-spec-helpers'
|
||||
|
||||
import path from 'path'
|
||||
import {Emitter} from 'event-kit'
|
||||
|
||||
import {NativeWatcherRegistry} from '../src/native-watcher-registry'
|
||||
|
||||
function findRootDirectory () {
|
||||
let current = process.cwd()
|
||||
while (true) {
|
||||
let next = path.resolve(current, '..')
|
||||
if (next === current) {
|
||||
return next
|
||||
} else {
|
||||
current = next
|
||||
}
|
||||
}
|
||||
}
|
||||
const ROOT = findRootDirectory()
|
||||
|
||||
function absolute (...parts) {
|
||||
const candidate = path.join(...parts)
|
||||
return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate)
|
||||
}
|
||||
|
||||
function parts (fullPath) {
|
||||
return fullPath.split(path.sep).filter(part => part.length > 0)
|
||||
}
|
||||
|
||||
class MockWatcher {
|
||||
constructor (normalizedPath) {
|
||||
this.normalizedPath = normalizedPath
|
||||
this.native = null
|
||||
}
|
||||
|
||||
getNormalizedPathPromise () {
|
||||
return Promise.resolve(this.normalizedPath)
|
||||
}
|
||||
|
||||
attachToNative (native, nativePath) {
|
||||
if (this.normalizedPath.startsWith(nativePath)) {
|
||||
if (this.native) {
|
||||
this.native.attached = this.native.attached.filter(each => each !== this)
|
||||
}
|
||||
this.native = native
|
||||
this.native.attached.push(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockNative {
|
||||
constructor (name) {
|
||||
this.name = name
|
||||
this.attached = []
|
||||
this.disposed = false
|
||||
this.stopped = false
|
||||
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
reattachTo (newNative, nativePath) {
|
||||
for (const watcher of this.attached) {
|
||||
watcher.attachToNative(newNative, nativePath)
|
||||
}
|
||||
}
|
||||
|
||||
onWillStop (callback) {
|
||||
return this.emitter.on('will-stop', callback)
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this.disposed = true
|
||||
}
|
||||
|
||||
stop () {
|
||||
this.stopped = true
|
||||
this.emitter.emit('will-stop')
|
||||
}
|
||||
}
|
||||
|
||||
describe('NativeWatcherRegistry', function () {
|
||||
let createNative, registry
|
||||
|
||||
beforeEach(function () {
|
||||
registry = new NativeWatcherRegistry(normalizedPath => createNative(normalizedPath))
|
||||
})
|
||||
|
||||
it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () {
|
||||
const watcher = new MockWatcher(absolute('some', 'path'))
|
||||
const NATIVE = new MockNative('created')
|
||||
createNative = () => NATIVE
|
||||
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(NATIVE)
|
||||
})
|
||||
|
||||
it('reuses an existing NativeWatcher on the same directory', async function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
const existingPath = absolute('existing', 'path')
|
||||
let firstTime = true
|
||||
createNative = () => {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
return EXISTING
|
||||
}
|
||||
|
||||
return new MockNative('nope')
|
||||
}
|
||||
await registry.attach(new MockWatcher(existingPath))
|
||||
|
||||
const watcher = new MockWatcher(existingPath)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('attaches to an existing NativeWatcher on a parent directory', async function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
const parentDir = absolute('existing', 'path')
|
||||
const subDir = path.join(parentDir, 'sub', 'directory')
|
||||
let firstTime = true
|
||||
createNative = () => {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
return EXISTING
|
||||
}
|
||||
|
||||
return new MockNative('nope')
|
||||
}
|
||||
await registry.attach(new MockWatcher(parentDir))
|
||||
|
||||
const watcher = new MockWatcher(subDir)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('adopts Watchers from NativeWatchers on child directories', async function () {
|
||||
const parentDir = absolute('existing', 'path')
|
||||
const childDir0 = path.join(parentDir, 'child', 'directory', 'zero')
|
||||
const childDir1 = path.join(parentDir, 'child', 'directory', 'one')
|
||||
const otherDir = absolute('another', 'path')
|
||||
|
||||
const CHILD0 = new MockNative('existing0')
|
||||
const CHILD1 = new MockNative('existing1')
|
||||
const OTHER = new MockNative('existing2')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else if (dir === childDir1) {
|
||||
return CHILD1
|
||||
} else if (dir === otherDir) {
|
||||
return OTHER
|
||||
} else if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else {
|
||||
throw new Error(`Unexpected path: ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const watcher0 = new MockWatcher(childDir0)
|
||||
await registry.attach(watcher0)
|
||||
|
||||
const watcher1 = new MockWatcher(childDir1)
|
||||
await registry.attach(watcher1)
|
||||
|
||||
const watcher2 = new MockWatcher(otherDir)
|
||||
await registry.attach(watcher2)
|
||||
|
||||
expect(watcher0.native).toBe(CHILD0)
|
||||
expect(watcher1.native).toBe(CHILD1)
|
||||
expect(watcher2.native).toBe(OTHER)
|
||||
|
||||
// Consolidate all three watchers beneath the same native watcher on the parent directory
|
||||
const watcher = new MockWatcher(parentDir)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(PARENT)
|
||||
|
||||
expect(watcher0.native).toBe(PARENT)
|
||||
expect(CHILD0.stopped).toBe(true)
|
||||
expect(CHILD0.disposed).toBe(true)
|
||||
|
||||
expect(watcher1.native).toBe(PARENT)
|
||||
expect(CHILD1.stopped).toBe(true)
|
||||
expect(CHILD1.disposed).toBe(true)
|
||||
|
||||
expect(watcher2.native).toBe(OTHER)
|
||||
expect(OTHER.stopped).toBe(false)
|
||||
expect(OTHER.disposed).toBe(false)
|
||||
})
|
||||
|
||||
describe('removing NativeWatchers', function () {
|
||||
it('happens when they stop', async function () {
|
||||
const STOPPED = new MockNative('stopped')
|
||||
const RUNNING = new MockNative('running')
|
||||
|
||||
const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped')
|
||||
const stoppedPathParts = stoppedPath.split(path.sep).filter(part => part.length > 0)
|
||||
const runningPath = absolute('watcher', 'that', 'will', 'continue', 'to', 'exist')
|
||||
const runningPathParts = runningPath.split(path.sep).filter(part => part.length > 0)
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === stoppedPath) {
|
||||
return STOPPED
|
||||
} else if (dir === runningPath) {
|
||||
return RUNNING
|
||||
} else {
|
||||
throw new Error(`Unexpected path: ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const stoppedWatcher = new MockWatcher(stoppedPath)
|
||||
await registry.attach(stoppedWatcher)
|
||||
|
||||
const runningWatcher = new MockWatcher(runningPath)
|
||||
await registry.attach(runningWatcher)
|
||||
|
||||
STOPPED.stop()
|
||||
|
||||
const runningNode = registry.tree.root.lookup(runningPathParts).when({
|
||||
parent: node => node,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})
|
||||
expect(runningNode).toBeTruthy()
|
||||
expect(runningNode.getNativeWatcher()).toBe(RUNNING)
|
||||
|
||||
const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({
|
||||
parent: () => false,
|
||||
missing: () => true,
|
||||
children: () => false
|
||||
})
|
||||
expect(stoppedNode).toBe(true)
|
||||
})
|
||||
|
||||
it('reassigns new child watchers when a parent watcher is stopped', async function () {
|
||||
const CHILD0 = new MockNative('child0')
|
||||
const CHILD1 = new MockNative('child1')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
const parentDir = absolute('parent')
|
||||
const childDir0 = path.join(parentDir, 'child0')
|
||||
const childDir1 = path.join(parentDir, 'child1')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else if (dir === childDir1) {
|
||||
return CHILD1
|
||||
} else {
|
||||
throw new Error(`Unexpected directory ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const parentWatcher = new MockWatcher(parentDir)
|
||||
const childWatcher0 = new MockWatcher(childDir0)
|
||||
const childWatcher1 = new MockWatcher(childDir1)
|
||||
|
||||
await registry.attach(parentWatcher)
|
||||
await Promise.all([
|
||||
registry.attach(childWatcher0),
|
||||
registry.attach(childWatcher1)
|
||||
])
|
||||
|
||||
// All three watchers should share the parent watcher's native watcher.
|
||||
expect(parentWatcher.native).toBe(PARENT)
|
||||
expect(childWatcher0.native).toBe(PARENT)
|
||||
expect(childWatcher1.native).toBe(PARENT)
|
||||
|
||||
// Stopping the parent should detach and recreate the child watchers.
|
||||
PARENT.stop()
|
||||
|
||||
expect(childWatcher0.native).toBe(CHILD0)
|
||||
expect(childWatcher1.native).toBe(CHILD1)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(parentDir)).when({
|
||||
parent: () => false,
|
||||
missing: () => false,
|
||||
children: () => true
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir0)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir1)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('consolidates children when splitting a parent watcher', async function () {
|
||||
const CHILD0 = new MockNative('child0')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
const parentDir = absolute('parent')
|
||||
const childDir0 = path.join(parentDir, 'child0')
|
||||
const childDir1 = path.join(parentDir, 'child0', 'child1')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else {
|
||||
throw new Error(`Unexpected directory ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const parentWatcher = new MockWatcher(parentDir)
|
||||
const childWatcher0 = new MockWatcher(childDir0)
|
||||
const childWatcher1 = new MockWatcher(childDir1)
|
||||
|
||||
await registry.attach(parentWatcher)
|
||||
await Promise.all([
|
||||
registry.attach(childWatcher0),
|
||||
registry.attach(childWatcher1)
|
||||
])
|
||||
|
||||
// All three watchers should share the parent watcher's native watcher.
|
||||
expect(parentWatcher.native).toBe(PARENT)
|
||||
expect(childWatcher0.native).toBe(PARENT)
|
||||
expect(childWatcher1.native).toBe(PARENT)
|
||||
|
||||
// Stopping the parent should detach and create the child watchers. Both child watchers should
|
||||
// share the same native watcher.
|
||||
PARENT.stop()
|
||||
|
||||
expect(childWatcher0.native).toBe(CHILD0)
|
||||
expect(childWatcher1.native).toBe(CHILD0)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(parentDir)).when({
|
||||
parent: () => false,
|
||||
missing: () => false,
|
||||
children: () => true
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir0)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir1)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -17,7 +17,8 @@ describe "PackageManager", ->
|
||||
spyOn(ModuleCache, 'add')
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "::getApmPath()", ->
|
||||
it "returns the path to the apm command", ->
|
||||
|
||||
186
spec/path-watcher-spec.js
Normal file
186
spec/path-watcher-spec.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/** @babel */
|
||||
|
||||
import {it, beforeEach, afterEach, promisifySome} from './async-spec-helpers'
|
||||
import tempCb from 'temp'
|
||||
import fsCb from 'fs-plus'
|
||||
import path from 'path'
|
||||
|
||||
import {CompositeDisposable} from 'event-kit'
|
||||
import {watchPath, stopAllWatchers} from '../src/path-watcher'
|
||||
|
||||
tempCb.track()
|
||||
|
||||
const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath'])
|
||||
const temp = promisifySome(tempCb, ['mkdir'])
|
||||
|
||||
describe('watchPath', function () {
|
||||
let subs
|
||||
|
||||
beforeEach(function () {
|
||||
subs = new CompositeDisposable()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
subs.dispose()
|
||||
await stopAllWatchers()
|
||||
})
|
||||
|
||||
function waitForChanges (watcher, ...fileNames) {
|
||||
const waiting = new Set(fileNames)
|
||||
let fired = false
|
||||
const relevantEvents = []
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = watcher.onDidChange(events => {
|
||||
for (const event of events) {
|
||||
if (waiting.delete(event.path)) {
|
||||
relevantEvents.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
if (!fired && waiting.size === 0) {
|
||||
fired = true
|
||||
resolve(relevantEvents)
|
||||
sub.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('watchPath()', function () {
|
||||
it('resolves getStartPromise() when the watcher begins listening', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher = watchPath(rootDir, {}, () => {})
|
||||
await watcher.getStartPromise()
|
||||
})
|
||||
|
||||
it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getStartPromise()
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
await watcher1.getStartPromise()
|
||||
|
||||
expect(watcher0.native).toBe(watcher1.native)
|
||||
})
|
||||
|
||||
it("reuses existing native watchers even while they're still starting", async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getAttachedPromise()
|
||||
expect(watcher0.native.isRunning()).toBe(false)
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
await watcher1.getAttachedPromise()
|
||||
|
||||
expect(watcher0.native).toBe(watcher1.native)
|
||||
|
||||
await Promise.all([watcher0.getStartPromise(), watcher1.getStartPromise()])
|
||||
})
|
||||
|
||||
it("doesn't attach new watchers to a native watcher that's stopping", async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getStartPromise()
|
||||
const native0 = watcher0.native
|
||||
|
||||
watcher0.dispose()
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
|
||||
expect(watcher1.native).not.toBe(native0)
|
||||
})
|
||||
|
||||
it('reuses an existing native watcher on a parent directory and filters events', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath)
|
||||
const rootFile = path.join(rootDir, 'rootfile.txt')
|
||||
const subDir = path.join(rootDir, 'subdir')
|
||||
const subFile = path.join(subDir, 'subfile.txt')
|
||||
|
||||
await fs.mkdir(subDir)
|
||||
|
||||
// Keep the watchers alive with an undisposed subscription
|
||||
const rootWatcher = watchPath(rootDir, {}, () => {})
|
||||
const childWatcher = watchPath(subDir, {}, () => {})
|
||||
|
||||
await Promise.all([
|
||||
rootWatcher.getStartPromise(),
|
||||
childWatcher.getStartPromise()
|
||||
])
|
||||
|
||||
expect(rootWatcher.native).toBe(childWatcher.native)
|
||||
expect(rootWatcher.native.isRunning()).toBe(true)
|
||||
|
||||
const firstChanges = Promise.all([
|
||||
waitForChanges(rootWatcher, subFile),
|
||||
waitForChanges(childWatcher, subFile)
|
||||
])
|
||||
|
||||
await fs.writeFile(subFile, 'subfile\n', {encoding: 'utf8'})
|
||||
await firstChanges
|
||||
|
||||
const nextRootEvent = waitForChanges(rootWatcher, rootFile)
|
||||
await fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'})
|
||||
|
||||
await nextRootEvent
|
||||
})
|
||||
|
||||
it('adopts existing child watchers and filters events appropriately to them', async function () {
|
||||
const parentDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath)
|
||||
|
||||
// Create the directory tree
|
||||
const rootFile = path.join(parentDir, 'rootfile.txt')
|
||||
const subDir0 = path.join(parentDir, 'subdir0')
|
||||
const subFile0 = path.join(subDir0, 'subfile0.txt')
|
||||
const subDir1 = path.join(parentDir, 'subdir1')
|
||||
const subFile1 = path.join(subDir1, 'subfile1.txt')
|
||||
|
||||
await fs.mkdir(subDir0)
|
||||
await fs.mkdir(subDir1)
|
||||
await Promise.all([
|
||||
fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}),
|
||||
fs.writeFile(subFile0, 'subfile 0\n', {encoding: 'utf8'}),
|
||||
fs.writeFile(subFile1, 'subfile 1\n', {encoding: 'utf8'})
|
||||
])
|
||||
|
||||
// Begin the child watchers and keep them alive
|
||||
const subWatcher0 = watchPath(subDir0, {}, () => {})
|
||||
const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0)
|
||||
|
||||
const subWatcher1 = watchPath(subDir1, {}, () => {})
|
||||
const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1)
|
||||
|
||||
await Promise.all(
|
||||
[subWatcher0, subWatcher1].map(watcher => watcher.getStartPromise())
|
||||
)
|
||||
expect(subWatcher0.native).not.toBe(subWatcher1.native)
|
||||
|
||||
// Create the parent watcher
|
||||
const parentWatcher = watchPath(parentDir, {}, () => {})
|
||||
const parentWatcherChanges = waitForChanges(parentWatcher, rootFile, subFile0, subFile1)
|
||||
|
||||
await parentWatcher.getStartPromise()
|
||||
|
||||
expect(subWatcher0.native).toBe(parentWatcher.native)
|
||||
expect(subWatcher1.native).toBe(parentWatcher.native)
|
||||
|
||||
// Ensure events are filtered correctly
|
||||
await Promise.all([
|
||||
fs.appendFile(rootFile, 'change\n', {encoding: 'utf8'}),
|
||||
fs.appendFile(subFile0, 'change\n', {encoding: 'utf8'}),
|
||||
fs.appendFile(subFile1, 'change\n', {encoding: 'utf8'})
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
subWatcherChanges0,
|
||||
subWatcherChanges1,
|
||||
parentWatcherChanges
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ Project = require '../src/project'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{Directory} = require 'pathwatcher'
|
||||
{stopAllWatchers} = require '../src/path-watcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
|
||||
describe "Project", ->
|
||||
@@ -13,9 +14,6 @@ describe "Project", ->
|
||||
# Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "serialization", ->
|
||||
deserializedProject = null
|
||||
|
||||
@@ -548,6 +546,59 @@ describe "Project", ->
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
|
||||
describe ".onDidChangeFiles()", ->
|
||||
sub = []
|
||||
events = []
|
||||
checkCallback = ->
|
||||
|
||||
beforeEach ->
|
||||
sub = atom.project.onDidChangeFiles (incoming) ->
|
||||
events.push incoming...
|
||||
checkCallback()
|
||||
|
||||
afterEach ->
|
||||
sub.dispose()
|
||||
|
||||
waitForEvents = (paths) ->
|
||||
remaining = new Set(fs.realpathSync(p) for p in paths)
|
||||
new Promise (resolve, reject) ->
|
||||
checkCallback = ->
|
||||
remaining.delete(event.path) for event in events
|
||||
resolve() if remaining.size is 0
|
||||
|
||||
expire = ->
|
||||
checkCallback = ->
|
||||
console.error "Paths not seen:", Array.from(remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
|
||||
checkCallback()
|
||||
setTimeout expire, 2000
|
||||
|
||||
it "reports filesystem changes within project paths", ->
|
||||
dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
fileOne = path.join(dirOne, 'file-one.txt')
|
||||
fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
# Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise -> stopAllWatchers()
|
||||
|
||||
runs -> atom.project.setPaths([dirOne])
|
||||
waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise()
|
||||
|
||||
runs ->
|
||||
expect(atom.project.watchersByPath[dirTwo]).toEqual undefined
|
||||
|
||||
fs.writeFileSync fileThree, "three\n"
|
||||
fs.writeFileSync fileTwo, "two\n"
|
||||
fs.writeFileSync fileOne, "one\n"
|
||||
|
||||
waitsForPromise -> waitForEvents [fileOne, fileTwo]
|
||||
|
||||
runs ->
|
||||
expect(events.some (event) -> event.path is fileThree).toBeFalsy()
|
||||
|
||||
describe ".onDidAddBuffer()", ->
|
||||
it "invokes the callback with added text buffers", ->
|
||||
buffers = []
|
||||
|
||||
@@ -37,7 +37,8 @@ describe "Windows Squirrel Update", ->
|
||||
WinShell.folderBackgroundContextMenu = new FakeShellOption()
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it "quits the app on all squirrel events", ->
|
||||
app = quit: jasmine.createSpy('quit')
|
||||
|
||||
@@ -15,7 +15,11 @@ describe('StyleManager', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('::addStyleSheet(source, params)', () => {
|
||||
|
||||
@@ -9,7 +9,8 @@ describe "atom.themes", ->
|
||||
|
||||
afterEach ->
|
||||
atom.themes.deactivateThemes()
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "theme getters and setters", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const TitleBar = require('../src/title-bar')
|
||||
const temp = require('temp')
|
||||
const temp = require('temp').track()
|
||||
|
||||
describe('TitleBar', () => {
|
||||
it('updates its title when document.title changes', () => {
|
||||
|
||||
@@ -28,7 +28,11 @@ describe('updateProcessEnv(launchEnv)', function () {
|
||||
}
|
||||
process.env = originalProcessEnv
|
||||
process.platform = originalProcessPlatform
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the launch environment appears to come from a shell', function () {
|
||||
|
||||
@@ -9,7 +9,13 @@ const {Disposable} = require('event-kit')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('WorkspaceElement', () => {
|
||||
afterEach(() => { temp.cleanupSync() })
|
||||
afterEach(() => {
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the workspace element is focused', () => {
|
||||
it('transfers focus to the active pane', () => {
|
||||
|
||||
@@ -25,7 +25,13 @@ describe('Workspace', () => {
|
||||
waitsForPromise(() => atom.workspace.itemLocationStore.clear())
|
||||
})
|
||||
|
||||
afterEach(() => temp.cleanupSync())
|
||||
afterEach(() => {
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
function simulateReload() {
|
||||
waitsForPromise(() => {
|
||||
|
||||
@@ -308,6 +308,21 @@ const configSchema = {
|
||||
description: 'Warn before opening files larger than this number of megabytes.',
|
||||
type: 'number',
|
||||
default: 40
|
||||
},
|
||||
fileSystemWatcher: {
|
||||
description: 'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Atom, but may help prevent crashes or freezes.',
|
||||
type: 'string',
|
||||
default: 'native',
|
||||
enum: [
|
||||
{
|
||||
value: 'native',
|
||||
description: 'Native operating system APIs'
|
||||
},
|
||||
{
|
||||
value: 'atom',
|
||||
description: 'Emulated with Atom events'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const {app} = require('electron')
|
||||
const nslog = require('nslog')
|
||||
const path = require('path')
|
||||
const temp = require('temp')
|
||||
const temp = require('temp').track()
|
||||
const parseCommandLine = require('./parse-command-line')
|
||||
const startCrashReporter = require('../crash-reporter-start')
|
||||
const atomPaths = require('../atom-paths')
|
||||
|
||||
436
src/native-watcher-registry.js
Normal file
436
src/native-watcher-registry.js
Normal file
@@ -0,0 +1,436 @@
|
||||
/** @babel */
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// Private: re-join the segments split from an absolute path to form another absolute path.
|
||||
function absolute (...parts) {
|
||||
const candidate = path.join(...parts)
|
||||
return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate)
|
||||
}
|
||||
|
||||
// Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to
|
||||
// each watcher with the most efficient coverage of native watchers.
|
||||
//
|
||||
// * If two watchers subscribe to the same directory, use a single native watcher for each.
|
||||
// * Re-use a native watcher watching a parent directory for a watcher on a child directory. If the parent directory
|
||||
// watcher is removed, it will be split into child watchers.
|
||||
// * If any child directories already being watched, stop and replace them with a watcher on the parent directory.
|
||||
//
|
||||
// Uses a trie whose structure mirrors the directory structure.
|
||||
class RegistryTree {
|
||||
|
||||
// Private: Construct a tree with no native watchers.
|
||||
//
|
||||
// * `basePathSegments` the position of this tree's root relative to the filesystem's root as an {Array} of directory
|
||||
// names.
|
||||
// * `createNative` {Function} used to construct new native watchers. It should accept an absolute path as an argument
|
||||
// and return a new {NativeWatcher}.
|
||||
constructor (basePathSegments, createNative) {
|
||||
this.basePathSegments = basePathSegments
|
||||
this.root = new RegistryNode()
|
||||
this.createNative = createNative
|
||||
}
|
||||
|
||||
// Private: Identify the native watcher that should be used to produce events at a watched path, creating a new one
|
||||
// if necessary.
|
||||
//
|
||||
// * `pathSegments` the path to watch represented as an {Array} of directory names relative to this {RegistryTree}'s
|
||||
// root.
|
||||
// * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root.
|
||||
add (pathSegments, attachToNative) {
|
||||
const absolutePathSegments = this.basePathSegments.concat(pathSegments)
|
||||
const absolutePath = absolute(...absolutePathSegments)
|
||||
|
||||
const attachToNew = (childPaths) => {
|
||||
const native = this.createNative(absolutePath)
|
||||
const leaf = new RegistryWatcherNode(native, absolutePathSegments, childPaths)
|
||||
this.root = this.root.insert(pathSegments, leaf)
|
||||
|
||||
const sub = native.onWillStop(() => {
|
||||
sub.dispose()
|
||||
this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode()
|
||||
})
|
||||
|
||||
attachToNative(native, absolutePath)
|
||||
return native
|
||||
}
|
||||
|
||||
this.root.lookup(pathSegments).when({
|
||||
parent: (parent, remaining) => {
|
||||
// An existing NativeWatcher is watching the same directory or a parent directory of the requested path.
|
||||
// Attach this Watcher to it as a filtering watcher and record it as a dependent child path.
|
||||
const native = parent.getNativeWatcher()
|
||||
parent.addChildPath(remaining)
|
||||
attachToNative(native, absolute(...parent.getAbsolutePathSegments()))
|
||||
},
|
||||
children: children => {
|
||||
// One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher
|
||||
// on the parent directory, note the subscribed child paths, and cleanly stop the child native watchers.
|
||||
const newNative = attachToNew(children.map(child => child.path))
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childNode = children[i].node
|
||||
const childNative = childNode.getNativeWatcher()
|
||||
childNative.reattachTo(newNative, absolutePath)
|
||||
childNative.dispose()
|
||||
childNative.stop()
|
||||
}
|
||||
},
|
||||
missing: () => attachToNew([])
|
||||
})
|
||||
}
|
||||
|
||||
// Private: Access the root node of the tree.
|
||||
getRoot () {
|
||||
return this.root
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this tree's structure for diagnostics and testing.
|
||||
print () {
|
||||
return this.root.print()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Private: Non-leaf node in a {RegistryTree} used by the {NativeWatcherRegistry} to cover the allocated {Watcher}
|
||||
// instances with the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory
|
||||
// in the filesystem tree.
|
||||
class RegistryNode {
|
||||
|
||||
// Private: Construct a new, empty node representing a node with no watchers.
|
||||
constructor () {
|
||||
this.children = {}
|
||||
}
|
||||
|
||||
// Private: Recursively discover any existing watchers corresponding to a path.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a
|
||||
// {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers
|
||||
// exist.
|
||||
lookup (pathSegments) {
|
||||
if (pathSegments.length === 0) {
|
||||
return new ChildrenResult(this.leaves([]))
|
||||
}
|
||||
|
||||
const child = this.children[pathSegments[0]]
|
||||
if (child === undefined) {
|
||||
return new MissingResult(this)
|
||||
}
|
||||
|
||||
return child.lookup(pathSegments.slice(1))
|
||||
}
|
||||
|
||||
// Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as
|
||||
// needed. Any existing children of the watched directory are removed.
|
||||
//
|
||||
// * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names.
|
||||
// * `leaf` initialized {RegistryWatcherNode} to insert
|
||||
//
|
||||
// Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should
|
||||
// replace their node references with the returned value.
|
||||
insert (pathSegments, leaf) {
|
||||
if (pathSegments.length === 0) {
|
||||
return leaf
|
||||
}
|
||||
|
||||
const pathKey = pathSegments[0]
|
||||
let child = this.children[pathKey]
|
||||
if (child === undefined) {
|
||||
child = new RegistryNode()
|
||||
}
|
||||
this.children[pathKey] = child.insert(pathSegments.slice(1), leaf)
|
||||
return this
|
||||
}
|
||||
|
||||
// Private: Remove a {RegistryWatcherNode} by its exact watched directory.
|
||||
//
|
||||
// * `pathSegments` absolute pre-split filesystem path of the node to remove.
|
||||
// * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode}
|
||||
// is split into child watchers rather than removed outright. See {RegistryWatcherNode.remove}.
|
||||
//
|
||||
// Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node
|
||||
// references with the returned value.
|
||||
remove (pathSegments, createSplitNative) {
|
||||
if (pathSegments.length === 0) {
|
||||
// Attempt to remove a path with child watchers. Do nothing.
|
||||
return this
|
||||
}
|
||||
|
||||
const pathKey = pathSegments[0]
|
||||
const child = this.children[pathKey]
|
||||
if (child === undefined) {
|
||||
// Attempt to remove a path that isn't watched. Do nothing.
|
||||
return this
|
||||
}
|
||||
|
||||
// Recurse
|
||||
const newChild = child.remove(pathSegments.slice(1), createSplitNative)
|
||||
if (newChild === null) {
|
||||
delete this.children[pathKey]
|
||||
} else {
|
||||
this.children[pathKey] = newChild
|
||||
}
|
||||
|
||||
// Remove this node if all of its children have been removed
|
||||
return Object.keys(this.children).length === 0 ? null : this
|
||||
}
|
||||
|
||||
// Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths
|
||||
// that they are watching.
|
||||
//
|
||||
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
|
||||
//
|
||||
// Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode}
|
||||
// instances beneath this node.
|
||||
leaves (prefix) {
|
||||
const results = []
|
||||
for (const p of Object.keys(this.children)) {
|
||||
results.push(...this.children[p].leaves(prefix.concat([p])))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this subtree for diagnostics and testing.
|
||||
print (indent = 0) {
|
||||
let spaces = ''
|
||||
for (let i = 0; i < indent; i++) {
|
||||
spaces += ' '
|
||||
}
|
||||
|
||||
let result = ''
|
||||
for (const p of Object.keys(this.children)) {
|
||||
result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a
|
||||
// {NativeWatcher}.
|
||||
class RegistryWatcherNode {
|
||||
|
||||
// Private: Allocate a new node to track a {NativeWatcher}.
|
||||
//
|
||||
// * `nativeWatcher` An existing {NativeWatcher} instance.
|
||||
// * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of
|
||||
// path segments.
|
||||
// * `childPaths` {Array} of child directories that are currently the responsibility of this
|
||||
// {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this
|
||||
// node's directory and the watched child path.
|
||||
constructor (nativeWatcher, absolutePathSegments, childPaths) {
|
||||
this.nativeWatcher = nativeWatcher
|
||||
this.absolutePathSegments = absolutePathSegments
|
||||
|
||||
// Store child paths as joined strings so they work as Set members.
|
||||
this.childPaths = new Set()
|
||||
for (let i = 0; i < childPaths.length; i++) {
|
||||
this.childPaths.add(path.join(...childPaths[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Assume responsibility for a new child path. If this node is removed, it will instead
|
||||
// split into a subtree with a new {RegistryWatcherNode} for each child path.
|
||||
//
|
||||
// * `childPathSegments` the {Array} of path segments between this node's directory and the watched
|
||||
// child directory.
|
||||
addChildPath (childPathSegments) {
|
||||
this.childPaths.add(path.join(...childPathSegments))
|
||||
}
|
||||
|
||||
// Private: Stop assuming responsbility for a previously assigned child path. If this node is
|
||||
// removed, the named child path will no longer be allocated a {RegistryWatcherNode}.
|
||||
//
|
||||
// * `childPathSegments` the {Array} of path segments between this node's directory and the no longer
|
||||
// watched child directory.
|
||||
removeChildPath (childPathSegments) {
|
||||
this.childPaths.delete(path.join(...childPathSegments))
|
||||
}
|
||||
|
||||
// Private: Accessor for the {NativeWatcher}.
|
||||
getNativeWatcher () {
|
||||
return this.nativeWatcher
|
||||
}
|
||||
|
||||
// Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names.
|
||||
getAbsolutePathSegments () {
|
||||
return this.absolutePathSegments
|
||||
}
|
||||
|
||||
// Private: Identify how this watcher relates to a request to watch a directory tree.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} referencing this node.
|
||||
lookup (pathSegments) {
|
||||
return new ParentResult(this, pathSegments)
|
||||
}
|
||||
|
||||
// Private: Remove this leaf node if the watcher's exact path matches. If this node is covering additional
|
||||
// {Watcher} instances on child paths, it will be split into a subtree.
|
||||
//
|
||||
// * `pathSegments` filesystem path of the node to remove.
|
||||
// * `createSplitNative` callback invoked with each {Array} of absolute child path segments to create a native
|
||||
// watcher on a subtree of this node.
|
||||
//
|
||||
// Returns: If `pathSegments` match this watcher's path exactly, returns `null` if this node has no `childPaths`
|
||||
// or a new {RegistryNode} on a newly allocated subtree if it did. If `pathSegments` does not match the watcher's
|
||||
// path, it's an attempt to remove a subnode that doesn't exist, so the remove call has no effect and returns
|
||||
// `this` unaltered.
|
||||
remove (pathSegments, createSplitNative) {
|
||||
if (pathSegments.length !== 0) {
|
||||
return this
|
||||
} else if (this.childPaths.size > 0) {
|
||||
let newSubTree = new RegistryTree(this.absolutePathSegments, createSplitNative)
|
||||
|
||||
for (const childPath of this.childPaths) {
|
||||
const childPathSegments = childPath.split(path.sep)
|
||||
newSubTree.add(childPathSegments, (native, attachmentPath) => {
|
||||
this.nativeWatcher.reattachTo(native, attachmentPath)
|
||||
})
|
||||
}
|
||||
|
||||
return newSubTree.getRoot()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Discover this {RegistryWatcherNode} instance.
|
||||
//
|
||||
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
|
||||
//
|
||||
// Returns: An {Array} containing a `{node, path}` object describing this node.
|
||||
leaves (prefix) {
|
||||
return [{node: this, path: prefix}]
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of
|
||||
// child paths that this node's {NativeWatcher} is responsible for.
|
||||
print (indent = 0) {
|
||||
let result = ''
|
||||
for (let i = 0; i < indent; i++) {
|
||||
result += ' '
|
||||
}
|
||||
result += '[watcher'
|
||||
if (this.childPaths.size > 0) {
|
||||
result += ` +${this.childPaths.size}`
|
||||
}
|
||||
result += ']\n'
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents
|
||||
// are present in the tree.
|
||||
class MissingResult {
|
||||
|
||||
// Private: Instantiate a new {MissingResult}.
|
||||
//
|
||||
// * `lastParent` the final succesfully traversed {RegistryNode}.
|
||||
constructor (lastParent) {
|
||||
this.lastParent = lastParent
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned
|
||||
// by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the
|
||||
// traversal.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.missing(this.lastParent)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested
|
||||
// directory is being watched by an existing {RegistryWatcherNode}.
|
||||
class ParentResult {
|
||||
|
||||
// Private: Instantiate a new {ParentResult}.
|
||||
//
|
||||
// * `parent` the {RegistryWatcherNode} that was discovered.
|
||||
// * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and
|
||||
// the requested directory. This will be empty for exact matches.
|
||||
constructor (parent, remainingPathSegments) {
|
||||
this.parent = parent
|
||||
this.remainingPathSegments = remainingPathSegments
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node
|
||||
// and the requested directory.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.parent(this.parent, this.remainingPathSegments)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested
|
||||
// directory are already being watched.
|
||||
class ChildrenResult {
|
||||
|
||||
// Private: Instantiate a new {ChildrenResult}.
|
||||
//
|
||||
// * `children` {Array} of the {RegistryWatcherNode} instances that were discovered.
|
||||
constructor (children) {
|
||||
this.children = children
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.children(this.children)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers
|
||||
// allocated to receive events for a desired set of directories by:
|
||||
//
|
||||
// 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times.
|
||||
// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory.
|
||||
// 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the
|
||||
// parent.
|
||||
class NativeWatcherRegistry {
|
||||
|
||||
// Private: Instantiate an empty registry.
|
||||
//
|
||||
// * `createNative` {Function} that will be called with a normalized filesystem path to create a new native
|
||||
// filesystem watcher.
|
||||
constructor (createNative) {
|
||||
this.tree = new RegistryTree([], createNative)
|
||||
}
|
||||
|
||||
// Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already
|
||||
// exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the
|
||||
// `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree
|
||||
// and attached to the watcher.
|
||||
//
|
||||
// If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will
|
||||
// be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to
|
||||
// the new watcher.
|
||||
//
|
||||
// * `watcher` an unattached {Watcher}.
|
||||
async attach (watcher) {
|
||||
const normalizedDirectory = await watcher.getNormalizedPathPromise()
|
||||
const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0)
|
||||
|
||||
this.tree.add(pathSegments, (native, nativePath) => {
|
||||
watcher.attachToNative(native, nativePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {NativeWatcherRegistry}
|
||||
641
src/path-watcher.js
Normal file
641
src/path-watcher.js
Normal file
@@ -0,0 +1,641 @@
|
||||
/** @babel */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const nsfw = require('nsfw')
|
||||
const {NativeWatcherRegistry} = require('./native-watcher-registry')
|
||||
|
||||
// Private: Associate native watcher action flags with descriptive String equivalents.
|
||||
const ACTION_MAP = new Map([
|
||||
[nsfw.actions.MODIFIED, 'modified'],
|
||||
[nsfw.actions.CREATED, 'created'],
|
||||
[nsfw.actions.DELETED, 'deleted'],
|
||||
[nsfw.actions.RENAMED, 'renamed']
|
||||
])
|
||||
|
||||
// Private: Possible states of a {NativeWatcher}.
|
||||
const WATCHER_STATE = {
|
||||
STOPPED: Symbol('stopped'),
|
||||
STARTING: Symbol('starting'),
|
||||
RUNNING: Symbol('running'),
|
||||
STOPPING: Symbol('stopping')
|
||||
}
|
||||
|
||||
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
|
||||
// any changes made to files outside of Atom, but it also has no overhead.
|
||||
class AtomBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const getRealPath = givenPath => {
|
||||
return new Promise(resolve => {
|
||||
fs.realpath(givenPath, (err, resolvedPath) => {
|
||||
err ? resolve(null) : resolve(resolvedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.subs.add(atom.workspace.observeTextEditors(async editor => {
|
||||
let realPath = await getRealPath(editor.getPath())
|
||||
if (!realPath || !realPath.startsWith(rootPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const announce = (action, oldPath) => {
|
||||
const payload = {action, path: realPath}
|
||||
if (oldPath) payload.oldPath = oldPath
|
||||
eventCallback([payload])
|
||||
}
|
||||
|
||||
const buffer = editor.getBuffer()
|
||||
|
||||
this.subs.add(buffer.onDidConflict(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidReload(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidSave(event => {
|
||||
if (event.path === realPath) {
|
||||
announce('modified')
|
||||
} else {
|
||||
const oldPath = realPath
|
||||
realPath = event.path
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
|
||||
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
|
||||
|
||||
this.subs.add(buffer.onDidChangePath(newPath => {
|
||||
if (newPath !== realPath) {
|
||||
const oldPath = realPath
|
||||
realPath = newPath
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
|
||||
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
|
||||
if (!treeViewPackage) return
|
||||
await treeViewPackage.activationPromise
|
||||
const treeViewModule = treeViewPackage.mainModule
|
||||
if (!treeViewModule) return
|
||||
const treeView = treeViewModule.getTreeViewInstance()
|
||||
|
||||
const isOpenInEditor = async eventPath => {
|
||||
const openPaths = await Promise.all(
|
||||
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
|
||||
)
|
||||
return openPaths.includes(eventPath)
|
||||
}
|
||||
|
||||
this.subs.add(treeView.onFileCreated(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath) return
|
||||
|
||||
eventCallback([{action: 'added', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryDeleted(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath || isOpenInEditor(realPath)) return
|
||||
|
||||
eventCallback([{action: 'deleted', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryMoved(async event => {
|
||||
const [realNewPath, realOldPath] = await Promise.all([
|
||||
getRealPath(event.newPath),
|
||||
getRealPath(event.initialPath)
|
||||
])
|
||||
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
|
||||
|
||||
eventCallback([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
|
||||
}))
|
||||
}
|
||||
|
||||
async stop () {
|
||||
this.subs && this.subs.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Implement a native watcher by translating events from an NSFW watcher.
|
||||
class NSFWBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const handler = events => {
|
||||
eventCallback(events.map(event => {
|
||||
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
|
||||
const payload = {action}
|
||||
|
||||
if (event.file) {
|
||||
payload.path = path.join(event.directory, event.file)
|
||||
} else {
|
||||
payload.oldPath = path.join(event.directory, event.oldFile)
|
||||
payload.path = path.join(event.directory, event.newFile)
|
||||
}
|
||||
|
||||
return payload
|
||||
}))
|
||||
}
|
||||
|
||||
this.watcher = await nsfw(
|
||||
rootPath,
|
||||
handler,
|
||||
{debounceMS: 100, errorCallback}
|
||||
)
|
||||
|
||||
await this.watcher.start()
|
||||
}
|
||||
|
||||
stop () {
|
||||
return this.watcher.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Map configuration settings from the feature flag to backend implementations.
|
||||
const BACKENDS = {
|
||||
atom: AtomBackend,
|
||||
native: NSFWBackend
|
||||
}
|
||||
|
||||
// Private: the backend implementation to fall back to if the config setting is invalid.
|
||||
const DEFAULT_BACKEND = BACKENDS.nsfw
|
||||
|
||||
// Private: Interface with and normalize events from a native OS filesystem watcher.
|
||||
class NativeWatcher {
|
||||
|
||||
// Private: Initialize a native watcher on a path.
|
||||
//
|
||||
// Events will not be produced until {start()} is called.
|
||||
constructor (normalizedPath) {
|
||||
this.normalizedPath = normalizedPath
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.backend = null
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.onEvents = this.onEvents.bind(this)
|
||||
this.onError = this.onError.bind(this)
|
||||
|
||||
this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => {
|
||||
if (this.state === WATCHER_STATE.STARTING) {
|
||||
// Wait for this watcher to finish starting.
|
||||
await new Promise(resolve => {
|
||||
const sub = this.onDidStart(() => {
|
||||
sub.dispose()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Re-read the config setting in case it's changed again while we were waiting for the watcher
|
||||
// to start.
|
||||
const Backend = this.getCurrentBackend()
|
||||
if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) {
|
||||
await this.stop()
|
||||
await this.start()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use.
|
||||
getCurrentBackend () {
|
||||
const setting = atom.config.get('core.fileSystemWatcher')
|
||||
return BACKENDS[setting] || DEFAULT_BACKEND
|
||||
}
|
||||
|
||||
// Private: Begin watching for filesystem events.
|
||||
//
|
||||
// Has no effect if the watcher has already been started.
|
||||
async start () {
|
||||
if (this.state !== WATCHER_STATE.STOPPED) {
|
||||
return
|
||||
}
|
||||
this.state = WATCHER_STATE.STARTING
|
||||
|
||||
const Backend = this.getCurrentBackend()
|
||||
|
||||
this.backend = new Backend()
|
||||
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
|
||||
|
||||
this.state = WATCHER_STATE.RUNNING
|
||||
this.emitter.emit('did-start')
|
||||
}
|
||||
|
||||
// Private: Return true if the underlying watcher is actively listening for filesystem events.
|
||||
isRunning () {
|
||||
return this.state === WATCHER_STATE.RUNNING
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when the filesystem watcher has been initialized.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidStart (callback) {
|
||||
return this.emitter.on('did-start', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher
|
||||
// automatically if it is not already running. The watcher will be stopped automatically when all subscribers
|
||||
// dispose their subscriptions.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidChange (callback) {
|
||||
this.start()
|
||||
|
||||
const sub = this.emitter.on('did-change', callback)
|
||||
return new Disposable(() => {
|
||||
sub.dispose()
|
||||
if (this.emitter.listenerCountForEventName('did-change') === 0) {
|
||||
this.stop()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onShouldDetach (callback) {
|
||||
return this.emitter.on('should-detach', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onWillStop (callback) {
|
||||
return this.emitter.on('will-stop', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when the filesystem watcher has been stopped.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidStop (callback) {
|
||||
return this.emitter.on('did-stop', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked with any errors reported from the watcher.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
// Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new
|
||||
// {NativeWatcher} instead.
|
||||
//
|
||||
// * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead.
|
||||
// * `watchedPath` absolute path watched by the new {NativeWatcher}.
|
||||
reattachTo (replacement, watchedPath) {
|
||||
this.emitter.emit('should-detach', {replacement, watchedPath})
|
||||
}
|
||||
|
||||
// Private: Stop the native watcher and release any operating system resources associated with it.
|
||||
//
|
||||
// Has no effect if the watcher is not running.
|
||||
async stop () {
|
||||
if (this.state !== WATCHER_STATE.RUNNING) {
|
||||
return
|
||||
}
|
||||
this.state = WATCHER_STATE.STOPPING
|
||||
this.emitter.emit('will-stop')
|
||||
|
||||
await this.backend.stop()
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.emitter.emit('did-stop')
|
||||
}
|
||||
|
||||
// Private: Detach any event subscribers.
|
||||
dispose () {
|
||||
this.emitter.dispose()
|
||||
}
|
||||
|
||||
// Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive.
|
||||
// Normalize and re-broadcast them to any subscribers.
|
||||
//
|
||||
// * `events` An Array of filesystem events.
|
||||
onEvents (events) {
|
||||
this.emitter.emit('did-change', events)
|
||||
}
|
||||
|
||||
// Private: Callback function invoked by the native watcher when an error occurs.
|
||||
//
|
||||
// * `err` The native filesystem error.
|
||||
onError (err) {
|
||||
this.emitter.emit('did-error', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by
|
||||
// calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles}
|
||||
// instead.
|
||||
//
|
||||
// Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources.
|
||||
//
|
||||
// Call {::dispose} to stop receiving events and, if possible, release underlying resources. A PathWatcher may be
|
||||
// added to a {CompositeDisposable} to manage its lifetime along with other {Disposable} resources like event
|
||||
// subscriptions.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
//
|
||||
// const disposable = watchPath('/var/log', {}, events => {
|
||||
// console.log(`Received batch of ${events.length} events.`)
|
||||
// for (const event of events) {
|
||||
// // "created", "modified", "deleted", "renamed"
|
||||
// console.log(`Event action: ${event.action}`)
|
||||
//
|
||||
// // absolute path to the filesystem entry that was touched
|
||||
// console.log(`Event path: ${event.path}`)
|
||||
//
|
||||
// if (event.action === 'renamed') {
|
||||
// console.log(`.. renamed from: ${event.oldPath}`)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Immediately stop receiving filesystem events. If this is the last
|
||||
// // watcher, asynchronously release any OS resources required to
|
||||
// // subscribe to these events.
|
||||
// disposable.dispose()
|
||||
// ```
|
||||
//
|
||||
// `watchPath` accepts the following arguments:
|
||||
//
|
||||
// `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch.
|
||||
//
|
||||
// `options` Control the watcher's behavior. Currently a placeholder.
|
||||
//
|
||||
// `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has
|
||||
// the keys: `action`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`,
|
||||
// `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted
|
||||
// upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path.
|
||||
class PathWatcher {
|
||||
|
||||
// Private: Instantiate a new PathWatcher. Call {watchPath} instead.
|
||||
//
|
||||
// * `nativeWatcherRegistry` {NativeWatcherRegistry} used to find and consolidate redundant watchers.
|
||||
// * `watchedPath` {String} containing the absolute path to the root of the watched filesystem tree.
|
||||
// * `options` See {watchPath} for options.
|
||||
//
|
||||
constructor (nativeWatcherRegistry, watchedPath, options) {
|
||||
this.watchedPath = watchedPath
|
||||
this.nativeWatcherRegistry = nativeWatcherRegistry
|
||||
|
||||
this.normalizedPath = null
|
||||
this.native = null
|
||||
this.changeCallbacks = new Map()
|
||||
|
||||
this.normalizedPathPromise = new Promise((resolve, reject) => {
|
||||
fs.realpath(watchedPath, (err, real) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.normalizedPath = real
|
||||
resolve(real)
|
||||
})
|
||||
})
|
||||
|
||||
this.attachedPromise = new Promise(resolve => {
|
||||
this.resolveAttachedPromise = resolve
|
||||
})
|
||||
this.startPromise = new Promise(resolve => {
|
||||
this.resolveStartPromise = resolve
|
||||
})
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
}
|
||||
|
||||
// Private: Return a {Promise} that will resolve with the normalized root path.
|
||||
getNormalizedPathPromise () {
|
||||
return this.normalizedPathPromise
|
||||
}
|
||||
|
||||
// Private: Return a {Promise} that will resolve the first time that this watcher is attached to a native watcher.
|
||||
getAttachedPromise () {
|
||||
return this.attachedPromise
|
||||
}
|
||||
|
||||
// Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events.
|
||||
// When testing filesystem watchers, it's important to await this promise before making filesystem changes that you
|
||||
// intend to assert about because there will be a delay between the instantiation of the watcher and the activation
|
||||
// of the underlying OS resources that feed it events.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
// const ROOT = path.join(__dirname, 'fixtures')
|
||||
// const FILE = path.join(ROOT, 'filename.txt')
|
||||
//
|
||||
// describe('something', function () {
|
||||
// it("doesn't miss events", async function () {
|
||||
// const watcher = watchPath(ROOT, {}, events => {})
|
||||
// await watcher.getStartPromise()
|
||||
// fs.writeFile(FILE, 'contents\n', err => {
|
||||
// // The watcher is listening and the event should be
|
||||
// // received asyncronously
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// ```
|
||||
getStartPromise () {
|
||||
return this.startPromise
|
||||
}
|
||||
|
||||
// Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the
|
||||
// spec of the callback's argument.
|
||||
//
|
||||
// * `callback` {Function} to be called with each batch of filesystem events.
|
||||
//
|
||||
// Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed.
|
||||
onDidChange (callback) {
|
||||
if (this.native) {
|
||||
const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback))
|
||||
this.changeCallbacks.set(callback, sub)
|
||||
|
||||
this.native.start()
|
||||
} else {
|
||||
// Attach to a new native listener and retry
|
||||
this.nativeWatcherRegistry.attach(this).then(() => {
|
||||
this.onDidChange(callback)
|
||||
})
|
||||
}
|
||||
|
||||
return new Disposable(() => {
|
||||
const sub = this.changeCallbacks.get(callback)
|
||||
this.changeCallbacks.delete(callback)
|
||||
sub.dispose()
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Invoke a {Function} when any errors related to this watcher are reported.
|
||||
//
|
||||
// * `callback` {Function} to be called when an error occurs.
|
||||
// * `err` An {Error} describing the failure condition.
|
||||
//
|
||||
// Returns a {Disposable}.
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
// Private: Wire this watcher to an operating system-level native watcher implementation.
|
||||
attachToNative (native) {
|
||||
this.subs.dispose()
|
||||
this.native = native
|
||||
|
||||
if (native.isRunning()) {
|
||||
this.resolveStartPromise()
|
||||
} else {
|
||||
this.subs.add(native.onDidStart(() => {
|
||||
this.resolveStartPromise()
|
||||
}))
|
||||
}
|
||||
|
||||
// Transfer any native event subscriptions to the new NativeWatcher.
|
||||
for (const [callback, formerSub] of this.changeCallbacks) {
|
||||
const newSub = native.onDidChange(events => this.onNativeEvents(events, callback))
|
||||
this.changeCallbacks.set(callback, newSub)
|
||||
formerSub.dispose()
|
||||
}
|
||||
|
||||
this.subs.add(native.onDidError(err => {
|
||||
this.emitter.emit('did-error', err)
|
||||
}))
|
||||
|
||||
this.subs.add(native.onShouldDetach(({replacement, watchedPath}) => {
|
||||
if (replacement !== native && this.normalizedPath.startsWith(watchedPath)) {
|
||||
this.attachToNative(replacement)
|
||||
}
|
||||
}))
|
||||
|
||||
this.subs.add(native.onWillStop(() => {
|
||||
this.subs.dispose()
|
||||
this.native = null
|
||||
}))
|
||||
|
||||
this.resolveAttachedPromise()
|
||||
}
|
||||
|
||||
// Private: Invoked when the attached native watcher creates a batch of native filesystem events. The native watcher's
|
||||
// events may include events for paths above this watcher's root path, so filter them to only include the relevant
|
||||
// ones, then re-broadcast them to our subscribers.
|
||||
onNativeEvents (events, callback) {
|
||||
const filtered = events.filter(event => event.path.startsWith(this.normalizedPath))
|
||||
|
||||
if (filtered.length > 0) {
|
||||
callback(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Unsubscribe all subscribers from filesystem events. Native resources will be release asynchronously,
|
||||
// but this watcher will stop broadcasting events immediately.
|
||||
dispose () {
|
||||
for (const sub of this.changeCallbacks.values()) {
|
||||
sub.dispose()
|
||||
}
|
||||
|
||||
this.emitter.dispose()
|
||||
this.subs.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}.
|
||||
class PathWatcherManager {
|
||||
|
||||
// Private: Access or lazily initialize the singleton manager instance.
|
||||
//
|
||||
// Returns the one and only {PathWatcherManager}.
|
||||
static instance () {
|
||||
if (!PathWatcherManager.theManager) {
|
||||
PathWatcherManager.theManager = new PathWatcherManager()
|
||||
}
|
||||
return PathWatcherManager.theManager
|
||||
}
|
||||
|
||||
// Private: Initialize global {PathWatcher} state.
|
||||
constructor () {
|
||||
this.live = new Set()
|
||||
this.nativeRegistry = new NativeWatcherRegistry(
|
||||
normalizedPath => {
|
||||
const nativeWatcher = new NativeWatcher(normalizedPath)
|
||||
|
||||
this.live.add(nativeWatcher)
|
||||
const sub = nativeWatcher.onWillStop(() => {
|
||||
this.live.delete(nativeWatcher)
|
||||
sub.dispose()
|
||||
})
|
||||
|
||||
return nativeWatcher
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments.
|
||||
createWatcher (rootPath, options, eventCallback) {
|
||||
const watcher = new PathWatcher(this.nativeRegistry, rootPath, options)
|
||||
watcher.onDidChange(eventCallback)
|
||||
return watcher
|
||||
}
|
||||
|
||||
// Private: Stop all living watchers.
|
||||
//
|
||||
// Returns a {Promise} that resolves when all native watcher resources are disposed.
|
||||
stopAllWatchers () {
|
||||
return Promise.all(
|
||||
Array.from(this.live, watcher => watcher.stop())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. If you only need to
|
||||
// watch events within the project's root paths, use {Project::onDidChangeFiles} instead.
|
||||
//
|
||||
// watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path
|
||||
// more than once, or the child of a watched path, will re-use the existing native watcher.
|
||||
//
|
||||
// * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch.
|
||||
// * `options` Control the watcher's behavior.
|
||||
// * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed.
|
||||
// * `events` {Array} of objects that describe the events that have occurred.
|
||||
// * `action` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`,
|
||||
// `"deleted"`, or `"renamed"`.
|
||||
// * `path` {String} containing the absolute path to the filesystem entry that was acted upon.
|
||||
// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path.
|
||||
//
|
||||
// Returns a {PathWatcher}. Note that every {PathWatcher} is a {Disposable}, so they can be managed by
|
||||
// [CompositeDisposables]{CompositeDisposable} if desired.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
//
|
||||
// const disposable = watchPath('/var/log', {}, events => {
|
||||
// console.log(`Received batch of ${events.length} events.`)
|
||||
// for (const event of events) {
|
||||
// // "created", "modified", "deleted", "renamed"
|
||||
// console.log(`Event action: ${event.action}`)
|
||||
// // absolute path to the filesystem entry that was touched
|
||||
// console.log(`Event path: ${event.path}`)
|
||||
// if (event.action === 'renamed') {
|
||||
// console.log(`.. renamed from: ${event.oldPath}`)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS
|
||||
// // resources required to subscribe to these events.
|
||||
// disposable.dispose()
|
||||
// ```
|
||||
//
|
||||
function watchPath (rootPath, options, eventCallback) {
|
||||
return PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback)
|
||||
}
|
||||
|
||||
// Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager
|
||||
// have stopped listening. This is useful for `afterEach()` blocks in unit tests.
|
||||
function stopAllWatchers () {
|
||||
return PathWatcherManager.instance().stopAllWatchers()
|
||||
}
|
||||
|
||||
module.exports = {watchPath, stopAllWatchers}
|
||||
@@ -4,6 +4,7 @@ _ = require 'underscore-plus'
|
||||
fs = require 'fs-plus'
|
||||
{Emitter, Disposable} = require 'event-kit'
|
||||
TextBuffer = require 'text-buffer'
|
||||
{watchPath} = require('./path-watcher')
|
||||
|
||||
DefaultDirectoryProvider = require './default-directory-provider'
|
||||
Model = require './model'
|
||||
@@ -28,11 +29,13 @@ class Project extends Model
|
||||
@repositoryPromisesByPath = new Map()
|
||||
@repositoryProviders = [new GitRepositoryProvider(this, config)]
|
||||
@loadPromisesByPath = {}
|
||||
@watchersByPath = {}
|
||||
@consumeServices(packageManager)
|
||||
|
||||
destroyed: ->
|
||||
buffer.destroy() for buffer in @buffers.slice()
|
||||
repository?.destroy() for repository in @repositories.slice()
|
||||
watcher.dispose() for _, watcher in @watchersByPath
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
@@ -114,6 +117,43 @@ class Project extends Model
|
||||
callback(buffer) for buffer in @getBuffers()
|
||||
@onDidAddBuffer callback
|
||||
|
||||
# Extended: Invoke a callback when a filesystem change occurs within any open
|
||||
# project path.
|
||||
#
|
||||
# ```js
|
||||
# const disposable = atom.project.onDidChangeFiles(events => {
|
||||
# for (const event of events) {
|
||||
# // "created", "modified", "deleted", or "renamed"
|
||||
# console.log(`Event action: ${event.type}`)
|
||||
#
|
||||
# // absolute path to the filesystem entry that was touched
|
||||
# console.log(`Event path: ${event.path}`)
|
||||
#
|
||||
# if (event.type === 'renamed') {
|
||||
# console.log(`.. renamed from: ${event.oldPath}`)
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# disposable.dispose()
|
||||
# ```
|
||||
#
|
||||
# To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}.
|
||||
#
|
||||
# * `callback` {Function} to be called with batches of filesystem events reported by
|
||||
# the operating system.
|
||||
# * `events` An {Array} of objects that describe a batch of filesystem events.
|
||||
# * `type` {String} describing the filesystem action that occurred. One of `"created"`,
|
||||
# `"modified"`, `"deleted"`, or `"renamed"`.
|
||||
# * `path` {String} containing the absolute path to the filesystem entry
|
||||
# that was acted upon.
|
||||
# * `oldPath` For rename events, {String} containing the filesystem entry's
|
||||
# former absolute path.
|
||||
#
|
||||
# Returns a {Disposable} to manage this event subscription.
|
||||
onDidChangeFiles: (callback) ->
|
||||
@emitter.on 'did-change-files', callback
|
||||
|
||||
###
|
||||
Section: Accessing the git repository
|
||||
###
|
||||
@@ -172,6 +212,9 @@ class Project extends Model
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
watcher.dispose() for _, watcher in @watchersByPath
|
||||
@watchersByPath = {}
|
||||
|
||||
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
|
||||
|
||||
@emitter.emit 'did-change-paths', projectPaths
|
||||
@@ -186,6 +229,11 @@ class Project extends Model
|
||||
return if existingDirectory.getPath() is directory.getPath()
|
||||
|
||||
@rootDirectories.push(directory)
|
||||
@watchersByPath[directory.getPath()] = watchPath directory.getPath(), {}, (events) =>
|
||||
@emitter.emit 'did-change-files', events
|
||||
|
||||
for root, watcher in @watchersByPath
|
||||
watcher.dispose() unless @rootDirectoryies.includes root
|
||||
|
||||
repo = null
|
||||
for provider in @repositoryProviders
|
||||
@@ -220,6 +268,7 @@ class Project extends Model
|
||||
[removedDirectory] = @rootDirectories.splice(indexToRemove, 1)
|
||||
[removedRepository] = @repositories.splice(indexToRemove, 1)
|
||||
removedRepository?.destroy() unless removedRepository in @repositories
|
||||
@watchersByPath[projectPath]?.dispose()
|
||||
@emitter.emit "did-change-paths", @getPaths()
|
||||
true
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user