<commit_before>// Load in dependencies
var assert = require('assert');
var fs = require('fs');
var spawn = require('child_process').spawn;
var util = require('util');
var _ = require('underscore');
var async = require('async');
var bufferedSpawn = require('buffered-spawn');
var Chalk = require('chalk').constructor;
var deepEqual = require('deep-equal');

// Define constants for our environment variables
var FOUNDRY_VERSION_KEY = 'FOUNDRY_VERSION';
var FOUNDRY_MESSAGE_KEY = 'FOUNDRY_MESSAGE';
var FOUNDRY_VERSION_VAR = '$FOUNDRY_VERSION';
var FOUNDRY_MESSAGE_VAR = '$FOUNDRY_MESSAGE';
if (process.platform === 'win32') {
  FOUNDRY_VERSION_VAR = '%FOUNDRY_VERSION%';
  FOUNDRY_MESSAGE_VAR = '%FOUNDRY_MESSAGE%';
}

// Define a class to wrap release internals
function Release(releaseCommands, options) {
  // Assert against releaseCommands
  if (!Array.isArray(releaseCommands)) {
    throw new Error('foundry expected `releaseCommands` to be an array. Received: ' + JSON.stringify(releaseCommands));
  }

  // Upcast our releaseCommands from strings to objects
  // TODO: If these one-offs for `releaseCommand` become unwieldy, consider writing a `ReleaseCommand` class
  //   with abstractions for `updateFiles`/etc and `specVersion`
  releaseCommands = releaseCommands.map(function upcastReleaseCommand (releaseCommand) {
    if (typeof releaseCommand === 'string') {
      return {
        type: 'releaseCommand',
        command: releaseCommand
      };
    } else {
      return releaseCommand;
    }
  });

  // Validate each of our command types
  releaseCommands.forEach(function validateReleaseCommand (releaseCommand) {
    if (releaseCommand.type === 'releaseCommand') {
      assert(releaseCommand.command, 'Expected `releaseCommand.command` to be defined but it was not. ' +
        'Received: ' + JSON.stringify(releaseCommand));
    } else if (releaseCommand.type === 'customCommand') {
      // Verify any of `updateFiles`, `commit`, `register`, or `publish`
      var hasValidCommand = Release.STEPS.some(function verifyStepExists (step) {
        return releaseCommand[step];
      });
      assert(hasValidCommand, 'Expected `customCommand` to have command for `updateFiles`, `commit`, `publish`, ' +
        'or `register` but none were defined. Received: ' + JSON.stringify(releaseCommand));
    } else {
      assert.fail('Unknown `releaseCommand.type` found, expected `releaseCommand` or `customCommand`. ' +
        'Received: ' + JSON.stringify(releaseCommand));
    }
  });

  // Define commands for our release commands to have a consistent API with custom commands
  releaseCommands = releaseCommands.map(function upcastReleaseCommand (releaseCommand) {
    if (releaseCommand.type === 'releaseCommand') {
      var command = releaseCommand.command;
      return _.defaults({}, releaseCommand, {
        // Example: foundry-release-npm update-files "$FOUNDRY_VERSION" "$FOUNDRY_MESSAGE"
        // DEV: We use only string concatenation for the later parts since `shell-quote` won't work for Windows
        updateFiles: command + ' update-files "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        commit: command + ' commit "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        publish: command + ' publish "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        register: command + ' register "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"'
      });
    } else {
      return releaseCommand;
    }
  });

  // Save releaseCommands and options for later
  this.releaseCommands = releaseCommands;
  this.options = options || {};
  this.stdout = this.options.stdout || process.stdout;
  this.customStdout = !!this.options.stdout;
  this.stderr = this.options.stderr || process.stderr;
  this.customStderr = !!this.options.stderr;
  this.chalk = new Chalk({enabled: this.options.color});
}
Release.RESUME_FILE = 'foundry-resume.json';
Release.STEPS = ['updateFiles', 'commit', 'publish', 'register'];
Release.specVersion = '2.0.0';
Release.readResumeFile = function (callback) {
  fs.readFile(Release.RESUME_FILE, 'utf8', callback);
};
Release.writeResumeFile = function (content, callback) {
  fs.writeFile(Release.RESUME_FILE, content, callback);
};
Release.prototype = {
  validateSpecVersion: function (callback) {
    var releaseCommands = this.releaseCommands;
    async.map(releaseCommands, function getSpecVersion (releaseCommand, cb) {
      // If the command is a release command, get the version
      //   e.g.: foundry-release-git --spec-version
      if (releaseCommand.type === 'releaseCommand') {
        bufferedSpawn(releaseCommand.command, ['--spec-version'], function handleSpecVersion (err, stdout, stderr) {
          // If there was an error
          if (err) {
            // If the command wasn't found, generate a better error
            if (err.code === 'ENOENT') {
              return cb(new Error('Attempted to run "' + releaseCommand.command + ' --spec-version" ' +
                'but it wasn\'t found. Please verify your dependencies are installed ' +
                'and the PATH environment variables are set up properly'));
            // Otherwise if there was stderr, callback with it as our error
            } else if (stderr) {
              return cb(new Error(stderr));
            // Otherwise, callback with our error
            } else {
              return cb(err);
            }
          // Otherwise, callback with a trimmed stdout (removes any newlines)
          } else {
            return cb(null, stdout.trim());
          }
        });
      // Otherwise, assume it's foundry's version
      } else {
        process.nextTick(function replyWithFoundrySpecVersion () {
          cb(null, Release.specVersion);
        });
      }
    }, function handleSpecVersions (err, specVersions) {
      // If there was an error for any command, callback with it
      if (err) {
        return callback(err);
      }

      // Otherwise, validate our spec versions
      var i = 0;
      var len = specVersions.length;
      for (; i < len; i++) {
        // If the specVersion doesn't line up, error out with library info
        var specVersion = specVersions[i];
        if (specVersion !== Release.specVersion) {
          var releaseCommand = releaseCommands[i];
          return callback(new Error('Expected release command "' + releaseCommand.command +
            '" to have a `--spec-version` of "' + Release.specVersion + '" but it was "' + specVersion + '"'));
        }
      }

      // Callback with no error
      return callback(null);
    });
  },

  getReleaseSteps: function (params) {
    // Generate a container for our steps
    var steps = [];
    var that = this;

    // For each of step keys, add their respective steps
    function addSteps(stepKey) {
      that.releaseCommands.forEach(function addReleaseStep (releaseComand) {
        if (releaseComand[stepKey]) {
          steps.push({
            step: releaseComand[stepKey],
            completed: false
          });
        }
      });
    }
    addSteps('updateFiles');
    addSteps('commit');
    // Only run register if we are at the 'register' semver (e.g. 1.0.0)
    if (params[FOUNDRY_VERSION_KEY] === '1.0.0') {
      addSteps('register');
    }
    addSteps('publish');

    // Return our steps
    return steps;
  },

  notifyUserReleaseSteps: function (params) {
    var stdout = this.stdout;
    var chalk = this.chalk;
    stdout.write(
      'Configuring steps with ' + chalk.bold('FOUNDRY_VERSION') + ': ' + params[FOUNDRY_VERSION_KEY] + '\n');
    stdout.write(
      'Configuring steps with ' + chalk.bold('FOUNDRY_MESSAGE') + ': ' + params[FOUNDRY_MESSAGE_KEY] + '\n');
  },

  release: function (version, callback) {
    var that = this;
    var params = {
      FOUNDRY_VERSION: version,
      FOUNDRY_MESSAGE: 'Release ' + version
    };
    async.series([
      function validateSpecVersionFn (cb) {
        that.validateSpecVersion(cb);
      },
      function prepareReleaseSteps (cb) {
        params.steps = that.getReleaseSteps(params);
        process.nextTick(cb);
      },
      function notifyUserReleaseSteps (cb) {
        that.notifyUserReleaseSteps(params);
        process.nextTick(cb);
      },
      function runReleaseSteps (cb) {
        that._runReleaseSteps(params, cb);
      }
    ], callback);
  },

  resume: function (params, callback) {
    var that = this;
    async.series([
      function validateSpecVersionFn (cb) {
        that.validateSpecVersion(cb);
      },
      function validateParams (cb) {
        // If there is no version nor message, error out
        if (params[FOUNDRY_VERSION_KEY] === undefined) {
          return cb(new Error('Expected `' + FOUNDRY_VERSION_KEY + '` to be defined in `' +
            Release.RESUME_FILE + '` but it was not.'));
        }
        if (params[FOUNDRY_MESSAGE_KEY] === undefined) {
          return cb(new Error('Expected `' + FOUNDRY_MESSAGE_KEY + '` to be defined in `' +
            Release.RESUME_FILE + '` but it was not.'));
        }

        // Verify our steps line up exactly (order and properties)
        var configSteps = that.getReleaseSteps(params);
        var resumeSteps = params.steps;
        var incompleteResumeSteps = resumeSteps.map(function normalizeCompletedProperties (step) {
          step = _.clone(step);
          // DEV: We are overly protective of our properties changing to ensure the tests fail when appropriate
          if (step.hasOwnProperty('completed')) {
            step.completed = false;
          }
          return step;
        });
        if (!deepEqual(incompleteResumeSteps, configSteps)) {
          return cb(new Error(
            'Expected `foundry-resume.json` and `.foundryrc/package.json` commands to line up exactly. ' +
            'Please do not adjust `foundry` configuration nor `foundry-resume.json`. ' +
            'Otherwise, the release will be terminate in an incomplete state.'));
        }
        process.nextTick(cb);
      },
      function notifyUserReleaseSteps (cb) {
        that.notifyUserReleaseSteps(params);
        process.nextTick(cb);
      },
      function runReleaseSteps (cb) {
        that._runReleaseSteps(params, cb);
      }
    ], callback);
  },

  _runReleaseSteps: function (params, callback) {
    var that = this;
    async.eachSeries(params.steps, function _runReleaseStepFn (stepInfo, cb) {
      // Localize common step variables
      var cmd = stepInfo.step;
      var stdout = that.stdout;
      var stderr = that.stderr;
      var chalk = that.chalk;

      // If the step has already been completed, skip it
      if (stepInfo.completed) {
        stdout.write(chalk.green('Step already complete') + ': ' + cmd + '\n');
        return process.nextTick(cb);
      }

      // Log the step we are running
      stdout.write(chalk.green('Running step') + ': ' + cmd + '\n');

      // Prepare our command environment
      var cmdEnv = _.defaults(_.object([
        [FOUNDRY_VERSION_KEY, params[FOUNDRY_VERSION_KEY]],
        [FOUNDRY_MESSAGE_KEY, params[FOUNDRY_MESSAGE_KEY]]
      ]), process.env);
      // DEV: We conditionally use 'pipe' on `stdout` to allow for buffering during testing
      var stdio = [process.stdin, that.customStdout ? 'pipe' : stdout, that.customStderr ? 'pipe' : stderr];
      var conf = {stdio: stdio, env: cmdEnv};

      // Prepare the command itself
      // DEV: We use `sh` or `cmd` to support interpretting environment variables like `npm run-script`
      // https://github.com/npm/npm/blob/v3.3.6/lib/utils/lifecycle.js#L210-L225
      // DEV: `-c` means command
      var sh = 'sh';
      var shFlag = '-c';
      if (process.platform === 'win32') {
        sh = process.env.comspec || 'cmd';
        // DEV: `/d` disables AutoRun command execution
        // DEV: `/s /c` means use the following command as is
        //   https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/cmd.mspx?mfr=true
        shFlag = '/d /s /c';
        conf.windowsVerbatimArguments = true;
      }

      // If we aren't on a dry run
      if (!that.options.dryRun) {
        // Run our command and pipe stdout if appropriate
        var child = spawn(sh, [shFlag, cmd], conf);
        if (that.customStdout) {
          child.stdout.on('data', function writeToStdout (data) {
            stdout.write(data);
          });
          child.stdout.on('error', function forwardErrorToStdout (err) {
            stdout.emit('error', err);
          });
        }
        if (that.customStderr) {
          child.stderr.on('data', function writeToStdout (data) {
            stderr.write(data);
          });
          child.stderr.on('error', function forwardErrorToStdout (err) {
            stderr.emit('error', err);
          });
        }

        // When the child exits, handle its exit code
        child.on('close', function handleChildClose (code) {
          // If the exit code was not zero, complain and leave
          if (code !== 0) {
            cb(new Error('Received non-zero exit code "' + code + '" from command "' + cmd + '"'));
          // Otherwise, mark the step as a success and continue
          } else {
            stepInfo.completed = true;
            cb(null);
          }
        });
      // Otherwise, callback shortly
      } else {
        process.nextTick(cb);
      }
    }, function handleFailure (err) {
      // If there was an error, output our progress to a file
      if (err) {
        // Output our error immediately
        // TODO: Figure out way to stack/concatenate errors instead of using `console.error`
        that.stderr.write('There was an error while executing a command\n');
        that.stderr.write(util.inspect(err));

        // Output params to disk in verbatim fashion
        var content = JSON.stringify(params, null, 2);
        Release.writeResumeFile(content, function handleOutputResumeFile (err2) {
          // If there was a secondary error, callback with it
          if (err2) {
            // TODO: Figure out way to stack/concatenate errors instead of using `console.error`
            that.stderr.write('Attempted to write `' + Release.RESUME_FILE + '` but failed\n');
            return callback(err2);
          }

          // Callback with an error about running resume
          callback(new Error('We have generated a `' + Release.RESUME_FILE + '` file. ' +
            'Once issues have been resolved, the current release can be resumed by running `foundry resume`.'));
        });
      // Otherwise, attempt to clean up resume file
      // DEV: This also covers the scenario where a release fails and then someone re-runs `release`
      } else {
        // DEV: We don't use `stat` + `unlink` since the file could be created in that time
        fs.unlink(Release.RESUME_FILE, function handleError (err) {
          // If there was an error about the file not existing, nullify it
          if (err && err.code === 'ENOENT') {
            err = null;
          }

          // Callback with our error
          callback(err);
        });
      }
    });
  }
};

