UNPKG

watch-detector

Version:

[![Build Status](https://travis-ci.org/chrmod/watch-detector.svg?branch=master)](https://travis-ci.org/chrmod/watch-detector)

243 lines (210 loc) 7 kB
'use strict'; const tmp = require('tmp'); const SilentError = require('silent-error'); const POLLING = 'polling'; const WATCHMAN = 'watchman'; const NODE = 'node'; const EVENTS = 'events'; const POSSIBLE_WATCHERS = [POLLING, WATCHMAN, NODE, EVENTS]; const WATCHMAN_INFO = 'Visit https://cli.emberjs.com/release/basic-use/#whyiswatchmanneeded for more info.'; const debug = require('heimdalljs-logger')('ember-cli:watcher'); class WatchPreference { constructor(watcher) { this.watcher = watcher || null; this._watchmanInfo = { enabled: false, version: null, canNestRoots: false, }; } watchmanWorks(details) { this._watchmanInfo.enabled = true; this._watchmanInfo.version = details.version; this._watchmanInfo.canNestRoots = true; } get watchmanInfo() { return this._watchmanInfo; } } /* * @public * * A testable class that encapsulates which watch technique to use. */ class WatchDetector { constructor(options) { this.childProcess = options.childProcess || require('child_process'); this.fs = options.fs || require('fs'); this.root = options.root; this.ui = options.ui || { writeLine(message) { console.log(message); }, }; this.tmp = undefined; // This exists, because during tests repeadelty testing the same directories this._doesNodeWorkCache = options.cache || {}; } /* * @private * * implements input options parsing and fallback. * * @method extractPreferenceFromOptions * @returns {WatchDetector) */ extractPreferenceFromOptions(options) { if (options.watcher === POLLING) { debug.info('skip detecting watchman, "polling" was selected.'); return new WatchPreference(POLLING); } else if (options.watcher === NODE) { debug.info('skip detecting watchman, "node" was selected.'); return new WatchPreference(NODE); } else { debug.info('no watcher preference set, lets attempt watchman'); return new WatchPreference(WATCHMAN); } } /* * @public * * Detect if `fs.watch` provided by node is capable of observing a directory * within `process.cwd()`. Although `fs.watch` is provided as a part of the * node stdlib, their exists platforms it ships to that do not implement the * required sub-system. For example, there exists `posix` targets, without both * inotify and FSEvents * * @method testIfNodeWatcherAppearsToWork * @returns {Boolean) outcome */ testIfNodeWatcherAppearsToWork() { let root = this.root; if (root in this._doesNodeWorkCache) { return this._doesNodeWorkCache[root]; } this._doesNodeWorkCache[root] = false; let tmpDir = tmp.dirSync(); try { let watcher = this.fs.watch(tmpDir.name, { persistent: false, recursive: false }); watcher.close(); } catch (e) { debug.info('testing if node watcher failed with: %o', e); return false; } finally { try { tmpDir.removeCallback(); // cleanup dir } catch (e) { // not much we can do, lets debug log. debug.info('cleaning up dir failed with: %o', e); } } // it seems we where able to at least watch and unwatch, this should catch // systems that have a very broken NodeWatcher. // // NOTE: we could also add a more advance chance, that triggers a change // and expects to be informed of the change. This can be added in a future // iteration. // this._doesNodeWorkCache[root] = true; return true; } /* * @public * * Selecting the best watcher is complicated, this method (given a preference) * will test and provide the best possible watcher available to the current * system and project. * * * @method findBestWatcherOption * @input {Object} options * @returns {Object} watch preference */ findBestWatcherOption(options) { let preference = this.extractPreferenceFromOptions(options); let original = options.watcher; if (original && POSSIBLE_WATCHERS.indexOf(original) === -1) { throw new SilentError( `Unknown Watcher: \`${original}\` supported watchers: [${POSSIBLE_WATCHERS.join(', ')}]` ); } if (preference.watcher === WATCHMAN) { preference = this.checkWatchman(); } let bestOption = preference; let actual; if (bestOption.watcher === NODE) { // although up to this point, we may believe Node is the best watcher // this may not be true because: // * not all platforms that run node, support node.watch // * not all file systems support node watch // let appearsToWork = this.testIfNodeWatcherAppearsToWork(); actual = new WatchPreference(appearsToWork ? NODE : POLLING); } else { actual = bestOption; } debug.info('foundBestWatcherOption, preference was: %o, actual: %o', options, actual); if ( actual /* if no preference was initial set, don't bother informing the user */ && original /* if no original was set, the fallback is expected */ && actual.watcher !== original && // events represents either watcherman or node, but no preference !(original === EVENTS && (actual.watcher === WATCHMAN || actual.watcher === NODE)) ) { // if there was an initial preference, but we had to fall back inform the user. this.ui.writeLine(`was unable to use: "${original}", fell back to: "${actual.watcher}"`); if (original === WATCHMAN) { this.ui.writeLine(WATCHMAN_INFO); } } return actual; } /* * @public * * Although watchman may be selected, it may not work due to: * * * invalid version * * it may be broken in detectable ways * * watchman executable may be something unexpected * * ??? * * @method checkWatchman * @returns {Object} watch preference + _watchmanInfo */ checkWatchman() { let result = new WatchPreference(null); debug.info('execSync("watchman version")'); try { let output = this.childProcess.execSync('watchman version', { stdio: [] }); debug.info('watchman version STDOUT: %s', output); let version; try { version = JSON.parse(output).version; } catch (e) { this.ui.writeLine('Looks like you have a different program called watchman.'); this.ui.writeLine(WATCHMAN_INFO); result.watcher = NODE; return result; } debug.info('detected watchman: %s', version); result.watcher = WATCHMAN; result.watchmanWorks({ version, }); return result; } catch (reason) { debug.info('detecting watchman failed %o', reason); result.watcher = NODE; return result; } } } WatchDetector.POLLING = POLLING; WatchDetector.WATCHMAN = WATCHMAN; WatchDetector.NODE = NODE; WatchDetector.EVENTS = EVENTS; WatchDetector.POSSIBLE_WATCHERS = POSSIBLE_WATCHERS; module.exports = WatchDetector;