<commit_before>// Evaluate configuration of supervisor, based on command line options,
// strong-cluster-control configuration, and strong-agent configuration.

var runctl = require('./runctl');
var chdir = require('./chdir');
var cluster = require('cluster');
var control = require('strong-cluster-control');
var dotenv = require('dotenv');
var fs = require('fs');
var open = require('fs').openSync;
var path = require('path');
var sendMetrics = require('./metrics');
var transformer = require('strong-log-transformer');
var util = require('util');
var Logger = require('./logger');

var logger = new Logger(process.stderr);

// Attach targeted control listener to cluster, for both master and workers.
require('./targetctl');

if (cluster.isWorker) {
  module.exports = control.loadOptions();
  module.exports.logger = logger;
  module.exports.profile = process.env.supervisor_profile != 'false';
  module.exports.sendMetrics = sendMetrics;
  return;
}

var detach = require('./detach');
var options = require('./options').parse(process.argv);
var pidfile = require('./pidfile');
var LogWriter = require('./log-writer');
var generateLogName = require('./expander').expand;
var SysLogStream = options.syslog && require('./syslog-stream');

if (options.help) {
  console.log(options.HELP, '\n');
  process.exit(0);
}

if (options.version) {
  console.log('v%s (%s, %s)',
    V('..'), F('strong-agent'), F('strong-cluster-control'));

  function P(package) {
    return require(path.join(package, 'package.json'));
  }
  function V(package) {
    return P(package).version;
  }
  function N(package) {
    return P(package).name;
  }
  function F(package) {
    return util.format('%s v%s', N(package), V(package));
  }

  process.exit(0);
}

// Reset argv to include only the options for the runner before letting
// cluster-control look for its configuration (it examines process.argv).
process.argv = options.argv;
var config = control.loadOptions({
  cluster: process.env.NODE_ENV === 'production' ? 'CPUs' : undefined
});

// Allow module consumers to use our logger
config.logger = logger;

// Reset argv so the runner options are not seen by app
process.argv = process.argv.slice(0, 2).concat(options.args);

// Communicate profile option to the worker using the environment, and master
// using config.
process.env.supervisor_profile = options.profile;
config.profile = options.profile;

// Communicate detach option to the master using config.
config.detach = options.detach;

var app = process.argv[2];
var dirname, basename;

try {
  var stat = fs.statSync(app);
} catch (er) {
  console.error('Invalid app (%s), try `%s --help`.\n', er, options.NAME);
  process.exit(1);
}

// We want to be in the directory of the file we are running, so we can pick up
// configuration stored in it's working directory.

if (stat.isFile()) {
  // The app is a file, we'll run it from its directory.
  chdir(path.dirname(app));
  app = path.basename(app);
}

if (stat.isDirectory()) {
  // The app is a directory (such as '.'), we'll look for what to run from it.
  chdir(app);

  // The canonical way to run is with `npm start`, but we can't do that for a
  // cluster worker, because the worker must start the runner (so it can require
  // strong-agent). Instead, we search for a runnable file, and if that fails,
  // just require the directory and let node do the right thing (which should be
  // to use package.main if it exists, index.js otherwise).
  function existing(file) {
    return fs.existsSync(file) ? file : null;
  }
  app = existing('server.js') || existing('app.js') || '.';
}

options.args[0] = app;
process.argv[2] = app;

// Load the app's .env after we are in its working directory, and before
// cluster.setupMaster() snapshots the environment.
dotenv.load();

// Set the metrics URL in the environment after .env is loaded, so that the
// CLI options override environment.
if (options.metrics) {
  process.env.STRONGLOOP_METRICS = options.metrics;
}

cluster.setupMaster({
  // XXX(rmg): node 0.11.x broke compatibility with 0.10.x for when process.argv
  //           is read and loaded into cluster.settings, so we have to manually
  //           reset args, exec, and execArgv after our changes
  //           see https://github.com/joyent/node/pull/7682 for upstream fix.
  args: process.argv.slice(2),
  exec: process.argv[1],
  execArgv: process.execArgv,
  // All worker output is being processed as individual log streams, so we can't
  // just let the supervisor's stdout/stderr be inherited.
  silent: true
});

var supervisorLog = process.stdout;
// If the logname doesn't change with input, the logs aren't per-worker
var isPerWorker = (generateLogName(options.log, {pid: 1, id: 1}) !==
                    generateLogName(options.log, {pid: 2, id: 2}));

if (options.syslog) {
  var errorLogger = new SysLogStream({level: 'CRIT'});
  var stdLogger = new SysLogStream({level: 'NOTICE'});
  supervisorLog = stdLogger;
  console.error('supervisor (%d) logging to syslog', process.pid);
} else if (!options.syslog && options.log && options.log !== '-') {
  supervisorLog = new LogWriter(process, options);
  console.error('supervisor (%d) logging to \'%s\'',
                process.pid, supervisorLog.name);
}

// In the recommended mode where worker logs are piped through the
// supervisor's stdout, each worker adds 6 listeners (3 per output stream)
// for piping alone. The default is 10.
supervisorLog.setMaxListeners(0);

if (config.clustered) {
  logger.sink = transformer({ tag: { pid: process.pid, worker: 0 },
                              timeStamp: options.timeStampSupervisorLogs });
  logger.sink.pipe(supervisorLog);
}

config.start = function start() {
  if (options.detach) {
    // Daemon shouldn't detach, runner does it.
    options.argv.forEach(function(v, i, a) {
      var substitute = {'-d': '--no-detach', '--detach': '--no-detach'};
      a[i] = substitute[v] || v;
    });
    try {
      var supervisorLogName = generateLogName(options.log,
                                              { id: 0,
                                                pid: process.pid });
      process.env.supervisor_detached = true;
      var child = detach(options.argv.concat(options.args), supervisorLogName);
      console.error('supervisor %d detached process %d, output logged to \'%s\'',
        process.pid, child.pid, supervisorLogName);
      process.exit();  // XXX should not be necessary!
    } catch (er) {
      console.error('supervisor failed to start: %s', er);
      console.error('%s', er.stack);
      process.exit(1); // XXX should not be necessary!
    }
    return;
  }

  cluster.on('fork', function(worker) {
    var tag = { pid: worker.process.pid, worker: worker.id };
    var logStream = isPerWorker
                  ? new LogWriter(worker, options)  // cleaned up by pipe()
                  : supervisorLog;                  // cleaned up by exit()
    var outLog = transformer({ timeStamp: options.timeStampWorkerLogs,
                               tag: tag });
    var errLog = transformer({ timeStamp: options.timeStampWorkerLogs,
                               tag: tag,
                               mergeLines: true });
    // When we have per-worker logs, each worker gets their own LogWriter, which
    // gets cleaned up when the stream piped into it emits 'end'.
    // When we don't have per-worker logs, we need to suppress the propagation
    // of 'end' from the pipe so we don't close the supervisor's log the first
    // time a worker exits.
    worker.process.stdout.pipe(outLog)
                         .pipe(stdLogger || logStream, {end: isPerWorker});
    worker.process.stderr.pipe(errLog)
                         .pipe(errorLogger || logStream, {end: isPerWorker});
    worker.logFile = logStream;
  });

  logger.info('supervisor starting (pid %d)', process.pid);

  if (options.pid) {
    try {
      pidfile.create(options.pid);
    } catch (er) {
      console.error('supervisor failed to create pid file: %s', er.message);
      process.exit(1);
    }
  }

  // Re-require this so that strong-agent (which may be required by now)
  // notices, and attaches cluster control instrumentation.
  require('strong-cluster-control');

  control.on('start', function() {
    logger.info('supervisor listening on \'%s\'', options.channel);

    runctl.start({
      channel: options.channel,
      logger: logger,
    });
  });
  control.on('setSize', function() {
    logger.info('supervisor size set to', this.options.size);
  });
  control.on('resize', function() {
    logger.info('supervisor resized to', this.options.size);
  });
  control.on('startWorker', function(w) {
    logger.info('supervisor started worker %d (pid %d)', w.id, w.process.pid);
  });
  control.on('stopWorker', function(w) {
    logger.info('supervisor stopped worker %d (pid %d)', w.id, w.process.pid);
  });
  control.on('stop', function(w) {
    logger.info('supervisor stopped');
    process.exit(); // XXX should not be necessary!
  });
  cluster.on('exit', function(worker, code, signal) {
    var exit = worker.suicide ? 'expected' : 'accidental';
    logger.error('supervisor worker id %s (pid %d) %s exit with %s',
                  worker.id, worker.process.pid, exit, signal || code
                 );
  });

  var lastSignal = null;
  process.on('exit', function(code) {
    if (code === 0 && lastSignal) {
      process.kill(process.pid, lastSignal);
      return;
    }
    logger.error('supervisor exiting with code %d', code);
  });

  process.once('SIGINT', function() {
    logger.warn('received SIGINT, shutting down');
    lastSignal = 'SIGINT';
    control.stop();
  });
  process.once('SIGTERM', function() {
    logger.warn('received SIGTERM, shutting down');
    lastSignal = 'SIGTERM';
    control.stop();
  });
  process.on('SIGHUP', function() {
    logger.warn('received SIGHUP, restarting workers');
    try {
      chdir(process.env.PWD);
    } catch (er) {
      logger.error('failed to chdir to \'%s\': %s', process.env.PWD, er);
    }
    control.restart();
  });

  if (supervisorLog !== process.stdout) {
    process.on('SIGUSR2', function() {
      logger.warn('received SIGUSR2, re-opening log files');
      supervisorLog.reOpen();
      for (var id in cluster.workers) {
        if (cluster.workers[id].logFile !== supervisorLog) {
          cluster.workers[id].logFile.reOpen();
        }
      }
      logger.info('log files reopened');
    });
  }
  return control.start(config);
};

