path-watcher.js
1 const fs = require('fs'); 2 const path = require('path'); 3 4 const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); 5 const nsfw = require('@atom/nsfw'); 6 const watcher = require('@atom/watcher'); 7 const { NativeWatcherRegistry } = require('./native-watcher-registry'); 8 9 // Private: Associate native watcher action flags with descriptive String equivalents. 10 const ACTION_MAP = new Map([ 11 [nsfw.actions.MODIFIED, 'modified'], 12 [nsfw.actions.CREATED, 'created'], 13 [nsfw.actions.DELETED, 'deleted'], 14 [nsfw.actions.RENAMED, 'renamed'] 15 ]); 16 17 // Private: Possible states of a {NativeWatcher}. 18 const WATCHER_STATE = { 19 STOPPED: Symbol('stopped'), 20 STARTING: Symbol('starting'), 21 RUNNING: Symbol('running'), 22 STOPPING: Symbol('stopping') 23 }; 24 25 // Private: Interface with and normalize events from a filesystem watcher implementation. 26 class NativeWatcher { 27 // Private: Initialize a native watcher on a path. 28 // 29 // Events will not be produced until {start()} is called. 30 constructor(normalizedPath) { 31 this.normalizedPath = normalizedPath; 32 this.emitter = new Emitter(); 33 this.subs = new CompositeDisposable(); 34 35 this.state = WATCHER_STATE.STOPPED; 36 37 this.onEvents = this.onEvents.bind(this); 38 this.onError = this.onError.bind(this); 39 } 40 41 // Private: Begin watching for filesystem events. 42 // 43 // Has no effect if the watcher has already been started. 44 async start() { 45 if (this.state !== WATCHER_STATE.STOPPED) { 46 return; 47 } 48 this.state = WATCHER_STATE.STARTING; 49 50 await this.doStart(); 51 52 this.state = WATCHER_STATE.RUNNING; 53 this.emitter.emit('did-start'); 54 } 55 56 doStart() { 57 return Promise.reject(new Error('doStart() not overridden')); 58 } 59 60 // Private: Return true if the underlying watcher is actively listening for filesystem events. 61 isRunning() { 62 return this.state === WATCHER_STATE.RUNNING; 63 } 64 65 // Private: Register a callback to be invoked when the filesystem watcher has been initialized. 66 // 67 // Returns: A {Disposable} to revoke the subscription. 68 onDidStart(callback) { 69 return this.emitter.on('did-start', callback); 70 } 71 72 // Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher 73 // automatically if it is not already running. The watcher will be stopped automatically when all subscribers 74 // dispose their subscriptions. 75 // 76 // Returns: A {Disposable} to revoke the subscription. 77 onDidChange(callback) { 78 this.start(); 79 80 const sub = this.emitter.on('did-change', callback); 81 return new Disposable(() => { 82 sub.dispose(); 83 if (this.emitter.listenerCountForEventName('did-change') === 0) { 84 this.stop(); 85 } 86 }); 87 } 88 89 // Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}. 90 // 91 // Returns: A {Disposable} to revoke the subscription. 92 onShouldDetach(callback) { 93 return this.emitter.on('should-detach', callback); 94 } 95 96 // Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped. 97 // 98 // Returns: A {Disposable} to revoke the subscription. 99 onWillStop(callback) { 100 return this.emitter.on('will-stop', callback); 101 } 102 103 // Private: Register a callback to be invoked when the filesystem watcher has been stopped. 104 // 105 // Returns: A {Disposable} to revoke the subscription. 106 onDidStop(callback) { 107 return this.emitter.on('did-stop', callback); 108 } 109 110 // Private: Register a callback to be invoked with any errors reported from the watcher. 111 // 112 // Returns: A {Disposable} to revoke the subscription. 113 onDidError(callback) { 114 return this.emitter.on('did-error', callback); 115 } 116 117 // Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new 118 // {NativeWatcher} instead. 119 // 120 // * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead. 121 // * `watchedPath` absolute path watched by the new {NativeWatcher}. 122 reattachTo(replacement, watchedPath, options) { 123 this.emitter.emit('should-detach', { replacement, watchedPath, options }); 124 } 125 126 // Private: Stop the native watcher and release any operating system resources associated with it. 127 // 128 // Has no effect if the watcher is not running. 129 async stop() { 130 if (this.state !== WATCHER_STATE.RUNNING) { 131 return; 132 } 133 this.state = WATCHER_STATE.STOPPING; 134 this.emitter.emit('will-stop'); 135 136 await this.doStop(); 137 138 this.state = WATCHER_STATE.STOPPED; 139 140 this.emitter.emit('did-stop'); 141 } 142 143 doStop() { 144 return Promise.resolve(); 145 } 146 147 // Private: Detach any event subscribers. 148 dispose() { 149 this.emitter.dispose(); 150 } 151 152 // Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive. 153 // Normalize and re-broadcast them to any subscribers. 154 // 155 // * `events` An Array of filesystem events. 156 onEvents(events) { 157 this.emitter.emit('did-change', events); 158 } 159 160 // Private: Callback function invoked by the native watcher when an error occurs. 161 // 162 // * `err` The native filesystem error. 163 onError(err) { 164 this.emitter.emit('did-error', err); 165 } 166 } 167 168 // Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss 169 // any changes made to files outside of Atom, but it also has no overhead. 170 class AtomNativeWatcher extends NativeWatcher { 171 async doStart() { 172 const getRealPath = givenPath => { 173 if (!givenPath) { 174 return Promise.resolve(null); 175 } 176 177 return new Promise(resolve => { 178 fs.realpath(givenPath, (err, resolvedPath) => { 179 err ? resolve(null) : resolve(resolvedPath); 180 }); 181 }); 182 }; 183 184 this.subs.add( 185 atom.workspace.observeTextEditors(async editor => { 186 let realPath = await getRealPath(editor.getPath()); 187 if (!realPath || !realPath.startsWith(this.normalizedPath)) { 188 return; 189 } 190 191 const announce = (action, oldPath) => { 192 const payload = { action, path: realPath }; 193 if (oldPath) payload.oldPath = oldPath; 194 this.onEvents([payload]); 195 }; 196 197 const buffer = editor.getBuffer(); 198 199 this.subs.add(buffer.onDidConflict(() => announce('modified'))); 200 this.subs.add(buffer.onDidReload(() => announce('modified'))); 201 this.subs.add( 202 buffer.onDidSave(event => { 203 if (event.path === realPath) { 204 announce('modified'); 205 } else { 206 const oldPath = realPath; 207 realPath = event.path; 208 announce('renamed', oldPath); 209 } 210 }) 211 ); 212 213 this.subs.add(buffer.onDidDelete(() => announce('deleted'))); 214 215 this.subs.add( 216 buffer.onDidChangePath(newPath => { 217 if (newPath !== this.normalizedPath) { 218 const oldPath = this.normalizedPath; 219 this.normalizedPath = newPath; 220 announce('renamed', oldPath); 221 } 222 }) 223 ); 224 }) 225 ); 226 227 // Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView. 228 const treeViewPackage = await atom.packages.getLoadedPackage('tree-view'); 229 if (!treeViewPackage) return; 230 await treeViewPackage.activationPromise; 231 const treeViewModule = treeViewPackage.mainModule; 232 if (!treeViewModule) return; 233 const treeView = treeViewModule.getTreeViewInstance(); 234 235 const isOpenInEditor = async eventPath => { 236 const openPaths = await Promise.all( 237 atom.workspace 238 .getTextEditors() 239 .map(editor => getRealPath(editor.getPath())) 240 ); 241 return openPaths.includes(eventPath); 242 }; 243 244 this.subs.add( 245 treeView.onFileCreated(async event => { 246 const realPath = await getRealPath(event.path); 247 if (!realPath) return; 248 249 this.onEvents([{ action: 'added', path: realPath }]); 250 }) 251 ); 252 253 this.subs.add( 254 treeView.onEntryDeleted(async event => { 255 const realPath = await getRealPath(event.path); 256 if (!realPath || (await isOpenInEditor(realPath))) return; 257 258 this.onEvents([{ action: 'deleted', path: realPath }]); 259 }) 260 ); 261 262 this.subs.add( 263 treeView.onEntryMoved(async event => { 264 const [realNewPath, realOldPath] = await Promise.all([ 265 getRealPath(event.newPath), 266 getRealPath(event.initialPath) 267 ]); 268 if ( 269 !realNewPath || 270 !realOldPath || 271 (await isOpenInEditor(realNewPath)) || 272 (await isOpenInEditor(realOldPath)) 273 ) 274 return; 275 276 this.onEvents([ 277 { action: 'renamed', path: realNewPath, oldPath: realOldPath } 278 ]); 279 }) 280 ); 281 } 282 } 283 284 // Private: Implement a native watcher by translating events from an NSFW watcher. 285 class NSFWNativeWatcher extends NativeWatcher { 286 async doStart(rootPath, eventCallback, errorCallback) { 287 const handler = events => { 288 this.onEvents( 289 events.map(event => { 290 const action = 291 ACTION_MAP.get(event.action) || `unexpected (${event.action})`; 292 const payload = { action }; 293 294 if (event.file) { 295 payload.path = path.join(event.directory, event.file); 296 } else { 297 payload.oldPath = path.join(event.directory, event.oldFile); 298 payload.path = path.join(event.directory, event.newFile); 299 } 300 301 return payload; 302 }) 303 ); 304 }; 305 306 this.watcher = await nsfw(this.normalizedPath, handler, { 307 debounceMS: 100, 308 errorCallback: this.onError 309 }); 310 311 await this.watcher.start(); 312 } 313 314 doStop() { 315 return this.watcher.stop(); 316 } 317 } 318 319 // Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by 320 // calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles} 321 // instead. 322 // 323 // Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources. 324 // 325 // Call {::dispose} to stop receiving events and, if possible, release underlying resources. A PathWatcher may be 326 // added to a {CompositeDisposable} to manage its lifetime along with other {Disposable} resources like event 327 // subscriptions. 328 // 329 // ```js 330 // const {watchPath} = require('atom') 331 // 332 // const disposable = await watchPath('/var/log', {}, events => { 333 // console.log(`Received batch of ${events.length} events.`) 334 // for (const event of events) { 335 // // "created", "modified", "deleted", "renamed" 336 // console.log(`Event action: ${event.action}`) 337 // 338 // // absolute path to the filesystem entry that was touched 339 // console.log(`Event path: ${event.path}`) 340 // 341 // if (event.action === 'renamed') { 342 // console.log(`.. renamed from: ${event.oldPath}`) 343 // } 344 // } 345 // }) 346 // 347 // // Immediately stop receiving filesystem events. If this is the last 348 // // watcher, asynchronously release any OS resources required to 349 // // subscribe to these events. 350 // disposable.dispose() 351 // ``` 352 // 353 // `watchPath` accepts the following arguments: 354 // 355 // `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. 356 // 357 // `options` Control the watcher's behavior. Currently a placeholder. 358 // 359 // `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has 360 // the keys: `action`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`, 361 // `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted 362 // upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path. 363 class PathWatcher { 364 // Private: Instantiate a new PathWatcher. Call {watchPath} instead. 365 // 366 // * `nativeWatcherRegistry` {NativeWatcherRegistry} used to find and consolidate redundant watchers. 367 // * `watchedPath` {String} containing the absolute path to the root of the watched filesystem tree. 368 // * `options` See {watchPath} for options. 369 // 370 constructor(nativeWatcherRegistry, watchedPath, options) { 371 this.watchedPath = watchedPath; 372 this.nativeWatcherRegistry = nativeWatcherRegistry; 373 374 this.normalizedPath = null; 375 this.native = null; 376 this.changeCallbacks = new Map(); 377 378 this.attachedPromise = new Promise(resolve => { 379 this.resolveAttachedPromise = resolve; 380 }); 381 382 this.startPromise = new Promise((resolve, reject) => { 383 this.resolveStartPromise = resolve; 384 this.rejectStartPromise = reject; 385 }); 386 387 this.normalizedPathPromise = new Promise((resolve, reject) => { 388 fs.realpath(watchedPath, (err, real) => { 389 if (err) { 390 reject(err); 391 return; 392 } 393 394 this.normalizedPath = real; 395 resolve(real); 396 }); 397 }); 398 this.normalizedPathPromise.catch(err => this.rejectStartPromise(err)); 399 400 this.emitter = new Emitter(); 401 this.subs = new CompositeDisposable(); 402 } 403 404 // Private: Return a {Promise} that will resolve with the normalized root path. 405 getNormalizedPathPromise() { 406 return this.normalizedPathPromise; 407 } 408 409 // Private: Return a {Promise} that will resolve the first time that this watcher is attached to a native watcher. 410 getAttachedPromise() { 411 return this.attachedPromise; 412 } 413 414 // Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events. 415 // When testing filesystem watchers, it's important to await this promise before making filesystem changes that you 416 // intend to assert about because there will be a delay between the instantiation of the watcher and the activation 417 // of the underlying OS resources that feed its events. 418 // 419 // PathWatchers acquired through `watchPath` are already started. 420 // 421 // ```js 422 // const {watchPath} = require('atom') 423 // const ROOT = path.join(__dirname, 'fixtures') 424 // const FILE = path.join(ROOT, 'filename.txt') 425 // 426 // describe('something', function () { 427 // it("doesn't miss events", async function () { 428 // const watcher = watchPath(ROOT, {}, events => {}) 429 // await watcher.getStartPromise() 430 // fs.writeFile(FILE, 'contents\n', err => { 431 // // The watcher is listening and the event should be 432 // // received asynchronously 433 // } 434 // }) 435 // }) 436 // ``` 437 getStartPromise() { 438 return this.startPromise; 439 } 440 441 // Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the 442 // spec of the callback's argument. 443 // 444 // * `callback` {Function} to be called with each batch of filesystem events. 445 // 446 // Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed. 447 onDidChange(callback) { 448 if (this.native) { 449 const sub = this.native.onDidChange(events => 450 this.onNativeEvents(events, callback) 451 ); 452 this.changeCallbacks.set(callback, sub); 453 454 this.native.start(); 455 } else { 456 // Attach to a new native listener and retry 457 this.nativeWatcherRegistry.attach(this).then(() => { 458 this.onDidChange(callback); 459 }); 460 } 461 462 return new Disposable(() => { 463 const sub = this.changeCallbacks.get(callback); 464 this.changeCallbacks.delete(callback); 465 sub.dispose(); 466 }); 467 } 468 469 // Extended: Invoke a {Function} when any errors related to this watcher are reported. 470 // 471 // * `callback` {Function} to be called when an error occurs. 472 // * `err` An {Error} describing the failure condition. 473 // 474 // Returns a {Disposable}. 475 onDidError(callback) { 476 return this.emitter.on('did-error', callback); 477 } 478 479 // Private: Wire this watcher to an operating system-level native watcher implementation. 480 attachToNative(native) { 481 this.subs.dispose(); 482 this.native = native; 483 484 if (native.isRunning()) { 485 this.resolveStartPromise(); 486 } else { 487 this.subs.add( 488 native.onDidStart(() => { 489 this.resolveStartPromise(); 490 }) 491 ); 492 } 493 494 // Transfer any native event subscriptions to the new NativeWatcher. 495 for (const [callback, formerSub] of this.changeCallbacks) { 496 const newSub = native.onDidChange(events => 497 this.onNativeEvents(events, callback) 498 ); 499 this.changeCallbacks.set(callback, newSub); 500 formerSub.dispose(); 501 } 502 503 this.subs.add( 504 native.onDidError(err => { 505 this.emitter.emit('did-error', err); 506 }) 507 ); 508 509 this.subs.add( 510 native.onShouldDetach(({ replacement, watchedPath }) => { 511 if ( 512 this.native === native && 513 replacement !== native && 514 this.normalizedPath.startsWith(watchedPath) 515 ) { 516 this.attachToNative(replacement); 517 } 518 }) 519 ); 520 521 this.subs.add( 522 native.onWillStop(() => { 523 if (this.native === native) { 524 this.subs.dispose(); 525 this.native = null; 526 } 527 }) 528 ); 529 530 this.resolveAttachedPromise(); 531 } 532 533 // Private: Invoked when the attached native watcher creates a batch of native filesystem events. The native watcher's 534 // events may include events for paths above this watcher's root path, so filter them to only include the relevant 535 // ones, then re-broadcast them to our subscribers. 536 onNativeEvents(events, callback) { 537 const isWatchedPath = eventPath => 538 eventPath.startsWith(this.normalizedPath); 539 540 const filtered = []; 541 for (let i = 0; i < events.length; i++) { 542 const event = events[i]; 543 544 if (event.action === 'renamed') { 545 const srcWatched = isWatchedPath(event.oldPath); 546 const destWatched = isWatchedPath(event.path); 547 548 if (srcWatched && destWatched) { 549 filtered.push(event); 550 } else if (srcWatched && !destWatched) { 551 filtered.push({ 552 action: 'deleted', 553 kind: event.kind, 554 path: event.oldPath 555 }); 556 } else if (!srcWatched && destWatched) { 557 filtered.push({ 558 action: 'created', 559 kind: event.kind, 560 path: event.path 561 }); 562 } 563 } else { 564 if (isWatchedPath(event.path)) { 565 filtered.push(event); 566 } 567 } 568 } 569 570 if (filtered.length > 0) { 571 callback(filtered); 572 } 573 } 574 575 // Extended: Unsubscribe all subscribers from filesystem events. Native resources will be released asynchronously, 576 // but this watcher will stop broadcasting events immediately. 577 dispose() { 578 for (const sub of this.changeCallbacks.values()) { 579 sub.dispose(); 580 } 581 582 this.emitter.dispose(); 583 this.subs.dispose(); 584 } 585 } 586 587 // Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher} backed by emulated Atom 588 // events or NSFW. 589 class PathWatcherManager { 590 // Private: Access the currently active manager instance, creating one if necessary. 591 static active() { 592 if (!this.activeManager) { 593 this.activeManager = new PathWatcherManager( 594 atom.config.get('core.fileSystemWatcher') 595 ); 596 this.sub = atom.config.onDidChange( 597 'core.fileSystemWatcher', 598 ({ newValue }) => { 599 this.transitionTo(newValue); 600 } 601 ); 602 } 603 return this.activeManager; 604 } 605 606 // Private: Replace the active {PathWatcherManager} with a new one that creates [NativeWatchers]{NativeWatcher} 607 // based on the value of `setting`. 608 static async transitionTo(setting) { 609 const current = this.active(); 610 611 if (this.transitionPromise) { 612 await this.transitionPromise; 613 } 614 615 if (current.setting === setting) { 616 return; 617 } 618 current.isShuttingDown = true; 619 620 let resolveTransitionPromise = () => {}; 621 this.transitionPromise = new Promise(resolve => { 622 resolveTransitionPromise = resolve; 623 }); 624 625 const replacement = new PathWatcherManager(setting); 626 this.activeManager = replacement; 627 628 await Promise.all( 629 Array.from(current.live, async ([root, native]) => { 630 const w = await replacement.createWatcher(root, {}, () => {}); 631 native.reattachTo(w.native, root, w.native.options || {}); 632 }) 633 ); 634 635 current.stopAllWatchers(); 636 637 resolveTransitionPromise(); 638 this.transitionPromise = null; 639 } 640 641 // Private: Initialize global {PathWatcher} state. 642 constructor(setting) { 643 this.setting = setting; 644 this.live = new Map(); 645 646 const initLocal = NativeConstructor => { 647 this.nativeRegistry = new NativeWatcherRegistry(normalizedPath => { 648 const nativeWatcher = new NativeConstructor(normalizedPath); 649 650 this.live.set(normalizedPath, nativeWatcher); 651 const sub = nativeWatcher.onWillStop(() => { 652 this.live.delete(normalizedPath); 653 sub.dispose(); 654 }); 655 656 return nativeWatcher; 657 }); 658 }; 659 660 if (setting === 'atom') { 661 initLocal(AtomNativeWatcher); 662 } else if (setting === 'experimental') { 663 // 664 } else if (setting === 'poll') { 665 // 666 } else { 667 initLocal(NSFWNativeWatcher); 668 } 669 670 this.isShuttingDown = false; 671 } 672 673 useExperimentalWatcher() { 674 return this.setting === 'experimental' || this.setting === 'poll'; 675 } 676 677 // Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments. 678 async createWatcher(rootPath, options, eventCallback) { 679 if (this.isShuttingDown) { 680 await this.constructor.transitionPromise; 681 return PathWatcherManager.active().createWatcher( 682 rootPath, 683 options, 684 eventCallback 685 ); 686 } 687 688 if (this.useExperimentalWatcher()) { 689 if (this.setting === 'poll') { 690 options.poll = true; 691 } 692 693 const w = await watcher.watchPath(rootPath, options, eventCallback); 694 this.live.set(rootPath, w.native); 695 return w; 696 } 697 698 const w = new PathWatcher(this.nativeRegistry, rootPath, options); 699 w.onDidChange(eventCallback); 700 await w.getStartPromise(); 701 return w; 702 } 703 704 // Private: Directly access the {NativeWatcherRegistry}. 705 getRegistry() { 706 if (this.useExperimentalWatcher()) { 707 return watcher.getRegistry(); 708 } 709 710 return this.nativeRegistry; 711 } 712 713 // Private: Sample watcher usage statistics. Only available for experimental watchers. 714 status() { 715 if (this.useExperimentalWatcher()) { 716 return watcher.status(); 717 } 718 719 return {}; 720 } 721 722 // Private: Return a {String} depicting the currently active native watchers. 723 print() { 724 if (this.useExperimentalWatcher()) { 725 return watcher.printWatchers(); 726 } 727 728 return this.nativeRegistry.print(); 729 } 730 731 // Private: Stop all living watchers. 732 // 733 // Returns a {Promise} that resolves when all native watcher resources are disposed. 734 stopAllWatchers() { 735 if (this.useExperimentalWatcher()) { 736 return watcher.stopAllWatchers(); 737 } 738 739 return Promise.all(Array.from(this.live, ([, w]) => w.stop())); 740 } 741 } 742 743 // Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. If you only need to 744 // watch events within the project's root paths, use {Project::onDidChangeFiles} instead. 745 // 746 // watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path 747 // more than once, or the child of a watched path, will re-use the existing native watcher. 748 // 749 // * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. 750 // * `options` Control the watcher's behavior. 751 // * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. 752 // * `events` {Array} of objects that describe the events that have occurred. 753 // * `action` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, 754 // `"deleted"`, or `"renamed"`. 755 // * `path` {String} containing the absolute path to the filesystem entry that was acted upon. 756 // * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. 757 // 758 // Returns a {Promise} that will resolve to a {PathWatcher} once it has started. Note that every {PathWatcher} 759 // is a {Disposable}, so they can be managed by a {CompositeDisposable} if desired. 760 // 761 // ```js 762 // const {watchPath} = require('atom') 763 // 764 // const disposable = await watchPath('/var/log', {}, events => { 765 // console.log(`Received batch of ${events.length} events.`) 766 // for (const event of events) { 767 // // "created", "modified", "deleted", "renamed" 768 // console.log(`Event action: ${event.action}`) 769 // // absolute path to the filesystem entry that was touched 770 // console.log(`Event path: ${event.path}`) 771 // if (event.action === 'renamed') { 772 // console.log(`.. renamed from: ${event.oldPath}`) 773 // } 774 // } 775 // }) 776 // 777 // // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS 778 // // resources required to subscribe to these events. 779 // disposable.dispose() 780 // ``` 781 // 782 function watchPath(rootPath, options, eventCallback) { 783 return PathWatcherManager.active().createWatcher( 784 rootPath, 785 options, 786 eventCallback 787 ); 788 } 789 790 // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager 791 // have stopped listening. This is useful for `afterEach()` blocks in unit tests. 792 function stopAllWatchers() { 793 return PathWatcherManager.active().stopAllWatchers(); 794 } 795 796 // Private: Show the currently active native watchers in a formatted {String}. 797 watchPath.printWatchers = function() { 798 return PathWatcherManager.active().print(); 799 }; 800 801 // Private: Access the active {NativeWatcherRegistry}. 802 watchPath.getRegistry = function() { 803 return PathWatcherManager.active().getRegistry(); 804 }; 805 806 // Private: Sample usage statistics for the active watcher. 807 watchPath.status = function() { 808 return PathWatcherManager.active().status(); 809 }; 810 811 // Private: Configure @atom/watcher ("experimental") directly. 812 watchPath.configure = function(...args) { 813 return watcher.configure(...args); 814 }; 815 816 module.exports = { watchPath, stopAllWatchers };