appnexus/sicksync

View on GitHub
src/doctor.js

Summary

Maintainability
C
1 day
Test Coverage
import os from 'os';
import { spawnSync } from 'child_process';
import _ from 'lodash';
import chalk from 'chalk';
import fs from 'fs-extra';

import { shellIntoRemote, getConfigPath } from './util';
import packageJson from '../package.json';

const configShape = {
  version: {
    type: 'String',
    func: _.isString,
  },
  debug: {
    type: 'Boolean',
    func: _.isBoolean,
  },
  projects: {
    type: 'Array',
    func: _.isArray,
  },
};

const projectShape = {
  hostname: {
    type: 'String',
    func: _.isString,
  },
  username: {
    type: 'String',
    func: _.isString,
  },
  sourceLocation: {
    type: 'String',
    func: _.isString,
  },
  destinationLocation: {
    type: 'String',
    func: _.isString,
  },
  excludes: {
    type: 'Array',
    func: _.isArray,
  },
  prefersEncrypted: {
    type: 'Boolean',
    func: _.isBoolean,
  },
  websocketPort: {
    type: 'String',
    func: _.isString,
  },
  followSymLinks: {
    type: 'Boolean',
    func: _.isBoolean,
  },
  project: {
    type: 'String',
    func: _.isString,
  },
};

function logTimeout(username, host) {
  console.info(chalk.red(`Timed out waiting to connect to ${host} with user ${username}`));
  console.info(`Check to make sure your machine is reachable by by sshing: ${username}@${host}`);
}

export function canShellIntoHost({ username, hostname }) {
  return new Promise((resolve, reject) => {
    const ssh = shellIntoRemote(`${username}@${hostname}`);

    const timeoutId = setTimeout(() => {
      logTimeout(username, hostname);
      ssh.kill();
      reject(false);
    }, 10000);

    ssh.stdout.on('data', () => {
      clearTimeout(timeoutId);
      console.info(chalk.green(`Successfully connected to ${hostname} with user ${username}!`));
      ssh.kill();
      resolve(true);
    });
  }) ;
}

export function hasSicksyncRemote({ username, hostname }) {
  return new Promise((resolve, reject) => {
    const ssh = shellIntoRemote(`${username}@${hostname}`);

    const timeoutId = setTimeout(() => {
      clearAndKill();
      logTimeout(username, hostname);
      reject(false);
    }, 10000);

    const clearAndKill = () => {
      ssh.kill();
      clearTimeout(timeoutId);
    };

        // See if we can find sicksync
    ssh.stdin.write('which sicksync\n');

    ssh.stdout.on('data', (data) => {
      const message = data.toString();

      if (_.includes(message, 'no sicksync in')) {
        clearAndKill();
        console.info(chalk.red(`Couldn't start sicksync on ${hostname} with user ${username}!`));
        console.info(`Check to ensure it's installed globally on ${hostname}: 'npm i -g sicksync'`);
        console.info(`Lastly, check to make sure your .bashrc or .zshrc contains the npm global path.`);
        reject(false);
      }

      if (_.includes(message, '/sicksync')) {
        clearAndKill();
        console.info(chalk.green(`Successfully found sicksync on host ${hostname} with user ${username}!`));
        resolve(true);
      }
    });
  });
}

export function hasRightSicksyncVerions({ hostname, username }) {
  return new Promise((resolve, reject) => {
    const ssh = shellIntoRemote(`${username}@${hostname}`);

    const timeoutId = setTimeout(() => {
      clearAndKill();
      logTimeout(username, hostname);
      reject(false);
    }, 10000);

    const clearAndKill = () => {
      ssh.kill();
      clearTimeout(timeoutId);
    };

        // See if sicksync is at the right version
    ssh.stdin.write('sicksync -V\n');

    ssh.stdout.on('data', (data) => {
      const message = data.toString();

      if (message.match(/^(\d+\.)?(\d+\.)?(\d+\.*)/g)) {
        const version = message.trim();
        clearAndKill();

        if (version !== packageJson.version) {
          console.info(chalk.red(hostname, 'is at version', version, 'but is locally at version', packageJson.version));
          console.info('Please make sure both machines are at the latest verions');
          reject(false);
        } else {
          console.info(chalk.green(hostname, 'has same version of sicksync!'));
          resolve(true);
        }
      }
    });
  });
}

