lib/utilities/windows-admin.js

'use strict';

const chalk = require('chalk');

class WindowsSymlinkChecker {
  /**
   *
   * On Windows, users will have a much better experience if symlinks are enabled
   * and usable. When queried, this object informs Windows users regarding
   * improving their build performance, and how.
   *
   *  > Windows Vista: nothing we can really do, so we fall back to junctions for folders + copying of files
   *  <= Windows Vista: symlinks are available but using them is somewhat tricky
   *    * if the user is an admin, the process needs to have been started with elevated privileges
   *    * if the user is not an admin, a specific setting needs to be enabled
   *  <= Windows 10
   *    * if developer mode is enabled, symlinks "just work"
   *    * https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10
   *
   * ```js
   * let checker = WindowsSymlinkChecker;
   * let {
   *   windows,
   *   elevated
   * } = await = checker.checkIfSymlinksNeedToBeEnabled(); // aslso emits helpful warnings
   * ```
   *
   * @public
   * @class WindowsSymlinkChecker
   */
  constructor(ui, isWindows, canSymlink, exec) {
    this.ui = ui;
    this.isWindows = isWindows;
    this.canSymlink = canSymlink;
    this.exec = exec;
  }

  /**
   *
   * if not windows, will fulfill with:
   *  `{ windows: false, elevated: null)`
   *
   * if windows, and elevated will fulfill with:
   *  `{ windows: false, elevated: true)`
   *
   * if windows, and is NOT elevated will fulfill with:
   *  `{ windows: false, elevated: false)`
   *
   *  will include heplful warning, so that users know (if possible) how to
   *  achieve better windows build performance
   *
   * @public
   * @method checkIfSymlinksNeedToBeEnabled
   * @return {Promise<Object>} Object describing whether we're on windows and if admin rights exist
   */
  static checkIfSymlinksNeedToBeEnabled(ui) {
    return this._setup(ui).checkIfSymlinksNeedToBeEnabled();
  }

  /**
   * sets up a WindowsSymlinkChecker
   *
   * providing it with defaults for:
   *
   * * if we are on windows
   * * if we can symlink
   * * a reference to exec
   *
   * @private
   * @method _setup
   * @param UI {UI}
   * @return {WindowsSymlinkChecker}
   */
  static _setup(ui) {
    const exec = require('child_process').exec;
    const symlinkOrCopy = require('symlink-or-copy');

    return new WindowsSymlinkChecker(ui, /^win/.test(process.platform), symlinkOrCopy.canSymlink, exec);
  }

  /**
   * @public
   * @method checkIfSymlinksNeedToBeEnabled
   * @return {Promise<Object>} Object describing whether we're on windows and if admin rights exist
   */
  checkIfSymlinksNeedToBeEnabled() {
    return new Promise((resolve) => {
      if (!this.isWindows) {
        resolve({
          windows: false,
          elevated: null,
        });
      } else if (this.canSymlink) {
        resolve({
          windows: true,
          elevated: null,
        });
      } else {
        resolve(this._checkForElevatedRights(this.ui));
      }
    });
  }

  /**
   *
   * Uses the eon-old command NET SESSION to determine whether or not the
   * current user has elevated rights (think sudo, but Windows).
   *
   * @private
   * @method _checkForElevatedRights
   * @param  {Object} ui - ui object used to call writeLine();
   * @return {Object} Object describing whether we're on windows and if admin rights exist
   */
  _checkForElevatedRights() {
    let ui = this.ui;
    let exec = this.exec;

    return new Promise((resolve) => {
      exec('NET SESSION', (error, stdout, stderr) => {
        let elevated = !stderr || stderr.length === 0;

        if (!elevated) {
          ui.writeLine(chalk.yellow('\nRunning without permission to symlink will degrade build performance.'));
          ui.writeLine('See https://cli.emberjs.com/release/appendix/windows/ for details.\n');
        }

        resolve({
          windows: true,
          elevated,
        });
      });
    });
  }
}

module.exports = WindowsSymlinkChecker;