config.sendMetrics = sendMetrics;

module.exports = config;
<commit_msg>run: fix --no-channel option<commit_after>// Evaluate configuration of supervisor, based on command line options,
// strong-cluster-control configuration, and strong-agent configuration.

var runctl = require('./runctl');
var chdir = require('./chdir');
var cluster = require('cluster');
var control = require('strong-cluster-control');
var dotenv = require('dotenv');
var fs = require('fs');
var open = require('fs').openSync;
var path = require('path');
var sendMetrics = require('./metrics');
var transformer = require('strong-log-transformer');
var util = require('util');
var Logger = require('./logger');

var logger = new Logger(process.stderr);

// Attach targeted control listener to cluster, for both master and workers.
require('./targetctl');

if (cluster.isWorker) {
  module.exports = control.loadOptions();
  module.exports.logger = logger;
  module.exports.profile = process.env.supervisor_profile != 'false';
  module.exports.sendMetrics = sendMetrics;
  return;
}

var detach = require('./detach');
var options = require('./options').parse(process.argv);
var pidfile = require('./pidfile');
var LogWriter = require('./log-writer');
var generateLogName = require('./expander').expand;
var SysLogStream = options.syslog && require('./syslog-stream');

if (options.help) {
  console.log(options.HELP, '\n');
  process.exit(0);
}

if (options.version) {
  console.log('v%s (%s, %s)',
    V('..'), F('strong-agent'), F('strong-cluster-control'));

  function P(package) {
    return require(path.join(package, 'package.json'));
  }
  function V(package) {
    return P(package).version;
  }
  function N(package) {
    return P(package).name;
  }
  function F(package) {
    return util.format('%s v%s', N(package), V(package));
  }

  process.exit(0);
}

// Reset argv to include only the options for the runner before letting
// cluster-control look for its configuration (it examines process.argv).
process.argv = options.argv;
var config = control.loadOptions({
  cluster: process.env.NODE_ENV === 'production' ? 'CPUs' : undefined
});

// Allow module consumers to use our logger
config.logger = logger;

// Reset argv so the runner options are not seen by app
process.argv = process.argv.slice(0, 2).concat(options.args);

// Communicate profile option to the worker using the environment, and master
// using config.
process.env.supervisor_profile = options.profile;
config.profile = options.profile;

// Communicate detach option to the master using config.
config.detach = options.detach;

var app = process.argv[2];
var dirname, basename;

try {
  var stat = fs.statSync(app);
} catch (er) {
  console.error('Invalid app (%s), try `%s --help`.\n', er, options.NAME);
  process.exit(1);
}

// We want to be in the directory of the file we are running, so we can pick up
// configuration stored in it's working directory.

if (stat.isFile()) {
  // The app is a file, we'll run it from its directory.
  chdir(path.dirname(app));
  app = path.basename(app);
}

if (stat.isDirectory()) {
  // The app is a directory (such as '.'), we'll look for what to run from it.
  chdir(app);

  // The canonical way to run is with `npm start`, but we can't do that for a
  // cluster worker, because the worker must start the runner (so it can require
  // strong-agent). Instead, we search for a runnable file, and if that fails,
  // just require the directory and let node do the right thing (which should be
  // to use package.main if it exists, index.js otherwise).
  function existing(file) {
    return fs.existsSync(file) ? file : null;
  }
  app = existing('server.js') || existing('app.js') || '.';
}

options.args[0] = app;
process.argv[2] = app;

// Load the app's .env after we are in its working directory, and before
// cluster.setupMaster() snapshots the environment.
dotenv.load();

// Set the metrics URL in the environment after .env is loaded, so that the
// CLI options override environment.
if (options.metrics) {
  process.env.STRONGLOOP_METRICS = options.metrics;
}

cluster.setupMaster({
  // XXX(rmg): node 0.11.x broke compatibility with 0.10.x for when process.argv
  //           is read and loaded into cluster.settings, so we have to manually
  //           reset args, exec, and execArgv after our changes
  //           see https://github.com/joyent/node/pull/7682 for upstream fix.
  args: process.argv.slice(2),
  exec: process.argv[1],
  execArgv: process.execArgv,
  // All worker output is being processed as individual log streams, so we can't
  // just let the supervisor's stdout/stderr be inherited.
  silent: true
});

var supervisorLog = process.stdout;
// If the logname doesn't change with input, the logs aren't per-worker
var isPerWorker = (generateLogName(options.log, {pid: 1, id: 1}) !==
                    generateLogName(options.log, {pid: 2, id: 2}));

if (options.syslog) {
  var errorLogger = new SysLogStream({level: 'CRIT'});
  var stdLogger = new SysLogStream({level: 'NOTICE'});
  supervisorLog = stdLogger;
  console.error('supervisor (%d) logging to syslog', process.pid);
} else if (!options.syslog && options.log && options.log !== '-') {
  supervisorLog = new LogWriter(process, options);
  console.error('supervisor (%d) logging to \'%s\'',
                process.pid, supervisorLog.name);
}

// In the recommended mode where worker logs are piped through the
// supervisor's stdout, each worker adds 6 listeners (3 per output stream)
// for piping alone. The default is 10.
supervisorLog.setMaxListeners(0);

if (config.clustered) {
  logger.sink = transformer({ tag: { pid: process.pid, worker: 0 },
                              timeStamp: options.timeStampSupervisorLogs });
  logger.sink.pipe(supervisorLog);
}

config.start = function start() {
  if (options.detach) {
    // Daemon shouldn't detach, runner does it.
    options.argv.forEach(function(v, i, a) {
      var substitute = {'-d': '--no-detach', '--detach': '--no-detach'};
      a[i] = substitute[v] || v;
    });
    try {
      var supervisorLogName = generateLogName(options.log,
                                              { id: 0,
                                                pid: process.pid });
      process.env.supervisor_detached = true;
      var child = detach(options.argv.concat(options.args), supervisorLogName);
      console.error('supervisor %d detached process %d, output logged to \'%s\'',
        process.pid, child.pid, supervisorLogName);
      process.exit();  // XXX should not be necessary!
    } catch (er) {
      console.error('supervisor failed to start: %s', er);
      console.error('%s', er.stack);
      process.exit(1); // XXX should not be necessary!
    }
    return;
  }

  cluster.on('fork', function(worker) {
    var tag = { pid: worker.process.pid, worker: worker.id };
    var logStream = isPerWorker
                  ? new LogWriter(worker, options)  // cleaned up by pipe()
                  : supervisorLog;                  // cleaned up by exit()
    var outLog = transformer({ timeStamp: options.timeStampWorkerLogs,
                               tag: tag });
    var errLog = transformer({ timeStamp: options.timeStampWorkerLogs,
                               tag: tag,
                               mergeLines: true });
    // When we have per-worker logs, each worker gets their own LogWriter, which
    // gets cleaned up when the stream piped into it emits 'end'.
    // When we don't have per-worker logs, we need to suppress the propagation
    // of 'end' from the pipe so we don't close the supervisor's log the first
    // time a worker exits.
    worker.process.stdout.pipe(outLog)
                         .pipe(stdLogger || logStream, {end: isPerWorker});
    worker.process.stderr.pipe(errLog)
                         .pipe(errorLogger || logStream, {end: isPerWorker});
    worker.logFile = logStream;
  });

  logger.info('supervisor starting (pid %d)', process.pid);

  if (options.pid) {
    try {
      pidfile.create(options.pid);
    } catch (er) {
      console.error('supervisor failed to create pid file: %s', er.message);
      process.exit(1);
    }
  }

  // Re-require this so that strong-agent (which may be required by now)
  // notices, and attaches cluster control instrumentation.
  require('strong-cluster-control');

  control.on('start', function() {
    if (!options.channel) return;

    logger.info('supervisor listening on \'%s\'', options.channel);

    runctl.start({
      channel: options.channel,
      logger: logger,
    });
  });
  control.on('setSize', function() {
    logger.info('supervisor size set to', this.options.size);
  });
  control.on('resize', function() {
    logger.info('supervisor resized to', this.options.size);
  });
  control.on('startWorker', function(w) {
    logger.info('supervisor started worker %d (pid %d)', w.id, w.process.pid);
  });
  control.on('stopWorker', function(w) {
    logger.info('supervisor stopped worker %d (pid %d)', w.id, w.process.pid);
  });
  control.on('stop', function(w) {
    logger.info('supervisor stopped');
    process.exit(); // XXX should not be necessary!
  });
  cluster.on('exit', function(worker, code, signal) {
    var exit = worker.suicide ? 'expected' : 'accidental';
    logger.error('supervisor worker id %s (pid %d) %s exit with %s',
                  worker.id, worker.process.pid, exit, signal || code
                 );
  });

  var lastSignal = null;
  process.on('exit', function(code) {
    if (code === 0 && lastSignal) {
      process.kill(process.pid, lastSignal);
      return;
    }
    logger.error('supervisor exiting with code %d', code);
  });

  process.once('SIGINT', function() {
    logger.warn('received SIGINT, shutting down');
    lastSignal = 'SIGINT';
    control.stop();
  });
  process.once('SIGTERM', function() {
    logger.warn('received SIGTERM, shutting down');
    lastSignal = 'SIGTERM';
    control.stop();
  });
  process.on('SIGHUP', function() {
    logger.warn('received SIGHUP, restarting workers');
    try {
      chdir(process.env.PWD);
    } catch (er) {
      logger.error('failed to chdir to \'%s\': %s', process.env.PWD, er);
    }
    control.restart();
  });

  if (supervisorLog !== process.stdout) {
    process.on('SIGUSR2', function() {
      logger.warn('received SIGUSR2, re-opening log files');
      supervisorLog.reOpen();
      for (var id in cluster.workers) {
        if (cluster.workers[id].logFile !== supervisorLog) {
          cluster.workers[id].logFile.reOpen();
        }
      }
      logger.info('log files reopened');
    });
  }
  return control.start(config);
};