export function hasConfig() {
  return new Promise((resolve, reject) => {
    const configPath = getConfigPath();

    fs.stat(configPath, (err/*, stats */) => {
      if (err) {
        console.info(chalk.red(`sicksync couldn't find a config file!`));
        console.info(`Be sure it's located at "~/.sicksync/config.json"`);
        console.info(`Or add a new project with "sicksync add-project" to create one`);
        reject(false);
      } else {
        console.info(chalk.green(`Found the sicksync config file!`));
        resolve(true);
      }
    });
  });
}

export function configHasRightShape(config) {
  return new Promise((resolve, reject) => {
    const hasRightShape = _.every(configShape, ({ func, type }, key) => {
      if (_.isUndefined(config[key])) {
        console.info(chalk.red(`Config is missing:`, key));
        console.info('Please make sure your config has a', key, 'property');
        return false;
      }

      if (!func(config[key])) {
        console.info(chalk.red(`Config`, key, `has the wrong type!`));
        console.info('Please make sure your', key, 'has type', type);
        return false;
      }

      return true;
    });

    if (hasRightShape) {
      console.info(chalk.green(`sicksync's config has all the right properties and looks good!`));
      resolve(true);
    } else {
      reject(false);
    }
  });
}

export function projectHasRightShape(project) {
  return new Promise((resolve, reject) => {
    const projectName = project.project;

    const hasRightShape = _.every(projectShape, ({ func, type }, key) => {
      if (_.isUndefined(project[key])) {
        console.info(chalk.red(`Project is missing:`, key));
        console.info('Please make sure your project has a', key, 'property');
        return false;
      }

      if (!func(project[key])) {
        console.info(chalk.red(projectName, key, `has the wrong type!`));
        console.info('Please make sure', projectName,  key, 'has type', type);
        return false;
      }

      return true;
    });

    if (hasRightShape) {
      console.info(chalk.green(project.project, `has all the right properties and looks good!`));
      resolve(true);
    } else {
      reject(false);
    }
  });
}

export function hasCygwinOnWindows(project) {
  return new Promise((resolve, reject) => {
    let erroredCommands = [];
    function test({command, args = [], expectError}) {
      const output = spawnSync(command, args, {encoding: 'utf8'});
      if (output.output) {
        const [, stdout, stderr] = output.output;
        if (stdout && stdout.match(command)) return;
        if (expectError && stderr.match(expectError)) return;
      }
      erroredCommands.push(command);
    }
    [{
      command: 'cygpath', args: ['--version'],
    }, {
      command: 'ssh', args: ['-V'], expectError: 'OpenSSH', // ssh -V outputs to stderr
    }, {
      command: 'rsync', args: ['--version'],
    }].forEach(test);
    if (!erroredCommands.length) {
      console.info(chalk.green(`Your Windows has Cygwin and all the required modules!`));
      return resolve(true);
    }
    erroredCommands = '`' + erroredCommands.join('`, `') + '`';
    const errMsg = 'Couldn\'t run: ' + erroredCommands + '. '
    + 'Please make sure you have Cygwin/MinGW/Babun installed, '
    + 'with ' + erroredCommands + ' module(s)';
    console.info(chalk.red(errMsg));
    reject(false);
  });
}

export function checkAll(config) {
  console.info(chalk.yellow('* Checking if config is present...'));

  hasConfig()
    .then(() => {
      console.info(chalk.yellow('\n* Checking config file...'));
      return configHasRightShape(config);
    })
    .then(() => {
      console.info(chalk.yellow('\n* Checking projects in config file...'));

      return Promise.all(_.map(config.projects, projectHasRightShape));
    })
    .then(() => {
      if (os.platform() === 'win32') {
        console.info(chalk.yellow('\n* Checking Cygwin on Windows...'));
        return hasCygwinOnWindows();
      } else {
        return Promise.resolve(true);
      }
    })
    .then(() => {
      console.info(chalk.yellow('\n* Checking host for each project'));

      return Promise.all(_.map(config.projects, canShellIntoHost));
    })
    .then(() => {
      console.info(chalk.yellow('\n* Checking sicksync on hosts for each project'));

      return Promise.all(_.map(config.projects, hasSicksyncRemote));
    })
    .then(() => {
      console.info(chalk.yellow('\n* Checking sicksync version on hosts for each project'));

      return Promise.all(_.map(config.projects, hasRightSicksyncVerions));
    })
    .then(() => {
      console.info(chalk.green('\nEverything looks good!'));
    });
}