<commit_before>/**
  AuthGateway
  @description Exposes an API to create a user token schema and registers
    the required middleware for verifying and unpacking user data into the req
    object, also exposes an API for generating user tokens with a given payload
  @author tylerFowler
**/
'use strict';

const crypto      = require('crypto');
const _           = require('underscore');
const Promise     = require('bluebird');
const JWT         = Promise.promisifyAll(require('jsonwebtoken'));
const RoleMgr     = require('./role_manager');
const AuthGWError = require('./errors');

module.exports = exports = AuthGW;

// secondary components
let middleware = exports.Middleware = {};

/**
  @constructor AuthGW
  @desc Constructor function that creates a new gateway manager instance with
    the configured settings
  @param { String[] } roles => list of all possible user roles from lowest
    access level to highest access level
  @param { String } authorityName => serves as the JWT 'issuer' name
  @param { Object[] } dataSchema => defines the keys to r/w to/from tokens
  @param { Object } opts
  @param { String } opts.tokenHeader :default => 'x-access-token'
  @param { String } opts.tokenAlgo :default => 'HS256'
  @param { String|Buffer } opts.tokenSecret :default => 24 random bytes
  @param { Number } opts.tokenExpiredCode :default => 419 (Token Expired)
**/
function AuthGW(roles, authorityName, dataSchema, opts) {
  if (!Array.isArray(roles)) roles = [roles];

  this.roles = roles;
  this.authorityName = authorityName;

  // if required is omitted default to false
  this.dataSchema = dataSchema.map(s => _.defaults(s, { required: false }));

  this.opts = Object.assign({
    tokenHeader: 'x-access-token',
    tokenAlgo: 'HS256', // HMAC w/ SHA-256
    tokenSecret: crypto.randomBytes(24),
    tokenExpiredCode: 419
  }, opts);

  this.RoleManager = new RoleMgr(roles);
}

/**
  @name AuthGW#createToken
  @desc Creates a new auth token with the given role & data
  @param { Object } data => written to the token, needs keys required by schema
  @param { String } role => one of the roles given to this authgw instance
  @param { Number } expiry => time to live of the token in minutes
  @returns { Promise<String> } token containing the raw encrypted token
  @returns { Promise<InvalidRoleError> } if the given role is not listed
**/
AuthGW.prototype.createToken = function createToken(data, role, expiry) {
  return Promise.attempt(() => {
    if (!_.contains(this.roles, role))
      throw new AuthGWError.InvalidRoleError(role);
  })
  .then(() =>
    new Promise(resolve => {
      let opts = {
        algorithm: this.opts.tokenAlgo,
        expiresIn: expiry * 60,
        issuer: this.authorityName
      };

      JWT.sign({data, role}, this.opts.tokenSecret, opts, resolve);
    })
  );
};

/**
  @name AuthGW#createTokenSync
  @desc Sync version of create token
  @see AutghGW#createToken
  @returns { String } string containing the encrypted token
  @throws InvalidRoleError if the given role is not listed
**/
AuthGW.prototype.createTokenSync = function createToken(data, role, expiry) {
  if (!_.contains(this.roles, role))
    throw new AutghGwError.InvalidRoleError(role);

  return JWT.sign(
    { data, role },
    this.opts.tokenSecret,
    {
      algorithm: this.opts.tokenAlgo,
      expiresIn: expiry * 60,
      issuer: this.authorityName
    }
  );
};

/**
  @name AuthGW#verifyToken
  @desc Verifies that a given token is valid, unexpired, and trusted
  @param { String } token is the encrypted auth token
  @returns Promise<Object> object containing the token's payload data if valid
**/
AuthGW.prototype.verifyToken = function verifyToken(token) {
  return JWT
  .verifyAsync(token, this.opts.tokenSecret, { issuer: this.authorityName });
};

/**
  @name AuthGW#verifyTokenSync
  @desc Sync version of verify token
  @see AuthGW#verifyToken
  @returns { Object } object containing the token's payload data if valid
**/
AuthGW.prototype.verifyTokenSync = function verifyToken(token) {
  return JWT
  .verify(token, this.opts.tokenSecret, { issuer: this.authorityName });
};

/**
  @name AuthGW#middleware
  @desc Factory that creates the two Express middleware functions necessary
    for writing the data we need & verifying the auth token
  @returns [ verifyTokenFn(req,res,next), injectDataFn(req,res,next) ]
**/
AuthGW.prototype.middleware = function createMiddleware() {
  return [
    middleware.verifyTokenExpress.call(this),
    middleware.injectTokenDataExpress.call(this)
  ];
};

/**
  @name Middleware#verifyToken
  @desc ExpressJS flavor of the verify token middleware, verifies the auth token
    Note that if no auth token was given we *will* move along, but without
    writing any data to the request object
  @returns { Function } verifyToken
  @returns 401 if the given token is invalid or expired
**/
middleware.verifyTokenExpress = function verifyToken() {
  return (req, res, next) => {
    const authToken = req.headers[this.opts.tokenHeader];

    // if there's no token just don't inject the user data
    if (!authToken) return next();

    this.verifyToken(authToken)
    .then(tokenData => {
      req._tokenData = tokenData.data;
      req.userRole = tokenData.role;
      next();
    })

    // NOTE: it is **vitally** important that the client does *not* send a
    // another request with this token after receiving this error code
    // (say to prompt signin) or else this will be sent again,
    // resulting in an infinite loop
    .catch(
      JWT.TokenExpiredError, () => res.sendStatus(this.opts.tokenExpiredCode)
    )

    // token is malformed or otherwise unreadable
    .catch(JWT.JsonWebTokenError, () =>
      res.status(401).send('Invalid authorization token')
    )

    // general error, send 500
    .catch(err => next(err));
  };
};

/**
  @name Middleware#injectTokenData
  @desc ExpressJS flavor of the injection middleware, injects the decoded
    auth token's data into the req object, returning an error if a required
    field from the schema is missing
  @returns { Function } injectTokenData
  @returns 401 if the given token is missing required data
**/
middleware.injectTokenDataExpress = function injectTokenData() {
  return (req, res, next) => {
    var err;
    if (!req._tokenData) return next();

    _.chain(req._tokenData)
    .pick(_.pluck(this.dataSchema, 'name'))
    .tap(filtered => {
      let result = validateSchema(filtered, this.dataSchema);
      if (!result.valid)
        err = new Error(`Required item ${result.missingKey} is missing`);
    })
    .each((v, k) => req[k] = v);

    if (err) next(err);
    else next();
  };
};

/** Utility Functions **/
/**
  @name validateSchema
  @desc Validates that the given data fulfills the given schema
  @param { Object } data to validate
  @param { Object } AuthGW schema to validate against
  @returns { valid, missingKey }
**/
function validateSchema(data, schema) {
  let missing = _.find(schema, s => s.required && !_.has(data, s.name));

  if (missing)
    return { valid: false, missingKey: missing.name };

  return { valid: true };
}
<commit_msg>Don't write keys to request if we've had an error<commit_after>/**
  AuthGateway
  @description Exposes an API to create a user token schema and registers
    the required middleware for verifying and unpacking user data into the req
    object, also exposes an API for generating user tokens with a given payload
  @author tylerFowler
**/
'use strict';

const crypto      = require('crypto');
const _           = require('underscore');
const Promise     = require('bluebird');
const JWT         = Promise.promisifyAll(require('jsonwebtoken'));
const RoleMgr     = require('./role_manager');
const AuthGWError = require('./errors');

module.exports = exports = AuthGW;

// secondary components
let middleware = exports.Middleware = {};

/**
  @constructor AuthGW
  @desc Constructor function that creates a new gateway manager instance with
    the configured settings
  @param { String[] } roles => list of all possible user roles from lowest
    access level to highest access level
  @param { String } authorityName => serves as the JWT 'issuer' name
  @param { Object[] } dataSchema => defines the keys to r/w to/from tokens
  @param { Object } opts
  @param { String } opts.tokenHeader :default => 'x-access-token'
  @param { String } opts.tokenAlgo :default => 'HS256'
  @param { String|Buffer } opts.tokenSecret :default => 24 random bytes
  @param { Number } opts.tokenExpiredCode :default => 419 (Token Expired)
**/
function AuthGW(roles, authorityName, dataSchema, opts) {
  if (!Array.isArray(roles)) roles = [roles];

  this.roles = roles;
  this.authorityName = authorityName;

  // if required is omitted default to false
  this.dataSchema = dataSchema.map(s => _.defaults(s, { required: false }));

  this.opts = Object.assign({
    tokenHeader: 'x-access-token',
    tokenAlgo: 'HS256', // HMAC w/ SHA-256
    tokenSecret: crypto.randomBytes(24),
    tokenExpiredCode: 419
  }, opts);

  this.RoleManager = new RoleMgr(roles);
}

/**
  @name AuthGW#createToken
  @desc Creates a new auth token with the given role & data
  @param { Object } data => written to the token, needs keys required by schema
  @param { String } role => one of the roles given to this authgw instance
  @param { Number } expiry => time to live of the token in minutes
  @returns { Promise<String> } token containing the raw encrypted token
  @returns { Promise<InvalidRoleError> } if the given role is not listed
**/
AuthGW.prototype.createToken = function createToken(data, role, expiry) {
  return Promise.attempt(() => {
    if (!_.contains(this.roles, role))
      throw new AuthGWError.InvalidRoleError(role);
  })
  .then(() =>
    new Promise(resolve => {
      let opts = {
        algorithm: this.opts.tokenAlgo,
        expiresIn: expiry * 60,
        issuer: this.authorityName
      };

      JWT.sign({data, role}, this.opts.tokenSecret, opts, resolve);
    })
  );
};

/**
  @name AuthGW#createTokenSync
  @desc Sync version of create token
  @see AutghGW#createToken
  @returns { String } string containing the encrypted token
  @throws InvalidRoleError if the given role is not listed
**/
AuthGW.prototype.createTokenSync = function createToken(data, role, expiry) {
  if (!_.contains(this.roles, role))
    throw new AutghGwError.InvalidRoleError(role);

  return JWT.sign(
    { data, role },
    this.opts.tokenSecret,
    {
      algorithm: this.opts.tokenAlgo,
      expiresIn: expiry * 60,
      issuer: this.authorityName
    }
  );
};

/**
  @name AuthGW#verifyToken
  @desc Verifies that a given token is valid, unexpired, and trusted
  @param { String } token is the encrypted auth token
  @returns Promise<Object> object containing the token's payload data if valid
**/
AuthGW.prototype.verifyToken = function verifyToken(token) {
  return JWT
  .verifyAsync(token, this.opts.tokenSecret, { issuer: this.authorityName });
};

/**
  @name AuthGW#verifyTokenSync
  @desc Sync version of verify token
  @see AuthGW#verifyToken
  @returns { Object } object containing the token's payload data if valid
**/
AuthGW.prototype.verifyTokenSync = function verifyToken(token) {
  return JWT
  .verify(token, this.opts.tokenSecret, { issuer: this.authorityName });
};