config.sendMetrics = sendMetrics;

module.exports = config;
<|endoftext|>


<commit_before>var fs = require('fs'),
    rc = require('rc'),
    yargs = require('yargs'),
    path = require('path'),
    util = require('util');

module.exports = Config;

var definitions = {
  'help': {
    alias: 'h',
    type: 'boolean',
    description: 'Display information about avaible options.',
    usage: {
      '--help': '           display short list of avaible options',
      '--help <option>': '  display quick help on <option>',
      '--help -l': '        display full usage info'
    },
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: false
  },
  'version': {
    alias: 'v',
    type: 'boolean',
    description: 'Display Node Inspector\'s version.',
    usage: '--version',
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: false
  },
  'web-port': {
    alias: ['port', 'p'],
    type: 'number',
    description: 'Port to listen on for Node Inspector\'s web interface.',
    usage: {
      '--web-port 8081': '',
      '-p 8081': ''
    },
    _isNodeInspectorOption: true,
    default: 8080
  },
  'web-host': {
    type: 'string',
    description: 'Host to listen on for Node Inspector\'s web interface.',
    usage: {
      '--web-host 127.0.0.1': '',
      '--web-host www.example.com': ''
    },
    _isNodeInspectorOption: true,
    default: '0.0.0.0'
  },
  'debug-port': {
    alias: 'd',
    type: 'number',
    description: 'Node/V8 debugger port (`node --debug={port}`).',
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: 5858
  },
  'save-live-edit': {
    type: 'boolean',
    description: 'Save live edit changes to disk (update the edited files).',
    usage: {
      '--save-live-edit': '',
      '--no-save-live-edit': '    disable saving live edit changes to disk'
    },
    _isNodeInspectorOption: true,
    default: false
  },
  'preload': {
    type: 'boolean',
    description: 'Preload *.js files. You can disable this option to speed up the startup.',
    usage: {
      '--preload': '',
      '--no-preload': '    disable preloading *.js files'
    },
    _isNodeInspectorOption: true,
    default: true
  },
  'inject': {
    type: 'boolean',
    description: 'Enable injection of debugger extensions into the debugged process.',
    usage: {
      '--inject': '',
      '--no-inject': '    disable injecting of debugger extensions'
    },
    _isNodeInspectorOption: true,
    default: true
  },
  'hidden': {
    type: 'string',
    description: 'Array of files to hide from the UI,\n' +
                 'breakpoints in these files will be ignored.\n' +
                 'All paths are interpreted as regular expressions.',
    usage: {
      '--hidden .*\\.test\\.js$ --hidden node_modules/': 'ignore node_modules directory' +
        'and all `*.test.js` files'
    },
    _isNodeInspectorOption: true,
    default: []
  },
  'stack-trace-limit': {
    type: 'number',
    description: 'Number of stack frames to show on a breakpoint.',
    _isNodeInspectorOption: true,
    default: 50
  },
  'ssl-key': {
    type: 'string',
    description: 'A file containing a valid SSL key.',
    usage: '--ssl-key ./ssl/key.pem --ssl-cert ./ssl/cert.pem',
    _isNodeInspectorOption: true,
    default: ''
  },
  'ssl-cert': {
    type: 'string',
    description: 'A file containing a valid SSL certificate.',
    usage: '--ssl-key ./ssl/key.pem --ssl-cert ./ssl/cert.pem',
    _isNodeInspectorOption: true,
    default: ''
  },
  'nodejs': {
    type: 'string',
    description: 'Pass NodeJS options to debugged process (`node --option={value}`).',
    usage: '--nodejs --harmony --nodejs --random_seed=2 app',
    _isNodeDebugOption: true,
    default: []
  },
  'debug-brk': {
    alias: 'b',
    type: 'boolean',
    description: 'Break on the first line (`node --debug-brk`).',
    _isNodeDebugOption: true,
    default: false
  },
  'cli': {
    alias: 'c',
    type: 'boolean',
    description: 'CLI mode, do not open browser.',
    usage: '--cli',
    _isNodeDebugOption: true,
    default: false
  }
};

function Config(argv, NODE_DEBUG_MODE) {
  var defaults = collectDefaultsFromDefinitions(NODE_DEBUG_MODE);
  var parsedArgv = parseArgs(argv);

  checkDeprecatedHiddenStyle(parsedArgv);

  var rcConfig = rc('node-inspector', defaults, parsedArgv);
  var config = normalizeOptions(rcConfig);

  checkDeprecatedNoPreloadStyle(config);
  checkDeprecatedWebHostStyle(config);

  util._extend(this, config);
}

function checkDeprecatedNoPreloadStyle(config) {
  if (config.noPreload !== undefined) {
    // Deprecated in v0.7.3
    console.warn('The config option `no-preload` is deprecated, use `preload` instead');
    config.preload = config.preload || !config.noPreload;
    delete config.noPreload;
  }
}

function checkDeprecatedWebHostStyle(config) {
  if (config.webHost === 'null') {
    // Deprecated in v0.8.0
    console.warn('You use deprecated syntax for web-host option. Use 0.0.0.0 instead of null');
    config.webHost = '0.0.0.0';
  }
}

function checkDeprecatedHiddenStyle(parsedArgv) {
  if (parsedArgv.hidden.length == 1) {
    // Deprecated in v0.8.0
    var isDeprecatedDefinitionStyle = false,
        hidden;
    try {
      hidden = JSON.parse(parsedArgv.hidden[0]);
      isDeprecatedDefinitionStyle = util.isArray(hidden);
    } catch (e) {}

    if (isDeprecatedDefinitionStyle) {
      console.warn('You use deprecated syntax for hidden option.\n' +
                   'Use `--hidden value1 --hidden value2` instead');
      parsedArgv.hidden = hidden;
    }
  }
}

Config._collectDefaults = function(NODE_DEBUG_MODE) {
  var dashedKeyDefaults = collectDefaultsFromDefinitions(NODE_DEBUG_MODE);
  return normalizeOptions(dashedKeyDefaults);
};

Config.serializeOptions = function(options, filter) {
  filter = filter || {};
  var result = [];
  Object.keys(options).forEach(function(key) {
    if (filter[key]) return;

    var serializedOption = serializeOption(keyToDashedKey(key), options[key]);
    if (serializedOption !== '')
      result.unshift(serializedOption);
  });
  return result;
};

function serializeOption(key, value) {
  var prefix = key.length > 1 ? '--' : '-';
  if (value === undefined) return '';
  if (value === true) {
    return prefix + key;
  } else if (value === false) {
    return prefix + key + '=false';
  } else if (util.isArray(value)) {
    if (!value.length) return '';
    return value.map(function(_value) {
      if (util.isRegExp(_value)) _value = _value.source;
      return prefix + key + '=' + _value;
    }).join(' ');
  } else {
    return prefix + key + '=' + value;
  }
}

Config.filterOptions = function(filterExpression, options) {
  var filteredOptions = {};

  Object.keys(options)
    .filter(function(key) {
      var value = options[key];
      var dashedKey = keyToDashedKey(key);
      var definition = definitions[dashedKey];
      return filterExpression(key, value, definition);
    })
    .forEach(function(key) {
      filteredOptions[key] = options[key];
    });

  return filteredOptions;
};

Config.filterNodeDebugOptions = Config.filterOptions.bind(Config, function(key, value, definition) {
  return !definition || definition && definition._isNodeInspectorOption;
});

Config.filterDefaultValues = Config.filterOptions.bind(Config, function(key, value, definition) {
  return !definition || definition && definition.default !== value;
});

Config.prototype.showVersion = function() {
  console.log('Node Inspector v' + require('../package.json').version);
};