// Export Release
module.exports = Release;
<commit_msg>Added fix for dry run overstepping boundaries<commit_after>// Load in dependencies
var assert = require('assert');
var fs = require('fs');
var spawn = require('child_process').spawn;
var util = require('util');
var _ = require('underscore');
var async = require('async');
var bufferedSpawn = require('buffered-spawn');
var Chalk = require('chalk').constructor;
var deepEqual = require('deep-equal');

// Define constants for our environment variables
var FOUNDRY_VERSION_KEY = 'FOUNDRY_VERSION';
var FOUNDRY_MESSAGE_KEY = 'FOUNDRY_MESSAGE';
var FOUNDRY_VERSION_VAR = '$FOUNDRY_VERSION';
var FOUNDRY_MESSAGE_VAR = '$FOUNDRY_MESSAGE';
if (process.platform === 'win32') {
  FOUNDRY_VERSION_VAR = '%FOUNDRY_VERSION%';
  FOUNDRY_MESSAGE_VAR = '%FOUNDRY_MESSAGE%';
}

// Define a class to wrap release internals
function Release(releaseCommands, options) {
  // Assert against releaseCommands
  if (!Array.isArray(releaseCommands)) {
    throw new Error('foundry expected `releaseCommands` to be an array. Received: ' + JSON.stringify(releaseCommands));
  }

  // Upcast our releaseCommands from strings to objects
  // TODO: If these one-offs for `releaseCommand` become unwieldy, consider writing a `ReleaseCommand` class
  //   with abstractions for `updateFiles`/etc and `specVersion`
  releaseCommands = releaseCommands.map(function upcastReleaseCommand (releaseCommand) {
    if (typeof releaseCommand === 'string') {
      return {
        type: 'releaseCommand',
        command: releaseCommand
      };
    } else {
      return releaseCommand;
    }
  });

  // Validate each of our command types
  releaseCommands.forEach(function validateReleaseCommand (releaseCommand) {
    if (releaseCommand.type === 'releaseCommand') {
      assert(releaseCommand.command, 'Expected `releaseCommand.command` to be defined but it was not. ' +
        'Received: ' + JSON.stringify(releaseCommand));
    } else if (releaseCommand.type === 'customCommand') {
      // Verify any of `updateFiles`, `commit`, `register`, or `publish`
      var hasValidCommand = Release.STEPS.some(function verifyStepExists (step) {
        return releaseCommand[step];
      });
      assert(hasValidCommand, 'Expected `customCommand` to have command for `updateFiles`, `commit`, `publish`, ' +
        'or `register` but none were defined. Received: ' + JSON.stringify(releaseCommand));
    } else {
      assert.fail('Unknown `releaseCommand.type` found, expected `releaseCommand` or `customCommand`. ' +
        'Received: ' + JSON.stringify(releaseCommand));
    }
  });

  // Define commands for our release commands to have a consistent API with custom commands
  releaseCommands = releaseCommands.map(function upcastReleaseCommand (releaseCommand) {
    if (releaseCommand.type === 'releaseCommand') {
      var command = releaseCommand.command;
      return _.defaults({}, releaseCommand, {
        // Example: foundry-release-npm update-files "$FOUNDRY_VERSION" "$FOUNDRY_MESSAGE"
        // DEV: We use only string concatenation for the later parts since `shell-quote` won't work for Windows
        updateFiles: command + ' update-files "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        commit: command + ' commit "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        publish: command + ' publish "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"',
        register: command + ' register "' + FOUNDRY_VERSION_VAR + '" "' + FOUNDRY_MESSAGE_VAR + '"'
      });
    } else {
      return releaseCommand;
    }
  });

  // Save releaseCommands and options for later
  this.releaseCommands = releaseCommands;
  this.options = options || {};
  this.stdout = this.options.stdout || process.stdout;
  this.customStdout = !!this.options.stdout;
  this.stderr = this.options.stderr || process.stderr;
  this.customStderr = !!this.options.stderr;
  this.chalk = new Chalk({enabled: this.options.color});
}
Release.RESUME_FILE = 'foundry-resume.json';
Release.STEPS = ['updateFiles', 'commit', 'publish', 'register'];
Release.specVersion = '2.0.0';
Release.readResumeFile = function (callback) {
  fs.readFile(Release.RESUME_FILE, 'utf8', callback);
};
Release.writeResumeFile = function (content, callback) {
  fs.writeFile(Release.RESUME_FILE, content, callback);
};
Release.prototype = {
  validateSpecVersion: function (callback) {
    var releaseCommands = this.releaseCommands;
    async.map(releaseCommands, function getSpecVersion (releaseCommand, cb) {
      // If the command is a release command, get the version
      //   e.g.: foundry-release-git --spec-version
      if (releaseCommand.type === 'releaseCommand') {
        bufferedSpawn(releaseCommand.command, ['--spec-version'], function handleSpecVersion (err, stdout, stderr) {
          // If there was an error
          if (err) {
            // If the command wasn't found, generate a better error
            if (err.code === 'ENOENT') {
              return cb(new Error('Attempted to run "' + releaseCommand.command + ' --spec-version" ' +
                'but it wasn\'t found. Please verify your dependencies are installed ' +
                'and the PATH environment variables are set up properly'));
            // Otherwise if there was stderr, callback with it as our error
            } else if (stderr) {
              return cb(new Error(stderr));
            // Otherwise, callback with our error
            } else {
              return cb(err);
            }
          // Otherwise, callback with a trimmed stdout (removes any newlines)
          } else {
            return cb(null, stdout.trim());
          }
        });
      // Otherwise, assume it's foundry's version
      } else {
        process.nextTick(function replyWithFoundrySpecVersion () {
          cb(null, Release.specVersion);
        });
      }
    }, function handleSpecVersions (err, specVersions) {
      // If there was an error for any command, callback with it
      if (err) {
        return callback(err);
      }

      // Otherwise, validate our spec versions
      var i = 0;
      var len = specVersions.length;
      for (; i < len; i++) {
        // If the specVersion doesn't line up, error out with library info
        var specVersion = specVersions[i];
        if (specVersion !== Release.specVersion) {
          var releaseCommand = releaseCommands[i];
          return callback(new Error('Expected release command "' + releaseCommand.command +
            '" to have a `--spec-version` of "' + Release.specVersion + '" but it was "' + specVersion + '"'));
        }
      }

      // Callback with no error
      return callback(null);
    });
  },

  getReleaseSteps: function (params) {
    // Generate a container for our steps
    var steps = [];
    var that = this;

    // For each of step keys, add their respective steps
    function addSteps(stepKey) {
      that.releaseCommands.forEach(function addReleaseStep (releaseComand) {
        if (releaseComand[stepKey]) {
          steps.push({
            step: releaseComand[stepKey],
            completed: false
          });
        }
      });
    }
    addSteps('updateFiles');
    addSteps('commit');
    // Only run register if we are at the 'register' semver (e.g. 1.0.0)
    if (params[FOUNDRY_VERSION_KEY] === '1.0.0') {
      addSteps('register');
    }
    addSteps('publish');

    // Return our steps
    return steps;
  },

  notifyUserReleaseSteps: function (params) {
    var stdout = this.stdout;
    var chalk = this.chalk;
    stdout.write(
      'Configuring steps with ' + chalk.bold('FOUNDRY_VERSION') + ': ' + params[FOUNDRY_VERSION_KEY] + '\n');
    stdout.write(
      'Configuring steps with ' + chalk.bold('FOUNDRY_MESSAGE') + ': ' + params[FOUNDRY_MESSAGE_KEY] + '\n');
  },

  release: function (version, callback) {
    var that = this;
    var params = {
      FOUNDRY_VERSION: version,
      FOUNDRY_MESSAGE: 'Release ' + version
    };
    async.series([
      function validateSpecVersionFn (cb) {
        that.validateSpecVersion(cb);
      },
      function prepareReleaseSteps (cb) {
        params.steps = that.getReleaseSteps(params);
        process.nextTick(cb);
      },
      function notifyUserReleaseSteps (cb) {
        that.notifyUserReleaseSteps(params);
        process.nextTick(cb);
      },
      function runReleaseSteps (cb) {
        that._runReleaseSteps(params, cb);
      }
    ], callback);
  },

  resume: function (params, callback) {
    var that = this;
    async.series([
      function validateSpecVersionFn (cb) {
        that.validateSpecVersion(cb);
      },
      function validateParams (cb) {
        // If there is no version nor message, error out
        if (params[FOUNDRY_VERSION_KEY] === undefined) {
          return cb(new Error('Expected `' + FOUNDRY_VERSION_KEY + '` to be defined in `' +
            Release.RESUME_FILE + '` but it was not.'));
        }
        if (params[FOUNDRY_MESSAGE_KEY] === undefined) {
          return cb(new Error('Expected `' + FOUNDRY_MESSAGE_KEY + '` to be defined in `' +
            Release.RESUME_FILE + '` but it was not.'));
        }

        // Verify our steps line up exactly (order and properties)
        var configSteps = that.getReleaseSteps(params);
        var resumeSteps = params.steps;
        var incompleteResumeSteps = resumeSteps.map(function normalizeCompletedProperties (step) {
          step = _.clone(step);
          // DEV: We are overly protective of our properties changing to ensure the tests fail when appropriate
          if (step.hasOwnProperty('completed')) {
            step.completed = false;
          }
          return step;
        });
        if (!deepEqual(incompleteResumeSteps, configSteps)) {
          return cb(new Error(
            'Expected `foundry-resume.json` and `.foundryrc/package.json` commands to line up exactly. ' +
            'Please do not adjust `foundry` configuration nor `foundry-resume.json`. ' +
            'Otherwise, the release will be terminate in an incomplete state.'));
        }
        process.nextTick(cb);
      },
      function notifyUserReleaseSteps (cb) {
        that.notifyUserReleaseSteps(params);
        process.nextTick(cb);
      },
      function runReleaseSteps (cb) {
        that._runReleaseSteps(params, cb);
      }
    ], callback);
  },

  _runReleaseSteps: function (params, callback) {
    var that = this;
    async.eachSeries(params.steps, function _runReleaseStepFn (stepInfo, cb) {
      // Localize common step variables
      var cmd = stepInfo.step;
      var stdout = that.stdout;
      var stderr = that.stderr;
      var chalk = that.chalk;

      // If the step has already been completed, skip it
      if (stepInfo.completed) {
        stdout.write(chalk.green('Step already complete') + ': ' + cmd + '\n');
        return process.nextTick(cb);
      }

      // Log the step we are running
      stdout.write(chalk.green('Running step') + ': ' + cmd + '\n');

      // Prepare our command environment
      var cmdEnv = _.defaults(_.object([
        [FOUNDRY_VERSION_KEY, params[FOUNDRY_VERSION_KEY]],
        [FOUNDRY_MESSAGE_KEY, params[FOUNDRY_MESSAGE_KEY]]
      ]), process.env);
      // DEV: We conditionally use 'pipe' on `stdout` to allow for buffering during testing
      var stdio = [process.stdin, that.customStdout ? 'pipe' : stdout, that.customStderr ? 'pipe' : stderr];
      var conf = {stdio: stdio, env: cmdEnv};

      // Prepare the command itself
      // DEV: We use `sh` or `cmd` to support interpretting environment variables like `npm run-script`
      // https://github.com/npm/npm/blob/v3.3.6/lib/utils/lifecycle.js#L210-L225
      // DEV: `-c` means command
      var sh = 'sh';
      var shFlag = '-c';
      if (process.platform === 'win32') {
        sh = process.env.comspec || 'cmd';
        // DEV: `/d` disables AutoRun command execution
        // DEV: `/s /c` means use the following command as is
        //   https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/cmd.mspx?mfr=true
        shFlag = '/d /s /c';
        conf.windowsVerbatimArguments = true;
      }

      // If we aren't on a dry run
      if (!that.options.dryRun) {
        // Run our command and pipe stdout if appropriate
        var child = spawn(sh, [shFlag, cmd], conf);
        if (that.customStdout) {
          child.stdout.on('data', function writeToStdout (data) {
            stdout.write(data);
          });
          child.stdout.on('error', function forwardErrorToStdout (err) {
            stdout.emit('error', err);
          });
        }
        if (that.customStderr) {
          child.stderr.on('data', function writeToStdout (data) {
            stderr.write(data);
          });
          child.stderr.on('error', function forwardErrorToStdout (err) {
            stderr.emit('error', err);
          });
        }

        // When the child exits, handle its exit code
        child.on('close', function handleChildClose (code) {
          // If the exit code was not zero, complain and leave
          if (code !== 0) {
            cb(new Error('Received non-zero exit code "' + code + '" from command "' + cmd + '"'));
          // Otherwise, mark the step as a success and continue
          } else {
            stepInfo.completed = true;
            cb(null);
          }
        });
      // Otherwise, callback shortly
      } else {
        process.nextTick(cb);
      }
    }, function handleFailure (err) {
      // If this is a dry run, callback with the error
      if (that.options.dryRun) {
        return callback(err);
      }

      // If there was an error, output our progress to a file
      if (err) {
        // Output our error immediately
        // TODO: Figure out way to stack/concatenate errors instead of using `console.error`
        that.stderr.write('There was an error while executing a command\n');
        that.stderr.write(util.inspect(err));

        // Output params to disk in verbatim fashion
        var content = JSON.stringify(params, null, 2);
        Release.writeResumeFile(content, function handleOutputResumeFile (err2) {
          // If there was a secondary error, callback with it
          if (err2) {
            // TODO: Figure out way to stack/concatenate errors instead of using `console.error`
            that.stderr.write('Attempted to write `' + Release.RESUME_FILE + '` but failed\n');
            return callback(err2);
          }

          // Callback with an error about running resume
          callback(new Error('We have generated a `' + Release.RESUME_FILE + '` file. ' +
            'Once issues have been resolved, the current release can be resumed by running `foundry resume`.'));
        });
      // Otherwise, attempt to clean up resume file
      // DEV: This also covers the scenario where a release fails and then someone re-runs `release`
      } else {
        // DEV: We don't use `stat` + `unlink` since the file could be created in that time
        fs.unlink(Release.RESUME_FILE, function handleError (err) {
          // If there was an error about the file not existing, nullify it
          if (err && err.code === 'ENOENT') {
            err = null;
          }

          // Callback with our error
          callback(err);
        });
      }
    });
  }
};