/**
  @name AuthGW#middleware
  @desc Factory that creates the two Express middleware functions necessary
    for writing the data we need & verifying the auth token
  @returns [ verifyTokenFn(req,res,next), injectDataFn(req,res,next) ]
**/
AuthGW.prototype.middleware = function createMiddleware() {
  return [
    middleware.verifyTokenExpress.call(this),
    middleware.injectTokenDataExpress.call(this)
  ];
};

/**
  @name Middleware#verifyToken
  @desc ExpressJS flavor of the verify token middleware, verifies the auth token
    Note that if no auth token was given we *will* move along, but without
    writing any data to the request object
  @returns { Function } verifyToken
  @returns 401 if the given token is invalid or expired
**/
middleware.verifyTokenExpress = function verifyToken() {
  return (req, res, next) => {
    const authToken = req.headers[this.opts.tokenHeader];

    // if there's no token just don't inject the user data
    if (!authToken) return next();

    this.verifyToken(authToken)
    .then(tokenData => {
      req._tokenData = tokenData.data;
      req.userRole = tokenData.role;
      next();
    })

    // NOTE: it is **vitally** important that the client does *not* send a
    // another request with this token after receiving this error code
    // (say to prompt signin) or else this will be sent again,
    // resulting in an infinite loop
    .catch(
      JWT.TokenExpiredError, () => res.sendStatus(this.opts.tokenExpiredCode)
    )

    // token is malformed or otherwise unreadable
    .catch(JWT.JsonWebTokenError, () =>
      res.status(401).send('Invalid authorization token')
    )

    // general error, send 500
    .catch(err => next(err));
  };
};

/**
  @name Middleware#injectTokenData
  @desc ExpressJS flavor of the injection middleware, injects the decoded
    auth token's data into the req object, returning an error if a required
    field from the schema is missing
  @returns { Function } injectTokenData
  @returns 401 if the given token is missing required data
**/
middleware.injectTokenDataExpress = function injectTokenData() {
  return (req, res, next) => {
    var err;
    if (!req._tokenData) return next();

    _.chain(req._tokenData)
    .pick(_.pluck(this.dataSchema, 'name'))
    .tap(filtered => {
      let result = validateSchema(filtered, this.dataSchema);
      if (!result.valid)
        err = new Error(`Required item ${result.missingKey} is missing`);
    })
    .each((v, k) => {if (err) return; req[k] = v; });

    if (err) next(err);
    else next();
  };
};

/** Utility Functions **/
/**
  @name validateSchema
  @desc Validates that the given data fulfills the given schema
  @param { Object } data to validate
  @param { Object } AuthGW schema to validate against
  @returns { valid, missingKey }
**/
function validateSchema(data, schema) {
  let missing = _.find(schema, s => s.required && !_.has(data, s.name));

  if (missing)
    return { valid: false, missingKey: missing.name };

  return { valid: true };
}
<|endoftext|>


<commit_before>/*
 * Copyright 2018 Resin.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const path = require('path');

const getMountpoints = (children) => {
  return children.filter((child) => {
    return child.mountpoint;
  }).map((child) => {
    return {
      path: child.mountpoint,
      label: child.label || child.partlabel
    };
  });
};

const getDescription = (device) => {
  const description = [
    device.label || '',
    device.vendor || '',
    device.model || ''
  ];
  if (device.children) {
    let subLabels = device.children
      .filter((c) => {
        return c.label && c.label !== device.label;
      })
      .map((c) => {
        return c.label;
      });
    subLabels = Array.from(new Set(subLabels));
    if (subLabels.length) {
      description.push(`(${subLabels.join(', ')})`);
    }
  }
  return description.join(' ').replace(/\s+/g, ' ').trim();
};

const resolveDeviceName = (name) => {
  if (!name) {
    return null;
  }
  if (!path.posix.isAbsolute(name)) {
    return path.posix.resolve('/dev', name);
  }
  return name;
};

const transform = (data) => {
  return data.blockdevices.map((device) => {
    device.name = resolveDeviceName(device.name);
    device.kname = resolveDeviceName(device.kname);
    return device;
  }).filter((device) => {
    // Omit loop devices, CD/DVD drives, and RAM
    return !device.name.startsWith('/dev/loop')
      && !device.name.startsWith('/dev/sr')
      && !device.name.startsWith('/dev/ram');
  }).map((device) => {
    const isVirtual = device.subsystems ? /^(block)$/i.test(device.subsystems) : null;
    const isSCSI = device.tran ? /^(sata|scsi|ata|ide|pci)$/i.test(device.tran) : null;
    const isUSB = device.tran ? /^(usb)$/i.test(device.tran) : null;
    const isReadOnly = Number(device.ro) === 1;
    const isRemovable = Number(device.rm) === 1 || Boolean(isVirtual);
    return {
      enumerator: 'lsblk:json',
      busType: (device.tran || 'UNKNOWN').toUpperCase(),
      busVersion: null,
      device: device.name,
      raw: device.kname || device.name,
      description: getDescription(device),
      error: null,
      size: Number(device.size) || null,
      blockSize: Number(device['phy-sec']) || 512,
      logicalBlockSize: Number(device['log-sec']) || 512,
      mountpoints: device.children ? getMountpoints(device.children) : [],
      isReadOnly: isReadOnly,
      isSystem: !isRemovable && !isVirtual,
      isVirtual: isVirtual,
      isRemovable: isRemovable,
      isCard: null,
      isSCSI: isSCSI,
      isUSB: isUSB,
      isUAS: null
    };
  });
};

const parse = (stdout) => {
  return transform(JSON.parse(stdout));
};

module.exports = parse;
<commit_msg>fix(lsblk): Fall back to mountpoint if no label given<commit_after>/*
 * Copyright 2018 Resin.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const path = require('path');

const getMountpoints = (children) => {
  return children.filter((child) => {
    return child.mountpoint;
  }).map((child) => {
    return {
      path: child.mountpoint,
      label: child.label || child.partlabel
    };
  });
};

const getDescription = (device) => {
  const description = [
    device.label || '',
    device.vendor || '',
    device.model || ''
  ];
  if (device.children) {
    let subLabels = device.children
      .filter((c) => {
        return c.label && c.label !== device.label || c.mountpoint;
      })
      .map((c) => {
        return c.label || c.mountpoint;
      });
    subLabels = Array.from(new Set(subLabels));
    if (subLabels.length) {
      description.push(`(${subLabels.join(', ')})`);
    }
  }
  return description.join(' ').replace(/\s+/g, ' ').trim();
};

const resolveDeviceName = (name) => {
  if (!name) {
    return null;
  }
  if (!path.posix.isAbsolute(name)) {
    return path.posix.resolve('/dev', name);
  }
  return name;
};

const transform = (data) => {
  return data.blockdevices.map((device) => {
    device.name = resolveDeviceName(device.name);
    device.kname = resolveDeviceName(device.kname);
    return device;
  }).filter((device) => {
    // Omit loop devices, CD/DVD drives, and RAM
    return !device.name.startsWith('/dev/loop')
      && !device.name.startsWith('/dev/sr')
      && !device.name.startsWith('/dev/ram');
  }).map((device) => {
    const isVirtual = device.subsystems ? /^(block)$/i.test(device.subsystems) : null;
    const isSCSI = device.tran ? /^(sata|scsi|ata|ide|pci)$/i.test(device.tran) : null;
    const isUSB = device.tran ? /^(usb)$/i.test(device.tran) : null;
    const isReadOnly = Number(device.ro) === 1;
    const isRemovable = Number(device.rm) === 1 || Boolean(isVirtual);
    return {
      enumerator: 'lsblk:json',
      busType: (device.tran || 'UNKNOWN').toUpperCase(),
      busVersion: null,
      device: device.name,
      raw: device.kname || device.name,
      description: getDescription(device),
      error: null,
      size: Number(device.size) || null,
      blockSize: Number(device['phy-sec']) || 512,
      logicalBlockSize: Number(device['log-sec']) || 512,
      mountpoints: device.children ? getMountpoints(device.children) : [],
      isReadOnly: isReadOnly,
      isSystem: !isRemovable && !isVirtual,
      isVirtual: isVirtual,
      isRemovable: isRemovable,
      isCard: null,
      isSCSI: isSCSI,
      isUSB: isUSB,
      isUAS: null
    };
  });
};

const parse = (stdout) => {
  return transform(JSON.parse(stdout));
};

module.exports = parse;
<|endoftext|>


<commit_before>// Copyright (c) 2015 Uber Technologies, Inc.

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

'use strict';

/*eslint no-console: 0*/
var childProcess = require('child_process');
var parseArgs = require('minimist');
var path = require('path');
var ldj = require('ldjson-stream');
var fs = require('fs');
var util = require('util');
var process = require('process');
var console = require('console');
var setTimeout = require('timers').setTimeout;
var assert = require('assert');
var dgram = require('dgram');

var server = path.join(__dirname, 'bench_server.js');
var relay = path.join(__dirname, 'relay_server.js');
var trace = path.join(__dirname, 'trace_server.js');
var bench = path.join(__dirname, 'multi_bench.js');

process.stderr.setMaxListeners(Infinity);

var SERVER_PORT = 7100;
var TRACE_SERVER_PORT = 7039;
var RELAY_SERVER_PORT = 7038;
var RELAY_TRACE_PORT = 7037;
var STATSD_PORT = 7036;
var INSTANCE_COUNT = 72;
var CLIENT_PORT = 7041;

function BenchmarkRunner(opts) {
    if (!(this instanceof BenchmarkRunner)) {
        return new BenchmarkRunner(opts);
    }

    var self = this;

    self.opts = opts;

    self.relayProcs = [];
    self.statsdServer = null;
    self.serverProcs = [];
    self.traceProc = null;
    self.benchProcs = [];
    self.benchCounter = 0;
}

BenchmarkRunner.prototype.start = function start() {
    var self = this;

    self.startStatsd();

    if (self.opts.multiProc) {
        self.startServer(SERVER_PORT, 24);
        self.startServer(SERVER_PORT + 24, 24);
        self.startServer(SERVER_PORT + 48, 24);
    } else {
        self.startServer(SERVER_PORT, INSTANCE_COUNT);
    }

    if (self.opts.trace) {
        self.startTraceServer();
    }

    if (self.opts.relay || self.opts.trace) {
        setTimeout(startRelayServers, 500);
    } else {
        setTimeout(startClient, 500);
    }

    function startRelayServers() {
        self.startRelay('bench-relay');
        if (self.opts.trace) {
            self.startRelay('trace-relay');
        }

        setTimeout(startClient, 500);
    }

    function startClient() {
        if (self.opts.multiProc) {
            self.startClient(CLIENT_PORT);
            self.startClient(CLIENT_PORT + 200);
            self.startClient(CLIENT_PORT + 300);
        } else {
            self.startClient(CLIENT_PORT);
        }

        if (self.opts.torch) {
            self.startTorch();
        }
    }
};

BenchmarkRunner.prototype.startStatsd = function startStatsd() {
    var self = this;

    self.statsdServer = dgram.createSocket('udp4');
    self.statsdServer.bind(STATSD_PORT);
};