Config.prototype.showHelp = function(NODE_DEBUG_MODE) {
  var cmd = getCmd();

  var inspectorOptions = [];
  var nodeDebugOptions = [];
  Object.keys(definitions).forEach(function(key) {
    if (definitions[key]._isNodeDebugOption && NODE_DEBUG_MODE) {
      nodeDebugOptions.push(key);
    } else if (definitions[key]._isNodeInspectorOption) {
      inspectorOptions.push(key);
    }
  }, this);

  var inspectorPart = color('green', '[node-inspector-options]');
  var optionsPart = color('cyan', '[options]');
  var scriptPart = color('red', '[script [script-arguments]]');
  var configurationParts = [inspectorPart];
  if (NODE_DEBUG_MODE) {
    configurationParts.unshift(optionsPart);
    configurationParts.push(scriptPart);
  }

  if (typeof this.help == 'string') {
    //Display help for target option
    showOptionHelp(this.help);
  } else if (this.l) {
    //Display options format and options usage info
    console.log('Usage:\n    %s %s\n', cmd, configurationParts.join(' '));
    if (NODE_DEBUG_MODE) {
      nodeDebugOptions.forEach(showOptionHelp);
    }
    inspectorOptions.forEach(showOptionHelp);
  } else {
    //Display options format, options list and some help information
    console.log(
        'Usage:\n    %s %s\n', cmd, configurationParts.join(' '));

    if (NODE_DEBUG_MODE) {
      console.log(
        'Where %s is one or more of:\n' +
        '    %s\n', optionsPart, nodeDebugOptions.join('\n    '));
    }
    console.log(
        'Where %s is one or more of:\n' +
        '    %s\n', inspectorPart, inspectorOptions.join('\n    '));
    console.log('Use:' + formatUsage(definitions.help.usage) + '\n');

    if (NODE_DEBUG_MODE) {
      console.log(
        'The %s argument is resolved relative to the current working\n' +
        'directory. If no such file exists, then env.PATH is searched.\n',
        color('red', '[script]'));
      console.log(
        'The default mode is to break on the first line of the script, to run\n' +
        'immediately on start use `--no-debug-brk` or press the Resume button.\n');
      console.log(
        'When there is no script specified, the module in the current working\n' +
        'directory is loaded in the REPL session as `m`. This allows you to call\n' +
        'and debug arbitrary functions exported by the current module.\n');
    }

    console.log(
        'Configuration can be also stored in a \'.node-inspectorrc\' file,\n' +
        'see README for more details:\n' +
        '  https://github.com/node-inspector/node-inspector#configuration');
  }
};

function getCmd() {
  return process.env.CMD || path.basename(process.argv[1]);
}

function color(_color, string) {
  var colors = util.inspect.colors;
  return '\u001b[' + colors[_color][0] + 'm' + string +
         '\u001b[' + colors[_color][1] + 'm';
}

function showOptionHelp(option) {
  var info = definitions[option];

  if (info) {
    var optionLine = '--' + option;
    if (info.alias) {
      var aliases = [].concat(info.alias);
      optionLine += ', ' + aliases.map(function(alias) {
        var prefix = alias.length == 1 ? '-' : '--';
        return prefix + alias;
      }).join(', ');
    }
    console.log(color('green', optionLine));
    console.log('  ' + info.description);
    if (info.default !== undefined) {
      console.log('  Default: ' + color('yellow', JSON.stringify(info.default)));
    }
    if (info.usage) {
      var formattedUsage = formatUsage(info.usage);
      console.log('  Usage:' + formattedUsage);
    }
    console.log();
  } else {
    console.error('Description for %s not found', option);
  }
}

function formatUsage(usage) {
  var formattedUsage = '';
  var cmd = getCmd();
  if (typeof usage == 'object') {
    Object.keys(usage).forEach(function(key) {
      formattedUsage += '\n    ' + cmd + ' ' + key + ' ' + color('grey', usage[key]);
    });
  } else if (typeof usage == 'string') {
    formattedUsage += '\n    ' + cmd + ' ' + usage;
  }
  return formattedUsage;
}

function normalizeOptions(options) {
  var normalizedOptions = {};

  Object.keys(options).forEach(function(key) {
    var camelKey = keyToCamelKey(key);
    normalizedOptions[camelKey] = options[key];
  });

  checkHiddenOption(normalizedOptions);
  checkSslOptions(normalizedOptions);

  return normalizedOptions;
}

function checkHiddenOption(options) {
  function toRegExp(string) {
    return new RegExp(string, 'i');
  }
  options.hidden = [].concat(options.hidden || []).map(toRegExp);
}

function checkSslOptions(options) {
  function realPath(path) {
    if (!path) return '';

    path = fs.realpathSync(path);
    if (!fs.existsSync(path)) throw new Error('The file "' + path + '" does not exist');

    return path;
  }

  options.sslKey = realPath(options.sslKey);
  options.sslCert = realPath(options.sslCert);
}

function keyToCamelKey(key) {
  return key.replace(/-./g, function(letter) {
    return letter.slice(1).toUpperCase();
  });
}

function keyToDashedKey(key) {
  return key.replace(/[A-Z]/g, function(letter) {
    return '-' + letter.toLowerCase();
  });
}

function collectDefaultsFromDefinitions(NODE_DEBUG_MODE) {
  var options = {};

  Object.keys(definitions).forEach(function(key) {
    if (util.isArray(definitions[key].default)) {
      options[key] = definitions[key].default.slice();
    } else {
      options[key] = definitions[key].default;
    }
  });

  if (NODE_DEBUG_MODE) {
    options['web-host'] = '127.0.0.1';
    options['debug-brk'] = true;
  }

  return options;
}

function collectAliasesFromDefinitions() {
  var aliases = [];

  Object.keys(definitions).forEach(function(key) {
    if (definitions[key].alias)
      aliases = aliases.concat(definitions[key].alias);
  });

  return aliases;
}

function parseArgs(argv) {
  var argvParser = yargs.options(definitions);

  function wasCustomized(key) {
    var definition = definitions[key];
    if (!definition) return true;

    var options = [key].concat(definition.alias || []);

    return argv.some(function(arg) {
      var optionMatcher = /^(?:--no)?-+(.*?)(?:=.*)?$/;
      arg = (optionMatcher.exec(arg) || [])[1];
      if (arg && options.indexOf(arg) !== -1) return true;
    });
  }

  //Preparse --nodejs options
  var nodejsArgs = [];
  var nodejsIndex = argv.indexOf('--nodejs');
  while (nodejsIndex !== -1) {
    var nodejsArg = argv.splice(nodejsIndex, 2)[1];
    if (nodejsArg !== undefined) {
      nodejsArgs.push(nodejsArg);
    }
    nodejsIndex = argv.indexOf('--nodejs');
  }

  var options = argvParser.parse(argv);
  var script = options._[0];

  if (script) {
    // We want to pass along subarguments, but re-parse our arguments.
    argv = argv.slice(0, argv.indexOf(script) + 1);
    options = argvParser.parse(argv);
  }

  //filter options
  var aliases = collectAliasesFromDefinitions();
  Object.keys(options).forEach(function(key) {
    if (aliases.indexOf(key) > -1) {
      //Filter aliases
      delete options[key];
    } else if (util.isArray(options[key])) {
      //Ignore array options
    } else if (/[A-Z]/.test(key)) {
      //filter camelKey options created by yargs
      delete options[key];
    } else if (!wasCustomized(key)) {
      //filter options with default values
      delete options[key];
    }
  });

  options['nodejs'] = nodejsArgs;

  return options;
}
<commit_msg>Fix config web-host deprecation checker<commit_after>var fs = require('fs'),
    rc = require('rc'),
    yargs = require('yargs'),
    path = require('path'),
    util = require('util');

module.exports = Config;

var definitions = {
  'help': {
    alias: 'h',
    type: 'boolean',
    description: 'Display information about avaible options.',
    usage: {
      '--help': '           display short list of avaible options',
      '--help <option>': '  display quick help on <option>',
      '--help -l': '        display full usage info'
    },
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: false
  },
  'version': {
    alias: 'v',
    type: 'boolean',
    description: 'Display Node Inspector\'s version.',
    usage: '--version',
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: false
  },
  'web-port': {
    alias: ['port', 'p'],
    type: 'number',
    description: 'Port to listen on for Node Inspector\'s web interface.',
    usage: {
      '--web-port 8081': '',
      '-p 8081': ''
    },
    _isNodeInspectorOption: true,
    default: 8080
  },
  'web-host': {
    type: 'string',
    description: 'Host to listen on for Node Inspector\'s web interface.',
    usage: {
      '--web-host 127.0.0.1': '',
      '--web-host www.example.com': ''
    },
    _isNodeInspectorOption: true,
    default: '0.0.0.0'
  },
  'debug-port': {
    alias: 'd',
    type: 'number',
    description: 'Node/V8 debugger port (`node --debug={port}`).',
    _isNodeInspectorOption: true,
    _isNodeDebugOption: true,
    default: 5858
  },
  'save-live-edit': {
    type: 'boolean',
    description: 'Save live edit changes to disk (update the edited files).',
    usage: {
      '--save-live-edit': '',
      '--no-save-live-edit': '    disable saving live edit changes to disk'
    },
    _isNodeInspectorOption: true,
    default: false
  },
  'preload': {
    type: 'boolean',
    description: 'Preload *.js files. You can disable this option to speed up the startup.',
    usage: {
      '--preload': '',
      '--no-preload': '    disable preloading *.js files'
    },
    _isNodeInspectorOption: true,
    default: true
  },
  'inject': {
    type: 'boolean',
    description: 'Enable injection of debugger extensions into the debugged process.',
    usage: {
      '--inject': '',
      '--no-inject': '    disable injecting of debugger extensions'
    },
    _isNodeInspectorOption: true,
    default: true
  },
  'hidden': {
    type: 'string',
    description: 'Array of files to hide from the UI,\n' +
                 'breakpoints in these files will be ignored.\n' +
                 'All paths are interpreted as regular expressions.',
    usage: {
      '--hidden .*\\.test\\.js$ --hidden node_modules/': 'ignore node_modules directory' +
        'and all `*.test.js` files'
    },
    _isNodeInspectorOption: true,
    default: []
  },
  'stack-trace-limit': {
    type: 'number',
    description: 'Number of stack frames to show on a breakpoint.',
    _isNodeInspectorOption: true,
    default: 50
  },
  'ssl-key': {
    type: 'string',
    description: 'A file containing a valid SSL key.',
    usage: '--ssl-key ./ssl/key.pem --ssl-cert ./ssl/cert.pem',
    _isNodeInspectorOption: true,
    default: ''
  },
  'ssl-cert': {
    type: 'string',
    description: 'A file containing a valid SSL certificate.',
    usage: '--ssl-key ./ssl/key.pem --ssl-cert ./ssl/cert.pem',
    _isNodeInspectorOption: true,
    default: ''
  },
  'nodejs': {
    type: 'string',
    description: 'Pass NodeJS options to debugged process (`node --option={value}`).',
    usage: '--nodejs --harmony --nodejs --random_seed=2 app',
    _isNodeDebugOption: true,
    default: []
  },
  'debug-brk': {
    alias: 'b',
    type: 'boolean',
    description: 'Break on the first line (`node --debug-brk`).',
    _isNodeDebugOption: true,
    default: false
  },
  'cli': {
    alias: 'c',
    type: 'boolean',
    description: 'CLI mode, do not open browser.',
    usage: '--cli',
    _isNodeDebugOption: true,
    default: false
  }
};