// Export Release
module.exports = Release;
<|endoftext|>


<commit_before>const Emitter = require('events').EventEmitter ;
const sip = require('drachtio-sip') ;
const delegate = require('delegates') ;
const assert = require('assert') ;
const noop = require('node-noop').noop;
const debug = require('debug')('drachtio:request');

class Request extends Emitter {

  constructor(msg, meta) {
    super() ;

    if (msg) {
      assert(msg instanceof sip.SipMessage) ;
      this.msg = msg ;
      this.meta = meta ;
    }
  }

  get res() {
    return this._res ;
  }
  set res(res) {
    this._res = res ;
    return this ;
  }

  get isNewInvite() {
    const to = this.getParsed('to') ;
    return this.method === 'INVITE' && !('tag' in to.params) ;
  }

  get url() {
    return this.uri ;
  }

  set agent(agent) {
    this._agent = agent ;
  }
  get agent() {
    return this._agent ;
  }

  set meta(meta) {
    debug(`Request#set meta ${JSON.stringify(meta)}`);
    this.source = meta.source ;
    this.source_address = meta.address ;
    this.source_port = meta.port ? parseInt(meta.port) : 5060 ;
    this.protocol = meta.protocol ;
    this.stackTime = meta.time ;
    this.stackTxnId = meta.transactionId ;
    this.stackDialogId = meta.dialogId ;
    if (meta.server) {
      this.server = meta.server;
    }
  }

  get meta() {
    return {
      source: this.source,
      source_address: this.source_address,
      source_port: this.source_port,
      protocol: this.protocol,
      time: this.stackTime,
      transactionId: this.stackTxnId,
      dialogId: this.stackDialogId
    } ;
  }

  /**
 * Cancel a request that was sent by the application
 * @param  {Request~cancelCallback} callback - invoked with cancel operation completes
 */
  cancel(callback) {
    if (!this._agent || this.source !== 'application') {
      throw new Error('Request#cancel can only be used for uac Request') ;
    }
    this._agent.request({_socket: this.socket, uri: this.uri,
      method: 'CANCEL', stackTxnId: this.stackTxnId}, callback) ;
  }
  /**
  * This callback is invoked when the application has sent a CANCEL for a request.
  * @callback Request~cancelCallback
  * @param {Error} err - if an error occurred while attempting to send the cancel
  * @param {Request} req - the cancel request that was sent
  */

  /**
  * Proxy an incoming request
  * @param  {Request~proxyOptions} opts - options governing the proxy operation
  * @param  {Request~proxyCallback} [callback] - callback invoked when proxy operation completes
  * @returns {Promise|Request} returns a Promise if not callback is supplied, otherwise the Request object
  */
  proxy(opts, callback) {
    if (this.source !== 'network') {
      throw new Error('Request#proxy can only be used for incoming requests') ;
    }
    opts = opts || {} ;

    //TODO: throw error if req.res.send has already been called (i.e. can't start off as UAS and then become a proxy)
    const destination = opts.destination || this.uri ;
    if (typeof destination === 'string') { opts.destination = [destination] ; }

    Object.assign(opts, {
      stackTxnId: this.stackTxnId,
      remainInDialog: opts.remainInDialog || opts.path || opts.recordRoute || false,
      provisionalTimeout: opts.provisionalTimeout || '',
      finalTimeout: opts.finalTimeout || '',
      followRedirects: opts.followRedirects || false,
      simultaneous: opts.forking === 'simultaneous',
      fullResponse: true
    }) ;

    //normalize sip uris
    opts.destination.forEach((value, index, array) => {
      const token = value.split(':') ;
      if (token[0] !== 'sip' && token[0] !== 'tel') {
        array[index] = 'sip:' + value ;
      }
    }) ;

    const result = {
      connected: false,
      responses: []
    } ;

    const __x = (callback) => {
      this._agent.proxy(this, opts, (token, rawMsg, meta) => {
        if ('NOK' === token[0]) {
          return callback(token[1]) ;
        }
        if ('done' === token[1]) {
          result.connected = (200 === result.finalStatus) ;
          return callback(null, result) ;
        }
        else {
          //add a new response to the array
          const address = meta.address ;
          const port = +meta.port;
          const msg = new sip.SipMessage(rawMsg) ;
          const obj = {
            time: meta.time,
            status: msg.status,
            msg: msg
          } ;
          let len = result.responses.length ;
          if (len === 0 || address !== result.responses[len - 1].address || port === result.responses[len - 1].port) {
            result.responses.push({
              address: address,
              port: port,
              msgs:[]
            }) ;
            len++ ;
          }
          result.responses[len - 1].msgs.push(obj) ;
          result.finalStatus = msg.status ;
          result.finalResponse = obj ;
        }
      }) ;
    };

    if (callback) {
      __x(callback);
      return this;
    }

    return new Promise((resolve, reject) => {
      __x((err, results) => {
        if (err) return reject(err);
        resolve(results);
      });
    });
  }

