<commit_before>// Wrapper around Apple's Instruments app
"use strict";

var spawn = require('child_process').spawn
  , through = require('through')
  , exec = require('child_process').exec
  , logger = require('../../server/logger.js').get('appium')
  , fs = require('fs')
  , _ = require('underscore')
  , net = require('net')
  , uuid = require('uuid-js')
  , path = require('path')
  , rimraf = require('rimraf')
  , mkdirp = require('mkdirp')
  , codes = require('../../server/status.js').codes;

var ERR_NEVER_CHECKED_IN = "Instruments never checked in"
  , ERR_CRASHED_ON_STARTUP = "Instruments crashed on startup";

var Instruments = function(opts) {
  this.app = opts.app;
  this.launchTimeout = opts.launchTimeout || 90000;
  this.killTimeout = 6000;
  this.killTimer = null;
  this.flakeyRetries = opts.flakeyRetries;
  this.launchTries = 0;
  this.neverConnected = false;
  this.udid = opts.udid;
  this.isSafariLauncherApp = opts.isSafariLauncherApp;
  this.bootstrap = opts.bootstrap;
  this.sock = opts.sock || '/tmp/instruments_sock';
  this.template = opts.template;
  this.withoutDelay = opts.withoutDelay;
  this.xcodeVersion = opts.xcodeVersion;
  this.webSocket = opts.webSocket;
  this.commandQueue = [];
  this.curCommand = null;
  this.resultHandler = this.defaultResultHandler;
  this.launchHandlerCalledBack = false;
  this.exitHandler = this.defaultExitHandler;
  this.socketConnectTimeout = null;
  this.hasConnected = false;
  this.traceDir = null;
  this.proc = null;
  this.shutdownCb = null;
  this.didLaunch = false;
  this.debugMode = false;
  this.onReceiveCommand = null;
  this.guid = uuid.create();
  this.bufferedData = "";
  this.instrumentsPath = "";
  this.eventRouter = {'cmd': this.commandHandler};
  this.socketServer = null;
  this.logNoColors = opts.logNoColors;
};

/* INITIALIZATION */

Instruments.prototype.start = function(launchCb, unexpectedExitCb) {
  if (this.didLaunch) {
    return launchCb(new Error("Called start() but we already launched"));
  }
  this.launchHandlerCb = launchCb;
  this.exitHandler = unexpectedExitCb;

  exec('xcrun -find instruments', function (err, stdout) {
    if (typeof stdout === "undefined") stdout = "";
    this.instrumentsPath = stdout.trim();
    if (err || !this.instrumentsPath) {
      logger.error("Could not find instruments binary");
      if (err) logger.error(err.message);
      return this.launchHandler(new Error("Could not find the instruments " +
          "binary. Please ensure `xcrun -find instruments` can locate it."));
    }
    logger.info("Instruments is at: " + this.instrumentsPath);

    this.initSocketServer(function(err) {
      if (err) return this.launchHandler(err);
      this.launch(function(err) {
        if (err) return this.launchHandler(err);
        // we don't call launchHandler in the success case; it's called
        // when instruments checks in, in the conn handler for the socket
        // server, or when instruments times out, in onSocketNeverConnect
      });
    }.bind(this));
  }.bind(this));
};

Instruments.prototype.launchHandler = function(err) {
  if (!this.launchHandlerCalledBack) {
    clearTimeout(this.socketConnectTimeout);
    if (!err) {
      this.didLaunch = true;
      this.neverConnected = false;
    } else {
      if (this.launchTries < this.flakeyRetries &&
          (err.message === ERR_NEVER_CHECKED_IN ||
           err.message === ERR_CRASHED_ON_STARTUP)) {
        this.launchTries++;
        logger.info("Attempting to retry launching instruments, this is " +
                    "retry #" + this.launchTries);
        return this.launch(function(err) {
          if (err) return this.launchHandler(err);
        }.bind(this));
      }
      logger.error(err.message);
    }
    this.launchHandlerCalledBack = true;
    if (err) this.killProc();
    this.launchHandlerCb(err);
  } else {
    logger.info("Trying to call back from instruments launch but we " +
                "already did");
  }
};

Instruments.prototype.killProc = function() {
  if (this.proc !== null) {
    logger.info("Sending sigkill to instruments");
    this.killTimer = setTimeout(function() {
      logger.info("Instruments didn't die after " + (this.killTimeout / 1000) +
                  " seconds; abandoning it completely");
      this.onInstrumentsExit(1);
    }.bind(this), this.killTimeout);
    this.proc.kill('SIGKILL');
  }
};

Instruments.prototype.onSocketNeverConnect = function() {
  logger.error("Instruments socket client never checked in; timing out".red);
  this.neverConnected = true;
  this.killProc();
};

Instruments.prototype.initSocketServer = function(cb) {
  this.socketServer = net.createServer({allowHalfOpen: true},
      this.onSocketConnect.bind(this));

  this.socketServer.on('close', function() {
    this.debug("Instruments socket server was closed");
  }.bind(this));

  // remove socket file if it currently exists
  try {
    fs.unlinkSync(this.sock);
  } catch (err) {
    // if we get any error other than "socket doesn't exist", fail
    if (err.message.indexOf("ENOENT") === -1) {
      return cb(err);
    }
  }

  this.socketServer.listen(this.sock, function(err) {
    if (err) return cb(err);
    this.debug("Instruments socket server started at " + this.sock);
    cb();
  }.bind(this));

};

Instruments.prototype.onSocketConnect = function(conn) {
  if (!this.hasConnected) {
    this.hasConnected = true;
    this.debug("Instruments is ready to receive commands");
    this.launchHandler();
  }
  conn.setEncoding('utf8'); // get strings from sockets rather than buffers

  conn.pipe(through(function(data) {
    // when data comes in, route it according to the "event" property
    this.debug("Socket data received (" + data.length + " bytes)");
    this.bufferedData += data;
  }.bind(this)));

  this.currentSocket = conn;

  conn.on('close', function() {
    this.currentSocket = null;
  }.bind(this));

  conn.on('end', function() {
    var data = this.bufferedData;
    this.bufferedData = "";
    try {
      data = JSON.parse(data);
    } catch (e) {
      logger.error("Couldn't parse JSON data from socket, maybe buffer issue?");
      logger.error(data);
      data = {
        event: 'cmd'
        , result: {
          status: codes.UnknownError
          , value: "Error parsing socket data from instruments"
        }
      };
    }
    if (!_.has(data, 'event')) {
      logger.error("Socket data came in without event, it was:");
      logger.error(JSON.stringify(data));
    } else if (!_.has(this.eventRouter, data.event)) {
      logger.error("Socket is asking for event '" + data.event +
                  "' which doesn't exist");
    } else {
      this.debug("Socket data being routed for '" + data.event + "' event");
      this.eventRouter[data.event].bind(this)(data, conn);
    }
  }.bind(this));
};

Instruments.prototype.launch = function(cb) {
  // prepare temp dir
  var tmpDir = "/tmp/appium-instruments/";
  try {
    rimraf.sync(tmpDir);
    mkdirp.sync(tmpDir);
  } catch (err) {
    return cb(err);
  }

  try {
    this.proc = this.spawnInstruments(tmpDir);
  } catch (err) {
    return cb(err);
  }
  // start waiting for instruments to launch successfully
  this.socketConnectTimeout = setTimeout(
        this.onSocketNeverConnect.bind(this),
        this.launchTimeout);

  this.proc.stdout.setEncoding('utf8');
  this.proc.stderr.setEncoding('utf8');
  this.proc.stdout.pipe(through(this.outputStreamHandler.bind(this)));
  this.proc.stderr.pipe(through(this.errorStreamHandler.bind(this)));
  this.proc.on('exit', this.onInstrumentsExit.bind(this));
};

Instruments.prototype.spawnInstruments = function(tmpDir) {
  var args = ["-t", this.template];
  if (this.udid) {
    args = args.concat(["-w", this.udid]);
    logger.info("Attempting to run app on real device with UDID " + this.udid);
  }
  args = args.concat([this.app]);
  args = args.concat(["-e", "UIASCRIPT", this.bootstrap]);
  args = args.concat(["-e", "UIARESULTSPATH", tmpDir]);
  var env = _.clone(process.env);
  var buildPath = path.resolve(__dirname, "../../../build");
  var isXcode4 = this.xcodeVersion !== null && this.xcodeVersion[0] === '4';
  var iwdPath = path.resolve(buildPath, isXcode4 ? "iwd4" : "iwd");
  env.CA_DEBUG_TRANSACTIONS = 1;
  if (this.withoutDelay && !this.udid) {
    env.DYLD_INSERT_LIBRARIES = path.resolve(iwdPath, "InstrumentsShim.dylib");
    env.LIB_PATH = iwdPath;
  }
  logger.info("Spawning instruments with command: " + this.instrumentsPath +
              " " + args.join(" "));
  logger.info("And extra without-delay env: " + JSON.stringify({
    DYLD_INSERT_LIBRARIES: env.DYLD_INSERT_LIBRARIES
    , LIB_PATH: env.LIB_PATH
  }));
  logger.info("And launch timeout: " + this.launchTimeout + "ms");
  return spawn(this.instrumentsPath, args, {env: env});
};

Instruments.prototype.onInstrumentsExit = function(code) {
  clearTimeout(this.killTimer);
  this.debug("Instruments exited with code " + code);

  if (this.neverConnected) {
    this.neverConnected = false; // reset so we can catch this again
    return this.launchHandler(new Error(ERR_NEVER_CHECKED_IN));
  }

  if (!this.didLaunch) {
    return this.launchHandler(new Error(ERR_CRASHED_ON_STARTUP));
  }

  this.cleanupInstruments();

  if (this.isSafariLauncherApp) {
    logger.info("Not worrying about instruments exit since we're using " +
                "SafariLauncher");
    this.launchHandler();
  } else if (this.shutdownCb !== null) {
    this.shutdownCb();
    this.shutdownCb = null;
  } else {
    this.exitHandler(code, this.traceDir);
  }

};

Instruments.prototype.cleanupInstruments = function() {
  logger.info("Cleaning up after instruments exit");
  this.proc = null;
  // make sure clear out command cbs so we can't have any lingering cbs
  // if a socket request makes it through after exit somehow
  this.curCommand = null;
  this.onReceiveCommand = null;

  if (this.currentSocket) {
    this.debug("Closing instruments client socket due to exit");
    this.currentSocket.end();
    this.currentSocket.destroy(); // close this
    this.socketServer.close();
  }

};

/* COMMAND LIFECYCLE */

Instruments.prototype.commandHandler = function(data, c) {
  var hasResult = typeof data.result !== "undefined";
  if (hasResult && !this.curCommand) {
    logger.info("Got a result when we weren't expecting one! Ignoring it");
    logger.info("Result was: " + JSON.stringify(data.result));
  } else if (!hasResult && this.curCommand) {
    logger.info("Instruments didn't send a result even though we were expecting one");
    hasResult = true;
    data.result = false;
  }

  if (hasResult && this.curCommand) {
    if (data.result) {
      this.debug("Got result from instruments: " + JSON.stringify(data.result));
    } else {
      this.debug("Got null result from instruments");
    }
    this.curCommand.cb(data.result);
    this.curCommand = null;
  }

  this.waitForCommand(function() {
    this.curCommand = this.commandQueue.shift();
    this.onReceiveCommand = null;
    this.debug("Sending command to instruments: " + this.curCommand.cmd);
    c.write(JSON.stringify({nextCommand: this.curCommand.cmd}));
    c.end();
    //this.debug("Closing our half of the connection");
  }.bind(this));
};