function Config(argv, NODE_DEBUG_MODE) {
  var defaults = collectDefaultsFromDefinitions(NODE_DEBUG_MODE);
  var parsedArgv = parseArgs(argv);

  checkDeprecatedHiddenStyle(parsedArgv);

  var rcConfig = rc('node-inspector', defaults, parsedArgv);
  var config = normalizeOptions(rcConfig);

  checkDeprecatedNoPreloadStyle(config);
  checkDeprecatedWebHostStyle(config);

  util._extend(this, config);
}

function checkDeprecatedNoPreloadStyle(config) {
  if (config.noPreload !== undefined) {
    // Deprecated in v0.7.3
    console.warn('The config option `no-preload` is deprecated, use `preload` instead');
    config.preload = config.preload || !config.noPreload;
    delete config.noPreload;
  }
}

function checkDeprecatedWebHostStyle(config) {
  if (config.webHost === 'null' || config.webHost === null) {
    // Deprecated in v0.8.0
    console.warn('You use deprecated syntax for web-host option. Use 0.0.0.0 instead of null');
    config.webHost = '0.0.0.0';
  }
}

function checkDeprecatedHiddenStyle(parsedArgv) {
  if (parsedArgv.hidden.length == 1) {
    // Deprecated in v0.8.0
    var isDeprecatedDefinitionStyle = false,
        hidden;
    try {
      hidden = JSON.parse(parsedArgv.hidden[0]);
      isDeprecatedDefinitionStyle = util.isArray(hidden);
    } catch (e) {}

    if (isDeprecatedDefinitionStyle) {
      console.warn('You use deprecated syntax for hidden option.\n' +
                   'Use `--hidden value1 --hidden value2` instead');
      parsedArgv.hidden = hidden;
    }
  }
}

Config._collectDefaults = function(NODE_DEBUG_MODE) {
  var dashedKeyDefaults = collectDefaultsFromDefinitions(NODE_DEBUG_MODE);
  return normalizeOptions(dashedKeyDefaults);
};

Config.serializeOptions = function(options, filter) {
  filter = filter || {};
  var result = [];
  Object.keys(options).forEach(function(key) {
    if (filter[key]) return;

    var serializedOption = serializeOption(keyToDashedKey(key), options[key]);
    if (serializedOption !== '')
      result.unshift(serializedOption);
  });
  return result;
};

function serializeOption(key, value) {
  var prefix = key.length > 1 ? '--' : '-';
  if (value === undefined) return '';
  if (value === true) {
    return prefix + key;
  } else if (value === false) {
    return prefix + key + '=false';
  } else if (util.isArray(value)) {
    if (!value.length) return '';
    return value.map(function(_value) {
      if (util.isRegExp(_value)) _value = _value.source;
      return prefix + key + '=' + _value;
    }).join(' ');
  } else {
    return prefix + key + '=' + value;
  }
}

Config.filterOptions = function(filterExpression, options) {
  var filteredOptions = {};

  Object.keys(options)
    .filter(function(key) {
      var value = options[key];
      var dashedKey = keyToDashedKey(key);
      var definition = definitions[dashedKey];
      return filterExpression(key, value, definition);
    })
    .forEach(function(key) {
      filteredOptions[key] = options[key];
    });

  return filteredOptions;
};

Config.filterNodeDebugOptions = Config.filterOptions.bind(Config, function(key, value, definition) {
  return !definition || definition && definition._isNodeInspectorOption;
});

Config.filterDefaultValues = Config.filterOptions.bind(Config, function(key, value, definition) {
  return !definition || definition && definition.default !== value;
});

Config.prototype.showVersion = function() {
  console.log('Node Inspector v' + require('../package.json').version);
};

Config.prototype.showHelp = function(NODE_DEBUG_MODE) {
  var cmd = getCmd();

  var inspectorOptions = [];
  var nodeDebugOptions = [];
  Object.keys(definitions).forEach(function(key) {
    if (definitions[key]._isNodeDebugOption && NODE_DEBUG_MODE) {
      nodeDebugOptions.push(key);
    } else if (definitions[key]._isNodeInspectorOption) {
      inspectorOptions.push(key);
    }
  }, this);

  var inspectorPart = color('green', '[node-inspector-options]');
  var optionsPart = color('cyan', '[options]');
  var scriptPart = color('red', '[script [script-arguments]]');
  var configurationParts = [inspectorPart];
  if (NODE_DEBUG_MODE) {
    configurationParts.unshift(optionsPart);
    configurationParts.push(scriptPart);
  }

  if (typeof this.help == 'string') {
    //Display help for target option
    showOptionHelp(this.help);
  } else if (this.l) {
    //Display options format and options usage info
    console.log('Usage:\n    %s %s\n', cmd, configurationParts.join(' '));
    if (NODE_DEBUG_MODE) {
      nodeDebugOptions.forEach(showOptionHelp);
    }
    inspectorOptions.forEach(showOptionHelp);
  } else {
    //Display options format, options list and some help information
    console.log(
        'Usage:\n    %s %s\n', cmd, configurationParts.join(' '));

    if (NODE_DEBUG_MODE) {
      console.log(
        'Where %s is one or more of:\n' +
        '    %s\n', optionsPart, nodeDebugOptions.join('\n    '));
    }
    console.log(
        'Where %s is one or more of:\n' +
        '    %s\n', inspectorPart, inspectorOptions.join('\n    '));
    console.log('Use:' + formatUsage(definitions.help.usage) + '\n');

    if (NODE_DEBUG_MODE) {
      console.log(
        'The %s argument is resolved relative to the current working\n' +
        'directory. If no such file exists, then env.PATH is searched.\n',
        color('red', '[script]'));
      console.log(
        'The default mode is to break on the first line of the script, to run\n' +
        'immediately on start use `--no-debug-brk` or press the Resume button.\n');
      console.log(
        'When there is no script specified, the module in the current working\n' +
        'directory is loaded in the REPL session as `m`. This allows you to call\n' +
        'and debug arbitrary functions exported by the current module.\n');
    }

    console.log(
        'Configuration can be also stored in a \'.node-inspectorrc\' file,\n' +
        'see README for more details:\n' +
        '  https://github.com/node-inspector/node-inspector#configuration');
  }
};

function getCmd() {
  return process.env.CMD || path.basename(process.argv[1]);
}

function color(_color, string) {
  var colors = util.inspect.colors;
  return '\u001b[' + colors[_color][0] + 'm' + string +
         '\u001b[' + colors[_color][1] + 'm';
}

function showOptionHelp(option) {
  var info = definitions[option];

  if (info) {
    var optionLine = '--' + option;
    if (info.alias) {
      var aliases = [].concat(info.alias);
      optionLine += ', ' + aliases.map(function(alias) {
        var prefix = alias.length == 1 ? '-' : '--';
        return prefix + alias;
      }).join(', ');
    }
    console.log(color('green', optionLine));
    console.log('  ' + info.description);
    if (info.default !== undefined) {
      console.log('  Default: ' + color('yellow', JSON.stringify(info.default)));
    }
    if (info.usage) {
      var formattedUsage = formatUsage(info.usage);
      console.log('  Usage:' + formattedUsage);
    }
    console.log();
  } else {
    console.error('Description for %s not found', option);
  }
}