  /**
  * Options governing a proxy operation
  * @typedef {Object} Request~proxyOptions
  * @property {string|Array} destination - an ordered list of one or more SIP URIs to proxy the request to
  * @property {boolean} [remainInDialog=false] - if true add a Record-Route header and emain in the SIP dialog
  * after the INVITE transaction.
  * @property {boolean} [followRedirects=false] - if true respond to 3XX redirect responses by generating
  * a new INVITE to the SIP URI in the Contact header of the response
  * @property {string} [forking=sequential] - 'simultaneous' or 'sequential'; dicates whether the proxy waits
  * for a failure response from one target before trying the next, or forks the request to all targets simultaneously
  * @property {string} [provisionalTimeout] - amount of time to wait for a 100 Trying response from a target before
  * trying the next target; valid syntax is '2s' or '1500ms' for example
  * @property {string} [finalTimeout] - amount of time to wait for a final response from a target before trying
  * the next target; syntax is as described above for provisionalTimeout
  */
  /**
  * This callback is invoked when proxy operation has completed.
  * @callback Request~proxyCallback
  * @param {Error} err - if an error occurred while attempting to proxy the request
  * @param {Request~proxyResults} results - results summarizing the proxy operation
  */

  // for compatibility with passport
  logIn(user, options, done) {
    if (typeof options === 'function') {
      done = options;
      options = {};
    }
    options = options || {};
    done = done || noop ;

    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }
    const session = (options.session === undefined) ? true : options.session;

    this[property] = user;
    if (session) {
      if (!this._passport) { throw new Error('passport.initialize() middleware not in use'); }
      if (typeof done !== 'function') { throw new Error('req#login requires a callback function'); }

      this._passport.instance.serializeUser(user, this, (err, obj) => {
        if (err) { this[property] = null; return done(err); }
        if (!this._passport.session) {
          this._passport.session = {};
        }
        this._passport.session.user = obj;
        this.session = this.session || {};
        this.session[this._passport.instance._key] = this._passport.session;
        done();
      });
    } else {
      done();
    }
  }

  // Terminate an existing login session.
  logOut() {
    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }

    this[property] = null;
    if (this._passport && this._passport.session) {
      delete this._passport.session.user;
    }
  }
  // Test if request is authenticated.
  isAuthenticated() {
    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }

    return (this[property]) ? true : false;
  }

  // Test if request is unauthenticated.
  isUnauthenticated() {
    return !this.isAuthenticated();
  }
}

module.exports = Request ;

delegate(Request.prototype, 'msg')
  .method('get')
  .method('has')
  .method('getParsedHeader')
  .method('set')
  .access('method')
  .access('uri')
  .access('headers')
  .access('body')
  .access('payload')
  .getter('type')
  .getter('raw')
  .getter('callingNumber')
  .getter('calledNumber')
  .getter('canFormDialog') ;

/**
 * response event triggered when a Request sent by the application receives a response from the network
 * @event Endpoint#destroy
 * @param {Response} res - SIP response received as a result of sending a SIP request
 */
<commit_msg>Fixed bug where request.js calls non-existent method<commit_after>const Emitter = require('events').EventEmitter ;
const sip = require('drachtio-sip') ;
const delegate = require('delegates') ;
const assert = require('assert') ;
const noop = require('node-noop').noop;
const debug = require('debug')('drachtio:request');

class Request extends Emitter {

  constructor(msg, meta) {
    super() ;

    if (msg) {
      assert(msg instanceof sip.SipMessage) ;
      this.msg = msg ;
      this.meta = meta ;
    }
  }

  get res() {
    return this._res ;
  }
  set res(res) {
    this._res = res ;
    return this ;
  }

  get isNewInvite() {
    const to = this.getParsedHeader('to') ;
    return this.method === 'INVITE' && !('tag' in to.params) ;
  }

  get url() {
    return this.uri ;
  }

  set agent(agent) {
    this._agent = agent ;
  }
  get agent() {
    return this._agent ;
  }

  set meta(meta) {
    debug(`Request#set meta ${JSON.stringify(meta)}`);
    this.source = meta.source ;
    this.source_address = meta.address ;
    this.source_port = meta.port ? parseInt(meta.port) : 5060 ;
    this.protocol = meta.protocol ;
    this.stackTime = meta.time ;
    this.stackTxnId = meta.transactionId ;
    this.stackDialogId = meta.dialogId ;
    if (meta.server) {
      this.server = meta.server;
    }
  }

  get meta() {
    return {
      source: this.source,
      source_address: this.source_address,
      source_port: this.source_port,
      protocol: this.protocol,
      time: this.stackTime,
      transactionId: this.stackTxnId,
      dialogId: this.stackDialogId
    } ;
  }

  /**
 * Cancel a request that was sent by the application
 * @param  {Request~cancelCallback} callback - invoked with cancel operation completes
 */
  cancel(callback) {
    if (!this._agent || this.source !== 'application') {
      throw new Error('Request#cancel can only be used for uac Request') ;
    }
    this._agent.request({_socket: this.socket, uri: this.uri,
      method: 'CANCEL', stackTxnId: this.stackTxnId}, callback) ;
  }
  /**
  * This callback is invoked when the application has sent a CANCEL for a request.
  * @callback Request~cancelCallback
  * @param {Error} err - if an error occurred while attempting to send the cancel
  * @param {Request} req - the cancel request that was sent
  */

  /**
  * Proxy an incoming request
  * @param  {Request~proxyOptions} opts - options governing the proxy operation
  * @param  {Request~proxyCallback} [callback] - callback invoked when proxy operation completes
  * @returns {Promise|Request} returns a Promise if not callback is supplied, otherwise the Request object
  */
  proxy(opts, callback) {
    if (this.source !== 'network') {
      throw new Error('Request#proxy can only be used for incoming requests') ;
    }
    opts = opts || {} ;

    //TODO: throw error if req.res.send has already been called (i.e. can't start off as UAS and then become a proxy)
    const destination = opts.destination || this.uri ;
    if (typeof destination === 'string') { opts.destination = [destination] ; }

    Object.assign(opts, {
      stackTxnId: this.stackTxnId,
      remainInDialog: opts.remainInDialog || opts.path || opts.recordRoute || false,
      provisionalTimeout: opts.provisionalTimeout || '',
      finalTimeout: opts.finalTimeout || '',
      followRedirects: opts.followRedirects || false,
      simultaneous: opts.forking === 'simultaneous',
      fullResponse: true
    }) ;

    //normalize sip uris
    opts.destination.forEach((value, index, array) => {
      const token = value.split(':') ;
      if (token[0] !== 'sip' && token[0] !== 'tel') {
        array[index] = 'sip:' + value ;
      }
    }) ;

    const result = {
      connected: false,
      responses: []
    } ;

    const __x = (callback) => {
      this._agent.proxy(this, opts, (token, rawMsg, meta) => {
        if ('NOK' === token[0]) {
          return callback(token[1]) ;
        }
        if ('done' === token[1]) {
          result.connected = (200 === result.finalStatus) ;
          return callback(null, result) ;
        }
        else {
          //add a new response to the array
          const address = meta.address ;
          const port = +meta.port;
          const msg = new sip.SipMessage(rawMsg) ;
          const obj = {
            time: meta.time,
            status: msg.status,
            msg: msg
          } ;
          let len = result.responses.length ;
          if (len === 0 || address !== result.responses[len - 1].address || port === result.responses[len - 1].port) {
            result.responses.push({
              address: address,
              port: port,
              msgs:[]
            }) ;
            len++ ;
          }
          result.responses[len - 1].msgs.push(obj) ;
          result.finalStatus = msg.status ;
          result.finalResponse = obj ;
        }
      }) ;
    };

    if (callback) {
      __x(callback);
      return this;
    }

    return new Promise((resolve, reject) => {
      __x((err, results) => {
        if (err) return reject(err);
        resolve(results);
      });
    });
  }

  /**
  * Options governing a proxy operation
  * @typedef {Object} Request~proxyOptions
  * @property {string|Array} destination - an ordered list of one or more SIP URIs to proxy the request to
  * @property {boolean} [remainInDialog=false] - if true add a Record-Route header and emain in the SIP dialog
  * after the INVITE transaction.
  * @property {boolean} [followRedirects=false] - if true respond to 3XX redirect responses by generating
  * a new INVITE to the SIP URI in the Contact header of the response
  * @property {string} [forking=sequential] - 'simultaneous' or 'sequential'; dicates whether the proxy waits
  * for a failure response from one target before trying the next, or forks the request to all targets simultaneously
  * @property {string} [provisionalTimeout] - amount of time to wait for a 100 Trying response from a target before
  * trying the next target; valid syntax is '2s' or '1500ms' for example
  * @property {string} [finalTimeout] - amount of time to wait for a final response from a target before trying
  * the next target; syntax is as described above for provisionalTimeout
  */
  /**
  * This callback is invoked when proxy operation has completed.
  * @callback Request~proxyCallback
  * @param {Error} err - if an error occurred while attempting to proxy the request
  * @param {Request~proxyResults} results - results summarizing the proxy operation
  */

  // for compatibility with passport
  logIn(user, options, done) {
    if (typeof options === 'function') {
      done = options;
      options = {};
    }
    options = options || {};
    done = done || noop ;

    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }
    const session = (options.session === undefined) ? true : options.session;

    this[property] = user;
    if (session) {
      if (!this._passport) { throw new Error('passport.initialize() middleware not in use'); }
      if (typeof done !== 'function') { throw new Error('req#login requires a callback function'); }

      this._passport.instance.serializeUser(user, this, (err, obj) => {
        if (err) { this[property] = null; return done(err); }
        if (!this._passport.session) {
          this._passport.session = {};
        }
        this._passport.session.user = obj;
        this.session = this.session || {};
        this.session[this._passport.instance._key] = this._passport.session;
        done();
      });
    } else {
      done();
    }
  }

  // Terminate an existing login session.
  logOut() {
    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }

    this[property] = null;
    if (this._passport && this._passport.session) {
      delete this._passport.session.user;
    }
  }
  // Test if request is authenticated.
  isAuthenticated() {
    let property = 'user';
    if (this._passport && this._passport.instance) {
      property = this._passport.instance._userProperty || 'user';
    }

    return (this[property]) ? true : false;
  }

  // Test if request is unauthenticated.
  isUnauthenticated() {
    return !this.isAuthenticated();
  }
}

module.exports = Request ;

delegate(Request.prototype, 'msg')
  .method('get')
  .method('has')
  .method('getParsedHeader')
  .method('set')
  .access('method')
  .access('uri')
  .access('headers')
  .access('body')
  .access('payload')
  .getter('type')
  .getter('raw')
  .getter('callingNumber')
  .getter('calledNumber')
  .getter('canFormDialog') ;