Instruments.prototype.waitForCommand = function(cb) {
  if (this.commandQueue.length) {
    cb();
  } else {
    this.onReceiveCommand = cb;
  }
};

Instruments.prototype.sendCommand = function(cmd, cb) {
  this.commandQueue.push({cmd: cmd, cb: cb});
  if (this.onReceiveCommand) {
    this.onReceiveCommand();
  }
};

/* PROCESS MANAGEMENT */

Instruments.prototype.shutdown = function(cb) {
  this.killProc();
  this.shutdownCb = cb;
};

Instruments.prototype.doExit = function() {
  console.log("Calling exit handler");
};

/* INSTRUMENTS STREAM MANIPULATION*/

Instruments.prototype.clearBufferChars = function(output) {
  // Instruments output is buffered, so for each log output we also output
  // a stream of very many ****. This function strips those out so all we
  // get is the log output we care about
  var re = /(\n|^)\*+\n?/g;
  output = output.toString();
  output = output.replace(re, "");
  return output;
};

Instruments.prototype.outputStreamHandler = function(output) {
  output = this.clearBufferChars(output);
  this.lookForShutdownInfo(output);
  this.resultHandler(output);
};

Instruments.prototype.errorStreamHandler = function(output) {
  logger.info(("[INST STDERR] " + output).yellow);
  if (this.webSocket) {
    var re = /Call to onAlert returned 'YES'/;
    var match = re.test(output);
    if (match) {
      logger.info("Emiting alert message...");
      this.webSocket.sockets.emit('alert', {message: output});
    }
  }
};

Instruments.prototype.lookForShutdownInfo = function(output) {
  var re = /Instruments Trace Complete.+Output : ([^\)]+)\)/;
  var match = re.exec(output);
  if (match) {
    this.traceDir = match[1];
  }
};

/* DEFAULT HANDLERS */

Instruments.prototype.setResultHandler = function(handler) {
  this.resultHandler = handler;
};

Instruments.prototype.defaultResultHandler = function(output) {
  // if we have multiple log lines, indent non-first ones
  if (output !== "") {
    output = output.replace(/\n/m, "\n       ");
    logger.info(("[INST] " + output).green);
  }
};

Instruments.prototype.defaultExitHandler = function(code, traceDir) {
  logger.info("Instruments exited with code " + code + " and trace dir " + traceDir);
};

/* MISC */

Instruments.prototype.setDebug = function(debug) {
  if (typeof debug === "undefined") {
    debug = true;
  }
  this.debugMode = debug;
};

Instruments.prototype.debug = function(msg) {
  var log = "[INSTSERVER] " + msg;
  if (!this.logNoColors) {
    log = log.grey;
  }

  logger.info(log);
};

module.exports = Instruments;
<commit_msg>fix forgotten bind issue<commit_after>// Wrapper around Apple's Instruments app
"use strict";

var spawn = require('child_process').spawn
  , through = require('through')
  , exec = require('child_process').exec
  , logger = require('../../server/logger.js').get('appium')
  , fs = require('fs')
  , _ = require('underscore')
  , net = require('net')
  , uuid = require('uuid-js')
  , path = require('path')
  , rimraf = require('rimraf')
  , mkdirp = require('mkdirp')
  , codes = require('../../server/status.js').codes;

var ERR_NEVER_CHECKED_IN = "Instruments never checked in"
  , ERR_CRASHED_ON_STARTUP = "Instruments crashed on startup";

var Instruments = function(opts) {
  this.app = opts.app;
  this.launchTimeout = opts.launchTimeout || 90000;
  this.killTimeout = 6000;
  this.killTimer = null;
  this.flakeyRetries = opts.flakeyRetries;
  this.launchTries = 0;
  this.neverConnected = false;
  this.udid = opts.udid;
  this.isSafariLauncherApp = opts.isSafariLauncherApp;
  this.bootstrap = opts.bootstrap;
  this.sock = opts.sock || '/tmp/instruments_sock';
  this.template = opts.template;
  this.withoutDelay = opts.withoutDelay;
  this.xcodeVersion = opts.xcodeVersion;
  this.webSocket = opts.webSocket;
  this.commandQueue = [];
  this.curCommand = null;
  this.resultHandler = this.defaultResultHandler;
  this.launchHandlerCalledBack = false;
  this.exitHandler = this.defaultExitHandler;
  this.socketConnectTimeout = null;
  this.hasConnected = false;
  this.traceDir = null;
  this.proc = null;
  this.shutdownCb = null;
  this.didLaunch = false;
  this.debugMode = false;
  this.onReceiveCommand = null;
  this.guid = uuid.create();
  this.bufferedData = "";
  this.instrumentsPath = "";
  this.eventRouter = {'cmd': this.commandHandler};
  this.socketServer = null;
  this.logNoColors = opts.logNoColors;
};

/* INITIALIZATION */