function formatUsage(usage) {
  var formattedUsage = '';
  var cmd = getCmd();
  if (typeof usage == 'object') {
    Object.keys(usage).forEach(function(key) {
      formattedUsage += '\n    ' + cmd + ' ' + key + ' ' + color('grey', usage[key]);
    });
  } else if (typeof usage == 'string') {
    formattedUsage += '\n    ' + cmd + ' ' + usage;
  }
  return formattedUsage;
}

function normalizeOptions(options) {
  var normalizedOptions = {};

  Object.keys(options).forEach(function(key) {
    var camelKey = keyToCamelKey(key);
    normalizedOptions[camelKey] = options[key];
  });

  checkHiddenOption(normalizedOptions);
  checkSslOptions(normalizedOptions);

  return normalizedOptions;
}

function checkHiddenOption(options) {
  function toRegExp(string) {
    return new RegExp(string, 'i');
  }
  options.hidden = [].concat(options.hidden || []).map(toRegExp);
}

function checkSslOptions(options) {
  function realPath(path) {
    if (!path) return '';

    path = fs.realpathSync(path);
    if (!fs.existsSync(path)) throw new Error('The file "' + path + '" does not exist');

    return path;
  }

  options.sslKey = realPath(options.sslKey);
  options.sslCert = realPath(options.sslCert);
}

function keyToCamelKey(key) {
  return key.replace(/-./g, function(letter) {
    return letter.slice(1).toUpperCase();
  });
}

function keyToDashedKey(key) {
  return key.replace(/[A-Z]/g, function(letter) {
    return '-' + letter.toLowerCase();
  });
}

function collectDefaultsFromDefinitions(NODE_DEBUG_MODE) {
  var options = {};

  Object.keys(definitions).forEach(function(key) {
    if (util.isArray(definitions[key].default)) {
      options[key] = definitions[key].default.slice();
    } else {
      options[key] = definitions[key].default;
    }
  });

  if (NODE_DEBUG_MODE) {
    options['web-host'] = '127.0.0.1';
    options['debug-brk'] = true;
  }

  return options;
}

function collectAliasesFromDefinitions() {
  var aliases = [];

  Object.keys(definitions).forEach(function(key) {
    if (definitions[key].alias)
      aliases = aliases.concat(definitions[key].alias);
  });

  return aliases;
}

function parseArgs(argv) {
  var argvParser = yargs.options(definitions);

  function wasCustomized(key) {
    var definition = definitions[key];
    if (!definition) return true;

    var options = [key].concat(definition.alias || []);

    return argv.some(function(arg) {
      var optionMatcher = /^(?:--no)?-+(.*?)(?:=.*)?$/;
      arg = (optionMatcher.exec(arg) || [])[1];
      if (arg && options.indexOf(arg) !== -1) return true;
    });
  }

  //Preparse --nodejs options
  var nodejsArgs = [];
  var nodejsIndex = argv.indexOf('--nodejs');
  while (nodejsIndex !== -1) {
    var nodejsArg = argv.splice(nodejsIndex, 2)[1];
    if (nodejsArg !== undefined) {
      nodejsArgs.push(nodejsArg);
    }
    nodejsIndex = argv.indexOf('--nodejs');
  }

  var options = argvParser.parse(argv);
  var script = options._[0];

  if (script) {
    // We want to pass along subarguments, but re-parse our arguments.
    argv = argv.slice(0, argv.indexOf(script) + 1);
    options = argvParser.parse(argv);
  }

  //filter options
  var aliases = collectAliasesFromDefinitions();
  Object.keys(options).forEach(function(key) {
    if (aliases.indexOf(key) > -1) {
      //Filter aliases
      delete options[key];
    } else if (util.isArray(options[key])) {
      //Ignore array options
    } else if (/[A-Z]/.test(key)) {
      //filter camelKey options created by yargs
      delete options[key];
    } else if (!wasCustomized(key)) {
      //filter options with default values
      delete options[key];
    }
  });

  options['nodejs'] = nodejsArgs;

  return options;
}
<|endoftext|>


<commit_before>/*    Copyright 2020 Firewalla INC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const rclient = require('../util/redis_manager.js').getRedisClient();

const mode = require('../net2/Mode.js')

const dhcp = require("../extension/dhcp/dhcp.js");

const sysManager = require('../net2/SysManager.js');

const redisKey = "sys:scan:dhcpserver";
const extensionManager = require('./ExtensionManager.js');
class DHCPServerSensor extends Sensor {
  constructor() {
    super();
  }

  async run() {
    let firstScanTime = this.config.firstScan * 1000 || 120 * 1000; // default to 120 seconds
    setTimeout(() => {
      this.checkAndRunOnce();
    }, firstScanTime);

    let interval = this.config.interval * 1000 || 10 * 60 * 1000; // 10 minutes
    setInterval(() => {
      this.checkAndRunOnce();
    }, interval);
    extensionManager.onGet("dhcpCheck", async (msg) => {
      return this.checkAndRunOnce();
    });
  }

  async checkAndRunOnce() {
    let serverStatus = false;
    await mode.reloadSetupMode();
    let routerIP = sysManager.myGateway();
    if (routerIP) {
      serverStatus = await dhcp.dhcpServerStatus(routerIP);
    }
    await rclient.setAsync(redisKey, serverStatus);
    await rclient.expireAsync(redisKey, 86400);
    return serverStatus;
  }
}

module.exports = DHCPServerSensor;
<commit_msg>update<commit_after>/*    Copyright 2020 Firewalla INC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const rclient = require('../util/redis_manager.js').getRedisClient();

const mode = require('../net2/Mode.js')

const dhcp = require("../extension/dhcp/dhcp.js");

const sysManager = require('../net2/SysManager.js');

const redisKey = "sys:scan:dhcpserver";
const extensionManager = require('./ExtensionManager.js');
class DHCPServerSensor extends Sensor {
  constructor() {
    super();
  }

  async run() {
    let firstScanTime = this.config.firstScan * 1000 || 120 * 1000; // default to 120 seconds
    setTimeout(() => {
      this.checkAndRunOnce();
    }, firstScanTime);

    let interval = this.config.interval * 1000 || 10 * 60 * 1000; // 10 minutes
    setInterval(() => {
      this.checkAndRunOnce();
    }, interval);
    extensionManager.onGet("dhcpCheck", async (msg) => {
      return await this.checkAndRunOnce();
    });
  }

  async checkAndRunOnce() {
    let serverStatus = false;
    await mode.reloadSetupMode();
    let routerIP = sysManager.myGateway();
    if (routerIP) {
      serverStatus = await dhcp.dhcpServerStatus(routerIP);
    }
    await rclient.setAsync(redisKey, serverStatus);
    await rclient.expireAsync(redisKey, 86400);
    return serverStatus;
  }
}

module.exports = DHCPServerSensor;
<|endoftext|>


<commit_before>/**
 * Specification: http://www.w3.org/TR/cors/#resource-processing-model
 */
var simpleMethods, simpleRequestHeaders, simpleResponseHeaders, toLowerCase;

// A method is said to be a simple method if it is a case-sensitive match for one of the following:
Object.defineProperty(exports, "simpleMethods", {
    get: function () {
        return [
            "GET",
            "HEAD",
            "POST"
        ];
    }
});
simpleMethods = exports.simpleMethods;

// A header is said to be a simple header if the header field name is an ASCII case-insensitive match for one of
// the following:
Object.defineProperty(exports, "simpleRequestHeaders", {
    get: function () {
        return [
            "accept",
            "accept-language",
            "content-language",
            "content-type"
        ];
    }
});
simpleRequestHeaders = exports.simpleRequestHeaders;

// A header is said to be a simple response header if the header field name is an ASCII case-insensitive
// match for one of the following:
Object.defineProperty(exports, "simpleResponseHeaders", {
    get: function () {
        return [
            "cache-control",
            "content-language",
            "content-type",
            "expires",
            "last-modified",
            "pragma"
        ];
    }
});
simpleResponseHeaders = exports.simpleResponseHeaders;

toLowerCase = function (array) {
    return array.map(function (el) {
        return el.toLowerCase();
    });
};