/**
 * response event triggered when a Request sent by the application receives a response from the network
 * @event Endpoint#destroy
 * @param {Response} res - SIP response received as a result of sending a SIP request
 */
<|endoftext|>


<commit_before>'use strict'

/*
 * Object capable of making API calls.
 */
class Request {
  constructor (options) {
    var URL = require('./url')

    this.options = options || {}
    this.debug = options.debug
    this.debugHeaders = options.debugHeaders
    var url = URL(options.host || 'https://api.heroku.com')
    this.host = url.host
    this.port = url.port
    this.secure = url.secure
    this.partial = options.partial
    this.userAgent = options.userAgent
    if (!this.userAgent) {
      var pjson = require('../package.json')
      this.userAgent = 'node-heroku-client/' + pjson.version
    }
    this.parseJSON = options.hasOwnProperty('parseJSON') ? options.parseJSON : true
    this.nextRange = 'id ..; max=1000'
    this.logger = options.logger
    this.middleware = options.middleware || function (_, cb) { cb() }
    this.certs = getCerts(this.debug)
    this.promise = new Promise((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
    if (process.env.HEROKU_HTTP_PROXY_HOST) {
      var tunnel = require('tunnel-agent')
      var tunnelFunc
      if (this.secure) {
        tunnelFunc = tunnel.httpsOverHttp
      } else {
        tunnelFunc = tunnel.httpOverHttp
      }
      var agentOpts = {
        proxy: {
          host: process.env.HEROKU_HTTP_PROXY_HOST,
          port: process.env.HEROKU_HTTP_PROXY_PORT || 8080,
          proxyAuth: process.env.HEROKU_HTTP_PROXY_AUTH
        }
      }
      if (this.certs.length > 0) {
        agentOpts.ca = this.certs
      }
      this.agent = tunnelFunc(agentOpts)
    } else {
      if (this.secure) {
        var https = require('https')
        this.agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 })
      } else {
        var http = require('http')
        this.agent = new http.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 })
      }
    }
  }

  /*
   * Perform the actual API request.
   */
  request () {
    var headers = Object.assign({
      'Accept': 'application/vnd.heroku+json; version=3',
      'Content-type': 'application/json',
      'User-Agent': this.userAgent,
      'Range': this.nextRange
    }, this.options.headers)
    // remove null|undefined headers
    for (var k in Object.keys(headers)) {
      if (headers[k] === null || headers[k] === undefined) {
        delete headers[k]
      }
    }

    var requestOptions = {
      agent: this.agent,
      host: this.host,
      port: this.port,
      path: this.options.path,
      auth: this.options.auth || ':' + this.options.token,
      method: this.options.method || 'GET',
      rejectUnauthorized: this.options.rejectUnauthorized,
      headers: headers
    }
    if (this.certs.length > 0) {
      requestOptions.ca = this.certs
    }

    let req
    if (this.secure) {
      var https = require('https')
      req = https.request(requestOptions, this.handleResponse.bind(this))
    } else {
      var http = require('http')
      req = http.request(requestOptions, this.handleResponse.bind(this))
    }

    this.logRequest(req)
    this.writeBody(req)
    this.setRequestTimeout(req)

    req.on('error', this.handleError.bind(this))

    req.end()

    return this.promise
  }

  /*
   * Handle an API response, returning the API response.
   */
  handleResponse (res) {
    this.middleware(res, () => {
      this.logResponse(res)
      if (res.statusCode === 304) {
        this.updateAggregate(this.cachedResponse.body)
        this.resolve(this.aggregate)
        return
      }
      concat(res).then((data) => {
        if (this.debug) {
          console.error('<-- ' + data)
        }
        if (res.statusCode.toString().match(/^2\d{2}$/)) {
          this.handleSuccess(res, data)
        } else {
          this.handleFailure(res, data)
        }
      })
    })
  }

  handleError (error) {
    if (!this.retries) this.retries = 0
    const isRetryAllowed = require('is-retry-allowed')
    if (this.retries >= 7 || !isRetryAllowed(error)) return this.reject(error)
    let noise = Math.random() * 100
    setTimeout(() => this.request(), (1 << this.retries) * 1000 + noise)
    this.retries++
  }

  logRequest (req) {
    if (this.debug) {
      console.error('--> ' + req.method + ' ' + req.path)
    }
    if (this.debugHeaders) {
      printHeaders(req._headers)
    }
  }

  /*
   * Log the API response.
   */
  logResponse (res) {
    if (this.logger) {
      this.logger.log({
        status: res.statusCode,
        content_length: res.headers['content-length'],
        request_id: res.headers['request-id']
      })
    }
    if (this.debug) {
      console.error('<-- ' + res.statusCode + ' ' + res.statusMessage)
    }
    if (this.debugHeaders) {
      printHeaders(res.headers)
    }
  }

  /*
   * If the request options include a body,
   * write the body to the request and set
   * an appropriate 'Content-length' header.
   */
  writeBody (req) {
    if (this.options.body) {
      var body = this.options.body
      if (this.options.json !== false) { body = JSON.stringify(body) }
      if (this.debug) {
        console.error('--> ' + body)
      }

      req.setHeader('Content-length', Buffer.byteLength(body, 'utf8'))
      req.write(body)
    } else {
      req.setHeader('Content-length', 0)
    }
  }

  /*
   * If the request options include a timeout,
   * set the timeout and provide a callback
   * function in case the request exceeds the
   * timeout period.
   */
  setRequestTimeout (req) {
    if (!this.options.timeout) return

    req.setTimeout(this.options.timeout, () => {
      var err = new Error('Request took longer than ' + this.options.timeout + 'ms to complete.')

      req.abort()

      this.reject(err)
    })
  }

  /*
   * Get the request body, and parse it (or not) as appropriate.
   * - Parse JSON by default.
   * - If parseJSON is `false`, it will not parse.
   */
  parseBody (body) {
    if (this.parseJSON) {
      return JSON.parse(body || '{}')
    } else {
      return body
    }
  }

  /*
   * In the event of a non-successful API request,
   * fail with an appropriate error message and
   * status code.
   */
  handleFailure (res, buffer) {
    var message = 'Expected response to be successful, got ' + res.statusCode
    var err

    err = new Error(message)
    err.statusCode = res.statusCode
    err.body = this.parseBody(buffer)

    this.reject(err)
  }

  /*
   * In the event of a successful API response,
   * respond with the response body.
   */
  handleSuccess (res, buffer) {
    var body = this.parseBody(buffer)

    if (!this.partial && res.headers['next-range']) {
      this.nextRequest(res.headers['next-range'], body)
    } else {
      this.updateAggregate(body)
      this.resolve(this.aggregate)
    }
  }

  /*
   * Since this request isn't the full response (206 or
   * 304 with a cached Next-Range), perform the next
   * request for more data.
   */
  nextRequest (nextRange, body) {
    this.updateAggregate(body)
    this.nextRange = nextRange
    // The initial range header passed in (if there was one), is no longer valid, and should no longer take precedence
    delete (this.options.headers.Range)
    this.request()
  }

  /*
   * If given an object, sets aggregate to object,
   * otherwise concats array onto aggregate.
   */
  updateAggregate (aggregate) {
    if (aggregate instanceof Array) {
      this.aggregate = this.aggregate || []
      this.aggregate = this.aggregate.concat(aggregate)
    } else {
      this.aggregate = aggregate
    }
  }
}

function sslCertFile () {
  return process.env.SSL_CERT_FILE ? [process.env.SSL_CERT_FILE] : []
}

function sslCertDir () {
  var certDir = process.env.SSL_CERT_DIR
  if (certDir) {
    var path = require('path')
    var fs = require('fs')
    return fs.readdirSync(certDir).map((f) => path.join(certDir, f))
  } else {
    return []
  }
}

function getCerts (debug) {
  var filenames = sslCertFile().concat(sslCertDir())

  if (filenames.length > 0 && debug) {
    console.error('Adding the following trusted certificate authorities')
  }

  return filenames.map(function (filename) {
    var fs = require('fs')
    if (debug) {
      console.error('  ' + filename)
    }
    return fs.readFileSync(filename)
  })
}

function printHeaders (headers) {
  var key
  var value
  for (key in headers) {
    if (headers.hasOwnProperty(key)) {
      value = key.toUpperCase() === 'AUTHORIZATION' ? 'REDACTED' : headers[key]
      console.error('    ' + key + '=' + value)
    }
  }
}

function concat (stream) {
  return new Promise((resolve) => {
    var strings = []
    stream.on('data', (data) => strings.push(data))
    stream.on('end', () => resolve(strings.join('')))
  })
}

module.exports = Request
<commit_msg>do not retry on 4xx errors<commit_after>'use strict'

/*
 * Object capable of making API calls.
 */