Instruments.prototype.start = function(launchCb, unexpectedExitCb) {
  if (this.didLaunch) {
    return launchCb(new Error("Called start() but we already launched"));
  }
  this.launchHandlerCb = launchCb;
  this.exitHandler = unexpectedExitCb;

  exec('xcrun -find instruments', function (err, stdout) {
    if (typeof stdout === "undefined") stdout = "";
    this.instrumentsPath = stdout.trim();
    if (err || !this.instrumentsPath) {
      logger.error("Could not find instruments binary");
      if (err) logger.error(err.message);
      return this.launchHandler(new Error("Could not find the instruments " +
          "binary. Please ensure `xcrun -find instruments` can locate it."));
    }
    logger.info("Instruments is at: " + this.instrumentsPath);

    this.initSocketServer(function(err) {
      if (err) return this.launchHandler(err);
      this.launch(function(err) {
        if (err) return this.launchHandler(err);
        // we don't call launchHandler in the success case; it's called
        // when instruments checks in, in the conn handler for the socket
        // server, or when instruments times out, in onSocketNeverConnect
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

Instruments.prototype.launchHandler = function(err) {
  if (!this.launchHandlerCalledBack) {
    clearTimeout(this.socketConnectTimeout);
    if (!err) {
      this.didLaunch = true;
      this.neverConnected = false;
    } else {
      if (this.launchTries < this.flakeyRetries &&
          (err.message === ERR_NEVER_CHECKED_IN ||
           err.message === ERR_CRASHED_ON_STARTUP)) {
        this.launchTries++;
        logger.info("Attempting to retry launching instruments, this is " +
                    "retry #" + this.launchTries);
        return this.launch(function(err) {
          if (err) return this.launchHandler(err);
        }.bind(this));
      }
      logger.error(err.message);
    }
    this.launchHandlerCalledBack = true;
    if (err) this.killProc();
    this.launchHandlerCb(err);
  } else {
    logger.info("Trying to call back from instruments launch but we " +
                "already did");
  }
};

Instruments.prototype.killProc = function() {
  if (this.proc !== null) {
    logger.info("Sending sigkill to instruments");
    this.killTimer = setTimeout(function() {
      logger.info("Instruments didn't die after " + (this.killTimeout / 1000) +
                  " seconds; abandoning it completely");
      this.onInstrumentsExit(1);
    }.bind(this), this.killTimeout);
    this.proc.kill('SIGKILL');
  }
};

Instruments.prototype.onSocketNeverConnect = function() {
  logger.error("Instruments socket client never checked in; timing out".red);
  this.neverConnected = true;
  this.killProc();
};

Instruments.prototype.initSocketServer = function(cb) {
  this.socketServer = net.createServer({allowHalfOpen: true},
      this.onSocketConnect.bind(this));

  this.socketServer.on('close', function() {
    this.debug("Instruments socket server was closed");
  }.bind(this));

  // remove socket file if it currently exists
  try {
    fs.unlinkSync(this.sock);
  } catch (err) {
    // if we get any error other than "socket doesn't exist", fail
    if (err.message.indexOf("ENOENT") === -1) {
      return cb(err);
    }
  }

  this.socketServer.listen(this.sock, function(err) {
    if (err) return cb(err);
    this.debug("Instruments socket server started at " + this.sock);
    cb();
  }.bind(this));

};

Instruments.prototype.onSocketConnect = function(conn) {
  if (!this.hasConnected) {
    this.hasConnected = true;
    this.debug("Instruments is ready to receive commands");
    this.launchHandler();
  }
  conn.setEncoding('utf8'); // get strings from sockets rather than buffers

  conn.pipe(through(function(data) {
    // when data comes in, route it according to the "event" property
    this.debug("Socket data received (" + data.length + " bytes)");
    this.bufferedData += data;
  }.bind(this)));

  this.currentSocket = conn;

  conn.on('close', function() {
    this.currentSocket = null;
  }.bind(this));

  conn.on('end', function() {
    var data = this.bufferedData;
    this.bufferedData = "";
    try {
      data = JSON.parse(data);
    } catch (e) {
      logger.error("Couldn't parse JSON data from socket, maybe buffer issue?");
      logger.error(data);
      data = {
        event: 'cmd'
        , result: {
          status: codes.UnknownError
          , value: "Error parsing socket data from instruments"
        }
      };
    }
    if (!_.has(data, 'event')) {
      logger.error("Socket data came in without event, it was:");
      logger.error(JSON.stringify(data));
    } else if (!_.has(this.eventRouter, data.event)) {
      logger.error("Socket is asking for event '" + data.event +
                  "' which doesn't exist");
    } else {
      this.debug("Socket data being routed for '" + data.event + "' event");
      this.eventRouter[data.event].bind(this)(data, conn);
    }
  }.bind(this));
};

Instruments.prototype.launch = function(cb) {
  // prepare temp dir
  var tmpDir = "/tmp/appium-instruments/";
  try {
    rimraf.sync(tmpDir);
    mkdirp.sync(tmpDir);
  } catch (err) {
    return cb(err);
  }

  try {
    this.proc = this.spawnInstruments(tmpDir);
  } catch (err) {
    return cb(err);
  }
  // start waiting for instruments to launch successfully
  this.socketConnectTimeout = setTimeout(
        this.onSocketNeverConnect.bind(this),
        this.launchTimeout);

  this.proc.stdout.setEncoding('utf8');
  this.proc.stderr.setEncoding('utf8');
  this.proc.stdout.pipe(through(this.outputStreamHandler.bind(this)));
  this.proc.stderr.pipe(through(this.errorStreamHandler.bind(this)));
  this.proc.on('exit', this.onInstrumentsExit.bind(this));
};

Instruments.prototype.spawnInstruments = function(tmpDir) {
  var args = ["-t", this.template];
  if (this.udid) {
    args = args.concat(["-w", this.udid]);
    logger.info("Attempting to run app on real device with UDID " + this.udid);
  }
  args = args.concat([this.app]);
  args = args.concat(["-e", "UIASCRIPT", this.bootstrap]);
  args = args.concat(["-e", "UIARESULTSPATH", tmpDir]);
  var env = _.clone(process.env);
  var buildPath = path.resolve(__dirname, "../../../build");
  var isXcode4 = this.xcodeVersion !== null && this.xcodeVersion[0] === '4';
  var iwdPath = path.resolve(buildPath, isXcode4 ? "iwd4" : "iwd");
  env.CA_DEBUG_TRANSACTIONS = 1;
  if (this.withoutDelay && !this.udid) {
    env.DYLD_INSERT_LIBRARIES = path.resolve(iwdPath, "InstrumentsShim.dylib");
    env.LIB_PATH = iwdPath;
  }
  logger.info("Spawning instruments with command: " + this.instrumentsPath +
              " " + args.join(" "));
  logger.info("And extra without-delay env: " + JSON.stringify({
    DYLD_INSERT_LIBRARIES: env.DYLD_INSERT_LIBRARIES
    , LIB_PATH: env.LIB_PATH
  }));
  logger.info("And launch timeout: " + this.launchTimeout + "ms");
  return spawn(this.instrumentsPath, args, {env: env});
};

Instruments.prototype.onInstrumentsExit = function(code) {
  clearTimeout(this.killTimer);
  this.debug("Instruments exited with code " + code);

  if (this.neverConnected) {
    this.neverConnected = false; // reset so we can catch this again
    return this.launchHandler(new Error(ERR_NEVER_CHECKED_IN));
  }

  if (!this.didLaunch) {
    return this.launchHandler(new Error(ERR_CRASHED_ON_STARTUP));
  }

  this.cleanupInstruments();

  if (this.isSafariLauncherApp) {
    logger.info("Not worrying about instruments exit since we're using " +
                "SafariLauncher");
    this.launchHandler();
  } else if (this.shutdownCb !== null) {
    this.shutdownCb();
    this.shutdownCb = null;
  } else {
    this.exitHandler(code, this.traceDir);
  }

};

Instruments.prototype.cleanupInstruments = function() {
  logger.info("Cleaning up after instruments exit");
  this.proc = null;
  // make sure clear out command cbs so we can't have any lingering cbs
  // if a socket request makes it through after exit somehow
  this.curCommand = null;
  this.onReceiveCommand = null;

  if (this.currentSocket) {
    this.debug("Closing instruments client socket due to exit");
    this.currentSocket.end();
    this.currentSocket.destroy(); // close this
    this.socketServer.close();
  }

};

/* COMMAND LIFECYCLE */

Instruments.prototype.commandHandler = function(data, c) {
  var hasResult = typeof data.result !== "undefined";
  if (hasResult && !this.curCommand) {
    logger.info("Got a result when we weren't expecting one! Ignoring it");
    logger.info("Result was: " + JSON.stringify(data.result));
  } else if (!hasResult && this.curCommand) {
    logger.info("Instruments didn't send a result even though we were expecting one");
    hasResult = true;
    data.result = false;
  }

  if (hasResult && this.curCommand) {
    if (data.result) {
      this.debug("Got result from instruments: " + JSON.stringify(data.result));
    } else {
      this.debug("Got null result from instruments");
    }
    this.curCommand.cb(data.result);
    this.curCommand = null;
  }

  this.waitForCommand(function() {
    this.curCommand = this.commandQueue.shift();
    this.onReceiveCommand = null;
    this.debug("Sending command to instruments: " + this.curCommand.cmd);
    c.write(JSON.stringify({nextCommand: this.curCommand.cmd}));
    c.end();
    //this.debug("Closing our half of the connection");
  }.bind(this));
};

Instruments.prototype.waitForCommand = function(cb) {
  if (this.commandQueue.length) {
    cb();
  } else {
    this.onReceiveCommand = cb;
  }
};

Instruments.prototype.sendCommand = function(cmd, cb) {
  this.commandQueue.push({cmd: cmd, cb: cb});
  if (this.onReceiveCommand) {
    this.onReceiveCommand();
  }
};

/* PROCESS MANAGEMENT */

Instruments.prototype.shutdown = function(cb) {
  this.killProc();
  this.shutdownCb = cb;
};

Instruments.prototype.doExit = function() {
  console.log("Calling exit handler");
};

/* INSTRUMENTS STREAM MANIPULATION*/

Instruments.prototype.clearBufferChars = function(output) {
  // Instruments output is buffered, so for each log output we also output
  // a stream of very many ****. This function strips those out so all we
  // get is the log output we care about
  var re = /(\n|^)\*+\n?/g;
  output = output.toString();
  output = output.replace(re, "");
  return output;
};

Instruments.prototype.outputStreamHandler = function(output) {
  output = this.clearBufferChars(output);
  this.lookForShutdownInfo(output);
  this.resultHandler(output);
};

Instruments.prototype.errorStreamHandler = function(output) {
  logger.info(("[INST STDERR] " + output).yellow);
  if (this.webSocket) {
    var re = /Call to onAlert returned 'YES'/;
    var match = re.test(output);
    if (match) {
      logger.info("Emiting alert message...");
      this.webSocket.sockets.emit('alert', {message: output});
    }
  }
};

Instruments.prototype.lookForShutdownInfo = function(output) {
  var re = /Instruments Trace Complete.+Output : ([^\)]+)\)/;
  var match = re.exec(output);
  if (match) {
    this.traceDir = match[1];
  }
};

/* DEFAULT HANDLERS */

Instruments.prototype.setResultHandler = function(handler) {
  this.resultHandler = handler;
};

Instruments.prototype.defaultResultHandler = function(output) {
  // if we have multiple log lines, indent non-first ones
  if (output !== "") {
    output = output.replace(/\n/m, "\n       ");
    logger.info(("[INST] " + output).green);
  }
};

Instruments.prototype.defaultExitHandler = function(code, traceDir) {
  logger.info("Instruments exited with code " + code + " and trace dir " + traceDir);
};

/* MISC */

Instruments.prototype.setDebug = function(debug) {
  if (typeof debug === "undefined") {
    debug = true;
  }
  this.debugMode = debug;
};

Instruments.prototype.debug = function(msg) {
  var log = "[INSTSERVER] " + msg;
  if (!this.logNoColors) {
    log = log.grey;
  }

  logger.info(log);
};

module.exports = Instruments;
<|endoftext|>


<commit_before>var Utils              = require('../../utils')
  , CustomEventEmitter = require("../../emitters/custom-event-emitter")

module.exports = (function() {
  var AbstractQuery = function(database, sequelize, callee, options) {}

  /**
    Inherit from CustomEventEmitter
  */
  Utils.inherit(AbstractQuery, CustomEventEmitter)

  /**
   * Execute the passed sql query.
   *
   * Examples:
   *
   *     query.run('SELECT 1')
   *
   * @param {String} sql - The SQL query which should be executed.
   * @api public
   */
  AbstractQuery.prototype.run = function(sql) {
    throw new Error("The run method wasn't overwritten!")
  }

  /**
   * Check the logging option of the instance and print deprecation warnings.
   *
   * @return {void}
   */
  AbstractQuery.prototype.checkLoggingOption = function() {
    if(this.options.logging === true) {
      console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log')
      this.options.logging = console.log
    }

    if(this.options.logging == console.log) {
      // using just console.log will break in node < 0.6
      this.options.logging = function(s) { console.log(s) }
    }
  }

  /**
   * High level function that handles the results of a query execution.
   *
   *
   * Example:
   *  query.formatResults([
   *    {
   *      UserWithNames: {
   *        name: 'barfooz',
   *        id: 1,
   *        createdAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        updatedAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST)
   *      },
   *      Tasks: {
   *        title: 'task',
   *        id: 1,
   *        createdAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        updatedAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        UserWithNameId: 1
   *      }
   *    }
   *  ])
   *
   * @param {Array} data - The result of the query execution.
   */
  AbstractQuery.prototype.formatResults = function(data) {
    var result  = this.callee

    if (isInsertQuery.call(this, data)) {
      handleInsertQuery.call(this, data)
    }

    if (isSelectQuery.call(this)) {
      result = handleSelectQuery.call(this, data)
    } else if (isShowTableQuery.call(this)) {
      result = handleShowTableQuery.call(this, data)
    } else if (isShowOrDescribeQuery.call(this)) {
      result = data
    }

    return result
  }

  /**
    Shortcut methods (success, ok) for listening for success events.

    Params:
      - fct: A function that gets executed once the *success* event was triggered.

    Result:
      The function returns the instance of the query.
  */
  AbstractQuery.prototype.success =
  AbstractQuery.prototype.ok =
  function(fct) {
    this.on('success', fct)
    return this
  }

  /**
    Shortcut methods (failure, fail, error) for listening for error events.

    Params:
      - fct: A function that gets executed once the *error* event was triggered.

    Result:
      The function returns the instance of the query.
  */
  AbstractQuery.prototype.failure =
  AbstractQuery.prototype.fail =
  AbstractQuery.prototype.error =
  function(fct) {
    this.on('error', fct)
    return this
  }

  /////////////
  // private //
  /////////////

  var queryResultHasJoin = function(results) {
    var hasJoin = !!results[0]

    hasJoin = hasJoin && (Utils._.keys(results[0]).length > 1)
    hasJoin = hasJoin && (Utils.isHash(results[0][Utils._.keys(results[0])[0]]))

    return hasJoin
  }

  var isInsertQuery = function(results) {
    var result = !!this.callee

    result = result && (this.sql.indexOf('INSERT INTO') === 0)
    result = result && results.hasOwnProperty('insertId')

    return result
  }

  var handleInsertQuery = function(results) {
    // add the inserted row id to the instance
    var autoIncrementField = this.callee.__factory.autoIncrementField
    this.callee[autoIncrementField] = results.insertId
  }

  var isShowTableQuery = function() {
    return (this.sql.indexOf('SHOW TABLES') === 0)
  }

  var handleShowTableQuery = function(results) {
    return Utils._.flatten(results.map(function(resultSet) {
      return Utils._.values(resultSet)
    }))
  }

  var isSelectQuery = function() {
    return (this.sql.indexOf('SELECT') === 0)
  }

  var handleSelectQuery = function(results) {
    var result = null

    if (this.options.raw) {
      result = results
    } else if (queryResultHasJoin(results)) {
      result = groupDataByCalleeFactory.call(this, results).map(function(result) {
        // let's build the actual dao instance first...
        var dao = this.callee.build(result[this.callee.tableName], { isNewRecord: false })

        // ... and afterwards the prefetched associations
        for (var tableName in result) {
          if (result.hasOwnProperty(tableName) && (tableName !== this.callee.tableName)) {
            buildAssociatedDaoInstances.call(this, tableName, result[tableName], dao)
          }
        }

        return dao
      }.bind(this))
    } else {
      result = results.map(function(result) {
        return this.callee.build(result, { isNewRecord: false })
      }.bind(this))
    }

    // return the first real model instance if options.plain is set (e.g. Model.find)
    if(this.options.plain) {
      result = (result.length === 0) ? null : result[0]
    }

    return result
  }

  var buildAssociatedDaoInstances = function(tableName, associationData, dao) {
    var associatedDao = this.sequelize.daoFactoryManager.getDAO(tableName, { attribute: 'tableName' })
      , association   = this.callee.getAssociation(associatedDao)
      , accessor      = Utils._.camelize(associatedDao.tableName)

    // downcase the first char
    accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)

    associationData.forEach(function(data) {
      var daoInstance = associatedDao.build(data, { isNewRecord: false })

      if (['BelongsTo', 'HasOne'].indexOf(association.associationType) > -1) {
        accessor = Utils.singularize(accessor)
        dao[accessor] = daoInstance
      } else {
        dao[accessor] = dao[accessor] || []
        dao[accessor].push(daoInstance)
      }
    })
  }

  var isShowOrDescribeQuery = function() {
    return (this.sql.indexOf('SHOW') === 0) || (this.sql.indexOf('DESCRIBE') === 0)
  }

  /**
    The function takes the result of the query execution and groups
    the associated data by the callee.

    Example:
      groupDataByCalleeFactory([
        {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 1 }
        }, {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 2 }
        }, {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 3 }
        }
      ])

    Result:
      Something like this:

      [
        {
          callee:  { some: 'data', id: 1 },
          association: [
            { foo: 'bar', id: 1 },
            { foo: 'bar', id: 2 },
            { foo: 'bar', id: 3 }
          ]
        }
      ]
  */
  var groupDataByCalleeFactory = function(data) {
    var result          = []
      , calleeTableName = this.callee.tableName

    data.forEach(function(row) {
      var calleeData    = row[calleeTableName]
        , existingEntry = result.filter(function(groupedRow) {
            return Utils._.isEqual(groupedRow[calleeTableName], calleeData)
          })[0]

      if (!existingEntry) {
        existingEntry = {}
        result.push(existingEntry)
        existingEntry[calleeTableName] = calleeData
      }

      for (var attrName in row) {
        if (row.hasOwnProperty(attrName) && (attrName !== calleeTableName)) {
          existingEntry[attrName] = existingEntry[attrName] || []
          existingEntry[attrName].push(row[attrName])
        }
      }
    })

    return result
  }

  return AbstractQuery
})()
<commit_msg>added send method + lowercased sql queries before checking of several parts<commit_after>var Utils              = require('../../utils')
  , CustomEventEmitter = require("../../emitters/custom-event-emitter")

module.exports = (function() {
  var AbstractQuery = function(database, sequelize, callee, options) {}

  /**
    Inherit from CustomEventEmitter
  */
  Utils.inherit(AbstractQuery, CustomEventEmitter)

  /**
   * Execute the passed sql query.
   *
   * Examples:
   *
   *     query.run('SELECT 1')
   *
   * @param {String} sql - The SQL query which should be executed.
   * @api public
   */
  AbstractQuery.prototype.run = function(sql) {
    throw new Error("The run method wasn't overwritten!")
  }

  /**
   * Check the logging option of the instance and print deprecation warnings.
   *
   * @return {void}
   */
  AbstractQuery.prototype.checkLoggingOption = function() {
    if(this.options.logging === true) {
      console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log')
      this.options.logging = console.log
    }

    if(this.options.logging == console.log) {
      // using just console.log will break in node < 0.6
      this.options.logging = function(s) { console.log(s) }
    }
  }

  /**
   * High level function that handles the results of a query execution.
   *
   *
   * Example:
   *  query.formatResults([
   *    {
   *      UserWithNames: {
   *        name: 'barfooz',
   *        id: 1,
   *        createdAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        updatedAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST)
   *      },
   *      Tasks: {
   *        title: 'task',
   *        id: 1,
   *        createdAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        updatedAt: Sat Oct 06 2012 14:46:36 GMT+0200 (CEST),
   *        UserWithNameId: 1
   *      }
   *    }
   *  ])
   *
   * @param {Array} data - The result of the query execution.
   */
  AbstractQuery.prototype.formatResults = function(data) {
    var result  = this.callee

    if (isInsertQuery.call(this, data)) {
      handleInsertQuery.call(this, data)
    }

    if (isSelectQuery.call(this)) {
      result = handleSelectQuery.call(this, data)
    } else if (isShowTableQuery.call(this)) {
      result = handleShowTableQuery.call(this, data)
    } else if (isShowOrDescribeQuery.call(this)) {
      result = data
    }

    return result
  }

  /**
    Shortcut methods (success, ok) for listening for success events.

    Params:
      - fct: A function that gets executed once the *success* event was triggered.

    Result:
      The function returns the instance of the query.
  */
  AbstractQuery.prototype.success =
  AbstractQuery.prototype.ok =
  function(fct) {
    this.on('success', fct)
    return this
  }

  /**
    Shortcut methods (failure, fail, error) for listening for error events.

    Params:
      - fct: A function that gets executed once the *error* event was triggered.

    Result:
      The function returns the instance of the query.
  */
  AbstractQuery.prototype.failure =
  AbstractQuery.prototype.fail =
  AbstractQuery.prototype.error =
  function(fct) {
    this.on('error', fct)
    return this
  }

  /**
   * This function is a wrapper for private methods.
   *
   * @param {String} fctName The name of the private method.
   *
   */
  AbstractQuery.prototype.send = function(fctName/*, arg1, arg2, arg3, ...*/) {
    var args = Array.prototype.slice.call(arguments).slice(1)
    return eval(fctName).apply(this, args)
  }

  /////////////
  // private //
  /////////////

  var queryResultHasJoin = function(results) {
    var hasJoin = !!results[0]

    hasJoin = hasJoin && (Utils._.keys(results[0]).length > 1)
    hasJoin = hasJoin && (Utils.isHash(results[0][Utils._.keys(results[0])[0]]))

    return hasJoin
  }

  var isInsertQuery = function(results) {
    var result = !!this.callee

    result = result && (this.sql.toLowerCase().indexOf('insert into') === 0)
    result = result && !!results && results.hasOwnProperty('insertId')

    return result
  }

  var handleInsertQuery = function(results) {
    // add the inserted row id to the instance
    var autoIncrementField = this.callee.__factory.autoIncrementField
    this.callee[autoIncrementField] = results.insertId
  }

  var isShowTableQuery = function() {
    return (this.sql.toLowerCase().indexOf('show tables') === 0)
  }

  var handleShowTableQuery = function(results) {
    return Utils._.flatten(results.map(function(resultSet) {
      return Utils._.values(resultSet)
    }))
  }

  var isSelectQuery = function() {
    return (this.sql.toLowerCase().indexOf('select') === 0)
  }

  var isUpdateQuery = function() {
    return (this.sql.toLowerCase().indexOf('update') === 0)
  }

  var handleSelectQuery = function(results) {
    var result = null

    if (this.options.raw) {
      result = results
    } else if (queryResultHasJoin(results)) {
      result = groupDataByCalleeFactory.call(this, results).map(function(result) {
        // let's build the actual dao instance first...
        var dao = this.callee.build(result[this.callee.tableName], { isNewRecord: false })

        // ... and afterwards the prefetched associations
        for (var tableName in result) {
          if (result.hasOwnProperty(tableName) && (tableName !== this.callee.tableName)) {
            buildAssociatedDaoInstances.call(this, tableName, result[tableName], dao)
          }
        }

        return dao
      }.bind(this))
    } else {
      result = results.map(function(result) {
        return this.callee.build(result, { isNewRecord: false })
      }.bind(this))
    }

    // return the first real model instance if options.plain is set (e.g. Model.find)
    if(this.options.plain) {
      result = (result.length === 0) ? null : result[0]
    }

    return result
  }

  var buildAssociatedDaoInstances = function(tableName, associationData, dao) {
    var associatedDao = this.sequelize.daoFactoryManager.getDAO(tableName, { attribute: 'tableName' })
      , association   = this.callee.getAssociation(associatedDao)
      , accessor      = Utils._.camelize(associatedDao.tableName)

    // downcase the first char
    accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)

    associationData.forEach(function(data) {
      var daoInstance = associatedDao.build(data, { isNewRecord: false })

      if (['BelongsTo', 'HasOne'].indexOf(association.associationType) > -1) {
        accessor = Utils.singularize(accessor)
        dao[accessor] = daoInstance
      } else {
        dao[accessor] = dao[accessor] || []
        dao[accessor].push(daoInstance)
      }
    })
  }

  var isShowOrDescribeQuery = function() {
    var result = false

    result = result || (this.sql.toLowerCase().indexOf('show') === 0)
    result = result || (this.sql.toLowerCase().indexOf('describe') === 0)

    return  result
  }

  /**
    The function takes the result of the query execution and groups
    the associated data by the callee.

    Example:
      groupDataByCalleeFactory([
        {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 1 }
        }, {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 2 }
        }, {
          callee: { some: 'data', id: 1 },
          association: { foo: 'bar', id: 3 }
        }
      ])

    Result:
      Something like this:

      [
        {
          callee:  { some: 'data', id: 1 },
          association: [
            { foo: 'bar', id: 1 },
            { foo: 'bar', id: 2 },
            { foo: 'bar', id: 3 }
          ]
        }
      ]
  */
  var groupDataByCalleeFactory = function(data) {
    var result          = []
      , calleeTableName = this.callee.tableName

    data.forEach(function(row) {
      var calleeData    = row[calleeTableName]
        , existingEntry = result.filter(function(groupedRow) {
            return Utils._.isEqual(groupedRow[calleeTableName], calleeData)
          })[0]

      if (!existingEntry) {
        existingEntry = {}
        result.push(existingEntry)
        existingEntry[calleeTableName] = calleeData
      }

      for (var attrName in row) {
        if (row.hasOwnProperty(attrName) && (attrName !== calleeTableName)) {
          existingEntry[attrName] = existingEntry[attrName] || []
          existingEntry[attrName].push(row[attrName])
        }
      }
    })

    return result
  }

  return AbstractQuery
})()
<|endoftext|>


<commit_before>'use strict';

// PostgreSQL
// -------
var _        = require('lodash');
var inherits = require('inherits');

var Client  = require('../../client');
var Promise = require('../../promise');

var pg;

// Always initialize with the "QueryBuilder" and "QueryCompiler"
// objects, which extend the base 'lib/query/builder' and
// 'lib/query/compiler', respectively.
function Client_PG(config) {
  Client.apply(this, arguments);
  if (config.returning) this.defaultReturning = config.returning;
  if (config.debug) this.isDebugging = true;
  if (config.connection) {
    this.initDriver();
    this.initRunner();
    this.connectionSettings = config.connection;
    this.initPool();
    this.pool = new this.Pool(config.pool);
  }
}
inherits(Client_PG, Client);

Client_PG.prototype.dialect = 'postgresql';

// Lazy load the pg dependency, since we might just be using
// the client to generate SQL strings.
Client_PG.prototype.initDriver = function() {
  pg = pg || (function() {
    try {
      return require('pg');
    } catch (e) {
      return require('pg.js');
    }
  })();
};

// Attach a `Formatter` constructor to the client object.
Client_PG.prototype.initFormatter = function() {
  require('./formatter')(this);
};

// Attaches the `Raw` constructor to the client object.
Client_PG.prototype.initRaw = function() {
  require('./raw')(this);
};

// Attaches the `FunctionHelper` constructor to the client object.
Client_PG.prototype.initFunctionHelper = function() {
  require('./functionhelper')(this);
};

// Attaches the `Transaction` constructor to the client object.
Client_PG.prototype.initTransaction = function() {
  require('./transaction')(this);
};

// Attaches `QueryBuilder` and `QueryCompiler` constructors
// to the client object.
Client_PG.prototype.initQuery = function() {
  require('./query')(this);
};

// Initializes a new pool instance for the current client.
Client_PG.prototype.initPool = function() {
  require('./pool')(this);
};

// Initialize the query "runner"
Client_PG.prototype.initRunner = function() {
  require('./runner')(this);
};

// Lazy-load the schema dependencies; we may not need to use them.
Client_PG.prototype.initSchema = function() {
  require('./schema')(this);
};

// Lazy-load the migration dependency
Client_PG.prototype.initMigrator = function() {
  require('./migrator')(this);
};

// Lazy-load the seeding dependency
Client_PG.prototype.initSeeder = function() {
  require('./seeder')(this);
};

var utils;

// Prep the bindings as needed by PostgreSQL.
Client_PG.prototype.prepBindings = function(bindings, tz) {
  /*jshint unused: false*/
  utils = utils || require('./utils');
  return _.map(bindings, utils.prepareValue);
};

// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
Client_PG.prototype.acquireRawConnection = Promise.method(function(callback) {
  /*jshint unused: false*/
  // TODO: use callback or remove callback
  var connection = new pg.Client(this.connectionSettings);
  this.databaseName = connection.database;

  var client = this;
  return new Promise(function(resolver, rejecter) {
    connection.connect(function(err, connection) {
      if (err) return rejecter(err);
      if (!client.version) {
        return client.checkVersion(connection).then(function(version) {
          client.version = version;
          resolver(connection);
        });
      }
      resolver(connection);
    });
  });
});

// Used to explicitly close a connection, called internally by the pool
// when a connection times out or the pool is shutdown.
Client_PG.prototype.destroyRawConnection = function(connection) {
  connection.end();
};

// In PostgreSQL, we need to do a version check to do some feature
// checking on the database.
Client_PG.prototype.checkVersion = function(connection) {
  return new Promise(function(resolver, rejecter) {
    connection.query('select version();', function(err, resp) {
      if (err) return rejecter(err);
      resolver(/^PostgreSQL (.*?) /.exec(resp.rows[0].version)[1]);
    });
  });
};

// Position the bindings for the query.
Client_PG.prototype.positionBindings = function(sql) {
  var questionCount = 0;
  return sql.replace(/\?/g, function() {
    questionCount++;
    return '$' + questionCount;
  });
};

module.exports = Client_PG;
<commit_msg>Don't die when pg client emits error<commit_after>'use strict';

// PostgreSQL
// -------
var _        = require('lodash');
var inherits = require('inherits');

var Client  = require('../../client');
var Promise = require('../../promise');

var pg;

// Always initialize with the "QueryBuilder" and "QueryCompiler"
// objects, which extend the base 'lib/query/builder' and
// 'lib/query/compiler', respectively.
function Client_PG(config) {
  Client.apply(this, arguments);
  if (config.returning) this.defaultReturning = config.returning;
  if (config.debug) this.isDebugging = true;
  if (config.connection) {
    this.initDriver();
    this.initRunner();
    this.connectionSettings = config.connection;
    this.initPool();
    this.pool = new this.Pool(config.pool);
  }
}
inherits(Client_PG, Client);

Client_PG.prototype.dialect = 'postgresql';

// Lazy load the pg dependency, since we might just be using
// the client to generate SQL strings.
Client_PG.prototype.initDriver = function() {
  pg = pg || (function() {
    try {
      return require('pg');
    } catch (e) {
      return require('pg.js');
    }
  })();
};

// Attach a `Formatter` constructor to the client object.
Client_PG.prototype.initFormatter = function() {
  require('./formatter')(this);
};

// Attaches the `Raw` constructor to the client object.
Client_PG.prototype.initRaw = function() {
  require('./raw')(this);
};

// Attaches the `FunctionHelper` constructor to the client object.
Client_PG.prototype.initFunctionHelper = function() {
  require('./functionhelper')(this);
};

// Attaches the `Transaction` constructor to the client object.
Client_PG.prototype.initTransaction = function() {
  require('./transaction')(this);
};

// Attaches `QueryBuilder` and `QueryCompiler` constructors
// to the client object.
Client_PG.prototype.initQuery = function() {
  require('./query')(this);
};

// Initializes a new pool instance for the current client.
Client_PG.prototype.initPool = function() {
  require('./pool')(this);
};

// Initialize the query "runner"
Client_PG.prototype.initRunner = function() {
  require('./runner')(this);
};

// Lazy-load the schema dependencies; we may not need to use them.
Client_PG.prototype.initSchema = function() {
  require('./schema')(this);
};

// Lazy-load the migration dependency
Client_PG.prototype.initMigrator = function() {
  require('./migrator')(this);
};

// Lazy-load the seeding dependency
Client_PG.prototype.initSeeder = function() {
  require('./seeder')(this);
};

var utils;

// Prep the bindings as needed by PostgreSQL.
Client_PG.prototype.prepBindings = function(bindings, tz) {
  /*jshint unused: false*/
  utils = utils || require('./utils');
  return _.map(bindings, utils.prepareValue);
};

function endConnection(client, connection, err) {
  if (!connection || connection.__knex__disposed) return;
  client.pool.genericPool.destroy(connection);
}

// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
Client_PG.prototype.acquireRawConnection = Promise.method(function(callback) {
  /*jshint unused: false*/
  // TODO: use callback or remove callback
  var connection = new pg.Client(this.connectionSettings);
  this.databaseName = connection.database;

  var client = this;
  return new Promise(function(resolver, rejecter) {
    connection.connect(function(err, connection) {
      if (err) return rejecter(err);
      connection.on('error', endConnection.bind(null, client, connection));
      connection.on('end', endConnection.bind(null, client, connection));
      if (!client.version) {
        return client.checkVersion(connection).then(function(version) {
          client.version = version;
          resolver(connection);
        });
      }
      resolver(connection);
    });
  });
});

// Used to explicitly close a connection, called internally by the pool
// when a connection times out or the pool is shutdown.
Client_PG.prototype.destroyRawConnection = function(connection) {
  connection.end();
};

// In PostgreSQL, we need to do a version check to do some feature
// checking on the database.
Client_PG.prototype.checkVersion = function(connection) {
  return new Promise(function(resolver, rejecter) {
    connection.query('select version();', function(err, resp) {
      if (err) return rejecter(err);
      resolver(/^PostgreSQL (.*?) /.exec(resp.rows[0].version)[1]);
    });
  });
};

// Position the bindings for the query.
Client_PG.prototype.positionBindings = function(sql) {
  var questionCount = 0;
  return sql.replace(/\?/g, function() {
    questionCount++;
    return '$' + questionCount;
  });
};

module.exports = Client_PG;
<|endoftext|>


<commit_before>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatNumber, formatPercentage } from 'common/format';
import StatisticBox, { STATISTIC_ORDER } from 'Interface/Others/StatisticBox';
import Analyzer from 'Parser/Core/Analyzer';

const debug = false;

// Brain Freeze appears to always fall after Flurry cast, but not always on same timestamp. Giving a margin here.
const PROC_WINDOW_MS = 100;

// If you have at least this many icicles *during* your frostbolt cast you should hold the proc for glacial spike, if talented. If you have fewer, the proc should be used immediately.
const MIN_ICICLES_DURING_FB_CAST_FOR_HOLD = 3;

class BrainFreeze extends Analyzer {
  lastFlurryTimestamp = null;

  overwrittenProcs = 0;
  okOverwrittenProcs = 0;
  expiredProcs = 0;
  totalProcs = 0;
  flurryWithoutProc = 0;

  // Tracks whether the last brain freeze generator to be cast was Ebonbolt or Frostbolt
  wasLastGeneratorEB = false;

  constructor(...args) {
    super(...args);
    this.glacialSpikeTalented = this.selectedCombatant.hasTalent(SPELLS.GLACIAL_SPIKE_TALENT.id);
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.BRAIN_FREEZE.id) {
      this.totalProcs += 1;
    }
  }

  on_byPlayer_refreshbuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.BRAIN_FREEZE.id) {
      return;
    }
    this.totalProcs += 1;

    if (!this.glacialSpikeTalented || this.wasLastGeneratorEB) {
      this.overwrittenProcs += 1;
      debug && this.debug("Brain Freeze proc overwritten w/o GS talented or by EB");
      return;
    }

    // Get the number of icicles the player has *after* the cast of the frostbolt
    const stacks = this.selectedCombatant.getBuff(SPELLS.ICICLES_BUFF.id).stacks || 0;

    // The icicle const relates to the number of icicles the player had *during* the cast of frostbolt, so increment by 1 to check against icicles *after* the cast
    if (stacks < (MIN_ICICLES_DURING_FB_CAST_FOR_HOLD + 1)) {
      this.overwrittenProcs += 1;
      debug && this.debug("Brain Freeze proc overwritten w/ GS talented with too few icicles");
    } else {
      this.okOverwrittenProcs += 1;
      debug && this.debug("Acceptable Brain Freeze proc overwritten w/ GS talented");
    }
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if(spellId === SPELLS.FROSTBOLT.id || spellId === SPELLS.EBONBOLT_TALENT.id) {
      this.wasLastGeneratorEB = spellId === SPELLS.EBONBOLT_TALENT.id;
    } else if (spellId === SPELLS.FLURRY.id) {
      this.lastFlurryTimestamp = this.owner.currentTimestamp;
      if (!this.selectedCombatant.hasBuff(SPELLS.BRAIN_FREEZE.id) && !this.selectedCombatant.hasBuff(SPELLS.WINTERS_REACH_BUFF.id)) {
        this.flurryWithoutProc += 1;
      }
    }

  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.BRAIN_FREEZE.id) {
      return;
    }
    if (!this.lastFlurryTimestamp || this.lastFlurryTimestamp + PROC_WINDOW_MS < this.owner.currentTimestamp) {
      this.expiredProcs += 1; // it looks like Brain Freeze is always removed after the cast, and always on same timestamp
      debug && this.debug("Brain Freeze proc expired");
    }
  }

  get overwrittenPercent() {
    return (this.overwrittenProcs / this.totalProcs) || 0;
  }

  get expiredPercent() {
    return (this.expiredProcs / this.totalProcs) || 0;
  }

  get wastedProcs() {
    return this.overwrittenProcs + this.expiredProcs;
  }

  get wastedPercent() {
    return (this.wastedProcs / this.totalProcs) || 0;
  }

  get usedProcs() {
    // Even though okOverwrittenProcs do not count against the player, they are not used procs
    return this.totalProcs - this.wastedProcs - this.okOverwrittenProcs;
  }

  get utilPercent() {
    return 1 - this.wastedPercent;
  }

  get utilSuggestionThresholds() {
    return {
      actual: this.utilPercent,
      isLessThan: {
        minor: 0.95,
        average: 0.90,
        major: 0.80,
      },
      style: 'percentage',
    };
  }

  // Percentages lowered from .00, .08, .16; with the addition of the forgiveness window it is almost as bad as letting BF expire when you waste a proc
  get overwriteSuggestionThresholds() {
    return {
      actual: this.overwrittenPercent,
      isGreaterThan: {
        minor: 0.00,
        average: 0.05,
        major: 0.10,
      },
      style: 'percentage',
    };
  }

  // there's almost never an excuse to let BF expire
  get expiredSuggestionThresholds() {
    return {
      actual: this.expiredPercent,
      isGreaterThan: {
        minor: 0.00,
        average: 0.03,
        major: 0.06,
      },
      style: 'percentage',
    };
  }

  get flurryWithoutProcSuggestionThresholds() {
    return {
      actual: this.flurryWithoutProc,
      isGreaterThan: {
        minor: 0,
        average: 0,
        major: 3,
      },
      style: 'number',
    };
  }

  suggestions(when) {

    when(this.overwriteSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        let suggestBuilder;
        if (this.glacialSpikeTalented) {
          suggestBuilder = suggest(<React.Fragment>You overwrote {formatPercentage(this.overwrittenPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs incorrectly. You should use <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> immediately following <SpellLink id={SPELLS.FROSTBOLT.id} /> if you have fewer than {MIN_ICICLES_DURING_FB_CAST_FOR_HOLD} <SpellLink id={SPELLS.ICICLES_BUFF.id} /> during the <SpellLink id={SPELLS.FROSTBOLT.id} /> cast. If you have {MIN_ICICLES_DURING_FB_CAST_FOR_HOLD} or more <SpellLink id={SPELLS.ICICLES_BUFF.id} /> during the <SpellLink id={SPELLS.FROSTBOLT.id} /> cast, save the <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> proc for <SpellLink id={SPELLS.GLACIAL_SPIKE_TALENT.id} />.</React.Fragment>);
        } else {
          suggestBuilder = suggest(<React.Fragment>You overwrote {formatPercentage(this.overwrittenPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs. Try to use your procs as soon as possible to avoid this.</React.Fragment>);
        }
        return suggestBuilder.icon(SPELLS.BRAIN_FREEZE.icon)
          .actual(`${formatPercentage(this.overwrittenPercent)}% overwritten`)
          .recommended(`Overwriting none is recommended`);
      });

    when(this.expiredSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<React.Fragment>You allowed {formatPercentage(this.expiredPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs to expire. Try to use your procs as soon as possible to avoid this.</React.Fragment>)
          .icon(SPELLS.BRAIN_FREEZE.icon)
          .actual(`${formatPercentage(this.expiredPercent)}% expired`)
          .recommended(`Letting none expire is recommended`);
      });

    when(this.flurryWithoutProcSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<React.Fragment>You cast <SpellLink id={SPELLS.FLURRY.id} /> without <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> or <SpellLink id={SPELLS.WINTERS_REACH_TRAIT.id} /> {this.flurryWithoutProc} times. The only time it is acceptable to hard case Flurry is if you have a proc of the Winter's Reach Azerite Trait.</React.Fragment>)
          .icon(SPELLS.FLURRY.icon)
          .actual(`${formatNumber(this.flurryWithoutProc)} casts`)
          .recommended(`Casting none is recommended`);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(15)}
        icon={<SpellIcon id={SPELLS.BRAIN_FREEZE.id} />}
        value={`${formatPercentage(this.utilPercent, 0)} %`}
        label="Brain Freeze Utilization"
        tooltip={`You got ${this.totalProcs} total procs.

${this.usedProcs} used

${this.overwrittenProcs + this.okOverwrittenProcs} overwritten${this.okOverwrittenProcs > 0 ? ` (${this.okOverwrittenProcs} of which were acceptable holds for Glacial Spike)` : ''}

${this.expiredProcs} expired

				`}
      />
    );
  }
}

export default BrainFreeze;
<commit_msg>Add a second option for BF munching suggestion wording<commit_after>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatNumber, formatPercentage } from 'common/format';
import StatisticBox, { STATISTIC_ORDER } from 'Interface/Others/StatisticBox';
import Analyzer from 'Parser/Core/Analyzer';