exports.create = function (options) {
    options = options || {};
    // TODO: Check if types match.
    options.origins = options.origins || [];
    options.methods = options.methods || simpleMethods;
    options.requestHeaders = options.hasOwnProperty("requestHeaders") ? toLowerCase(options.requestHeaders) : simpleRequestHeaders;
    options.responseHeaders = options.hasOwnProperty("responseHeaders") ? toLowerCase(options.responseHeaders) : simpleResponseHeaders;
    options.maxAge = options.maxAge || null;
    options.supportsCredentials = options.supportsCredentials || false;
    return function (req, res, next) {
        var originMatches, methodMatches, headersMatch, requestMethod
          , requestHeaders, exposedHeaders;
        // If the Origin header is not present terminate this set of steps.
        if (!req.headers.hasOwnProperty("origin")) {
            // The request is outside the scope of the CORS specification. If there is no Origin header,
            // it could be a same-origin request. Let's let the user-agent handle this situation.
            next();
        } else {
            // If the value of the Origin header is not a case-sensitive match for any of the values in
            // list of origins, do not set any additional headers and terminate this set of steps.
            originMatches = false;
            if (options.origins.length > 0) {
                options.origins.forEach(function (optionsOrigin) {
                    if (optionsOrigin === req.headers.origin) {
                        originMatches = true;
                    }
                });
            } else {
                // Always matching is acceptable since the list of origins can be unbounded.
                originMatches = true;
            }
            if (originMatches === false) {
                next();
            } else {
                // Respond to preflight request.
                if (req.method === "OPTIONS") {
                    // If there is no Access-Control-Request-Method header or if parsing failed, do not set
                    // any additional headers and terminate this set of steps.
                    if (!req.headers.hasOwnProperty("access-control-request-method")) {
                        next();
                    } else {
                        requestMethod = req.headers["access-control-request-method"];
                        // If there are no Access-Control-Request-Headers headers let header field-names be the
                        // empty list. If parsing failed do not set any additional headers and terminate this set
                        // of steps.
                        if (req.headers.hasOwnProperty("access-control-request-headers")) {
                            requestHeaders = req.headers["access-control-request-headers"].split(/,\s*/);
                        } else {
                            requestHeaders = [];
                        }
                        // If method is not a case-sensitive match for any of the values in list of methods do not
                        // set any additional headers and terminate this set of steps.
                        methodMatches = false;
                        options.methods.forEach(function (optionsMethod) {
                            if (optionsMethod === requestMethod) {
                                methodMatches = true;
                            }
                        });
                        if (methodMatches === false) {
                            next();
                        } else {
                            // If any of the header field-names is not a ASCII case-insensitive match for any of
                            // the values in list of headers do not set any additional headers and terminate this
                            // set of steps.
                            headersMatch = 0;
                            requestHeaders.forEach(function (requestHeader) {
                                var requestHeaderMatches;
                                requestHeaderMatches = false;
                                // Browsers automatically add Origin to Access-Control-Request-Headers. However, Origin is not one of the
                                // simple request headers. Therefore, the header is accepted even if it is not in the list of request
                                // headers because CORS would not work without it.
                                if (requestHeader.match(/origin/i)) {
                                    requestHeaderMatches = true;
                                } else {
                                    options.requestHeaders.forEach(function (optionsRequestHeader) {
                                        if (requestHeader === optionsRequestHeader) {
                                            requestHeaderMatches = true;
                                        }
                                    });
                                }
                                if (requestHeaderMatches === true) {
                                    headersMatch++;
                                }
                            });
                            if (headersMatch !== requestHeaders.length) {
                                next();
                            } else {
                                if (options.supportsCredentials === true) {
                                    // If the resource supports credentials add a single Access-Control-Allow-Origin
                                    // header, with the value of the Origin header as value, and add a single
                                    // Access-Control-Allow-Credentials header with the case-sensitive string "true"
                                    // as value.
                                    res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                                    res.setHeader("Access-Control-Allow-Credentials", "true");
                                } else {
                                    // Otherwise, add a single Access-Control-Allow-Origin header, with either the
                                    // value of the Origin header or the string "*" as value.
                                    if (options.origins.length > 0) {
                                        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                                    } else {
                                        res.setHeader("Access-Control-Allow-Origin", "*");
                                    }
                                }
                                // Optionally add a single Access-Control-Max-Age header with as value the amount
                                // of seconds the user agent is allowed to cache the result of the request.
                                if (options.maxAge !== null) {
                                    res.setHeader("Access-Control-Max-Age", options.maxAge);
                                }
                                // Add one or more Access-Control-Allow-Methods headers consisting of (a subset
                                // of) the list of methods.
                                res.setHeader("Access-Control-Allow-Methods", options.methods.join(","));
                                // Add one or more Access-Control-Allow-Headers headers consisting of (a subset
                                // of) the list of headers.
                                res.setHeader("Access-Control-Allow-Headers", options.requestHeaders.join(","));
                                // And out.
                                next();
                            }
                        }
                    }
                } else {
                    if (options.supportsCredentials === true) {
                        // If the resource supports credentials add a single Access-Control-Allow-Origin header,
                        // with the value of the Origin header as value, and add a single
                        // Access-Control-Allow-Credentials header with the literal string "true" as value.
                        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                        res.setHeader("Access-Control-Allow-Credentials", "true");
                    } else {
                        // Otherwise, add a single Access-Control-Allow-Origin header, with either the value of
                        // the Origin header or the literal string "*" as value.
                        // If the list of origins is empty, use "*" as value.
                        if (options.origins.length > 0) {
                            res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                        } else {
                            res.setHeader("Access-Control-Allow-Origin", "*");
                        }
                    }
                    // If the resource wants to expose more than just simple response headers add one or more
                    // Access-Control-Expose-Headers headers, with as values the filed names of the additional
                    // headers to expose.
                    exposedHeaders = [];
                    options.responseHeaders.forEach(function (optionsResponseHeader) {
                        var isSimpleResponseHeader;
                        isSimpleResponseHeader = false;
                        simpleResponseHeaders.forEach(function (simpleResponseHeader) {
                            if (optionsResponseHeader === simpleResponseHeader) {
                                isSimpleResponseHeader = true;
                            }
                        });
                        if (isSimpleResponseHeader === false) {
                            exposedHeaders.push(optionsResponseHeader);
                        }
                    });
                    if (exposedHeaders.length > 0) {
                        res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(","));
                    }
                    // And out.
                    next();
                }
            }
        };
    };
};
<commit_msg>Update comments.<commit_after>/**
 * Specification: http://www.w3.org/TR/cors/#resource-processing-model
 */
var simpleMethods, simpleRequestHeaders, simpleResponseHeaders, toLowerCase;

// A method is said to be a simple method if it is a case-sensitive match for one of the following:
Object.defineProperty(exports, "simpleMethods", {
    get: function () {
        return [
            "GET",
            "HEAD",
            "POST"
        ];
    }
});
simpleMethods = exports.simpleMethods;

// A header is said to be a simple header if the header field name is an ASCII case-insensitive match for one of
// the following:
Object.defineProperty(exports, "simpleRequestHeaders", {
    get: function () {
        return [
            "accept",
            "accept-language",
            "content-language",
            "content-type"
        ];
    }
});
simpleRequestHeaders = exports.simpleRequestHeaders;

// A header is said to be a simple response header if the header field name is an ASCII case-insensitive
// match for one of the following:
Object.defineProperty(exports, "simpleResponseHeaders", {
    get: function () {
        return [
            "cache-control",
            "content-language",
            "content-type",
            "expires",
            "last-modified",
            "pragma"
        ];
    }
});
simpleResponseHeaders = exports.simpleResponseHeaders;

toLowerCase = function (array) {
    return array.map(function (el) {
        return el.toLowerCase();
    });
};