class Request {
  constructor (options) {
    var URL = require('./url')

    this.options = options || {}
    this.debug = options.debug
    this.debugHeaders = options.debugHeaders
    var url = URL(options.host || 'https://api.heroku.com')
    this.host = url.host
    this.port = url.port
    this.secure = url.secure
    this.partial = options.partial
    this.userAgent = options.userAgent
    if (!this.userAgent) {
      var pjson = require('../package.json')
      this.userAgent = 'node-heroku-client/' + pjson.version
    }
    this.parseJSON = options.hasOwnProperty('parseJSON') ? options.parseJSON : true
    this.nextRange = 'id ..; max=1000'
    this.logger = options.logger
    this.middleware = options.middleware || function (_, cb) { cb() }
    this.certs = getCerts(this.debug)
    this.promise = new Promise((resolve, reject) => {
      this.resolve = resolve
      this.reject = reject
    })
    if (process.env.HEROKU_HTTP_PROXY_HOST) {
      var tunnel = require('tunnel-agent')
      var tunnelFunc
      if (this.secure) {
        tunnelFunc = tunnel.httpsOverHttp
      } else {
        tunnelFunc = tunnel.httpOverHttp
      }
      var agentOpts = {
        proxy: {
          host: process.env.HEROKU_HTTP_PROXY_HOST,
          port: process.env.HEROKU_HTTP_PROXY_PORT || 8080,
          proxyAuth: process.env.HEROKU_HTTP_PROXY_AUTH
        }
      }
      if (this.certs.length > 0) {
        agentOpts.ca = this.certs
      }
      this.agent = tunnelFunc(agentOpts)
    } else {
      if (this.secure) {
        var https = require('https')
        this.agent = new https.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 })
      } else {
        var http = require('http')
        this.agent = new http.Agent({ maxSockets: Number(process.env.HEROKU_CLIENT_MAX_SOCKETS) || 5000 })
      }
    }
  }

  /*
   * Perform the actual API request.
   */
  request () {
    var headers = Object.assign({
      'Accept': 'application/vnd.heroku+json; version=3',
      'Content-type': 'application/json',
      'User-Agent': this.userAgent,
      'Range': this.nextRange
    }, this.options.headers)
    // remove null|undefined headers
    for (var k in Object.keys(headers)) {
      if (headers[k] === null || headers[k] === undefined) {
        delete headers[k]
      }
    }

    var requestOptions = {
      agent: this.agent,
      host: this.host,
      port: this.port,
      path: this.options.path,
      auth: this.options.auth || ':' + this.options.token,
      method: this.options.method || 'GET',
      rejectUnauthorized: this.options.rejectUnauthorized,
      headers: headers
    }
    if (this.certs.length > 0) {
      requestOptions.ca = this.certs
    }

    let req
    if (this.secure) {
      var https = require('https')
      req = https.request(requestOptions, this.handleResponse.bind(this))
    } else {
      var http = require('http')
      req = http.request(requestOptions, this.handleResponse.bind(this))
    }

    this.logRequest(req)
    this.writeBody(req)
    this.setRequestTimeout(req)

    req.on('error', this.handleError.bind(this))

    req.end()

    return this.promise
  }

  /*
   * Handle an API response, returning the API response.
   */
  handleResponse (res) {
    this.middleware(res, () => {
      this.logResponse(res)
      if (res.statusCode === 304) {
        this.updateAggregate(this.cachedResponse.body)
        this.resolve(this.aggregate)
        return
      }
      concat(res).then((data) => {
        if (this.debug) {
          console.error('<-- ' + data)
        }
        if (res.statusCode.toString().match(/^2\d{2}$/)) {
          this.handleSuccess(res, data)
        } else {
          this.handleFailure(res, data)
        }
      })
    })
  }

  isRetryAllowed (error) {
    const isRetryAllowed = require('is-retry-allowed')
    if (!isRetryAllowed(error)) return false
    if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) return false
    return true
  }

  handleError (error) {
    if (!this.retries) this.retries = 0
    if (this.retries >= 7 || !this.isRetryAllowed(error)) return this.reject(error)
    let noise = Math.random() * 100
    setTimeout(() => this.request(), (1 << this.retries) * 1000 + noise)
    this.retries++
  }

  logRequest (req) {
    if (this.debug) {
      console.error('--> ' + req.method + ' ' + req.path)
    }
    if (this.debugHeaders) {
      printHeaders(req._headers)
    }
  }

  /*
   * Log the API response.
   */
  logResponse (res) {
    if (this.logger) {
      this.logger.log({
        status: res.statusCode,
        content_length: res.headers['content-length'],
        request_id: res.headers['request-id']
      })
    }
    if (this.debug) {
      console.error('<-- ' + res.statusCode + ' ' + res.statusMessage)
    }
    if (this.debugHeaders) {
      printHeaders(res.headers)
    }
  }

  /*
   * If the request options include a body,
   * write the body to the request and set
   * an appropriate 'Content-length' header.
   */
  writeBody (req) {
    if (this.options.body) {
      var body = this.options.body
      if (this.options.json !== false) { body = JSON.stringify(body) }
      if (this.debug) {
        console.error('--> ' + body)
      }

      req.setHeader('Content-length', Buffer.byteLength(body, 'utf8'))
      req.write(body)
    } else {
      req.setHeader('Content-length', 0)
    }
  }

  /*
   * If the request options include a timeout,
   * set the timeout and provide a callback
   * function in case the request exceeds the
   * timeout period.
   */
  setRequestTimeout (req) {
    if (!this.options.timeout) return

    req.setTimeout(this.options.timeout, () => {
      var err = new Error('Request took longer than ' + this.options.timeout + 'ms to complete.')

      req.abort()

      this.reject(err)
    })
  }

  /*
   * Get the request body, and parse it (or not) as appropriate.
   * - Parse JSON by default.
   * - If parseJSON is `false`, it will not parse.
   */
  parseBody (body) {
    if (this.parseJSON) {
      return JSON.parse(body || '{}')
    } else {
      return body
    }
  }

  /*
   * In the event of a non-successful API request,
   * fail with an appropriate error message and
   * status code.
   */
  handleFailure (res, buffer) {
    var message = 'Expected response to be successful, got ' + res.statusCode
    var err

    err = new Error(message)
    err.statusCode = res.statusCode
    err.body = this.parseBody(buffer)

    this.reject(err)
  }

  /*
   * In the event of a successful API response,
   * respond with the response body.
   */
  handleSuccess (res, buffer) {
    var body = this.parseBody(buffer)

    if (!this.partial && res.headers['next-range']) {
      this.nextRequest(res.headers['next-range'], body)
    } else {
      this.updateAggregate(body)
      this.resolve(this.aggregate)
    }
  }

  /*
   * Since this request isn't the full response (206 or
   * 304 with a cached Next-Range), perform the next
   * request for more data.
   */
  nextRequest (nextRange, body) {
    this.updateAggregate(body)
    this.nextRange = nextRange
    // The initial range header passed in (if there was one), is no longer valid, and should no longer take precedence
    delete (this.options.headers.Range)
    this.request()
  }

  /*
   * If given an object, sets aggregate to object,
   * otherwise concats array onto aggregate.
   */
  updateAggregate (aggregate) {
    if (aggregate instanceof Array) {
      this.aggregate = this.aggregate || []
      this.aggregate = this.aggregate.concat(aggregate)
    } else {
      this.aggregate = aggregate
    }
  }
}

function sslCertFile () {
  return process.env.SSL_CERT_FILE ? [process.env.SSL_CERT_FILE] : []
}

function sslCertDir () {
  var certDir = process.env.SSL_CERT_DIR
  if (certDir) {
    var path = require('path')
    var fs = require('fs')
    return fs.readdirSync(certDir).map((f) => path.join(certDir, f))
  } else {
    return []
  }
}

function getCerts (debug) {
  var filenames = sslCertFile().concat(sslCertDir())

  if (filenames.length > 0 && debug) {
    console.error('Adding the following trusted certificate authorities')
  }

  return filenames.map(function (filename) {
    var fs = require('fs')
    if (debug) {
      console.error('  ' + filename)
    }
    return fs.readFileSync(filename)
  })
}

function printHeaders (headers) {
  var key
  var value
  for (key in headers) {
    if (headers.hasOwnProperty(key)) {
      value = key.toUpperCase() === 'AUTHORIZATION' ? 'REDACTED' : headers[key]
      console.error('    ' + key + '=' + value)
    }
  }
}

function concat (stream) {
  return new Promise((resolve) => {
    var strings = []
    stream.on('data', (data) => strings.push(data))
    stream.on('end', () => resolve(strings.join('')))
  })
}

module.exports = Request
<|endoftext|>


