/ src / path-watcher.js
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 };