const debug = false;

// Brain Freeze appears to always fall after Flurry cast, but not always on same timestamp. Giving a margin here.
const PROC_WINDOW_MS = 100;

// If you have at least this many icicles *during* your frostbolt cast you should hold the proc for glacial spike, if talented. If you have fewer, the proc should be used immediately.
const MIN_ICICLES_DURING_FB_CAST_FOR_HOLD = 3;

class BrainFreeze extends Analyzer {
  lastFlurryTimestamp = null;

  overwrittenProcs = 0;
  okOverwrittenProcs = 0;
  expiredProcs = 0;
  totalProcs = 0;
  flurryWithoutProc = 0;

  // Tracks whether the last brain freeze generator to be cast was Ebonbolt or Frostbolt
  wasLastGeneratorEB = false;

  constructor(...args) {
    super(...args);
    this.glacialSpikeTalented = this.selectedCombatant.hasTalent(SPELLS.GLACIAL_SPIKE_TALENT.id);
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (spellId === SPELLS.BRAIN_FREEZE.id) {
      this.totalProcs += 1;
    }
  }

  on_byPlayer_refreshbuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.BRAIN_FREEZE.id) {
      return;
    }
    this.totalProcs += 1;

    if (!this.glacialSpikeTalented || this.wasLastGeneratorEB) {
      this.overwrittenProcs += 1;
      debug && this.debug("Brain Freeze proc overwritten w/o GS talented or by EB");
      return;
    }

    // Get the number of icicles the player has *after* the cast of the frostbolt
    const stacks = this.selectedCombatant.getBuff(SPELLS.ICICLES_BUFF.id).stacks || 0;

    // The icicle const relates to the number of icicles the player had *during* the cast of frostbolt, so increment by 1 to check against icicles *after* the cast
    if (stacks < (MIN_ICICLES_DURING_FB_CAST_FOR_HOLD + 1)) {
      this.overwrittenProcs += 1;
      debug && this.debug("Brain Freeze proc overwritten w/ GS talented with too few icicles");
    } else {
      this.okOverwrittenProcs += 1;
      debug && this.debug("Acceptable Brain Freeze proc overwritten w/ GS talented");
    }
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if(spellId === SPELLS.FROSTBOLT.id || spellId === SPELLS.EBONBOLT_TALENT.id) {
      this.wasLastGeneratorEB = spellId === SPELLS.EBONBOLT_TALENT.id;
    } else if (spellId === SPELLS.FLURRY.id) {
      this.lastFlurryTimestamp = this.owner.currentTimestamp;
      if (!this.selectedCombatant.hasBuff(SPELLS.BRAIN_FREEZE.id) && !this.selectedCombatant.hasBuff(SPELLS.WINTERS_REACH_BUFF.id)) {
        this.flurryWithoutProc += 1;
      }
    }

  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.BRAIN_FREEZE.id) {
      return;
    }
    if (!this.lastFlurryTimestamp || this.lastFlurryTimestamp + PROC_WINDOW_MS < this.owner.currentTimestamp) {
      this.expiredProcs += 1; // it looks like Brain Freeze is always removed after the cast, and always on same timestamp
      debug && this.debug("Brain Freeze proc expired");
    }
  }

  get overwrittenPercent() {
    return (this.overwrittenProcs / this.totalProcs) || 0;
  }

  get expiredPercent() {
    return (this.expiredProcs / this.totalProcs) || 0;
  }

  get wastedProcs() {
    return this.overwrittenProcs + this.expiredProcs;
  }

  get wastedPercent() {
    return (this.wastedProcs / this.totalProcs) || 0;
  }

  get usedProcs() {
    // Even though okOverwrittenProcs do not count against the player, they are not used procs
    return this.totalProcs - this.wastedProcs - this.okOverwrittenProcs;
  }

  get utilPercent() {
    return 1 - this.wastedPercent;
  }

  get utilSuggestionThresholds() {
    return {
      actual: this.utilPercent,
      isLessThan: {
        minor: 0.95,
        average: 0.90,
        major: 0.80,
      },
      style: 'percentage',
    };
  }

  // Percentages lowered from .00, .08, .16; with the addition of the forgiveness window it is almost as bad as letting BF expire when you waste a proc
  get overwriteSuggestionThresholds() {
    return {
      actual: this.overwrittenPercent,
      isGreaterThan: {
        minor: 0.00,
        average: 0.05,
        major: 0.10,
      },
      style: 'percentage',
    };
  }

  // there's almost never an excuse to let BF expire
  get expiredSuggestionThresholds() {
    return {
      actual: this.expiredPercent,
      isGreaterThan: {
        minor: 0.00,
        average: 0.03,
        major: 0.06,
      },
      style: 'percentage',
    };
  }

  get flurryWithoutProcSuggestionThresholds() {
    return {
      actual: this.flurryWithoutProc,
      isGreaterThan: {
        minor: 0,
        average: 0,
        major: 3,
      },
      style: 'number',
    };
  }

  suggestions(when) {

    when(this.overwriteSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        let suggestBuilder;
        if (this.glacialSpikeTalented) {
          suggestBuilder = suggest(<React.Fragment>You overwrote {formatPercentage(this.overwrittenPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs incorrectly. You should use <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> immediately following <SpellLink id={SPELLS.FROSTBOLT.id} /> if you have fewer than {MIN_ICICLES_DURING_FB_CAST_FOR_HOLD} <SpellLink id={SPELLS.ICICLES_BUFF.id} /> during the <SpellLink id={SPELLS.FROSTBOLT.id} /> cast. If you have {MIN_ICICLES_DURING_FB_CAST_FOR_HOLD} or more <SpellLink id={SPELLS.ICICLES_BUFF.id} /> during the <SpellLink id={SPELLS.FROSTBOLT.id} /> cast, save the <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> proc for <SpellLink id={SPELLS.GLACIAL_SPIKE_TALENT.id} />.</React.Fragment>);
          // suggestBuilder = suggest(<React.Fragment>You overwrote {formatPercentage(this.overwrittenPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs incorrectly. You should only hold <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> for <SpellLink id={SPELLS.GLACIAL_SPIKE_TALENT.id} /> if you have {CONST_NAME} icicles, or would have {CONST_NAME} <SpellLink id={SPELLS.ICICLES_BUFF.id} /> after your current <SpellLink id={SPELLS.FROSTBOLT.id} /> cast. If you have {MIN_ICICLES_DURING_FB_CAST_FOR_HOLD} or more <SpellLink id={SPELLS.ICICLES_BUFF.id} /> during the <SpellLink id={SPELLS.FROSTBOLT.id} /> cast, save the <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> proc for <SpellLink id={SPELLS.GLACIAL_SPIKE_TALENT.id} />.</React.Fragment>);
        } else {
          suggestBuilder = suggest(<React.Fragment>You overwrote {formatPercentage(this.overwrittenPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs. Try to use your procs as soon as possible to avoid this.</React.Fragment>);
        }
        return suggestBuilder.icon(SPELLS.BRAIN_FREEZE.icon)
          .actual(`${formatPercentage(this.overwrittenPercent)}% overwritten`)
          .recommended(`Overwriting none is recommended`);
      });

    when(this.expiredSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<React.Fragment>You allowed {formatPercentage(this.expiredPercent)}% of your <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> procs to expire. Try to use your procs as soon as possible to avoid this.</React.Fragment>)
          .icon(SPELLS.BRAIN_FREEZE.icon)
          .actual(`${formatPercentage(this.expiredPercent)}% expired`)
          .recommended(`Letting none expire is recommended`);
      });

    when(this.flurryWithoutProcSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<React.Fragment>You cast <SpellLink id={SPELLS.FLURRY.id} /> without <SpellLink id={SPELLS.BRAIN_FREEZE.id} /> or <SpellLink id={SPELLS.WINTERS_REACH_TRAIT.id} /> {this.flurryWithoutProc} times. The only time it is acceptable to hard case Flurry is if you have a proc of the Winter's Reach Azerite Trait.</React.Fragment>)
          .icon(SPELLS.FLURRY.icon)
          .actual(`${formatNumber(this.flurryWithoutProc)} casts`)
          .recommended(`Casting none is recommended`);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(15)}
        icon={<SpellIcon id={SPELLS.BRAIN_FREEZE.id} />}
        value={`${formatPercentage(this.utilPercent, 0)} %`}
        label="Brain Freeze Utilization"
        tooltip={`You got ${this.totalProcs} total procs.

${this.usedProcs} used

${this.overwrittenProcs + this.okOverwrittenProcs} overwritten${this.okOverwrittenProcs > 0 ? ` (${this.okOverwrittenProcs} of which were acceptable holds for Glacial Spike)` : ''}

${this.expiredProcs} expired

				`}
      />
    );
  }
}