<commit_before>
define([
  './assert',
  './util',
  'q/q'
], function (Assert, Util, Q) {

  "use strict";

  function Request(context, options) {
    var deferred = Q.defer();
	try {
	  // TODO: Don't use jQuery and instead ship own minimal implementation.
	  Assert(typeof $ === "function", "Global `$` from jquery not found");
      if (context._dev || options.url.indexOf("localhost") !== -1 || options.url.indexOf("webrtc.hookflash.me") !== -1) {
         options.url = options.url.replace(/^https/, "http");
      }
	  options.method = options.method || "GET";
	  $.ajax(options).done(function(data, textStatus, jqXHR) {
		return deferred.resolve({
		  statusCode: 200,
		  body: data
		});
	  }).fail(function(jqXHR, textStatus, errorThrown) {
	  	var statusCode = jqXHR.status || 0;
  		var error = new Error("Got status '" + statusCode + "' (" + errorThrown + ") while calling '" + options.url + "'");
  		error.statusCode = statusCode;
  		error.statusText = errorThrown;
		return deferred.reject(error);
	  });
	} catch(err) {
	  return deferred.reject(err);
	}
  	return deferred.promise;
  }

  Request.makeRequestTo = function(context, url, handler, method, extra) {
  	try {
  		Assert.isObject(context);
  		Assert.isString(context.appid);
  		Assert.isString(context.identityDomain);
  		Assert.isString(url);
  		Assert.isString(handler);
  		Assert.isString(method);
		var id = Util.randomHex(32);
  		var payload = {
			"request": {
				"$domain": context.identityDomain,
				"$appid": context.appid,
				"$id": id,
				"$handler": handler,
				"$method": method
			}
  		};
  		if (extra) {
	  		Assert(typeof extra, "object");
	  		for (var key in extra) {
	  			payload.request[key] = extra[key];
	  		}
  		}
		if (context._debug) {
			context.getLogger().log("[request] Send (object):", payload);
		}
		payload = JSON.stringify(payload);
		if (context._debug) {
			context.getLogger().log("[request] Send (json):", payload);
		}
		var options = {
			dataType: "json",
			method: "POST",
			url: url,
			data: payload,
			contentType: "application/json"
//			crossDomain: true
		};

		// HACK: Remove once resolved: https://github.com/openpeer/opjs/issues/37
		// PROBLEM: If content-type it not standard form, client sends an OPTIONS request which server is not responding to.
//		if (!context._dev && url.indexOf("localhost") === -1 && url.indexOf("webrtc.hookflash.me") === -1 && url !== ("http://" + Util.getHost() + "/.well-known/openpeer-services-get")) delete options.contentType;
//		if (url.indexOf("hcs-javascript.hookflash.me") !== -1) delete options.contentType;

		return new Request(context, options).then(function(response) {
	        Assert.isObject(response);
	        Assert.isObject(response.body);
	        Assert.isObject(response.body.result);
			if (context._debug) {
				context.getLogger().log("[request] Received:", response.body.result);
			}
	        Assert.equal(response.body.result.$id, id);
	        return response.body.result;
	    });
  	} catch(err) {
  		return Q.reject(err);
  	}
  }

  function RequestPlain(options) {
    var deferred = Q.defer();

	  $.ajax(options).done(function(data, textStatus, jqXHR) {
		return deferred.resolve({
		  statusCode: 200,
		  body: data
		});
	  }).fail(function(jqXHR, textStatus, errorThrown) {
	  	var statusCode = jqXHR.status || 0;
  		var error = new Error("Got status '" + statusCode + "' (" + errorThrown + ") while calling '" + options.url + "'");
  		error.statusCode = statusCode;
  		error.statusText = errorThrown;
		return deferred.reject(error);
	  });

  	return deferred.promise;
  };

  Request.requestPlain = function(payload, method, url) {
    payload = JSON.stringify(payload);

    var options = {
      dataType: 'json',
      method: method,
      url: url,
      data: payload
    };

    return new RequestPlain(options);
  };

  // `target[0] - window`
  // `target[1] - origin`
  Request.postNotifyTo = function(context, target, handler, method, extra) {
  	try {
  		Assert.isObject(context);
  		Assert.isString(context.appid);
  		Assert.isString(context.identityDomain);
  		Assert.isObject(target);
  		Assert.isObject(target[0]);
  		Assert.isString(target[1]);
  		Assert.isString(handler);
  		Assert.isString(method);
		var id = Util.randomHex(32);
  		var payload = {
			"notify": {
				"$domain": context.identityDomain,
				"$appid": context.appid,
				"$id": id,
				"$handler": handler,
				"$method": method
			}
  		};
  		if (extra) {
	  		Assert(typeof extra, "object");
	  		for (var key in extra) {
	  			payload.notify[key] = extra[key];
	  		}
  		}
		if (context._debug) {
			context.getLogger().log("[request] Send (object):", payload);
		}
		payload = JSON.stringify(payload);
		if (context._debug) {
			context.getLogger().log("[request] Send (json):", payload);
		}
		var deferred = Q.defer();
		var timeoutId = setTimeout(function() {
			window.removeEventListener("message", listener, false);
			return deferred.reject(new Error("No post message response received in time!"));
		}, 20 * 1000);
		var listener = function(event) {
			var result = null;
			if (typeof event.data === "string" && event.data.substring(0, 1) === "{") {
				try {
					result = JSON.parse(event.data).result;
				} catch(err) {}
			}
			if (result) {
				if (context._debug) {
					context.getLogger().log("[request] Received:", result);
				}
	            if (result.$id === id) {
					clearTimeout(timeoutId);
					window.removeEventListener("message", listener, false);
					return deferred.resolve(result);
	            }
	        }
		}
		window.addEventListener("message", listener, false);
	    target[0].window.postMessage(payload, target[1]);
	    return deferred.promise;
  	} catch(err) {
  		return Q.reject(err);
  	}
  }

  return Request;

});
<commit_msg>cleanup<commit_after>
define([
  './assert',
  './util',
  'q/q'
], function (Assert, Util, Q) {

  "use strict";

  function Request(context, options) {
    var deferred = Q.defer();
	try {
	  // TODO: Don't use jQuery and instead ship own minimal implementation.
	  Assert(typeof $ === "function", "Global `$` from jquery not found");
      if (context._dev || options.url.indexOf("localhost") !== -1 || options.url.indexOf("webrtc.hookflash.me") !== -1) {
         options.url = options.url.replace(/^https/, "http");
      }
	  options.method = options.method || "GET";
	  $.ajax(options).done(function(data, textStatus, jqXHR) {
		return deferred.resolve({
		  statusCode: 200,
		  body: data
		});
	  }).fail(function(jqXHR, textStatus, errorThrown) {
	  	var statusCode = jqXHR.status || 0;
  		var error = new Error("Got status '" + statusCode + "' (" + errorThrown + ") while calling '" + options.url + "'");
  		error.statusCode = statusCode;
  		error.statusText = errorThrown;
		return deferred.reject(error);
	  });
	} catch(err) {
	  return deferred.reject(err);
	}
  	return deferred.promise;
  }

  Request.makeRequestTo = function(context, url, handler, method, extra) {
  	try {
  		Assert.isObject(context);
  		Assert.isString(context.appid);
  		Assert.isString(context.identityDomain);
  		Assert.isString(url);
  		Assert.isString(handler);
  		Assert.isString(method);
		var id = Util.randomHex(32);
  		var payload = {
			"request": {
				"$domain": context.identityDomain,
				"$appid": context.appid,
				"$id": id,
				"$handler": handler,
				"$method": method
			}
  		};
  		if (extra) {
	  		Assert(typeof extra, "object");
	  		for (var key in extra) {
	  			payload.request[key] = extra[key];
	  		}
  		}
		if (context._debug) {
			context.getLogger().log("[request] Send (object):", payload);
		}
		payload = JSON.stringify(payload);
		if (context._debug) {
			context.getLogger().log("[request] Send (json):", payload);
		}
		var options = {
			dataType: "json",
			method: "POST",
			url: url,
			data: payload,
			contentType: "application/json"
//			crossDomain: true
		};

		// HACK: Remove once resolved: https://github.com/openpeer/opjs/issues/37
		// PROBLEM: If content-type it not standard form, client sends an OPTIONS request which server is not responding to.
//		if (!context._dev && url.indexOf("localhost") === -1 && url.indexOf("webrtc.hookflash.me") === -1 && url !== ("http://" + Util.getHost() + "/.well-known/openpeer-services-get")) delete options.contentType;
//		if (url.indexOf("hcs-javascript.hookflash.me") !== -1) delete options.contentType;

		return new Request(context, options).then(function(response) {
	        Assert.isObject(response);
	        Assert.isObject(response.body);
	        Assert.isObject(response.body.result);
			if (context._debug) {
				context.getLogger().log("[request] Received:", response.body.result);
			}
	        Assert.equal(response.body.result.$id, id);
	        return response.body.result;
	    });
  	} catch(err) {
  		return Q.reject(err);
  	}
  }

  Request.requestPlain = function(payload, method, url) {
    var options = {
		dataType: 'json',
		method: method,
		url: url,
		data: (payload && JSON.stringify(payload)) || null
    };
    var deferred = Q.defer();
	$.ajax(options).done(function(data, textStatus, jqXHR) {
		return deferred.resolve({
		    statusCode: 200,
		    body: data
		});
	}).fail(function(jqXHR, textStatus, errorThrown) {
		var statusCode = jqXHR.status || 0;
		var error = new Error("Got status '" + statusCode + "' (" + errorThrown + ") while calling '" + options.url + "'");
		error.statusCode = statusCode;
		error.statusText = errorThrown;
		return deferred.reject(error);
	});
  	return deferred.promise;
  };

  // `target[0] - window`
  // `target[1] - origin`
  Request.postNotifyTo = function(context, target, handler, method, extra) {
  	try {
  		Assert.isObject(context);
  		Assert.isString(context.appid);
  		Assert.isString(context.identityDomain);
  		Assert.isObject(target);
  		Assert.isObject(target[0]);
  		Assert.isString(target[1]);
  		Assert.isString(handler);
  		Assert.isString(method);
		var id = Util.randomHex(32);
  		var payload = {
			"notify": {
				"$domain": context.identityDomain,
				"$appid": context.appid,
				"$id": id,
				"$handler": handler,
				"$method": method
			}
  		};
  		if (extra) {
	  		Assert(typeof extra, "object");
	  		for (var key in extra) {
	  			payload.notify[key] = extra[key];
	  		}
  		}
		if (context._debug) {
			context.getLogger().log("[request] Send (object):", payload);
		}
		payload = JSON.stringify(payload);
		if (context._debug) {
			context.getLogger().log("[request] Send (json):", payload);
		}
		var deferred = Q.defer();
		var timeoutId = setTimeout(function() {
			window.removeEventListener("message", listener, false);
			return deferred.reject(new Error("No post message response received in time!"));
		}, 20 * 1000);
		var listener = function(event) {
			var result = null;
			if (typeof event.data === "string" && event.data.substring(0, 1) === "{") {
				try {
					result = JSON.parse(event.data).result;
				} catch(err) {}
			}
			if (result) {
				if (context._debug) {
					context.getLogger().log("[request] Received:", result);
				}
	            if (result.$id === id) {
					clearTimeout(timeoutId);
					window.removeEventListener("message", listener, false);
					return deferred.resolve(result);
	            }
	        }
		}
		window.addEventListener("message", listener, false);
	    target[0].window.postMessage(payload, target[1]);
	    return deferred.promise;
  	} catch(err) {
  		return Q.reject(err);
  	}
  }

  return Request;

});
<|endoftext|>


<commit_before>
const _ = require('busyman')
const debug = require('debug')('mqtt-node:request')
const CNST = require('./constants')

const request = {}

request.register = function (qn, callback) {
  const objectList = qn.getSmartObject().objectList()
  const data = {
    transId: null,
    lifetime: qn.lifetime,
    objList: {},
    ip: qn.ip,
    mac: qn.mac,
    version: qn.version
  }

  _.forEach(objectList, (rec) => {
    data.objList[rec.oid] = rec.iid
  })

  return req(qn, 'register', data, (err, rsp) => {
    if (!err && rsp.status === 201) { // Created
      process.nextTick(() => {
        qn.emit('registered')
      })
    }
    callback(err, rsp)
  })
}

request.deregister = function (qn, callback) {
  return req(qn, 'deregister', { transId: null }, (err, rsp) => {
    if (!err) {
      if (rsp.status === CNST.RSP.deleted) {
        qn.close(false, () => {
          process.nextTick(() => {
            qn.emit('deregistered')
          })
          callback(null, rsp)
        })
      } else {
        qn.close(true, () => {
          callback(null, rsp)
        })
      }
    } else {
      callback(err)
    }
  })
}

request.checkin = function (qn, callback) {
  return req(qn, 'schedule', { transId: null, sleep: false }, callback)
}

request.checkout = function (qn, duration, callback) {
  return req(qn, 'schedule', { transId: null, sleep: true, duration }, callback)
}

request.update = function (qn, devAttrs, callback) {
  // Change of mac address and clientId at runtime will be ignored
  return req(qn, 'update', _.omit(devAttrs, ['mac', 'clientId']), callback)
}

request.notify = function (qn, data, callback) {
  return req(qn, 'notify', data, (err, rsp) => {
    if (rsp && rsp.cancel) qn.disableReport(data.oid, data.iid, data.rid)

    if (_.isFunction(callback)) callback(err, rsp)
  })
}

request.ping = function (qn, callback) {
  const txTime = _.now()
  const data = {
    transId: null
  }

  return req(qn, 'ping', data, (err, rsp) => {
    if (!err && rsp.status !== CNST.RSP.timeout) rsp.data = _.now() - txTime // rxTime - txTime

    if (_.isFunction(callback)) callback(err, rsp)
  })
}

function reqTimeout (qn, key, delay) {
  delay = delay || 10000 // default to 10 secs if not given
  qn._tobjs[key] = setTimeout(() => {
    qn.emit(key, null, { status: CNST.RSP.timeout })
    delete qn._tobjs[key]
  }, delay)
}

function req (qn, intf, data, callback) {
  let evt
  let err

  if (!qn.mc || !qn._connected) {
    process.nextTick(() => {
      if (_.isFunction(callback)) callback(new Error('No connection.'))
      else qn._emitError(err)
    })
  } else {
    data.transId = qn._nextTransId(intf)
    evt = `${intf}:rsp:${data.transId}`
    reqTimeout(qn, evt, CNST.REQ_TIMEOUT)

    qn.once(evt, (err, rsp) => {
      // if not timeout yet, clear it
      if (!_.isUndefined(qn._tobjs[evt])) {
        clearTimeout(qn._tobjs[evt])
        delete qn._tobjs[evt]
      }

      rsp.transId = (rsp.status === 408) ? data.transId : rsp.transId
      debug('RSP <-- %s, transId: %d, status: %s', intf, rsp.transId, rsp.status)

      if (_.isFunction(callback)) callback(err, rsp)
    })

    return qn.publish(qn._pubics[intf], data, (err, rsp) => {
      debug('REQ --> %s, transId: %d', intf, data.transId)
      // if mqtt publish fails
      if (err) qn.emit(evt, err, null)
    })
  }
}

module.exports = request
<commit_msg>Refactor request<commit_after>const _ = require('busyman')
const debug = require('debug')('mqtt-node:request')
const CNST = require('./constants')

function reqTimeout (qn, key, delay) {
  delay = delay || 10000 // default to 10 secs if not given
  qn._tobjs[key] = setTimeout(() => {
    qn.emit(key, null, { status: CNST.RSP.timeout })
    delete qn._tobjs[key]
  }, delay)
}