BenchmarkRunner.prototype.startServer =
function startServer(serverPort, instances) {
    var self = this;

    var serverProc = run(server, [
        self.opts.trace ? '--trace' : '--no-trace',
        '--traceRelayHostPort', '127.0.0.1:' + RELAY_TRACE_PORT,
        '--port', String(serverPort),
        '--instances', String(instances)
    ]);
    self.serverProcs.push(serverProc);
    serverProc.stdout.pipe(process.stderr);
    serverProc.stderr.pipe(process.stderr);
};

BenchmarkRunner.prototype.startTraceServer =
function startTraceServer() {
    var self = this;

    self.traceProc = run(trace);
    self.traceProc.stdout.pipe(process.stderr);
    self.traceProc.stderr.pipe(process.stdout);
};

BenchmarkRunner.prototype.startRelay =
function startRelay(type) {
    var self = this;

    // type = 'bench-relay'
    var relayProc = run(relay, [
        '--benchPort', String(SERVER_PORT),
        '--tracePort', String(TRACE_SERVER_PORT),
        '--benchRelayPort', String(RELAY_SERVER_PORT),
        '--traceRelayPort', String(RELAY_TRACE_PORT),
        '--type', type,
        '--instances', String(INSTANCE_COUNT),
        self.opts.trace ? '--trace' : '--no-trace',
        self.opts.debug ? '--debug' : '--no-debug'
    ]);
    self.relayProcs.push(relayProc);
    relayProc.stdout.pipe(process.stderr);
    relayProc.stderr.pipe(process.stderr);
};

BenchmarkRunner.prototype.startClient =
function startClient(clientPort) {
    var self = this;

    self.benchCounter++;

    var args = self.opts['--'];
    args = args.concat([
        '--benchPort', String(SERVER_PORT),
        '--clientPort', String(clientPort),
        '--instanceNumber', String(self.benchCounter)
    ]);
    var benchProc = run(bench, args);
    self.benchProcs.push(benchProc);

    benchProc.stderr.pipe(process.stderr);

    benchProc.stdout
        .pipe(ldj.parse())
        .on('data', function onChunk(result) {
            console.log(util.format(
                '%s: %s, %s/%s min/max/avg/p95: %s/%s/%s/%s %sms total, %s ops/sec',
                String(result.instanceNumber),
                lpad(result.descr, 13),
                lpad(result.pipeline, 5),
                result.numClients,
                lpad(result.min, 4),
                lpad(result.max, 4),
                lpad(result.mean.toFixed(2), 7),
                lpad(result.p95.toFixed(2), 7),
                lpad(result.elapsed, 6),
                lpad(typeof result.rate === 'number' ?
                    result.rate.toFixed(2) : 'NaN', 8
                )
            ));
        });

    if (self.opts.output) {
        benchProc.stdout
            .pipe(fs.createWriteStream(self.opts.output, {
                encoding: 'utf8'
            }));
    }

    benchProc.once('close', function onClose() {
        if (--self.benchCounter === 0) {
            self.close();
        }
    });
};

BenchmarkRunner.prototype.close = function close() {
    var self = this;

    console.error('benchmark finished');
    for (var i = 0; i < self.serverProcs.length; i++) {
        self.serverProcs[i].kill();
    }
    if (self.traceProc) {
        self.traceProc.kill();
    }

    for (i = 0; i < self.relayProcs.length; i++) {
        self.relayProcs[i].kill();
    }

    self.statsdServer.close();
};

BenchmarkRunner.prototype.startTorch = function startTorch() {
    var self = this;

    assert(self.opts.torch === 'client' ||
           self.opts.torch === 'relay' ||
           self.opts.torch === 'server',
           'Torch flag must be client or relay'
    );
    assert(self.opts.torchFile, 'torchFile needed');

    var torchPid;
    var torchFile = self.opts.torchFile;
    var torchTime = self.opts.torchTime || '30';
    var torchDelay = self.opts.torchDelay || 10 * 1000;
    var torchType = self.opts.torchType || 'raw';

    if (self.opts.torch === 'relay') {
        torchPid = self.relayProcs[0].pid;
    } else if (self.opts.torch === 'client') {
        torchPid = self.benchProc.pid;
    } else if (self.opts.torch === 'server') {
        torchPid = self.serverProc.pid;
    }

    setTimeout(function delayTorching() {
        var torchProc = childProcess.spawn('sudo', [
            'torch', torchPid, torchType, torchTime
        ]);
        torchProc.stdout.pipe(
            fs.createWriteStream(torchFile)
        );
        torchProc.stderr.pipe(process.stderr);
        console.error('starting flaming');

        torchProc.once('close', function onTorchClose() {
            console.error('finished flaming');
        });
    }, torchDelay);
};

if (require.main === module) {
    var argv = parseArgs(process.argv.slice(2), {
        '--': true,
        alias: {
            o: 'output'
        },
        boolean: ['relay', 'trace', 'debug']
    });
    var runner = BenchmarkRunner(argv);
    runner.start();
}

function lpad(input, len, chr) {
    var str = input.toString();
    chr = chr || ' ';

    while (str.length < len) {
        str = chr + str;
    }
    return str;
}

function run(script, args) {
    var name = script.replace(/\.js$/, '');
    args = args ? args.slice(0) : [];
    args.unshift(script);
    var child = childProcess.spawn(process.execPath, args);
    console.error('running', name, child.pid);
    return child;
}
<commit_msg>benchmarks: write JSON chunked to shared file stream<commit_after>// Copyright (c) 2015 Uber Technologies, Inc.

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

'use strict';

/*eslint no-console: 0*/
var childProcess = require('child_process');
var parseArgs = require('minimist');
var path = require('path');
var ldj = require('ldjson-stream');
var fs = require('fs');
var util = require('util');
var process = require('process');
var console = require('console');
var setTimeout = require('timers').setTimeout;
var assert = require('assert');
var dgram = require('dgram');

var server = path.join(__dirname, 'bench_server.js');
var relay = path.join(__dirname, 'relay_server.js');
var trace = path.join(__dirname, 'trace_server.js');
var bench = path.join(__dirname, 'multi_bench.js');

process.stderr.setMaxListeners(Infinity);

var SERVER_PORT = 7100;
var TRACE_SERVER_PORT = 7039;
var RELAY_SERVER_PORT = 7038;
var RELAY_TRACE_PORT = 7037;
var STATSD_PORT = 7036;
var INSTANCE_COUNT = 72;
var CLIENT_PORT = 7041;

function BenchmarkRunner(opts) {
    if (!(this instanceof BenchmarkRunner)) {
        return new BenchmarkRunner(opts);
    }

    var self = this;

    self.opts = opts;

    self.relayProcs = [];
    self.statsdServer = null;
    self.serverProcs = [];
    self.traceProc = null;
    self.benchProcs = [];
    self.benchCounter = 0;
    self.fileStream = null;
}

BenchmarkRunner.prototype.start = function start() {
    var self = this;

    self.startStatsd();

    if (self.opts.multiProc) {
        self.startServer(SERVER_PORT, 24);
        self.startServer(SERVER_PORT + 24, 24);
        self.startServer(SERVER_PORT + 48, 24);
    } else {
        self.startServer(SERVER_PORT, INSTANCE_COUNT);
    }

    if (self.opts.trace) {
        self.startTraceServer();
    }

    if (self.opts.relay || self.opts.trace) {
        setTimeout(startRelayServers, 500);
    } else {
        setTimeout(startClient, 500);
    }

    function startRelayServers() {
        self.startRelay('bench-relay');
        if (self.opts.trace) {
            self.startRelay('trace-relay');
        }

        setTimeout(startClient, 500);
    }

    function startClient() {
        self.openFileStream();

        if (self.opts.multiProc) {
            self.startClient(CLIENT_PORT);
            self.startClient(CLIENT_PORT + 200);
            self.startClient(CLIENT_PORT + 300);
        } else {
            self.startClient(CLIENT_PORT);
        }

        if (self.opts.torch) {
            self.startTorch();
        }
    }
};

BenchmarkRunner.prototype.startStatsd = function startStatsd() {
    var self = this;

    self.statsdServer = dgram.createSocket('udp4');
    self.statsdServer.bind(STATSD_PORT);
};

BenchmarkRunner.prototype.startServer =
function startServer(serverPort, instances) {
    var self = this;

    var serverProc = run(server, [
        self.opts.trace ? '--trace' : '--no-trace',
        '--traceRelayHostPort', '127.0.0.1:' + RELAY_TRACE_PORT,
        '--port', String(serverPort),
        '--instances', String(instances)
    ]);
    self.serverProcs.push(serverProc);
    serverProc.stdout.pipe(process.stderr);
    serverProc.stderr.pipe(process.stderr);
};

BenchmarkRunner.prototype.startTraceServer =
function startTraceServer() {
    var self = this;

    self.traceProc = run(trace);
    self.traceProc.stdout.pipe(process.stderr);
    self.traceProc.stderr.pipe(process.stdout);
};

BenchmarkRunner.prototype.startRelay =
function startRelay(type) {
    var self = this;

    // type = 'bench-relay'
    var relayProc = run(relay, [
        '--benchPort', String(SERVER_PORT),
        '--tracePort', String(TRACE_SERVER_PORT),
        '--benchRelayPort', String(RELAY_SERVER_PORT),
        '--traceRelayPort', String(RELAY_TRACE_PORT),
        '--type', type,
        '--instances', String(INSTANCE_COUNT),
        self.opts.trace ? '--trace' : '--no-trace',
        self.opts.debug ? '--debug' : '--no-debug'
    ]);
    self.relayProcs.push(relayProc);
    relayProc.stdout.pipe(process.stderr);
    relayProc.stderr.pipe(process.stderr);
};

BenchmarkRunner.prototype.openFileStream =
function openFileStream() {
    var self = this;

    if (self.opts.output) {
        self.fileStream = fs.createWriteStream(self.opts.output, {
            encoding: 'utf8'
        });
    }
};

BenchmarkRunner.prototype.startClient =
function startClient(clientPort) {
    var self = this;

    self.benchCounter++;

    var args = self.opts['--'];
    args = args.concat([
        '--benchPort', String(SERVER_PORT),
        '--clientPort', String(clientPort),
        '--instanceNumber', String(self.benchCounter)
    ]);
    var benchProc = run(bench, args);
    self.benchProcs.push(benchProc);

    benchProc.stderr.pipe(process.stderr);

    benchProc.stdout
        .pipe(ldj.parse())
        .on('data', function onChunk(result) {
            console.log(util.format(
                '%s: %s, %s/%s min/max/avg/p95: %s/%s/%s/%s %sms total, %s ops/sec',
                String(result.instanceNumber),
                lpad(result.descr, 13),
                lpad(result.pipeline, 5),
                result.numClients,
                lpad(result.min, 4),
                lpad(result.max, 4),
                lpad(result.mean.toFixed(2), 7),
                lpad(result.p95.toFixed(2), 7),
                lpad(result.elapsed, 6),
                lpad(typeof result.rate === 'number' ?
                    result.rate.toFixed(2) : 'NaN', 8
                )
            ));

            if (self.fileStream) {
                self.fileStream.write(JSON.stringify(result) + '\n');
            }
        })
        .on('end', function onEnd() {
            if (self.fileStream) {
                self.fileStream.end();
            }
        });

    benchProc.once('close', function onClose() {
        if (--self.benchCounter === 0) {
            self.close();
        }
    });
};