export default BrainFreeze;
<|endoftext|>


<commit_before>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2015, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * 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 License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

var _ = require('lodash');
var async = require('async');
var sundial = require('sundial');
var crcCalculator = require('../crc.js');
var struct = require('../struct.js')();

var TZOUtil = require('../TimezoneOffsetUtil');

var isBrowser = typeof window !== 'undefined';
var debug = isBrowser ? require('../bows')('VerioIQDriver') : debug;

module.exports = function (config) {
  var cfg = _.clone(config);
  cfg.deviceData = null;
  var serialDevice = config.deviceComms;
  // With no date & time settings changes available,
  // timezone is applied across-the-board
  cfg.tzoUtil = new TZOUtil(cfg.timezone, new Date().toISOString(), []);

  var STX = 0x02;
  var ETX = 0x03;
  var EPOCH = 946684800000; // device timestamp counter starts on 01/01/2000, epoch added for Unix timestamp

  var LINK_CTRL_MASK = {
    MORE: 0x10,
    DISC: 0x08,
    ACK : 0x04,
    E   : 0x02,
    S   : 0x01,
    NONE: 0x00
  };

  var send_bit = 0;
  var expected_receive_bit = 0;

  var buildLinkControlByte = function(lcb) {
    lcb |= send_bit;
    lcb |= expected_receive_bit;
    return lcb;
  };

  var buildPacket = function (linkctrl, payloadLength, payload) {
    var datalen = payloadLength + 6;
    var buf = new ArrayBuffer(datalen);
    var bytes = new Uint8Array(buf);
    var link = buildLinkControlByte(linkctrl);
    var ctr = struct.pack(bytes, 0, 'bbb', STX, datalen, link);
    if (payloadLength) {
      ctr += struct.copyBytes(bytes, ctr, payload, payloadLength);
    }
    bytes[ctr++] = ETX;
    var crc = crcCalculator.calcCRC_A(bytes, ctr);
    struct.pack(bytes, ctr, 's', crc);
    return buf;
  };

  var buildReadHeader = function() {
    var cmd = [0x04, 0x06];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildReadSerialNumber = function() {
    var cmd = [0x04, 0x0B, 0x00, 0x02];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildReadRecordNumber = function(recnum) {
    var cmd = [0x04, 0x21];
    struct.pack(cmd, 2, 's', recnum);
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildGetRecordCount = function() {
    var cmd = [0x04, 0x27, 0x00];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildGetUnits = function() {
    var cmd = [0x04,0x09,0x02,0x02];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var extractPacket = function (bytes) {
    var packet = {
      bytes: bytes,
      valid: false,
      packet_len: 0,
      lcb: 0,
      payload: null,
      crc: 0
    };

    if (bytes[0] != STX) {
      return packet;
    }

    var plen = bytes.length;
    var packet_len = struct.extractByte(bytes, 1);
    if (packet_len > plen) {
      return packet;
    }

    packet.packet_len = packet_len;
    packet.crc = struct.extractShort(bytes, packet_len - 2);
    var crc = crcCalculator.calcCRC_A(bytes, packet_len - 2);
    if (crc != packet.crc) {
      return packet;
    }

    packet.lcb = bytes[2];
    packet.payload = new Uint8Array(packet_len - 6);
    for (var i = 0; i < packet_len - 6; ++i) {
      packet.payload[i] = bytes[i + 3];
    }

    packet.valid = true;
    return packet;
  };

  var oneTouchPacketHandler = function (buffer) {

    var discardCount = 0;
    while (buffer.len() > discardCount && buffer.get(0) != STX) {
      ++discardCount;
    }
    if (discardCount) {
      buffer.discard(discardCount);
    }

    if (buffer.len() < 6) {
      return false;
    }

    var packet = extractPacket(buffer.bytes());
    if (packet.packet_len !== 0) {
      buffer.discard(packet.packet_len);
    }

    if (packet.valid) {
      return packet;
    } else {
      return null;
    }
  };

  var listenForPacket = function (timeout, parser, callback) {
    var abortTimer = setTimeout(function () {
      clearInterval(listenTimer);
      debug('TIMEOUT');
      callback(new Error('Timeout error'), null);
    }, timeout);

    var listenTimer = setInterval(function () {
      if (serialDevice.hasAvailablePacket()) {
        var pkt = serialDevice.nextPacket();
        clearTimeout(abortTimer);
        clearInterval(listenTimer);
        pkt.parsed_payload = parser(pkt);
        callback(null, pkt);
      }
    }, 20);
  };

  var oneTouchCommandResponse = function (commandpacket, callback) {
    serialDevice.writeSerial(commandpacket.packet, function () {
        listenForPacket(1000, commandpacket.parser, function(err, result) {
          if (err) {
            callback(err, null);
          } else {
            callback(null, result);
          }
        });
    });
  };

  var readHeader = function() {
    return {
      packet: buildReadHeader(),
      parser: function (result) {
        // chars between 2 to 9 of payload give us model
        var model = String.fromCharCode.apply(null, result.payload.subarray(2,9));
        return { model: model};
      }
    };
  };

  var readSerialNumber = function() {
    return {
      packet: buildReadSerialNumber(),
      parser: function (result) {
        var sernum = String.fromCharCode.apply(null, result.payload.subarray(2));
        return { serialNumber: sernum.replace(/\0/g, '')}; //remove trailing null characters
      }
    };
  };

  var readRecordCount = function() {
    return {
      packet: buildGetRecordCount(),
      parser: function (result) {
        return struct.unpack(result.payload, 0, '..s', ['nrecs']);
      }
    };
  };

  var readRecordNumber = function(n) {
    return {
      packet: buildReadRecordNumber(n),
      parser: function (result) {
        return struct.unpack(result.payload, 0, '..isb', ['timestamp', 'glucose','control']);
      }
    };
  };

  var readUnits = function() {
    return {
      packet: buildGetUnits(),
      parser: function (result) {
        var rec = struct.unpack(result.payload, 0, '..i', ['units']);
        if(rec.units === 0) {
          return {units:'mg/dL'};
        }
        else if(rec.units === 1){
          return {units:'mmol/L'};
        }
        else {
          return {units:'unknown'};
        }
      }
    };
  };

  var getDeviceInfo = function (obj, cb) {
    var cmd = readHeader();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getSerialNumber = function (obj, cb) {
    var cmd = readSerialNumber();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getRecordCount = function (obj, cb) {
    var cmd = readRecordCount();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getUnits = function (obj, cb) {
    var cmd = readUnits();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getOneRecord = function (recnum, cb) {
    var cmd = readRecordNumber(recnum);
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to read record #', recnum);
        debug(err);
        cb(err, null);
      } else {
        result.parsed_payload.index = recnum;
        cb(null, result.parsed_payload);
      }
    });
  };

  var prepBGData = function (progress, data) {
    //build missing data.id
    data.id = 'OneTouch'+ data.model + '-' + data.serialNumber;
    cfg.builder.setDefaults({ deviceId: data.id});
    var dataToPost = [];
    if (data.bgmReadings.length > 0) {
      for (var i = 0; i < data.bgmReadings.length; ++i) {
        var datum = data.bgmReadings[i];

        if(datum.control === 1) { // TODO: data model for control test
          console.log('Discarding control test');
          continue;
        }

        var jsDate = new Date((datum.timestamp * 1000) + EPOCH); // date/time is a Unix timestamp

        var smbg = cfg.builder.makeSMBG()
          .with_value(datum.glucose)
          .with_deviceTime(sundial.formatDeviceTime(jsDate))
          .with_units('mg/dL') //even for mmol/L meters, the value returned is in mg/dL
          .set('index', datum.index);
        cfg.tzoUtil.fillInUTCInfo(smbg, jsDate);
        delete smbg.index;
        dataToPost.push(smbg.done());
      }
    }else{
      debug('Device has no records to upload');
    }

    return dataToPost;
  };

  var probe = function (cb) {
    debug('attempting probe of oneTouch VerioIQ');
      var cmd = readHeader();
      oneTouchCommandResponse(cmd, function (err, result) {
        if (err) {
          debug('Failure trying to talk to device.');
          debug(err);
          debug(result);
        }
        cb(err, result);
      });
  };

  return {
    // using the default detect for this driver
    // detect: function(cb) {
    // },

    setup: function (deviceInfo, progress, cb) {
      debug('in setup!');
      progress(100);
      cb(null, {deviceInfo: deviceInfo});
    },

    connect: function (progress, data, cb) {
      debug('in connect!');
      cfg.deviceComms.connect(data.deviceInfo, oneTouchPacketHandler, probe, function(err) {
        if (err) {
          return cb(err);
        }
        getDeviceInfo({}, function(commsErr, obj) {
          if (commsErr) {
            cb(commsErr, null);
          } else {
            getSerialNumber(obj, function (err, result) {
              progress(100);
              data.connect = true;
              _.assign(data, obj);
              cb(null, data);
            });
          }
        });
      });
    },

    getConfigInfo: function (progress, data, cb) {
      getRecordCount({}, function(err, obj) {
        progress(100);
        _.assign(data, obj);
        getUnits(obj, function(err, result) {
          data.getConfigInfo = true;
          data.units = result.units;  //TODO: return as settings when available in data model
          debug('getConfigInfo', data);
          cb(err, data);
        });
      });
    },

    fetchData: function (progress, data, cb) {
      function getOneRecordWithProgress(recnum, cb) {
        progress(100.0 * recnum / data.nrecs);
        setTimeout(function() {
          getOneRecord(recnum, cb);
        }, 20);
      }

      async.timesSeries(data.nrecs, getOneRecordWithProgress, function(err, result) {
        if (err) {
          debug('fetchData failed');
          debug(err);
          debug(result);
        } else {
          debug('fetchData', result);
        }
        data.fetchData = true;
        data.bgmReadings = result;
        progress(100);
        cb(err, data);
      });
    },

    processData: function (progress, data, cb) {
      progress(0);
      data.post_records = prepBGData(progress, data);
      debug('post_records:',data.post_records);
      var ids = {};
      for (var i = 0; i < data.post_records.length; ++i) {
        var id = data.post_records[i].time + '|' + data.post_records[i].deviceId;
        if (ids[id]) {
          debug('duplicate!');
          debug(data.post_records[ids[id] - 1]);
          debug(data.post_records[i]);
        } else {
          ids[id] = i + 1;
        }
      }
      progress(100);
      data.processData = true;
      cb(null, data);
    },

    uploadData: function (progress, data, cb) {
      progress(0);
      var sessionInfo = {
        deviceTags: ['bgm'],
        deviceManufacturers: ['LifeScan'],
        deviceModel: data.model,
        deviceSerialNumber: data.serialNumber,
        deviceId: data.id,
        start: sundial.utcDateString(),
        timeProcessing: cfg.tzoUtil.type,
        tzName : cfg.timezone,
        version: cfg.version
      };

      cfg.api.upload.toPlatform(data.post_records, sessionInfo, progress, cfg.groupId, function (err, result) {
        if (err) {
          debug(err);
          progress(100);
          return cb(err, null);
        } else {
          progress(100);
          return cb(null, data);
        }
      });
    },

    disconnect: function (progress, data, cb) {
      progress(100);
      data.disconnect = true;
      cb(null, data);
    },

    cleanup: function (progress, data, cb) {
      cfg.deviceComms.clearPacketHandler();
      cfg.deviceComms.disconnect(function() {
        progress(100);
        data.cleanup = true;
        cb(null, data);
      });
    }
  };
};
<commit_msg>annotating as out-of-range if above 600 or below 20 mg/dL<commit_after>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2015, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * 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 License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

var _ = require('lodash');
var async = require('async');
var sundial = require('sundial');
var crcCalculator = require('../crc.js');
var struct = require('../struct.js')();

var TZOUtil = require('../TimezoneOffsetUtil');
var annotate = require('../eventAnnotations');

var isBrowser = typeof window !== 'undefined';
var debug = isBrowser ? require('../bows')('VerioIQDriver') : debug;

module.exports = function (config) {
  var cfg = _.clone(config);
  cfg.deviceData = null;
  var serialDevice = config.deviceComms;
  // With no date & time settings changes available,
  // timezone is applied across-the-board
  cfg.tzoUtil = new TZOUtil(cfg.timezone, new Date().toISOString(), []);

  var STX = 0x02;
  var ETX = 0x03;
  var EPOCH = 946684800000; // device timestamp counter starts on 01/01/2000, epoch added for Unix timestamp

  var LINK_CTRL_MASK = {
    MORE: 0x10,
    DISC: 0x08,
    ACK : 0x04,
    E   : 0x02,
    S   : 0x01,
    NONE: 0x00
  };

  var send_bit = 0;
  var expected_receive_bit = 0;

  var buildLinkControlByte = function(lcb) {
    lcb |= send_bit;
    lcb |= expected_receive_bit;
    return lcb;
  };

  var buildPacket = function (linkctrl, payloadLength, payload) {
    var datalen = payloadLength + 6;
    var buf = new ArrayBuffer(datalen);
    var bytes = new Uint8Array(buf);
    var link = buildLinkControlByte(linkctrl);
    var ctr = struct.pack(bytes, 0, 'bbb', STX, datalen, link);
    if (payloadLength) {
      ctr += struct.copyBytes(bytes, ctr, payload, payloadLength);
    }
    bytes[ctr++] = ETX;
    var crc = crcCalculator.calcCRC_A(bytes, ctr);
    struct.pack(bytes, ctr, 's', crc);
    return buf;
  };

  var buildReadHeader = function() {
    var cmd = [0x04, 0x06];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildReadSerialNumber = function() {
    var cmd = [0x04, 0x0B, 0x00, 0x02];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildReadRecordNumber = function(recnum) {
    var cmd = [0x04, 0x21];
    struct.pack(cmd, 2, 's', recnum);
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildGetRecordCount = function() {
    var cmd = [0x04, 0x27, 0x00];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var buildGetUnits = function() {
    var cmd = [0x04,0x09,0x02,0x02];
    return buildPacket(LINK_CTRL_MASK.NONE, cmd.length, cmd);
  };

  var extractPacket = function (bytes) {
    var packet = {
      bytes: bytes,
      valid: false,
      packet_len: 0,
      lcb: 0,
      payload: null,
      crc: 0
    };

    if (bytes[0] != STX) {
      return packet;
    }

    var plen = bytes.length;
    var packet_len = struct.extractByte(bytes, 1);
    if (packet_len > plen) {
      return packet;
    }

    packet.packet_len = packet_len;
    packet.crc = struct.extractShort(bytes, packet_len - 2);
    var crc = crcCalculator.calcCRC_A(bytes, packet_len - 2);
    if (crc != packet.crc) {
      return packet;
    }

    packet.lcb = bytes[2];
    packet.payload = new Uint8Array(packet_len - 6);
    for (var i = 0; i < packet_len - 6; ++i) {
      packet.payload[i] = bytes[i + 3];
    }

    packet.valid = true;
    return packet;
  };

  var oneTouchPacketHandler = function (buffer) {

    var discardCount = 0;
    while (buffer.len() > discardCount && buffer.get(0) != STX) {
      ++discardCount;
    }
    if (discardCount) {
      buffer.discard(discardCount);
    }

    if (buffer.len() < 6) {
      return false;
    }

    var packet = extractPacket(buffer.bytes());
    if (packet.packet_len !== 0) {
      buffer.discard(packet.packet_len);
    }

    if (packet.valid) {
      return packet;
    } else {
      return null;
    }
  };

  var listenForPacket = function (timeout, parser, callback) {
    var abortTimer = setTimeout(function () {
      clearInterval(listenTimer);
      debug('TIMEOUT');
      callback(new Error('Timeout error'), null);
    }, timeout);

    var listenTimer = setInterval(function () {
      if (serialDevice.hasAvailablePacket()) {
        var pkt = serialDevice.nextPacket();
        clearTimeout(abortTimer);
        clearInterval(listenTimer);
        pkt.parsed_payload = parser(pkt);
        callback(null, pkt);
      }
    }, 20);
  };

  var oneTouchCommandResponse = function (commandpacket, callback) {
    serialDevice.writeSerial(commandpacket.packet, function () {
        listenForPacket(1000, commandpacket.parser, function(err, result) {
          if (err) {
            callback(err, null);
          } else {
            callback(null, result);
          }
        });
    });
  };

  var readHeader = function() {
    return {
      packet: buildReadHeader(),
      parser: function (result) {
        // chars between 2 to 9 of payload give us model
        var model = String.fromCharCode.apply(null, result.payload.subarray(2,9));
        return { model: model};
      }
    };
  };

  var readSerialNumber = function() {
    return {
      packet: buildReadSerialNumber(),
      parser: function (result) {
        var sernum = String.fromCharCode.apply(null, result.payload.subarray(2));
        return { serialNumber: sernum.replace(/\0/g, '')}; //remove trailing null characters
      }
    };
  };

  var readRecordCount = function() {
    return {
      packet: buildGetRecordCount(),
      parser: function (result) {
        return struct.unpack(result.payload, 0, '..s', ['nrecs']);
      }
    };
  };

  var readRecordNumber = function(n) {
    return {
      packet: buildReadRecordNumber(n),
      parser: function (result) {
        return struct.unpack(result.payload, 0, '..isb', ['timestamp', 'glucose','control']);
      }
    };
  };

  var readUnits = function() {
    return {
      packet: buildGetUnits(),
      parser: function (result) {
        var rec = struct.unpack(result.payload, 0, '..i', ['units']);
        if(rec.units === 0) {
          return {units:'mg/dL'};
        }
        else if(rec.units === 1){
          return {units:'mmol/L'};
        }
        else {
          return {units:'unknown'};
        }
      }
    };
  };

  var getDeviceInfo = function (obj, cb) {
    var cmd = readHeader();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getSerialNumber = function (obj, cb) {
    var cmd = readSerialNumber();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getRecordCount = function (obj, cb) {
    var cmd = readRecordCount();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getUnits = function (obj, cb) {
    var cmd = readUnits();
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to talk to device.');
        debug(err);
        cb(err, null);
      } else {
        _.assign(obj, result.parsed_payload);
        cb(null, obj);
      }
    });
  };

  var getOneRecord = function (recnum, cb) {
    var cmd = readRecordNumber(recnum);
    oneTouchCommandResponse(cmd, function (err, result) {
      if (err) {
        debug('Failure trying to read record #', recnum);
        debug(err);
        cb(err, null);
      } else {
        result.parsed_payload.index = recnum;
        cb(null, result.parsed_payload);
      }
    });
  };

  var prepBGData = function (progress, data) {
    //build missing data.id
    data.id = 'OneTouch'+ data.model + '-' + data.serialNumber;
    cfg.builder.setDefaults({ deviceId: data.id});
    var dataToPost = [];
    if (data.bgmReadings.length > 0) {
      for (var i = 0; i < data.bgmReadings.length; ++i) {
        var datum = data.bgmReadings[i];

        if(datum.control === 1) { // TODO: data model for control test
          console.log('Discarding control test');
          continue;
        }

        var jsDate = new Date((datum.timestamp * 1000) + EPOCH); // date/time is a Unix timestamp

        var smbg = cfg.builder.makeSMBG()
          .with_value(datum.glucose)
          .with_deviceTime(sundial.formatDeviceTime(jsDate))
          .with_units('mg/dL') //even for mmol/L meters, the value returned is in mg/dL
          .set('index', datum.index);
        cfg.tzoUtil.fillInUTCInfo(smbg, jsDate);
        delete smbg.index;

        var annotation = null;
        if (datum.glucose < 20) {
          annotation = {
            code: 'bg/out-of-range',
            value: 'low',
            threshold: 20
          };
        } else if (datum.glucose > 600) {
          annotation = {
            code: 'bg/out-of-range',
            value: 'high',
            threshold: 600
          };
        }

        if(annotation) {
          annotate.annotateEvent(smbg, annotation);
        }

        dataToPost.push(smbg.done());
      }
    }else{
      debug('Device has no records to upload');
    }

    return dataToPost;
  };

  var probe = function (cb) {
    debug('attempting probe of oneTouch VerioIQ');
      var cmd = readHeader();
      oneTouchCommandResponse(cmd, function (err, result) {
        if (err) {
          debug('Failure trying to talk to device.');
          debug(err);
          debug(result);
        }
        cb(err, result);
      });
  };

  return {
    // using the default detect for this driver
    // detect: function(cb) {
    // },

    setup: function (deviceInfo, progress, cb) {
      debug('in setup!');
      progress(100);
      cb(null, {deviceInfo: deviceInfo});
    },

    connect: function (progress, data, cb) {
      debug('in connect!');
      cfg.deviceComms.connect(data.deviceInfo, oneTouchPacketHandler, probe, function(err) {
        if (err) {
          return cb(err);
        }
        getDeviceInfo({}, function(commsErr, obj) {
          if (commsErr) {
            cb(commsErr, null);
          } else {
            getSerialNumber(obj, function (err, result) {
              progress(100);
              data.connect = true;
              _.assign(data, obj);
              cb(null, data);
            });
          }
        });
      });
    },

    getConfigInfo: function (progress, data, cb) {
      getRecordCount({}, function(err, obj) {
        progress(100);
        _.assign(data, obj);
        getUnits(obj, function(err, result) {
          data.getConfigInfo = true;
          data.units = result.units;  //TODO: return as settings when available in data model
          debug('getConfigInfo', data);
          cb(err, data);
        });
      });
    },

    fetchData: function (progress, data, cb) {
      function getOneRecordWithProgress(recnum, cb) {
        progress(100.0 * recnum / data.nrecs);
        setTimeout(function() {
          getOneRecord(recnum, cb);
        }, 20);
      }

      async.timesSeries(data.nrecs, getOneRecordWithProgress, function(err, result) {
        if (err) {
          debug('fetchData failed');
          debug(err);
          debug(result);
        } else {
          debug('fetchData', result);
        }
        data.fetchData = true;
        data.bgmReadings = result;
        progress(100);
        cb(err, data);
      });
    },

    processData: function (progress, data, cb) {
      progress(0);
      data.post_records = prepBGData(progress, data);
      debug('post_records:',data.post_records);
      var ids = {};
      for (var i = 0; i < data.post_records.length; ++i) {
        var id = data.post_records[i].time + '|' + data.post_records[i].deviceId;
        if (ids[id]) {
          debug('duplicate!');
          debug(data.post_records[ids[id] - 1]);
          debug(data.post_records[i]);
        } else {
          ids[id] = i + 1;
        }
      }
      progress(100);
      data.processData = true;
      cb(null, data);
    },

    uploadData: function (progress, data, cb) {
      progress(0);
      var sessionInfo = {
        deviceTags: ['bgm'],
        deviceManufacturers: ['LifeScan'],
        deviceModel: data.model,
        deviceSerialNumber: data.serialNumber,
        deviceId: data.id,
        start: sundial.utcDateString(),
        timeProcessing: cfg.tzoUtil.type,
        tzName : cfg.timezone,
        version: cfg.version
      };

      cfg.api.upload.toPlatform(data.post_records, sessionInfo, progress, cfg.groupId, function (err, result) {
        if (err) {
          debug(err);
          progress(100);
          return cb(err, null);
        } else {
          progress(100);
          return cb(null, data);
        }
      });
    },

    disconnect: function (progress, data, cb) {
      progress(100);
      data.disconnect = true;
      cb(null, data);
    },

    cleanup: function (progress, data, cb) {
      cfg.deviceComms.clearPacketHandler();
      cfg.deviceComms.disconnect(function() {
        progress(100);
        data.cleanup = true;
        cb(null, data);
      });
    }
  };
};
<|endoftext|>