function req (qn, intf, data, callback) {
  let evt
  let err

  if (!qn.mc || !qn._connected) {
    process.nextTick(() => {
      if (_.isFunction(callback)) callback(new Error('No connection.'))
      else qn._emitError(err)
    })
  } else {
    data.transId = qn._nextTransId(intf)
    evt = `${intf}:rsp:${data.transId}`
    reqTimeout(qn, evt, CNST.REQ_TIMEOUT)

    qn.once(evt, (err, rsp) => {
      // if not timeout yet, clear it
      if (!_.isUndefined(qn._tobjs[evt])) {
        clearTimeout(qn._tobjs[evt])
        delete qn._tobjs[evt]
      }

      rsp.transId = (rsp.status === 408) ? data.transId : rsp.transId
      debug('RSP <-- %s, transId: %d, status: %s', intf, rsp.transId, rsp.status)

      if (_.isFunction(callback)) callback(err, rsp)
    })

    return qn.publish(qn._pubics[intf], data, (err, rsp) => {
      debug('REQ --> %s, transId: %d', intf, data.transId)
      // if mqtt publish fails
      if (err) qn.emit(evt, err, null)
    })
  }
}

module.exports = {
  register (qn, callback) {
    const objectList = qn.getSmartObject().objectList()
    const data = {
      transId: null,
      lifetime: qn.lifetime,
      objList: {},
      ip: qn.ip,
      mac: qn.mac,
      version: qn.version
    }

    objectList.forEach((rec) => (data.objList[rec.oid] = rec.iid))

    return req(qn, 'register', data, (err, rsp) => {
      if (!err && rsp.status === 201) { // Created
        process.nextTick(() => {
          qn.emit('registered')
        })
      }
      callback(err, rsp)
    })
  },
  deregister (qn, callback) {
    return req(qn, 'deregister', { transId: null }, (err, rsp) => {
      if (!err) {
        if (rsp.status === CNST.RSP.deleted) {
          qn.close(false, () => {
            process.nextTick(() => {
              qn.emit('deregistered')
            })
            callback(null, rsp)
          })
        } else {
          qn.close(true, () => {
            callback(null, rsp)
          })
        }
      } else {
        callback(err)
      }
    })
  },
  checkin: (qn, callback) => req(qn, 'schedule', { transId: null, sleep: false }, callback),
  checkout: (qn, duration, callback) => req(qn, 'schedule', { transId: null, sleep: true, duration }, callback),
  update: (qn, devAttrs, callback) => req(qn, 'update', _.omit(devAttrs, ['mac', 'clientId']), callback), // Change of mac address and clientId at runtime will be ignored
  notify (qn, data, callback) {
    return req(qn, 'notify', data, (err, rsp) => {
      if (rsp && rsp.cancel) qn.disableReport(data.oid, data.iid, data.rid)
      if (_.isFunction(callback)) callback(err, rsp)
    })
  },
  ping (qn, callback) {
    const txTime = _.now()
    const data = {
      transId: null
    }

    return req(qn, 'ping', data, (err, rsp) => {
      if (!err && rsp.status !== CNST.RSP.timeout) rsp.data = _.now() - txTime // rxTime - txTime
      if (_.isFunction(callback)) callback(err, rsp)
    })
  }
}
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013 Fabio Massaioli
 *
 * Code from Node http module:
 *   Copyright Joyent, Inc. and other Node contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

var util = require('util')
  , InputStream = require('./streams.js').InputStream;

exports.Request = Request;

/**
 * function Request(conn, id, role, keepalive)
 * FastCGI request object. Compatible with http.IncomingMessage
 */

function Request(conn, id, role, keepalive) {
  if (!(this instanceof Request))
    return new Request(conn, id, role, keepalive);

  InputStream.call(this);

  this.id = id;
  this.role = role;
  this.keepalive = keepalive;
  this.connection = conn;
  this.timeout = 0;

  this.params = {};
  this.data = new InputStream(); // NOTE: data stream for filter requests

  this.httpVersion = null;
  this.url = '';
  this.method = null;
  this.headers = {};
  this.trailers = {}; // NOTE: Provided for http.IncomingMessage compatibility: CGI Spec does not allow trailers
  this.contentLength = 0;

  this.readable = true;
  this.complete = false;

  // NOTE: not a real socket, provided for http.IncomingMessage compatibility
  this.socket = {
    setEncoding: conn.socket.setEncoding.bind(conn.socket),
    setTimeout: conn.socket.setTimeout.bind(conn.socket),
    setKeepAlive: conn.socket.setKeepAlive.bind(conn.socket),
    remoteAddress: null,
    remotePort: null,
    localAddress: null,
    localPort: null,
    get bytesRead() { return conn.socket.bytesRead; },
    get bytesWritten() { return conn.socket.bytesWritten; }
  };

}
util.inherits(Request, InputStream);

Request.headerExpression = /^HTTP_/;

Request.prototype.close = function() {
  delete this.connection.requests[this.id];
  if (!this.keepalive) this.connection.socket.end();

  delete this.connection;

  this.emit('close');
};

Request.prototype._param = function(name, value) {
  this.params[name] = value;

  if (Request.headerExpression.test(name)) {
    field = name.slice(5).replace('_', '-').toLowerCase();
    if (this.headers[field] === undefined) this.headers[field] = value;
  }

  else if (name === 'CONTENT_LENGTH') {
    this.headers['content-length'] = value;
    this.contentLength = parseInt(value);
  }
  else if (name === 'CONTENT_TYPE')
    this.headers['content-type'] = value;

  else if (name === 'REMOTE_ADDR')
    this.socket.remoteAddress = value;
  else if (name === 'REMOTE_PORT')
    this.socket.remotePort = parseInt(value);
  else if (name === 'SERVER_ADDR')
    this.socket.localAddress = value;
  else if (name === 'SERVER_PORT')
    this.socket.localPort = parseInt(value);

  else if (name === 'SERVER_PROTOCOL') {
    this.httpVersion = value.slice(5);
    numbers = this.httpVersion.split('.');
    this.httpVersionMajor = parseInt(numbers[0]);
    this.httpVersionMinor = parseInt(numbers[1]);
  }

  else if (name === 'REQUEST_METHOD')
    this.method = value;
  else if (name === 'REQUEST_URI')
    this.url = value;
};

Request.prototype.setTimeout = function(msecs, callback) {
  if (this._timeout_ref) {
    clearTimeout(this._timeout_ref);
  }

  this.timeout = msecs;
  if (callback) this.on('timeout', callback);

  if (msecs > 0) this._timeout_ref = setTimeout(function(self) {
    self.emit('timeout');
  }, msecs, this);
  else this._timeout_ref = undefined;
};

Request.prototype._resetTimeout = function() {
  if (this.timeout || this._timeout_ref) {
    if (this._timeout_ref) clearTimeout(this._timeout_ref);
    if (this.timeout > 0) {
      this._timeout_ref = setTimeout(function(self) {
        self.emit('timeout');
      }, this.timeout, this);
    } else this._timeout_ref = undefined;
  }
};

Request.prototype._read = function(size) {
  while (this._input.length && this.push(this._input.shift()));
};
<commit_msg>Remove _read method from request<commit_after>/**
 * Copyright (c) 2013 Fabio Massaioli
 *
 * Code from Node http module:
 *   Copyright Joyent, Inc. and other Node contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

var util = require('util')
  , InputStream = require('./streams.js').InputStream;

exports.Request = Request;

/**
 * function Request(conn, id, role, keepalive)
 * FastCGI request object. Compatible with http.IncomingMessage
 */

function Request(conn, id, role, keepalive) {
  if (!(this instanceof Request))
    return new Request(conn, id, role, keepalive);

  InputStream.call(this);

  this.id = id;
  this.role = role;
  this.keepalive = keepalive;
  this.connection = conn;
  this.timeout = 0;

  this.params = {};
  this.data = new InputStream(); // NOTE: data stream for filter requests

  this.httpVersion = null;
  this.url = '';
  this.method = null;
  this.headers = {};
  this.trailers = {}; // NOTE: Provided for http.IncomingMessage compatibility: CGI Spec does not allow trailers
  this.contentLength = 0;

  this.readable = true;
  this.complete = false;

  // NOTE: not a real socket, provided for http.IncomingMessage compatibility
  this.socket = {
    setEncoding: conn.socket.setEncoding.bind(conn.socket),
    setTimeout: conn.socket.setTimeout.bind(conn.socket),
    setKeepAlive: conn.socket.setKeepAlive.bind(conn.socket),
    remoteAddress: null,
    remotePort: null,
    localAddress: null,
    localPort: null,
    get bytesRead() { return conn.socket.bytesRead; },
    get bytesWritten() { return conn.socket.bytesWritten; }
  };

}
util.inherits(Request, InputStream);

Request.headerExpression = /^HTTP_/;

Request.prototype.close = function() {
  delete this.connection.requests[this.id];
  if (!this.keepalive) this.connection.socket.end();

  delete this.connection;

  this.emit('close');
};

Request.prototype._param = function(name, value) {
  this.params[name] = value;

  if (Request.headerExpression.test(name)) {
    field = name.slice(5).replace('_', '-').toLowerCase();
    if (this.headers[field] === undefined) this.headers[field] = value;
  }

  else if (name === 'CONTENT_LENGTH') {
    this.headers['content-length'] = value;
    this.contentLength = parseInt(value);
  }
  else if (name === 'CONTENT_TYPE')
    this.headers['content-type'] = value;

  else if (name === 'REMOTE_ADDR')
    this.socket.remoteAddress = value;
  else if (name === 'REMOTE_PORT')
    this.socket.remotePort = parseInt(value);
  else if (name === 'SERVER_ADDR')
    this.socket.localAddress = value;
  else if (name === 'SERVER_PORT')
    this.socket.localPort = parseInt(value);

  else if (name === 'SERVER_PROTOCOL') {
    this.httpVersion = value.slice(5);
    numbers = this.httpVersion.split('.');
    this.httpVersionMajor = parseInt(numbers[0]);
    this.httpVersionMinor = parseInt(numbers[1]);
  }

  else if (name === 'REQUEST_METHOD')
    this.method = value;
  else if (name === 'REQUEST_URI')
    this.url = value;
};

Request.prototype.setTimeout = function(msecs, callback) {
  if (this._timeout_ref) {
    clearTimeout(this._timeout_ref);
  }

  this.timeout = msecs;
  if (callback) this.on('timeout', callback);

  if (msecs > 0) this._timeout_ref = setTimeout(function(self) {
    self.emit('timeout');
  }, msecs, this);
  else this._timeout_ref = undefined;
};

Request.prototype._resetTimeout = function() {
  if (this.timeout || this._timeout_ref) {
    if (this._timeout_ref) clearTimeout(this._timeout_ref);
    if (this.timeout > 0) {
      this._timeout_ref = setTimeout(function(self) {
        self.emit('timeout');
      }, this.timeout, this);
    } else this._timeout_ref = undefined;
  }
};
<|endoftext|>