BenchmarkRunner.prototype.close = function close() {
    var self = this;

    console.error('benchmark finished');
    for (var i = 0; i < self.serverProcs.length; i++) {
        self.serverProcs[i].kill();
    }
    if (self.traceProc) {
        self.traceProc.kill();
    }

    for (i = 0; i < self.relayProcs.length; i++) {
        self.relayProcs[i].kill();
    }

    self.statsdServer.close();
};

BenchmarkRunner.prototype.startTorch = function startTorch() {
    var self = this;

    assert(self.opts.torch === 'client' ||
           self.opts.torch === 'relay' ||
           self.opts.torch === 'server',
           'Torch flag must be client or relay'
    );
    assert(self.opts.torchFile, 'torchFile needed');

    var torchPid;
    var torchFile = self.opts.torchFile;
    var torchTime = self.opts.torchTime || '30';
    var torchDelay = self.opts.torchDelay || 10 * 1000;
    var torchType = self.opts.torchType || 'raw';

    if (self.opts.torch === 'relay') {
        torchPid = self.relayProcs[0].pid;
    } else if (self.opts.torch === 'client') {
        torchPid = self.benchProc.pid;
    } else if (self.opts.torch === 'server') {
        torchPid = self.serverProc.pid;
    }

    setTimeout(function delayTorching() {
        var torchProc = childProcess.spawn('sudo', [
            'torch', torchPid, torchType, torchTime
        ]);
        torchProc.stdout.pipe(
            fs.createWriteStream(torchFile)
        );
        torchProc.stderr.pipe(process.stderr);
        console.error('starting flaming');

        torchProc.once('close', function onTorchClose() {
            console.error('finished flaming');
        });
    }, torchDelay);
};

if (require.main === module) {
    var argv = parseArgs(process.argv.slice(2), {
        '--': true,
        alias: {
            o: 'output'
        },
        boolean: ['relay', 'trace', 'debug']
    });
    var runner = BenchmarkRunner(argv);
    runner.start();
}

function lpad(input, len, chr) {
    var str = input.toString();
    chr = chr || ' ';

    while (str.length < len) {
        str = chr + str;
    }
    return str;
}

function run(script, args) {
    var name = script.replace(/\.js$/, '');
    args = args ? args.slice(0) : [];
    args.unshift(script);
    var child = childProcess.spawn(process.execPath, args);
    console.error('running', name, child.pid);
    return child;
}
<|endoftext|>


<commit_before>/*!
 * jQuery.textcomplete.js
 *
 * Repositiory: https://github.com/yuku-t/jquery-textcomplete
 * License:     MIT
 * Author:      Yuku Takahashi
 */

