watch-detector
Version:
[](https://travis-ci.org/chrmod/watch-detector)
243 lines (210 loc) • 7 kB
JavaScript
'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;