exports.create = function (options) {
    options = options || {};
    // TODO: Check if types match.
    options.origins = options.origins || [];
    options.methods = options.methods || simpleMethods;
    options.requestHeaders = options.hasOwnProperty("requestHeaders") ? toLowerCase(options.requestHeaders) : simpleRequestHeaders;
    options.responseHeaders = options.hasOwnProperty("responseHeaders") ? toLowerCase(options.responseHeaders) : simpleResponseHeaders;
    options.maxAge = options.maxAge || null;
    options.supportsCredentials = options.supportsCredentials || false;
    return function (req, res, next) {
        var originMatches, methodMatches, headersMatch, requestMethod
          , requestHeaders, exposedHeaders;
        // If the Origin header is not present terminate this set of steps.
        if (!req.headers.hasOwnProperty("origin")) {
            // The request is outside the scope of the CORS specification. If there is no Origin header,
            // it could be a same-origin request. Let's let the user-agent handle this situation.
            next();
        } else {
            // If the value of the Origin header is not a case-sensitive match for any of the values in
            // list of origins, do not set any additional headers and terminate this set of steps.
            originMatches = false;
            if (options.origins.length > 0) {
                options.origins.forEach(function (optionsOrigin) {
                    if (optionsOrigin === req.headers.origin) {
                        originMatches = true;
                    }
                });
            } else {
                // Always matching is acceptable since the list of origins can be unbounded.
                originMatches = true;
            }
            if (originMatches === false) {
                next();
            } else {
                // Respond to preflight request.
                if (req.method === "OPTIONS") {
                    // If there is no Access-Control-Request-Method header or if parsing failed, do not set
                    // any additional headers and terminate this set of steps.
                    if (!req.headers.hasOwnProperty("access-control-request-method")) {
                        next();
                    } else {
                        requestMethod = req.headers["access-control-request-method"];
                        // If there are no Access-Control-Request-Headers headers let header field-names be the
                        // empty list. If parsing failed do not set any additional headers and terminate this set
                        // of steps.
                        if (req.headers.hasOwnProperty("access-control-request-headers")) {
                            requestHeaders = req.headers["access-control-request-headers"].split(/,\s*/);
                        } else {
                            requestHeaders = [];
                        }
                        // If method is not a case-sensitive match for any of the values in list of methods do not
                        // set any additional headers and terminate this set of steps.
                        methodMatches = false;
                        options.methods.forEach(function (optionsMethod) {
                            if (optionsMethod === requestMethod) {
                                methodMatches = true;
                            }
                        });
                        if (methodMatches === false) {
                            next();
                        } else {
                            // If any of the header field-names is not a ASCII case-insensitive match for any of
                            // the values in list of headers do not set any additional headers and terminate this
                            // set of steps.
                            headersMatch = 0;
                            requestHeaders.forEach(function (requestHeader) {
                                var requestHeaderMatches;
                                requestHeaderMatches = false;
                                // Browsers automatically add Origin to Access-Control-Request-Headers. However, Origin is not one of the
                                // simple request headers. Therefore, the header is accepted even if it is not in the list of request
                                // headers because CORS would not work without it.
                                if (requestHeader.match(/origin/i)) {
                                    requestHeaderMatches = true;
                                } else {
                                    options.requestHeaders.forEach(function (optionsRequestHeader) {
                                        if (requestHeader === optionsRequestHeader) {
                                            requestHeaderMatches = true;
                                        }
                                    });
                                }
                                if (requestHeaderMatches === true) {
                                    headersMatch++;
                                }
                            });
                            if (headersMatch !== requestHeaders.length) {
                                next();
                            } else {
                                if (options.supportsCredentials === true) {
                                    // If the resource supports credentials add a single Access-Control-Allow-Origin
                                    // header, with the value of the Origin header as value, and add a single
                                    // Access-Control-Allow-Credentials header with the literal string "true"
                                    // as value.
                                    res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                                    res.setHeader("Access-Control-Allow-Credentials", "true");
                                } else {
                                    // Otherwise, add a single Access-Control-Allow-Origin header, with either the
                                    // value of the Origin header or the string "*" as value.
                                    if (options.origins.length > 0) {
                                        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                                    } else {
                                        res.setHeader("Access-Control-Allow-Origin", "*");
                                    }
                                }
                                // Optionally add a single Access-Control-Max-Age header with as value the amount
                                // of seconds the user agent is allowed to cache the result of the request.
                                if (options.maxAge !== null) {
                                    res.setHeader("Access-Control-Max-Age", options.maxAge);
                                }
                                // Add one or more Access-Control-Allow-Methods headers consisting of (a subset
                                // of) the list of methods.
                                res.setHeader("Access-Control-Allow-Methods", options.methods.join(","));
                                // Add one or more Access-Control-Allow-Headers headers consisting of (a subset
                                // of) the list of headers.
                                res.setHeader("Access-Control-Allow-Headers", options.requestHeaders.join(","));
                                // And out.
                                next();
                            }
                        }
                    }
                } else {
                    if (options.supportsCredentials === true) {
                        // If the resource supports credentials add a single Access-Control-Allow-Origin header,
                        // with the value of the Origin header as value, and add a single
                        // Access-Control-Allow-Credentials header with the literal string "true" as value.
                        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                        res.setHeader("Access-Control-Allow-Credentials", "true");
                    } else {
                        // Otherwise, add a single Access-Control-Allow-Origin header, with either the value of
                        // the Origin header or the literal string "*" as value.
                        // If the list of origins is empty, use "*" as value.
                        if (options.origins.length > 0) {
                            res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
                        } else {
                            res.setHeader("Access-Control-Allow-Origin", "*");
                        }
                    }
                    // If the list of exposed headers is not empty add one or more Access-Control-Expose-Headers
                    // headers, with as values the header field names given in the list of exposed headers.
                    exposedHeaders = [];
                    options.responseHeaders.forEach(function (optionsResponseHeader) {
                        var isSimpleResponseHeader;
                        isSimpleResponseHeader = false;
                        simpleResponseHeaders.forEach(function (simpleResponseHeader) {
                            if (optionsResponseHeader === simpleResponseHeader) {
                                isSimpleResponseHeader = true;
                            }
                        });
                        if (isSimpleResponseHeader === false) {
                            exposedHeaders.push(optionsResponseHeader);
                        }
                    });
                    if (exposedHeaders.length > 0) {
                        res.setHeader("Access-Control-Expose-Headers", exposedHeaders.join(","));
                    }
                    // And out.
                    next();
                }
            }
        };
    };
};
<|endoftext|>


<commit_before>const cheerio = require('cheerio');
const request = require('request');
const moment = require('moment');

var nameRegex = /\s-\s.*/;
var instrRegex = /Instructor:\s+/;
var updateRegex = /Last\sUpdate:\s+/;
var htmlRegex = /index\.html/;

var Course = function (url, secret) {
  this.url = url;
  this.secret = secret;
}

Course.prototype.scrapeInfo = function () {
  var course = this;
  return new Promise(function(resolve, reject) {
    request(course.url, function (error, response, body) {
      if (!error) {
        var $ = cheerio.load(body);
        course.name = $('font').eq(0).text().replace(nameRegex, '');
        course.instructor = $('font').eq(1).text().replace(instrRegex, '');
        course.last_update = moment($('font').eq(4).text().replace(updateRegex, ''), 'M/D/YYYY h:mm:ss A').toString();
        resolve(course);
      } else {
        reject(error);
      }
    });
  });
};

Course.prototype.scrapeScore = function () {
  var course = this;
  var url = course.url.replace(htmlRegex, 'coursestand.html');
  return new Promise(function(resolve, reject) {
    request(url, function (error, response, body) {
      if (!error) {
        var $ = cheerio.load(body);
        var tables = $('table');
        var table;
        tables.each(function () {
          if ($(this).attr('cellpadding') == 3) {
            table = $(this);
          }
        })
        // scrape categories info from the first three rows
        var row = table.children().first();
        var numCategories = row.children().length - 3;

        var categories = [];

        var item = row.children().first().next().next(); // first category
        var prop = row.next().children().first().next().next(); // first prop of first category
        var maxValue = row.next().next().children().first().next().next(); // first value of first prop
        for (var i = 0; i < numCategories; i++) {
          var newCategory = {};
          newCategory.name = item.text();
          newCategory.props = [];
          for (var j = 0; j < item.attr('colspan'); j++) {
            if (maxValue.text().trim().length > 0) {
              newCategory.props.push({
                name: prop.text(),
                maxValue: maxValue.text()
              });
            } else {
              newCategory.props.push({
                name: prop.text()
              });
            }
            prop = prop.next();
            maxValue = maxValue.next();
          }
          categories.push(newCategory);
          item = item.next();
        }

        for (var i = 0; i < table.children().length; i++) {
          if (row.children().first().text() == course.secret) {
            break;
          }
          row = row.next();
        }
        course.categories = categories;
        // overall score and/or rank)
        // some courses do not provide rank for overall grade
        if (item.attr('colspan') == 2) {
          course.overallRank = parseInt(row.children().last().prev().text());
          course.overallScore = row.children().last().text();
        } else {
          course.overallScore = row.children().last().text();
        }
        resolve(course);
      } else {
        reject(error);
      }
    });
  });
};

module.exports = Course;
<commit_msg>Move categories assignment<commit_after>const cheerio = require('cheerio');
const request = require('request');
const moment = require('moment');

var nameRegex = /\s-\s.*/;
var instrRegex = /Instructor:\s+/;
var updateRegex = /Last\sUpdate:\s+/;
var htmlRegex = /index\.html/;

var Course = function (url, secret) {
  this.url = url;
  this.secret = secret;
}

Course.prototype.scrapeInfo = function () {
  var course = this;
  return new Promise(function(resolve, reject) {
    request(course.url, function (error, response, body) {
      if (!error) {
        var $ = cheerio.load(body);
        course.name = $('font').eq(0).text().replace(nameRegex, '');
        course.instructor = $('font').eq(1).text().replace(instrRegex, '');
        course.last_update = moment($('font').eq(4).text().replace(updateRegex, ''), 'M/D/YYYY h:mm:ss A').toString();
        resolve(course);
      } else {
        reject(error);
      }
    });
  });
};

Course.prototype.scrapeScore = function () {
  var course = this;
  var url = course.url.replace(htmlRegex, 'coursestand.html');
  return new Promise(function(resolve, reject) {
    request(url, function (error, response, body) {
      if (!error) {
        var $ = cheerio.load(body);
        var tables = $('table');
        var table;
        tables.each(function () {
          if ($(this).attr('cellpadding') == 3) {
            table = $(this);
          }
        })
        // scrape categories info from the first three rows
        var row = table.children().first();
        var numCategories = row.children().length - 3;

        var categories = [];

        var item = row.children().first().next().next(); // first category
        var prop = row.next().children().first().next().next(); // first prop of first category
        var maxValue = row.next().next().children().first().next().next(); // first value of first prop
        for (var i = 0; i < numCategories; i++) {
          var newCategory = {};
          newCategory.name = item.text();
          newCategory.props = [];
          for (var j = 0; j < item.attr('colspan'); j++) {
            if (maxValue.text().trim().length > 0) {
              newCategory.props.push({
                name: prop.text(),
                maxValue: maxValue.text()
              });
            } else {
              newCategory.props.push({
                name: prop.text()
              });
            }
            prop = prop.next();
            maxValue = maxValue.next();
          }
          categories.push(newCategory);
          item = item.next();
        }
        course.categories = categories;

        for (var i = 0; i < table.children().length; i++) {
          if (row.children().first().text() == course.secret) {
            break;
          }
          row = row.next();
        }

        // overall score and/or rank
        // some courses do not provide rank for overall grade
        if (item.attr('colspan') == 2) {
          course.overallRank = parseInt(row.children().last().prev().text());
          course.overallScore = row.children().last().text();
        } else {
          course.overallScore = row.children().last().text();
        }
        resolve(course);
      } else {
        reject(error);
      }
    });
  });
};

module.exports = Course;
<|endoftext|>