;(function ($) {

  'use strict';

  /**
   * Exclusive execution control utility.
   */
  var lock = function (func) {
    var free, locked;
    free = function () { locked = false; };
    return function () {
      var args;
      if (locked) return;
      locked = true;
      args = toArray(arguments);
      args.unshift(free);
      func.apply(this, args);
    };
  };

  /**
   * Convert arguments into a real array.
   */
  var toArray = function (args) {
    var result;
    result = Array.prototype.slice.call(args);
    return result;
  };

  /**
   * Get the styles of any element from property names.
   */
  var getStyles = (function () {
    var color;
    color = $('

').css(['color']).color;
    if (typeof color !== 'undefined') {
      return function ($el, properties) {
        return $el.css(properties);
      };
    } else {  // for jQuery 1.8 or below
      return function ($el, properties) {
        var styles;
        styles = {};
        $.each(properties, function (i, property) {
          styles[property] = $el.css(property);
        });
        return styles;
      };
    }
  })();

  /**
   * Default template function.
   */
  var identity = function (obj) { return obj; };

  /**
   * Memoize a search function.
   */
  var memoize = function (func) {
    var memo = {};
    return function (term, callback) {
      if (memo[term]) {
        callback(memo[term]);
      } else {
        func.call(this, term, function (data) {
          memo[term] = (memo[term] || []).concat(data);
          callback.apply(null, arguments);
        });
      }
    };
  };

  /**
   * Determine if the array contains a given value.
   */
  var include = function (array, value) {
    var i, l;
    if (array.indexOf) return array.indexOf(value) != -1;
    for (i = 0, l = array.length; i < l; i++) {
      if (array[i] === value) return true;
    }
    return false;
  };

  /**
   * Textarea manager class.
   */
  var Completer = (function () {
    var html, css, $baseWrapper, $baseList, _id;

    html = {
      wrapper: '

',
      list: '

'
    };
    css = {
      wrapper: {
        position: 'relative'
      },
      list: {
        position: 'absolute',
        top: 0,
        left: 0,
        zIndex: '100',
        display: 'none'
      }
    };
    $baseWrapper = $(html.wrapper).css(css.wrapper);
    $baseList = $(html.list).css(css.list);
    _id = 0;

    function Completer($el) {
      var focus;
      this.el = $el.get(0);  // textarea element
      focus = this.el === document.activeElement;
      // Cannot wrap $el at initialize method lazily due to Firefox's behavior.
      this.$el = wrapElement($el); // Focus is lost
      this.id = 'textComplete' + _id++;
      this.strategies = [];
      if (focus) {
        this.initialize();
        this.$el.focus();
      } else {
        this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
      }
    }

    /**
     * Completer's public methods
     */
    $.extend(Completer.prototype, {

      /**
       * Prepare ListView and bind events.
       */
      initialize: function () {
        var $list, globalEvents;
        $list = $baseList.clone();
        this.listView = new ListView($list, this);
        this.$el
          .before($list)
          .on({
            'keyup.textComplete': $.proxy(this.onKeyup, this),
            'keydown.textComplete': $.proxy(this.listView.onKeydown,
                                            this.listView)
          });
        globalEvents = {};
        globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
        globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
        $(document).on(globalEvents);
      },

      /**
       * Register strategies to the completer.
       */
      register: function (strategies) {
        this.strategies = this.strategies.concat(strategies);
      },

      /**
       * Show autocomplete list next to the caret.
       */
      renderList: function (data) {
        if (this.clearAtNext) {
          this.listView.clear();
          this.clearAtNext = false;
        }
        if (data.length) {
          if (!this.listView.shown) {
            this.listView
                .setPosition(this.getCaretPosition())
                .clear()
                .activate();
            this.listView.strategy = this.strategy;
          }
          data = data.slice(0, this.strategy.maxCount);
          this.listView.render(data);
        }

        if (!this.listView.data.length && this.listView.shown) {
          this.listView.deactivate();
        }
      },

      searchCallbackFactory: function (free) {
        var self = this;
        return function (data, keep) {
          self.renderList(data);
          if (!keep) {
            // This is the last callback for this search.
            free();
            self.clearAtNext = true;
          }
        };
      },

      /**
       * Keyup event handler.
       */
      onKeyup: function (e) {
        var searchQuery, term;
        if (this.skipSearch(e)) { return; }

        searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
        if (searchQuery.length) {
          term = searchQuery[1];
          if (this.term === term) return; // Ignore shift-key or something.
          this.term = term;
          this.search(searchQuery);
        } else {
          this.term = null;
          this.listView.deactivate();
        }
      },

      /**
       * Suppress searching if it returns true.
       */
      skipSearch: function (e) {
        if (this.skipNextKeyup) {
          this.skipNextKeyup = false;
          return true;
        }
        switch (e.keyCode) {
          case 40:
          case 38:
            return true;
        }
      },

      onSelect: function (value) {
        var pre, post, newSubStr;
        pre = this.getTextFromHeadToCaret();
        post = this.el.value.substring(this.el.selectionEnd);

        newSubStr = this.strategy.replace(value);
        if ($.isArray(newSubStr)) {
          post = newSubStr[1] + post;
          newSubStr = newSubStr[0];
        }
        pre = pre.replace(this.strategy.match, newSubStr);
        this.$el.val(pre + post)
                .trigger('change')
                .trigger('textComplete:select', value);
        this.el.focus();
        this.el.selectionStart = this.el.selectionEnd = pre.length;
        this.skipNextKeyup = true;
      },

      /**
       * Global click event handler.
       */
      onClickDocument: function (e) {
        if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
          this.listView.deactivate();
        }
      },

      /**
       * Global keyup event handler.
       */
      onKeyupDocument: function (e) {
        if (this.listView.shown && e.keyCode === 27) { // ESC
          this.listView.deactivate();
          this.$el.focus();
        }
      },

      /**
       * Remove all event handlers and the wrapper element.
       */
      destroy: function () {
        var $wrapper;
        this.$el.off('.textComplete');
        $(document).off('.' + this.id);
        if (this.listView) { this.listView.destroy(); }
        $wrapper = this.$el.parent();
        $wrapper.after(this.$el).remove();
        this.$el.data('textComplete', void 0);
        this.$el = null;
      },

      // Helper methods
      // ==============

      /**
       * Returns caret's relative coordinates from textarea's left top corner.
       */
      getCaretPosition: function () {
        // Browser native API does not provide the way to know the position of
        // caret in pixels, so that here we use a kind of hack to accomplish
        // the aim. First of all it puts a div element and completely copies
        // the textarea's style to the element, then it inserts the text and a
        // span element into the textarea.
        // Consequently, the span element's position is the thing what we want.

        if (this.el.selectionEnd === 0) return;
        var properties, css, $div, $span, position, dir;

        dir = this.$el.attr('dir') || this.$el.css('direction');
        properties = ['border-width', 'font-family', 'font-size', 'font-style',
          'font-variant', 'font-weight', 'height', 'letter-spacing',
          'word-spacing', 'line-height', 'text-decoration', 'text-align',
          'width', 'padding-top', 'padding-right', 'padding-bottom',
          'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
          'margin-left'
        ];
        css = $.extend({
          position: 'absolute',
          overflow: 'auto',
          'white-space': 'pre-wrap',
          top: 0,
          left: -9999,
          direction: dir
        }, getStyles(this.$el, properties));

        $div = $('

').css(css).text(this.getTextFromHeadToCaret());
        $span = $('').text('.').appendTo($div);
        this.$el.before($div);
        position = $span.position();
        position.top += $span.height() - this.$el.scrollTop();
        if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
        $div.remove();
        return position;
      },

      getTextFromHeadToCaret: function () {
        var text, selectionEnd, range;
        selectionEnd = this.el.selectionEnd;
        if (typeof selectionEnd === 'number') {
          text = this.el.value.substring(0, selectionEnd);
        } else if (document.selection) {
          range = this.el.createTextRange();
          range.moveStart('character', 0);
          range.moveEnd('textedit');
          text = range.text;
        }
        return text;
      },

      /**
       * Parse the value of textarea and extract search query.
       */
      extractSearchQuery: function (text) {
        // If a search query found, it returns used strategy and the query
        // term. If the caret is currently in a code block or search query does
        // not found, it returns an empty array.

        var i, l, strategy, match;
        for (i = 0, l = this.strategies.length; i < l; i++) {
          strategy = this.strategies[i];
          match = text.match(strategy.match);
          if (match) { return [strategy, match[strategy.index]]; }
        }
        return [];
      },

      search: lock(function (free, searchQuery) {
        var term;
        this.strategy = searchQuery[0];
        term = searchQuery[1];
        this.strategy.search(term, this.searchCallbackFactory(free));
      })
    });

    /**
     * Completer's private functions
     */
    var wrapElement = function ($el) {
      return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
    };

    return Completer;
  })();

  /**
   * Dropdown menu manager class.
   */
  var ListView = (function () {

    function ListView($el, completer) {
      this.data = [];
      this.$el = $el;
      this.index = 0;
      this.completer = completer;

      this.$el.on('click.textComplete', 'li.textcomplete-item',
                  $.proxy(this.onClick, this));
    }

    $.extend(ListView.prototype, {
      shown: false,

      render: function (data) {
        var html, i, l, index, val;

        html = '';
        for (i = 0, l = data.length; i < l; i++) {
          val = data[i];
          if (include(this.data, val)) continue;
          index = this.data.length;
          this.data.push(val);
          html += '
';
          html +=   this.strategy.template(val);
          html += '
';
          if (this.data.length === this.strategy.maxCount) break;
        }
        this.$el.append(html);
        if (!this.data.length) {
          this.deactivate();
        } else {
          this.activateIndexedItem();
        }
      },

      clear: function () {
        this.data = [];
        this.$el.html('');
        this.index = 0;
        return this;
      },

      activateIndexedItem: function () {
        this.$el.find('.active').removeClass('active');
        this.getActiveItem().addClass('active');
      },

      getActiveItem: function () {
        return $(this.$el.children().get(this.index));
      },

      activate: function () {
        if (!this.shown) {
          this.$el.show();
          this.completer.$el.trigger('textComplete:show');
          this.shown = true;
        }
        return this;
      },

      deactivate: function () {
        if (this.shown) {
          this.$el.hide();
          this.completer.$el.trigger('textComplete:hide');
          this.shown = false;
          this.data = this.index = null;
        }
        return this;
      },

      setPosition: function (position) {
        this.$el.css(position);
        return this;
      },

      select: function (index) {
        var self = this;
        this.completer.onSelect(this.data[index]);
        // Deactive at next tick to allow other event handlers to know whether
        // the dropdown has been shown or not.
        setTimeout(function () { self.deactivate(); }, 0);
      },

      onKeydown: function (e) {
        if (!this.shown) return;
        if (e.keyCode === 38) {         // UP
          e.preventDefault();
          if (this.index === 0) {
            this.index = this.data.length-1;
          } else {
            this.index -= 1;
          }
          this.activateIndexedItem();
        } else if (e.keyCode === 40) {  // DOWN
          e.preventDefault();
          if (this.index === this.data.length - 1) {
            this.index = 0;
          } else {
            this.index += 1;
          }
          this.activateIndexedItem();
        } else if (e.keyCode === 13 || e.keyCode === 9) {  // ENTER or TAB
          e.preventDefault();
          this.select(parseInt(this.getActiveItem().data('index'), 10));
        }
      },

      onClick: function (e) {
        var $e = $(e.target);
        e.originalEvent.keepTextCompleteDropdown = true;
        if (!$e.hasClass('textcomplete-item')) {
          $e = $e.parents('li.textcomplete-item');
        }
        this.select(parseInt($e.data('index'), 10));
      },

      destroy: function () {
        this.deactivate();
        this.$el.off('click.textComplete').remove();
        this.$el = null;
      }
    });

    return ListView;
  })();

  $.fn.textcomplete = function (strategies) {
    var i, l, strategy, dataKey;

    dataKey = 'textComplete';

    if (strategies === 'destroy') {
      return this.each(function () {
        var completer = $(this).data(dataKey);
        if (completer) { completer.destroy(); }
      });
    }

    for (i = 0, l = strategies.length; i < l; i++) {
      strategy = strategies[i];
      if (!strategy.template) {
        strategy.template = identity;
      }
      if (strategy.index == null) {
        strategy.index = 2;
      }
      if (strategy.cache) {
        strategy.search = memoize(strategy.search);
      }
      strategy.maxCount || (strategy.maxCount = 10);
    }

    return this.each(function () {
      var $this, completer;
      $this = $(this);
      completer = $this.data(dataKey);
      if (!completer) {
        completer = new Completer($this);
        $this.data(dataKey, completer);
      }
      completer.register(strategies);
    });
  };

})(window.jQuery || window.Zepto);
<commit_msg>Clean up data correctly in deactivate<commit_after>/*!
 * jQuery.textcomplete.js
 *
 * Repositiory: https://github.com/yuku-t/jquery-textcomplete
 * License:     MIT
 * Author:      Yuku Takahashi
 */

;(function ($) {

  'use strict';

  /**
   * Exclusive execution control utility.
   */
  var lock = function (func) {
    var free, locked;
    free = function () { locked = false; };
    return function () {
      var args;
      if (locked) return;
      locked = true;
      args = toArray(arguments);
      args.unshift(free);
      func.apply(this, args);
    };
  };

  /**
   * Convert arguments into a real array.
   */
  var toArray = function (args) {
    var result;
    result = Array.prototype.slice.call(args);
    return result;
  };

  /**
   * Get the styles of any element from property names.
   */
  var getStyles = (function () {
    var color;
    color = $('

').css(['color']).color;
    if (typeof color !== 'undefined') {
      return function ($el, properties) {
        return $el.css(properties);
      };
    } else {  // for jQuery 1.8 or below
      return function ($el, properties) {
        var styles;
        styles = {};
        $.each(properties, function (i, property) {
          styles[property] = $el.css(property);
        });
        return styles;
      };
    }
  })();

  /**
   * Default template function.
   */
  var identity = function (obj) { return obj; };

  /**
   * Memoize a search function.
   */
  var memoize = function (func) {
    var memo = {};
    return function (term, callback) {
      if (memo[term]) {
        callback(memo[term]);
      } else {
        func.call(this, term, function (data) {
          memo[term] = (memo[term] || []).concat(data);
          callback.apply(null, arguments);
        });
      }
    };
  };

  /**
   * Determine if the array contains a given value.
   */
  var include = function (array, value) {
    var i, l;
    if (array.indexOf) return array.indexOf(value) != -1;
    for (i = 0, l = array.length; i < l; i++) {
      if (array[i] === value) return true;
    }
    return false;
  };

  /**
   * Textarea manager class.
   */
  var Completer = (function () {
    var html, css, $baseWrapper, $baseList, _id;

    html = {
      wrapper: '

',
      list: '

'
    };
    css = {
      wrapper: {
        position: 'relative'
      },
      list: {
        position: 'absolute',
        top: 0,
        left: 0,
        zIndex: '100',
        display: 'none'
      }
    };
    $baseWrapper = $(html.wrapper).css(css.wrapper);
    $baseList = $(html.list).css(css.list);
    _id = 0;

    function Completer($el) {
      var focus;
      this.el = $el.get(0);  // textarea element
      focus = this.el === document.activeElement;
      // Cannot wrap $el at initialize method lazily due to Firefox's behavior.
      this.$el = wrapElement($el); // Focus is lost
      this.id = 'textComplete' + _id++;
      this.strategies = [];
      if (focus) {
        this.initialize();
        this.$el.focus();
      } else {
        this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
      }
    }

    /**
     * Completer's public methods
     */
    $.extend(Completer.prototype, {

      /**
       * Prepare ListView and bind events.
       */
      initialize: function () {
        var $list, globalEvents;
        $list = $baseList.clone();
        this.listView = new ListView($list, this);
        this.$el
          .before($list)
          .on({
            'keyup.textComplete': $.proxy(this.onKeyup, this),
            'keydown.textComplete': $.proxy(this.listView.onKeydown,
                                            this.listView)
          });
        globalEvents = {};
        globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
        globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
        $(document).on(globalEvents);
      },

      /**
       * Register strategies to the completer.
       */
      register: function (strategies) {
        this.strategies = this.strategies.concat(strategies);
      },

      /**
       * Show autocomplete list next to the caret.
       */
      renderList: function (data) {
        if (this.clearAtNext) {
          this.listView.clear();
          this.clearAtNext = false;
        }
        if (data.length) {
          if (!this.listView.shown) {
            this.listView
                .setPosition(this.getCaretPosition())
                .clear()
                .activate();
            this.listView.strategy = this.strategy;
          }
          data = data.slice(0, this.strategy.maxCount);
          this.listView.render(data);
        }

        if (!this.listView.data.length && this.listView.shown) {
          this.listView.deactivate();
        }
      },

      searchCallbackFactory: function (free) {
        var self = this;
        return function (data, keep) {
          self.renderList(data);
          if (!keep) {
            // This is the last callback for this search.
            free();
            self.clearAtNext = true;
          }
        };
      },

      /**
       * Keyup event handler.
       */
      onKeyup: function (e) {
        var searchQuery, term;
        if (this.skipSearch(e)) { return; }

        searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
        if (searchQuery.length) {
          term = searchQuery[1];
          if (this.term === term) return; // Ignore shift-key or something.
          this.term = term;
          this.search(searchQuery);
        } else {
          this.term = null;
          this.listView.deactivate();
        }
      },

      /**
       * Suppress searching if it returns true.
       */
      skipSearch: function (e) {
        if (this.skipNextKeyup) {
          this.skipNextKeyup = false;
          return true;
        }
        switch (e.keyCode) {
          case 40:
          case 38:
            return true;
        }
      },

      onSelect: function (value) {
        var pre, post, newSubStr;
        pre = this.getTextFromHeadToCaret();
        post = this.el.value.substring(this.el.selectionEnd);

        newSubStr = this.strategy.replace(value);
        if ($.isArray(newSubStr)) {
          post = newSubStr[1] + post;
          newSubStr = newSubStr[0];
        }
        pre = pre.replace(this.strategy.match, newSubStr);
        this.$el.val(pre + post)
                .trigger('change')
                .trigger('textComplete:select', value);
        this.el.focus();
        this.el.selectionStart = this.el.selectionEnd = pre.length;
        this.skipNextKeyup = true;
      },

      /**
       * Global click event handler.
       */
      onClickDocument: function (e) {
        if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
          this.listView.deactivate();
        }
      },

      /**
       * Global keyup event handler.
       */
      onKeyupDocument: function (e) {
        if (this.listView.shown && e.keyCode === 27) { // ESC
          this.listView.deactivate();
          this.$el.focus();
        }
      },

      /**
       * Remove all event handlers and the wrapper element.
       */
      destroy: function () {
        var $wrapper;
        this.$el.off('.textComplete');
        $(document).off('.' + this.id);
        if (this.listView) { this.listView.destroy(); }
        $wrapper = this.$el.parent();
        $wrapper.after(this.$el).remove();
        this.$el.data('textComplete', void 0);
        this.$el = null;
      },

      // Helper methods
      // ==============

      /**
       * Returns caret's relative coordinates from textarea's left top corner.
       */
      getCaretPosition: function () {
        // Browser native API does not provide the way to know the position of
        // caret in pixels, so that here we use a kind of hack to accomplish
        // the aim. First of all it puts a div element and completely copies
        // the textarea's style to the element, then it inserts the text and a
        // span element into the textarea.
        // Consequently, the span element's position is the thing what we want.

        if (this.el.selectionEnd === 0) return;
        var properties, css, $div, $span, position, dir;

        dir = this.$el.attr('dir') || this.$el.css('direction');
        properties = ['border-width', 'font-family', 'font-size', 'font-style',
          'font-variant', 'font-weight', 'height', 'letter-spacing',
          'word-spacing', 'line-height', 'text-decoration', 'text-align',
          'width', 'padding-top', 'padding-right', 'padding-bottom',
          'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
          'margin-left'
        ];
        css = $.extend({
          position: 'absolute',
          overflow: 'auto',
          'white-space': 'pre-wrap',
          top: 0,
          left: -9999,
          direction: dir
        }, getStyles(this.$el, properties));

        $div = $('

').css(css).text(this.getTextFromHeadToCaret());
        $span = $('').text('.').appendTo($div);
        this.$el.before($div);
        position = $span.position();
        position.top += $span.height() - this.$el.scrollTop();
        if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
        $div.remove();
        return position;
      },

      getTextFromHeadToCaret: function () {
        var text, selectionEnd, range;
        selectionEnd = this.el.selectionEnd;
        if (typeof selectionEnd === 'number') {
          text = this.el.value.substring(0, selectionEnd);
        } else if (document.selection) {
          range = this.el.createTextRange();
          range.moveStart('character', 0);
          range.moveEnd('textedit');
          text = range.text;
        }
        return text;
      },

      /**
       * Parse the value of textarea and extract search query.
       */
      extractSearchQuery: function (text) {
        // If a search query found, it returns used strategy and the query
        // term. If the caret is currently in a code block or search query does
        // not found, it returns an empty array.

        var i, l, strategy, match;
        for (i = 0, l = this.strategies.length; i < l; i++) {
          strategy = this.strategies[i];
          match = text.match(strategy.match);
          if (match) { return [strategy, match[strategy.index]]; }
        }
        return [];
      },

      search: lock(function (free, searchQuery) {
        var term;
        this.strategy = searchQuery[0];
        term = searchQuery[1];
        this.strategy.search(term, this.searchCallbackFactory(free));
      })
    });

    /**
     * Completer's private functions
     */
    var wrapElement = function ($el) {
      return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
    };

    return Completer;
  })();

  /**
   * Dropdown menu manager class.
   */
  var ListView = (function () {

    function ListView($el, completer) {
      this.data = [];
      this.$el = $el;
      this.index = 0;
      this.completer = completer;

      this.$el.on('click.textComplete', 'li.textcomplete-item',
                  $.proxy(this.onClick, this));
    }

    $.extend(ListView.prototype, {
      shown: false,

      render: function (data) {
        var html, i, l, index, val;

        html = '';
        for (i = 0, l = data.length; i < l; i++) {
          val = data[i];
          if (include(this.data, val)) continue;
          index = this.data.length;
          this.data.push(val);
          html += '
';
          html +=   this.strategy.template(val);
          html += '
';
          if (this.data.length === this.strategy.maxCount) break;
        }
        this.$el.append(html);
        if (!this.data.length) {
          this.deactivate();
        } else {
          this.activateIndexedItem();
        }
      },

      clear: function () {
        this.data = [];
        this.$el.html('');
        this.index = 0;
        return this;
      },

      activateIndexedItem: function () {
        this.$el.find('.active').removeClass('active');
        this.getActiveItem().addClass('active');
      },

      getActiveItem: function () {
        return $(this.$el.children().get(this.index));
      },

      activate: function () {
        if (!this.shown) {
          this.$el.show();
          this.completer.$el.trigger('textComplete:show');
          this.shown = true;
        }
        return this;
      },

      deactivate: function () {
        if (this.shown) {
          this.$el.hide();
          this.completer.$el.trigger('textComplete:hide');
          this.shown = false;
          this.data = [];
          this.index = null;
        }
        return this;
      },

      setPosition: function (position) {
        this.$el.css(position);
        return this;
      },

      select: function (index) {
        var self = this;
        this.completer.onSelect(this.data[index]);
        // Deactive at next tick to allow other event handlers to know whether
        // the dropdown has been shown or not.
        setTimeout(function () { self.deactivate(); }, 0);
      },

      onKeydown: function (e) {
        if (!this.shown) return;
        if (e.keyCode === 38) {         // UP
          e.preventDefault();
          if (this.index === 0) {
            this.index = this.data.length-1;
          } else {
            this.index -= 1;
          }
          this.activateIndexedItem();
        } else if (e.keyCode === 40) {  // DOWN
          e.preventDefault();
          if (this.index === this.data.length - 1) {
            this.index = 0;
          } else {
            this.index += 1;
          }
          this.activateIndexedItem();
        } else if (e.keyCode === 13 || e.keyCode === 9) {  // ENTER or TAB
          e.preventDefault();
          this.select(parseInt(this.getActiveItem().data('index'), 10));
        }
      },

      onClick: function (e) {
        var $e = $(e.target);
        e.originalEvent.keepTextCompleteDropdown = true;
        if (!$e.hasClass('textcomplete-item')) {
          $e = $e.parents('li.textcomplete-item');
        }
        this.select(parseInt($e.data('index'), 10));
      },

      destroy: function () {
        this.deactivate();
        this.$el.off('click.textComplete').remove();
        this.$el = null;
      }
    });

    return ListView;
  })();

  $.fn.textcomplete = function (strategies) {
    var i, l, strategy, dataKey;

    dataKey = 'textComplete';

    if (strategies === 'destroy') {
      return this.each(function () {
        var completer = $(this).data(dataKey);
        if (completer) { completer.destroy(); }
      });
    }

    for (i = 0, l = strategies.length; i < l; i++) {
      strategy = strategies[i];
      if (!strategy.template) {
        strategy.template = identity;
      }
      if (strategy.index == null) {
        strategy.index = 2;
      }
      if (strategy.cache) {
        strategy.search = memoize(strategy.search);
      }
      strategy.maxCount || (strategy.maxCount = 10);
    }

    return this.each(function () {
      var $this, completer;
      $this = $(this);
      completer = $this.data(dataKey);
      if (!completer) {
        completer = new Completer($this);
        $this.data(dataKey, completer);
      }
      completer.register(strategies);
    });
  };

})(window.jQuery || window.Zepto);
<|endoftext|>


<commit_before>/**
 * Generate SQl-UPDATE-Compatible string to conditions and operations
 *
 * @link https://github.com/codeschool/sqlite-parser
 */
const pkgInfo = require('../package.json');

const _ = require('lodash');
const util = require('util');
const debug = require('debug')(pkgInfo.name);
const parser = require('sqlite-parser');

const LOGIC_AND = 'and';
const LOGIC_OR = 'or';

// operators supported
const OPERATORS = new Map();
OPERATORS.set('<>', 'neq');
OPERATORS.set('>=', 'gte');
OPERATORS.set('<=', 'lte');
OPERATORS.set('=', 'eq');
OPERATORS.set('>', 'gt');
OPERATORS.set('<', 'lt');

// string patterns supported
const PATTERN = /^\s?set\s+(.+)\s+where\s+(.+)$/i;

/**
 * 从 ast 语法树节点中解析出正确的值
 * @param {object} ast
 */
const parseValue = (ast) => {
    let value = '';
    if (ast.variant === 'decimal') {
        value = Number(ast.value);
    } else if (ast.variant === 'column') {
        value = ast.name;
    } else {
        value = ast.value;
    }

    try {
        value = JSON.parse(value);
        return (typeof value === 'undefined') ? null : value;
    } catch (e) {
        return value;
    }
};

/**
 * 往 obj 中增加一个新的条件
 * @param {*} obj 存储条件的对象
 * @param {object} ast 解析条件的 ast
 */
const addCondition = (obj, ast) => {
    debug('CONDITION: \n' + util.inspect(ast, { depth: 20, colorize: true }));
    const { left, right, operation } = ast;
    const key = left.name;
    const value = parseValue(right);  // FIXME string literals needs quotes
    if (typeof obj[key] === 'undefined') {
        obj[key] = {};
    }
    obj[key][OPERATORS.get(operation)] = value;
    return obj;
};

/**
 * 把更新还原成代码，参数实例:
 *
 * [{
    type: 'assignment',
    target: { type: 'identifier', variant: 'column', name: 'name' },
    value: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '+',
        left: { type: 'identifier', variant: 'column', name: 'counter' },
        right: { type: 'literal', variant: 'decimal', value: '1' }
    }
}],
 */
const parseOperation = (set) => {
    if (Array.isArray(set) === false) {
        return {};
    }

    const _parseAssignment = (ast) => {
        if (ast.left.type === 'expression') {    // TODO parse nested
            return `${_parseAssignment(ast.left)} ${ast.operation} ${_parseAssignment(ast.right)}`;
        }

        return `${parseValue(ast.left)} ${ast.operation} ${parseValue(ast.right)}`;
    };

    const operations = {};
    set.forEach((op) => {
        const key = op.target.name;
        let value = '';
        if (op.value.type === 'expression') {
            value = _parseAssignment(op.value);
            debug('OPERATION: \n' + util.inspect({ op, key, value }, { depth: 20, colorize: true }));
        } else {
            value = parseValue(op.value);
        }

        operations[key] = value;
    });

    return operations;
};

/**
 * 把条件数组解析成对象，参数实例:
 * {
    type: 'expression',
    format: 'binary',
    variant: 'operation',
    operation: 'and',
    left: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '=',
        left: { type: 'identifier', variant: 'column', name: 'id' },
        right: { type: 'literal', variant: 'decimal', value: '123' }
    },
    right: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '=',
        left: { type: 'identifier', variant: 'column', name: 'uid' },
        right: { type: 'literal', variant: 'decimal', value: '456' }
    }
}
 */
const parseCondition = (where) => {
    const conditions = [];

    const parseAndCondition = (ast) => {
        const condition = {};
        debugger;

        addCondition(condition, ast.right);

        while (OPERATORS.has(ast.left.operation) === false) {
            addCondition(condition, ast.left.right);
            ast = ast.left;
        }

        addCondition(condition, ast.left);

        conditions.push(condition);
    };

    // 如果是 AND 结果中只有1个，目前 AND 内部不支持 OR
    if (where.operation === LOGIC_AND) {
        parseAndCondition(where);

    // 如果是 OR，结果中是多组条件
    } else if (where.operation === LOGIC_OR) {
        const cRight = {};
        addCondition(cRight, where.right);
        conditions.push(cRight);

        while (OPERATORS.has(where.left.operation) === false) {
            if (where.left.operation === LOGIC_AND) {
                parseAndCondition(_.cloneDeep(where.left));
            } else if (where.left.right.operation === LOGIC_AND) {
                parseAndCondition(_.cloneDeep(where.left.right));
            } else {
                const cTmp = {};
                addCondition(cTmp, where.left.right);
                conditions.push(cTmp);
            }

            where = where.left;
        }

        if (where.operation === LOGIC_OR) {
            const cLeft = {};
            addCondition(cLeft, where.left);
            conditions.push(cLeft);
        }

    // 只有1个条件的情形
    } else {
        const tmp = {};
        addCondition(tmp, where);
        conditions.push(tmp);
    }

    return conditions.length > 1 ? conditions.reverse() : conditions.pop();
};

/**
 * Parse expression into condition and operation
 * @param {string} expression
 * @returns {object} null if failed { operation, condition }
 */
const parse = (expression) => {
    if (!expression || typeof expression !== 'string') {
        debug(`${pkgInfo.name}: expect 'expression' param to be an non-empty string`);
        return null;
    }

    const sanitized = expression.replace(/\s+/g, ' ').replace(/，/g, ',').replace(/＞/g,'>').replace(/＜/g,'<');
    const matches = sanitized.match(PATTERN);
    if (matches === null) {
        debug(`${pkgInfo.name}: 'expression' must be in 'SET xxx=xxx WHERE yyy=yyy' format, expression: ${expression}`);
        return null;
    }

    try {
        const ast = parser(`UPDATE config ${sanitized}`);
        const { set, where } = ast.statement[0];
        debug('AST: \n' + util.inspect(ast, { depth: 20, colorize: true }));
        return {
            operation: parseOperation(set),
            condition: where.length ? parseCondition(where.pop()) : null,
        };
    } catch (e) {
        debug(`${pkgInfo.name}: rule parse failed: ${e.message}`);
        if (process.env.NODE_ENV !== 'production') {
            console.log(e);
        }
        return null;
    }
};

module.exports = {
    parse,
    parseOperation,
    parseCondition,
    parseValue,
};
<commit_msg>remove debugger statement<commit_after>/**
 * Generate SQl-UPDATE-Compatible string to conditions and operations
 *
 * @link https://github.com/codeschool/sqlite-parser
 */
const pkgInfo = require('../package.json');

const _ = require('lodash');
const util = require('util');
const debug = require('debug')(pkgInfo.name);
const parser = require('sqlite-parser');

const LOGIC_AND = 'and';
const LOGIC_OR = 'or';

// operators supported
const OPERATORS = new Map();
OPERATORS.set('<>', 'neq');
OPERATORS.set('>=', 'gte');
OPERATORS.set('<=', 'lte');
OPERATORS.set('=', 'eq');
OPERATORS.set('>', 'gt');
OPERATORS.set('<', 'lt');

// string patterns supported
const PATTERN = /^\s?set\s+(.+)\s+where\s+(.+)$/i;

/**
 * 从 ast 语法树节点中解析出正确的值
 * @param {object} ast
 */
const parseValue = (ast) => {
    let value = '';
    if (ast.variant === 'decimal') {
        value = Number(ast.value);
    } else if (ast.variant === 'column') {
        value = ast.name;
    } else {
        value = ast.value;
    }

    try {
        value = JSON.parse(value);
        return (typeof value === 'undefined') ? null : value;
    } catch (e) {
        return value;
    }
};

/**
 * 往 obj 中增加一个新的条件
 * @param {*} obj 存储条件的对象
 * @param {object} ast 解析条件的 ast
 */
const addCondition = (obj, ast) => {
    debug('CONDITION: \n' + util.inspect(ast, { depth: 20, colorize: true }));
    const { left, right, operation } = ast;
    const key = left.name;
    const value = parseValue(right);  // FIXME string literals needs quotes
    if (typeof obj[key] === 'undefined') {
        obj[key] = {};
    }
    obj[key][OPERATORS.get(operation)] = value;
    return obj;
};

/**
 * 把更新还原成代码，参数实例:
 *
 * [{
    type: 'assignment',
    target: { type: 'identifier', variant: 'column', name: 'name' },
    value: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '+',
        left: { type: 'identifier', variant: 'column', name: 'counter' },
        right: { type: 'literal', variant: 'decimal', value: '1' }
    }
}],
 */
const parseOperation = (set) => {
    if (Array.isArray(set) === false) {
        return {};
    }

    const _parseAssignment = (ast) => {
        if (ast.left.type === 'expression') {    // TODO parse nested
            return `${_parseAssignment(ast.left)} ${ast.operation} ${_parseAssignment(ast.right)}`;
        }

        return `${parseValue(ast.left)} ${ast.operation} ${parseValue(ast.right)}`;
    };

    const operations = {};
    set.forEach((op) => {
        const key = op.target.name;
        let value = '';
        if (op.value.type === 'expression') {
            value = _parseAssignment(op.value);
            debug('OPERATION: \n' + util.inspect({ op, key, value }, { depth: 20, colorize: true }));
        } else {
            value = parseValue(op.value);
        }

        operations[key] = value;
    });

    return operations;
};

/**
 * 把条件数组解析成对象，参数实例:
 * {
    type: 'expression',
    format: 'binary',
    variant: 'operation',
    operation: 'and',
    left: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '=',
        left: { type: 'identifier', variant: 'column', name: 'id' },
        right: { type: 'literal', variant: 'decimal', value: '123' }
    },
    right: {
        type: 'expression',
        format: 'binary',
        variant: 'operation',
        operation: '=',
        left: { type: 'identifier', variant: 'column', name: 'uid' },
        right: { type: 'literal', variant: 'decimal', value: '456' }
    }
}
 */
const parseCondition = (where) => {
    const conditions = [];

    const parseAndCondition = (ast) => {
        const condition = {};

        addCondition(condition, ast.right);

        while (OPERATORS.has(ast.left.operation) === false) {
            addCondition(condition, ast.left.right);
            ast = ast.left;
        }

        addCondition(condition, ast.left);

        conditions.push(condition);
    };

    // 如果是 AND 结果中只有1个，目前 AND 内部不支持 OR
    if (where.operation === LOGIC_AND) {
        parseAndCondition(where);

    // 如果是 OR，结果中是多组条件
    } else if (where.operation === LOGIC_OR) {
        const cRight = {};
        addCondition(cRight, where.right);
        conditions.push(cRight);

        while (OPERATORS.has(where.left.operation) === false) {
            if (where.left.operation === LOGIC_AND) {
                parseAndCondition(_.cloneDeep(where.left));
            } else if (where.left.right.operation === LOGIC_AND) {
                parseAndCondition(_.cloneDeep(where.left.right));
            } else {
                const cTmp = {};
                addCondition(cTmp, where.left.right);
                conditions.push(cTmp);
            }

            where = where.left;
        }

        if (where.operation === LOGIC_OR) {
            const cLeft = {};
            addCondition(cLeft, where.left);
            conditions.push(cLeft);
        }

    // 只有1个条件的情形
    } else {
        const tmp = {};
        addCondition(tmp, where);
        conditions.push(tmp);
    }

    return conditions.length > 1 ? conditions.reverse() : conditions.pop();
};

/**
 * Parse expression into condition and operation
 * @param {string} expression
 * @returns {object} null if failed { operation, condition }
 */
const parse = (expression) => {
    if (!expression || typeof expression !== 'string') {
        debug(`${pkgInfo.name}: expect 'expression' param to be an non-empty string`);
        return null;
    }

    const sanitized = expression.replace(/\s+/g, ' ').replace(/，/g, ',').replace(/＞/g,'>').replace(/＜/g,'<');
    const matches = sanitized.match(PATTERN);
    if (matches === null) {
        debug(`${pkgInfo.name}: 'expression' must be in 'SET xxx=xxx WHERE yyy=yyy' format, expression: ${expression}`);
        return null;
    }

    try {
        const ast = parser(`UPDATE config ${sanitized}`);
        const { set, where } = ast.statement[0];
        debug('AST: \n' + util.inspect(ast, { depth: 20, colorize: true }));
        return {
            operation: parseOperation(set),
            condition: where.length ? parseCondition(where.pop()) : null,
        };
    } catch (e) {
        debug(`${pkgInfo.name}: rule parse failed: ${e.message}`);
        if (process.env.NODE_ENV !== 'production') {
            console.log(e);
        }
        return null;
    }
};

module.exports = {
    parse,
    parseOperation,
    parseCondition,
    parseValue,
};
<|endoftext|>


<commit_before>var util = require('util'),
io = require('socket.io'),
Tumblr = require('tumblr2'),
parseCookie = require('../node_modules/express/node_modules/connect/lib/utils').parseCookie;

module.exports = function(app, config) {
    return {
	get: function(req,res) {
	    if (req.session && req.session.auth && req.session.auth.loggedIn) {
		app.logger.log("got auth:" + util.inspect(req.session.auth));
		BackupInit(app, config);
		res.render('backup', { title: 'Tumbsup',
				     host: config.host,
				     blog: req.session.auth.tumblr.user.blogs[0],
				     limit: config.likes_limit});
	    } else {
		res.render('login', { title: 'Tumbsup', host: config.host });
	    }
	}
    };
};

function BackupInit(app, config) {
    var sio = io.listen(app);
    sio.set('log level', 1);
    sio.set('authorization', function(data, accept) {
	if (data.headers.cookie) {
	    data.cookie = parseCookie(data.headers.cookie);
	    data.sessionID = data.cookie['express.sid'];
	    // (literally) get the session data from the session store
	    app.sessionStore.get(data.sessionID, function (err, session) {
		if (err || !session) {
		    // if we cannot grab a session, turn down the connection
		    accept('Error', false);
		} else {
		    // save the session data and accept the connection
		    data.session = session;
		    accept(null, true);
		}
	    });
	} else {
	    return accept('No cookie transmitted.', false);
	    }
    });

    sio.sockets.on('connection', function(socket) {
	var Progressbar = require('./progressbar');
	var FileStore = require('./filestore');
	var session = socket.handshake.session;
	app.logger.log('got handshake:\n'+util.inspect(session, false, null, true));

	// we've got three steps to take:
	// * download followings
	// * determining photos to download
	// * download photos
	// * create zip file

	// sets remote progress via socket
	var progressbar = new Progressbar(socket, 4);

	socket.on('tumbsup', function(last) {

	    var getLikes = function(followings_file) {
		var oAuthConfig = getOAuthConfig(config, session);

		app.logger.log('getting likes for %s since %s',
			       session.auth.tumblr.user.name,
			       last);
		app.logger.log(session.auth.tumblr.user.likes);

		var num_likes = Math.min(session.auth.tumblr.user.likes,
					 config.likes_limit);
		app.logger.log("getting "+num_likes+" likes");
		progressbar.next_step().status('Determining posts to download...');

		var step = Math.min(num_likes, 20);
		var loops = Math.ceil(num_likes/step);
		var cnt = 0;
		var all_liked_posts = [];
		var cb = function(liked_posts, old_offset) {
		    cnt++;
		    all_liked_posts[old_offset/step] = liked_posts;
		    progressbar.progress(cnt/loops);
		    if (old_offset === 0)
			socket.emit('set last', liked_posts[0].id);

		    if (cnt == loops) {
			var stopped = false;
			var all_urls = all_liked_posts.reduce(function(acc, posts) {
			    var urls = [];
			    posts.forEach(function(post) {
				if (post.id == last) {
				    app.logger.log(
					"collecting urls stopped at %d for %d with %d",
					old_offset, post.id, last);
				    stopped = true;
				}
				if (stopped)
				    return;
				app.logger.log(
				    util.inspect(post, false, null, true));
				if (post.type == 'video') {
				    if (post.video_url)
					urls.push(post.video_url);
				} else if (post.type == 'photo' && post.photos) {
				    post.photos.forEach(function(photo) {
					urls.push(photo.original_size.url);
				    });
				}
			    });
			    return acc.concat(urls);
			}, []);
			app.logger.log("all urls: %s", util.inspect(all_urls));

			var files = new FileStore(app,
						  config,
						  all_urls.length+1,
						  socket,
						  progressbar);
			files.add(followings_file);

			getPhotos(app, config, all_urls, files, socket, progressbar);
		    }
		};
		var offset = 0;
		while (offset+step <= num_likes) {
		    getUserLikes(app, oAuthConfig, offset, step, cb);
		    offset += step;
		}
	    };

	    getUserFollowing(app, config, session, progressbar, getLikes);

	}); // end socket.on
    }); // end sio.sockets.on
}

function getUserFollowing(app, config, session, progressbar, then) {
    app.logger.log("getting %d followings", num_following);

    progressbar.init().status('Retrieving followings ...');

    var oAuthConfig = getOAuthConfig(config, session);

    var num_following = session.auth.tumblr.user.following;

    var tumblr = new Tumblr(oAuthConfig);

    var offset = 0;
    var limit = 20;

    var following = [];
    var num_requests = Math.ceil(num_following/limit);
    var num_responses = 0;

    var resultHandler = function(err, res) {
        num_responses++;

        progressbar.progress(num_responses/num_requests);

        app.logger.log(util.inspect(res, false, null, true));

        if (!err)
            following = following.concat(res.blogs);

        if (num_responses == num_requests ) {
            var filename = config.cache_dir+session.auth.tumblr.user.name
                +"_following.json";
            app.logger.log("got %d followings", following.length);
            var file = fs.createWriteStream(filename);
            file.write(JSON.stringify(following, null, 4));
            file.end();
            then(filename);
        }
    };

    while (offset < num_following) {
        tumblr.getUserFollowing({limit: limit, offset: offset},
                                resultHandler);
        offset += limit;
    }
}

function getUserLikes(app, oAuthConfig, offset, step, cb) {
    app.logger.log("getting user likes for offset %d", offset);
    new Tumblr(oAuthConfig).getUserLikes({limit: step, offset: offset},
                                         function(err, res) {
        app.logger.log("got likes with offset "+offset);
        //app.logger.log(util.inspect(res, false, null, true));
        if (err) {
            app.logger.log("error: "+err);
            return cb([], offset);
        }

        cb(res.liked_posts, offset);
    });
}

var Url = require('url');
var http = require('http');
var fs = require('fs');
function getPhotos(app, config, urls, files, socket, progressbar) {
    var num_photos = urls.length;
    app.logger.log("got "+num_photos+" urls");

    if (num_photos === 0) {
        progressbar.init().status('Sorry, there are no posts to download.');
        return;
    }
    progressbar.next_step().status('Downloading '+num_photos+' posts...');

    urls.forEach(function(url) {
        app.logger.log("checking url "+url);

        var filename = config.cache_dir+url.substring(url.lastIndexOf('/')+1);
        if (fs.existsSync(filename)) {
            app.logger.log("file already downloaded "+filename);
            files.add(filename);
        } else {
            app.logger.log("getting file "+filename);
            url = Url.parse(url);
            http.get(url, function(response) {
                var file = fs.createWriteStream(filename);
                response.on('data', function(chunk){ file.write(chunk); });
                response.on('end', function(foo) {
                    file.end();
                    app.logger.log("wrote file "+filename);
                    files.add(filename);
                });
                response.on('close', function(err) {
                    console.error(err);
                    if (!files.has(filename))
                        files.dec();
                });
            });
        }
    });
}

function getOAuthConfig(config, session) {
    var t = session.auth.tumblr;
    var c = {
        consumerKey: config.consumerKey,
        consumerSecret: config.consumerSecret,
        accessTokenKey: t.accessToken,
        accessTokenSecret: t.accessTokenSecret
    };
    return c;
}
<commit_msg>fix init<commit_after>var util = require('util'),
io = require('socket.io'),
Tumblr = require('tumblr2'),
parseCookie = require('../node_modules/express/node_modules/connect/lib/utils').parseCookie;

module.exports = function(app, config) {
    BackupInit(app, config);
    return {
	get: function(req,res) {
	    if (req.session && req.session.auth && req.session.auth.loggedIn) {
		app.logger.log("got auth:" + util.inspect(req.session.auth));
		res.render('backup', { title: 'Tumbsup',
	                               host: config.host,
				       blog: req.session.auth.tumblr.user.blogs[0],
				       limit: config.likes_limit});
	    } else {
		res.render('login', { title: 'Tumbsup', host: config.host });
	    }
	}
    };
};

function BackupInit(app, config) {
    var sio = io.listen(app);
    sio.set('log level', 1);
    sio.set('authorization', function(data, accept) {
	if (data.headers.cookie) {
	    data.cookie = parseCookie(data.headers.cookie);
	    data.sessionID = data.cookie['express.sid'];
	    // (literally) get the session data from the session store
	    app.sessionStore.get(data.sessionID, function (err, session) {
		if (err || !session) {
		    // if we cannot grab a session, turn down the connection
		    accept('Error', false);
		} else {
		    // save the session data and accept the connection
		    data.session = session;
		    accept(null, true);
		}
	    });
	} else {
	    return accept('No cookie transmitted.', false);
	    }
    });

    sio.sockets.on('connection', function(socket) {
	var Progressbar = require('./progressbar');
	var FileStore = require('./filestore');
	var session = socket.handshake.session;
	app.logger.log('got handshake:\n'+util.inspect(session, false, null, true));

	// we've got three steps to take:
	// * download followings
	// * determining photos to download
	// * download photos
	// * create zip file

	// sets remote progress via socket
	var progressbar = new Progressbar(socket, 4);

	socket.on('tumbsup', function(last) {

	    var getLikes = function(followings_file) {
		var oAuthConfig = getOAuthConfig(config, session);

		app.logger.log('getting likes for %s since %s',
			       session.auth.tumblr.user.name,
			       last);
		app.logger.log(session.auth.tumblr.user.likes);

		var num_likes = Math.min(session.auth.tumblr.user.likes,
					 config.likes_limit);
		app.logger.log("getting "+num_likes+" likes");
		progressbar.next_step().status('Determining posts to download...');

		var step = Math.min(num_likes, 20);
		var loops = Math.ceil(num_likes/step);
		var cnt = 0;
		var all_liked_posts = [];
		var cb = function(liked_posts, old_offset) {
		    cnt++;
		    all_liked_posts[old_offset/step] = liked_posts;
		    progressbar.progress(cnt/loops);
		    if (old_offset === 0)
			socket.emit('set last', liked_posts[0].id);

		    if (cnt == loops) {
			var stopped = false;
			var all_urls = all_liked_posts.reduce(function(acc, posts) {
			    var urls = [];
			    posts.forEach(function(post) {
				if (post.id == last) {
				    app.logger.log(
					"collecting urls stopped at %d for %d with %d",
					old_offset, post.id, last);
				    stopped = true;
				}
				if (stopped)
				    return;
				app.logger.log(
				    util.inspect(post, false, null, true));
				if (post.type == 'video') {
				    if (post.video_url)
					urls.push(post.video_url);
				} else if (post.type == 'photo' && post.photos) {
				    post.photos.forEach(function(photo) {
					urls.push(photo.original_size.url);
				    });
				}
			    });
			    return acc.concat(urls);
			}, []);
			app.logger.log("all urls: %s", util.inspect(all_urls));

			var files = new FileStore(app,
						  config,
						  all_urls.length+1,
						  socket,
						  progressbar);
			files.add(followings_file);

			getPhotos(app, config, all_urls, files, socket, progressbar);
		    }
		};
		var offset = 0;
		while (offset+step <= num_likes) {
		    getUserLikes(app, oAuthConfig, offset, step, cb);
		    offset += step;
		}
	    };

	    getUserFollowing(app, config, session, progressbar, getLikes);

	}); // end socket.on
    }); // end sio.sockets.on
}

function getUserFollowing(app, config, session, progressbar, then) {
    app.logger.log("getting %d followings", num_following);

    progressbar.init().status('Retrieving followings ...');

    var oAuthConfig = getOAuthConfig(config, session);

    var num_following = session.auth.tumblr.user.following;

    var tumblr = new Tumblr(oAuthConfig);

    var offset = 0;
    var limit = 20;

    var following = [];
    var num_requests = Math.ceil(num_following/limit);
    var num_responses = 0;

    var resultHandler = function(err, res) {
        num_responses++;

        progressbar.progress(num_responses/num_requests);

        app.logger.log(util.inspect(res, false, null, true));

        if (!err)
            following = following.concat(res.blogs);

        if (num_responses == num_requests ) {
            var filename = config.cache_dir+session.auth.tumblr.user.name
                +"_following.json";
            app.logger.log("got %d followings", following.length);
            var file = fs.createWriteStream(filename);
            file.write(JSON.stringify(following, null, 4));
            file.end();
            then(filename);
        }
    };

    while (offset < num_following) {
        tumblr.getUserFollowing({limit: limit, offset: offset},
                                resultHandler);
        offset += limit;
    }
}

function getUserLikes(app, oAuthConfig, offset, step, cb) {
    app.logger.log("getting user likes for offset %d", offset);
    new Tumblr(oAuthConfig).getUserLikes({limit: step, offset: offset},
                                         function(err, res) {
        app.logger.log("got likes with offset "+offset);
        //app.logger.log(util.inspect(res, false, null, true));
        if (err) {
            app.logger.log("error: "+err);
            return cb([], offset);
        }

        cb(res.liked_posts, offset);
    });
}

var Url = require('url');
var http = require('http');
var fs = require('fs');
function getPhotos(app, config, urls, files, socket, progressbar) {
    var num_photos = urls.length;
    app.logger.log("got "+num_photos+" urls");

    if (num_photos === 0) {
        progressbar.init().status('Sorry, there are no posts to download.');
        return;
    }
    progressbar.next_step().status('Downloading '+num_photos+' posts...');

    urls.forEach(function(url) {
        app.logger.log("checking url "+url);

        var filename = config.cache_dir+url.substring(url.lastIndexOf('/')+1);
        if (fs.existsSync(filename)) {
            app.logger.log("file already downloaded "+filename);
            files.add(filename);
        } else {
            app.logger.log("getting file "+filename);
            url = Url.parse(url);
            http.get(url, function(response) {
                var file = fs.createWriteStream(filename);
                response.on('data', function(chunk){ file.write(chunk); });
                response.on('end', function(foo) {
                    file.end();
                    app.logger.log("wrote file "+filename);
                    files.add(filename);
                });
                response.on('close', function(err) {
                    console.error(err);
                    if (!files.has(filename))
                        files.dec();
                });
            });
        }
    });
}

function getOAuthConfig(config, session) {
    var t = session.auth.tumblr;
    var c = {
        consumerKey: config.consumerKey,
        consumerSecret: config.consumerSecret,
        accessTokenKey: t.accessToken,
        accessTokenSecret: t.accessTokenSecret
    };
    return c;
}
<|endoftext|>