<commit_before>#!/usr/bin/env node
/*
 * main.js
 *
 * Copyright (C) 2009-13 by RStudio, Inc.
 *
 * This program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

require('./core/log');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var url = require('url');
var util = require('util');
var client_sessions = require('client-sessions');
var connect = require('connect');
var optimist = require('optimist');
var Q = require('q');
var _ = require('underscore');
var Handlebars = require('handlebars');
var connect_util = require('./core/connect-util');
var fsutil = require('./core/fsutil');
var paths = require('./core/paths');
var qutil = require('./core/qutil');
var render = require('./core/render');
var shutdown = require('./core/shutdown');
var proxy_http = require('./proxy/http');
var proxy_sockjs = require('./proxy/sockjs');
var router = require('./router/router')
var config_router = require('./router/config-router');
var Server = require('./server/server');
var SchedulerRegistry = require('./scheduler/scheduler-registry');
var SimpleScheduler = require('./scheduler/simple-scheduler');
var TcpTransport = require('./transport/tcp').Transport;
var UnixSocketTransport = require('./transport/unix-socket').Transport;
var SimpleEventBus = require('./events/simple-event-bus');
var LocalConfigRouter = require('./router/local-config-router');
var SquashRunAsRouter = require('./router/squash-run-as-router.js');

// Version strings
(function() {
  if (fs.existsSync(paths.projectFile('VERSION'))) {
    SHINY_SERVER_VERSION = fs.readFileSync(
      paths.projectFile('VERSION'),
      { encoding: 'ascii' }
    ).trim();
  } else {
    var packageInfo =
      JSON.parse(fs.readFileSync(paths.projectFile('package.json')));
    SHINY_SERVER_VERSION = packageInfo['version'].trim() + '.0';
  }
})();

var shinyVersionString = 'Shiny Server v' + SHINY_SERVER_VERSION;
var nodeVersionString = 'Node.js ' + process.version;
var versionString = shinyVersionString + ' (' + nodeVersionString + ')';

// --version
if (optimist.argv.version) {
  console.log(shinyVersionString);
  console.log(nodeVersionString);
  process.exit(0);
}

logger.info(versionString);

var unlinkPidFile = function() {};
if (optimist.argv.pidfile) {
  var pidfile = optimist.argv.pidfile;
  if (typeof(pidfile) !== 'string') {
    console.error('ERROR: Argument is required for pidfile');
    process.exit(1);
  }
  pidfile = path.resolve(pidfile);
  logger.info('Using pidfile ' + pidfile);
  if (!fsutil.createPidFile(pidfile)) {
    console.error('ERROR: Could not lock pidfile. Is another instance of ' +
                  'Shiny Server running?');
    process.exit(1);
  }
  unlinkPidFile = function() {
    fs.unlink(pidfile, function(err) {
      logger.warn('Error deleting pidfile: ' + err);
    });
  };
  process.on('exit', function() {
    unlinkPidFile();
  });
} else {
  logger.debug('No pidfile requested');
}

var configFilePath = '/etc/shiny-server/shiny-server.conf';
if (optimist.argv._.length >= 1) {
  configFilePath = path.resolve(optimist.argv._[0]);
}

logger.info('Using config file "' + configFilePath + '"');

// A simple router function that does nothing but respond "OK". Can be used for
// load balancer health checks, for example.
function ping(req, res) {
  if (url.parse(req.url).pathname == '/ping') {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('OK');
    return true;
  }
  return false;
}

// We'll need an eventBus...
var eventBus = new SimpleEventBus();

// ...routers...
var indirectRouter = new router.IndirectRouter(new router.NullRouter());
var localConfigRouter = new LocalConfigRouter(
    new router.RestartRouter(
      router.join(indirectRouter, ping)
    ), eventBus
  );
var metarouter = new SquashRunAsRouter(localConfigRouter);

// ...a scheduler registry...
var schedulerRegistry = new SchedulerRegistry(eventBus);

// ...a transport (connects this process with worker procs)...
var transport = new TcpTransport();

// ...an HTTP proxy...
var shinyProxy = new proxy_http.ShinyProxy(
  metarouter,
  schedulerRegistry
);

var clientSessionMiddleware = client_sessions({
  secret: crypto.randomBytes(16).toString('hex')
});

// Setup a placeholder middleware function until we can create one after
// parsing the config.
var sockjsServer = false;
var sockjsHandler = function(req, res){
  return false;
}

var app = connect();
app.use(clientSessionMiddleware);
app.use(function(req, res, next) {
  if (!sockjsHandler(req, res))
    next();
});
// Set up
var assets = connect.static(paths.projectFile('assets'),
                            {maxAge:86400000}); // one day
// Precompile JS template
var ssjsTemplate = fs.readFileSync(paths.projectFile('assets/shiny-server.js'), 'utf-8');
var ssjs = Handlebars.compile(ssjsTemplate);

var allProtocols = ['websocket', 'xdr-streaming', 'xhr-streaming',
  'iframe-eventsource', 'iframe-htmlfile', 'xdr-polling',
  'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'];
function renderSSJS(prots, reconnect){
  var protStr = "['" + prots.join("','") + "']";
  return ssjs({protocols: protStr, reconnect: reconnect ? 'true' : 'false'});
}
var renderedSSJS = renderSSJS(allProtocols, true);

app.use(connect_util.filterByRegex(
  /\b__assets__\/.+/,
  function (req, res, next) {
    // Need to trim off the directory, as if we got here via a connect route
    req.originalUrl = req.originalUrl || req.url;
    req.url = req.url.replace(/^.*\b__assets__\//, '');
    // jcheng 8/16/2013: Having trouble chaining error404 handler with
    // the asset handler. Just check explicitly for now, since the assets
    // dir only contains a single file right now.
    if (req.url === 'shiny-server.js'){
      res.setHeader('Content-Type', 'application/javascript');

      // Disable cache so server-side updates to available protocols show immediately.
      res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');

      res.end(renderedSSJS);
    } else if (req.url === 'sockjs-0.3.min.js' || req.url === 'shiny-server.css')
      assets(req, res);
    else
      render.error404(req, res);
  }
));
app.use(shinyProxy.httpListener);

// Now create a server and hook everything up.
var server = new Server();
server.on('connection', function(socket) {
  // Close HTTP connections that haven't seen traffic in 45 seconds.
  //
  // SockJS sends a heartbeat every 25s so as long as we wait significantly
  // longer than that to timeout, we shouldn't need to worry about closing
  // active connections.
  socket.setTimeout(45 * 1000);
});
server.on('request', _.bind(app.handle, app));
server.on('error', function(err) {
  logger.error('HTTP server error (' + err.listenKey + '): ' + err.message);
});
server.on('clientError', function(err) {
  // ETIMEDOUT, EPIPE, ECONNRESET, "This socket is closed." are all very
  // very common occurrences.
  logger.debug('HTTP client error (' + err.listenKey + '): ' + err.message);
});

server.on('upgrade', function(request, socket, head) {
  if (!sockjsServer){
    logger.warn("Can't route sockJS traffic until configuration file is parsed.");
    res.end();
    return;
  }
  clientSessionMiddleware(request, null, function() {
    sockjsHandler.upgrade(request, socket, head);
  });
});

var requestLogger = null;
server.on('request', function(req, res) {
  if (requestLogger)
    requestLogger(req, res);
});

var loadConfig_p = qutil.serialized(function() {
  return config_router.createRouter_p(configFilePath, schedulerRegistry)
  .then(function(configRouter) {
    indirectRouter.setRouter(configRouter);
    localConfigRouter.setAppOverride(configRouter.getAppOverride());
    server.setAddresses(configRouter.getAddresses());
    schedulerRegistry.setTransport(transport);
    transport.setSocketDir(configRouter.socketDir);

    // Create SockJS server
    sockjsServer = proxy_sockjs.createServer(metarouter, schedulerRegistry);
    sockjsHandler = sockjsServer.middleware();

    // Set the available protocols
    var prots = _.clone(allProtocols);
    _.each(_.uniq(configRouter.disabledProtocols), function(p){
      if (!_.contains(allProtocols, p)){
        throw new Error("Unrecognized disabled protocol: '"+p+"'. Should be one of " + (prots.join(',')));
      }
      prots = _.without(prots, p);
    });
    if (prots.length === 0){
      throw new Error("All protocols were disabled. You must leave at least one protocol enabled.");
    }
    if (configRouter.disabledProtocols && configRouter.disabledProtocols.length > 0){
      logger.info("Disabled protocols: " + configRouter.disabledProtocols.join(","));
    }

    var reconnect = configRouter.reconnect;

    renderedSSJS = renderSSJS(prots, reconnect);

    return createLogger_p(configRouter.accessLogSpec)
    .then(function(logfunc) {
      requestLogger = logfunc;
      logger.trace('Config loaded');
    });
  })
  .fail(function(err) {
    if (err.code === 'ENOENT') {
      logger.error('Error loading config: File "' + configFilePath + '" does not exist');
    } else {
      logger.error('Error loading config: ' + err.message);
    }
  });
});

loadConfig_p().done();

function createLogger_p(logSpec) {
  if (!logSpec || !logSpec.path) {
    logger.debug('No access log configured');
    return Q.resolve(null);
  }

  logger.debug('Access log path: ' + logSpec.path);

  try {
    var stream = fs.createWriteStream(logSpec.path, {flags: 'a'});
    var next = function(){};
    var log = connect.logger({stream: stream, format: logSpec.format});
    return Q.resolve(function(req, res) {
      log(req, res, next);
    });

  } catch (err) {
    return Q.reject(err);
  }
  return Q.nfcall(fs.open, logSpec.path, 'a', 0660)
  .then(function(fd) {
  })
  .fail(function(err) {
    logger.error('Error creating access log: ' + err.message);
    return null;
  });
}

// On SIGHUP (i.e., initctl reload), reload configuration
process.on('SIGHUP', function() {
  logger.info('SIGHUP received, reloading configuration');
  render.flushCache();
  loadConfig_p().done();
});

// On SIGUSR1, write worker registry contents to log
process.on('SIGUSR1', function() {
  schedulerRegistry.dump();
});

// Clean up worker processes on shutdown

var needsCleanup = true;
function gracefulShutdown() {
  // Sometimes the signal gets sent twice. No idea why.
  if (!needsCleanup)
    return;

  // On SIGINT/SIGTERM (i.e. normal termination) we wait a second before
  // exiting so the clients can all be notified
  shutdown.shuttingDown = true;
  try {
    server.destroy();
  } catch (err) {
    logger.error('Error while attempting to stop server: ' + err.message);
  }
  logger.info('Shutting down worker processes (with notification)');
  schedulerRegistry.shutdown();
  needsCleanup = false;
  setTimeout(process.exit, 500);
}

function lastDitchShutdown() {
  if (!needsCleanup)
    return;
  // More-violent shutdown (e.g. uncaught exception), no chance to notify
  // workers as timers won't be scheduled
  shutdown.shuttingDown = true;
  logger.info('Shutting down worker processes');
  schedulerRegistry.shutdown();
}

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('SIGABRT', gracefulShutdown);
process.on('uncaughtException2', gracefulShutdown);
process.on('uncaughtException', function(err) {
  logger.error('Uncaught exception: ' + err);
  logger.error(err.stack);
  process.emit('uncaughtException2', err);
  throw err;
});
process.on('exit', lastDitchShutdown);

if (optimist.argv.agent || process.env.DEBUG_PORT) {
  // NOTE: At the time of this writing, webkit-devtools-agent only supports
  // Node 0.8.x, not 0.10.x
  require('webkit-devtools-agent');
} else {
  // Ignore SIGUSR2, which would otherwise cause exit
  process.on('SIGUSR2', function() {
    logger.info('Ignoring SIGUSR2 (env variable DEBUG_PORT is not set)');
  });
}

if (optimist.argv.agent)
  setTimeout(function(){
    process.kill(process.pid, 'SIGUSR2');
  }, 1000);

if (optimist.argv.memlog) {
  var memstatsPath = 'mem-' + process.pid + '.csv';
  logger.info('Writing memory log to ' + memstatsPath);
  var memstatsStream = fs.createWriteStream(memstatsPath, {
    encoding: 'utf-8',
    mode: 0664
  });
  memstatsStream.write('rss,heapTotal,heapUsed\n');
  setInterval(function() {
    var snapshot = process.memoryUsage();
    memstatsStream.write(snapshot.rss + ',' + snapshot.heapTotal + ',' + snapshot.heapUsed + '\n');
  }, 2000);
}
<commit_msg>Don't forbid storage of templated files.<commit_after>#!/usr/bin/env node
/*
 * main.js
 *
 * Copyright (C) 2009-13 by RStudio, Inc.
 *
 * This program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

require('./core/log');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var url = require('url');
var util = require('util');
var client_sessions = require('client-sessions');
var connect = require('connect');
var optimist = require('optimist');
var Q = require('q');
var _ = require('underscore');
var Handlebars = require('handlebars');
var connect_util = require('./core/connect-util');
var fsutil = require('./core/fsutil');
var paths = require('./core/paths');
var qutil = require('./core/qutil');
var render = require('./core/render');
var shutdown = require('./core/shutdown');
var proxy_http = require('./proxy/http');
var proxy_sockjs = require('./proxy/sockjs');
var router = require('./router/router')
var config_router = require('./router/config-router');
var Server = require('./server/server');
var SchedulerRegistry = require('./scheduler/scheduler-registry');
var SimpleScheduler = require('./scheduler/simple-scheduler');
var TcpTransport = require('./transport/tcp').Transport;
var UnixSocketTransport = require('./transport/unix-socket').Transport;
var SimpleEventBus = require('./events/simple-event-bus');
var LocalConfigRouter = require('./router/local-config-router');
var SquashRunAsRouter = require('./router/squash-run-as-router.js');

// Version strings
(function() {
  if (fs.existsSync(paths.projectFile('VERSION'))) {
    SHINY_SERVER_VERSION = fs.readFileSync(
      paths.projectFile('VERSION'),
      { encoding: 'ascii' }
    ).trim();
  } else {
    var packageInfo =
      JSON.parse(fs.readFileSync(paths.projectFile('package.json')));
    SHINY_SERVER_VERSION = packageInfo['version'].trim() + '.0';
  }
})();

var shinyVersionString = 'Shiny Server v' + SHINY_SERVER_VERSION;
var nodeVersionString = 'Node.js ' + process.version;
var versionString = shinyVersionString + ' (' + nodeVersionString + ')';

// --version
if (optimist.argv.version) {
  console.log(shinyVersionString);
  console.log(nodeVersionString);
  process.exit(0);
}

logger.info(versionString);

var unlinkPidFile = function() {};
if (optimist.argv.pidfile) {
  var pidfile = optimist.argv.pidfile;
  if (typeof(pidfile) !== 'string') {
    console.error('ERROR: Argument is required for pidfile');
    process.exit(1);
  }
  pidfile = path.resolve(pidfile);
  logger.info('Using pidfile ' + pidfile);
  if (!fsutil.createPidFile(pidfile)) {
    console.error('ERROR: Could not lock pidfile. Is another instance of ' +
                  'Shiny Server running?');
    process.exit(1);
  }
  unlinkPidFile = function() {
    fs.unlink(pidfile, function(err) {
      logger.warn('Error deleting pidfile: ' + err);
    });
  };
  process.on('exit', function() {
    unlinkPidFile();
  });
} else {
  logger.debug('No pidfile requested');
}

var configFilePath = '/etc/shiny-server/shiny-server.conf';
if (optimist.argv._.length >= 1) {
  configFilePath = path.resolve(optimist.argv._[0]);
}

logger.info('Using config file "' + configFilePath + '"');

// A simple router function that does nothing but respond "OK". Can be used for
// load balancer health checks, for example.
function ping(req, res) {
  if (url.parse(req.url).pathname == '/ping') {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('OK');
    return true;
  }
  return false;
}

// We'll need an eventBus...
var eventBus = new SimpleEventBus();

// ...routers...
var indirectRouter = new router.IndirectRouter(new router.NullRouter());
var localConfigRouter = new LocalConfigRouter(
    new router.RestartRouter(
      router.join(indirectRouter, ping)
    ), eventBus
  );
var metarouter = new SquashRunAsRouter(localConfigRouter);

// ...a scheduler registry...
var schedulerRegistry = new SchedulerRegistry(eventBus);

// ...a transport (connects this process with worker procs)...
var transport = new TcpTransport();

// ...an HTTP proxy...
var shinyProxy = new proxy_http.ShinyProxy(
  metarouter,
  schedulerRegistry
);

var clientSessionMiddleware = client_sessions({
  secret: crypto.randomBytes(16).toString('hex')
});

// Setup a placeholder middleware function until we can create one after
// parsing the config.
var sockjsServer = false;
var sockjsHandler = function(req, res){
  return false;
}

var app = connect();
app.use(clientSessionMiddleware);
app.use(function(req, res, next) {
  if (!sockjsHandler(req, res))
    next();
});
// Set up
var assets = connect.static(paths.projectFile('assets'),
                            {maxAge:86400000}); // one day
// Precompile JS template
var ssjsTemplate = fs.readFileSync(paths.projectFile('assets/shiny-server.js'), 'utf-8');
var ssjs = Handlebars.compile(ssjsTemplate);

var allProtocols = ['websocket', 'xdr-streaming', 'xhr-streaming',
  'iframe-eventsource', 'iframe-htmlfile', 'xdr-polling',
  'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'];
function renderSSJS(prots, reconnect){
  var protStr = "['" + prots.join("','") + "']";
  return ssjs({protocols: protStr, reconnect: reconnect ? 'true' : 'false'});
}
var renderedSSJS = renderSSJS(allProtocols, true);

app.use(connect_util.filterByRegex(
  /\b__assets__\/.+/,
  function (req, res, next) {
    // Need to trim off the directory, as if we got here via a connect route
    req.originalUrl = req.originalUrl || req.url;
    req.url = req.url.replace(/^.*\b__assets__\//, '');
    // jcheng 8/16/2013: Having trouble chaining error404 handler with
    // the asset handler. Just check explicitly for now, since the assets
    // dir only contains a single file right now.
    if (req.url === 'shiny-server.js'){
      res.setHeader('Content-Type', 'application/javascript');

      // Disable cache so server-side updates to available protocols show immediately.
      res.setHeader('Cache-Control', 'no-cache, must-revalidate');
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');

      res.end(renderedSSJS);
    } else if (req.url === 'sockjs-0.3.min.js' || req.url === 'shiny-server.css')
      assets(req, res);
    else
      render.error404(req, res);
  }
));
app.use(shinyProxy.httpListener);

// Now create a server and hook everything up.
var server = new Server();
server.on('connection', function(socket) {
  // Close HTTP connections that haven't seen traffic in 45 seconds.
  //
  // SockJS sends a heartbeat every 25s so as long as we wait significantly
  // longer than that to timeout, we shouldn't need to worry about closing
  // active connections.
  socket.setTimeout(45 * 1000);
});
server.on('request', _.bind(app.handle, app));
server.on('error', function(err) {
  logger.error('HTTP server error (' + err.listenKey + '): ' + err.message);
});
server.on('clientError', function(err) {
  // ETIMEDOUT, EPIPE, ECONNRESET, "This socket is closed." are all very
  // very common occurrences.
  logger.debug('HTTP client error (' + err.listenKey + '): ' + err.message);
});

server.on('upgrade', function(request, socket, head) {
  if (!sockjsServer){
    logger.warn("Can't route sockJS traffic until configuration file is parsed.");
    res.end();
    return;
  }
  clientSessionMiddleware(request, null, function() {
    sockjsHandler.upgrade(request, socket, head);
  });
});

var requestLogger = null;
server.on('request', function(req, res) {
  if (requestLogger)
    requestLogger(req, res);
});

var loadConfig_p = qutil.serialized(function() {
  return config_router.createRouter_p(configFilePath, schedulerRegistry)
  .then(function(configRouter) {
    indirectRouter.setRouter(configRouter);
    localConfigRouter.setAppOverride(configRouter.getAppOverride());
    server.setAddresses(configRouter.getAddresses());
    schedulerRegistry.setTransport(transport);
    transport.setSocketDir(configRouter.socketDir);

    // Create SockJS server
    sockjsServer = proxy_sockjs.createServer(metarouter, schedulerRegistry);
    sockjsHandler = sockjsServer.middleware();

    // Set the available protocols
    var prots = _.clone(allProtocols);
    _.each(_.uniq(configRouter.disabledProtocols), function(p){
      if (!_.contains(allProtocols, p)){
        throw new Error("Unrecognized disabled protocol: '"+p+"'. Should be one of " + (prots.join(',')));
      }
      prots = _.without(prots, p);
    });
    if (prots.length === 0){
      throw new Error("All protocols were disabled. You must leave at least one protocol enabled.");
    }
    if (configRouter.disabledProtocols && configRouter.disabledProtocols.length > 0){
      logger.info("Disabled protocols: " + configRouter.disabledProtocols.join(","));
    }

    var reconnect = configRouter.reconnect;

    renderedSSJS = renderSSJS(prots, reconnect);

    return createLogger_p(configRouter.accessLogSpec)
    .then(function(logfunc) {
      requestLogger = logfunc;
      logger.trace('Config loaded');
    });
  })
  .fail(function(err) {
    if (err.code === 'ENOENT') {
      logger.error('Error loading config: File "' + configFilePath + '" does not exist');
    } else {
      logger.error('Error loading config: ' + err.message);
    }
  });
});

loadConfig_p().done();

function createLogger_p(logSpec) {
  if (!logSpec || !logSpec.path) {
    logger.debug('No access log configured');
    return Q.resolve(null);
  }

  logger.debug('Access log path: ' + logSpec.path);

  try {
    var stream = fs.createWriteStream(logSpec.path, {flags: 'a'});
    var next = function(){};
    var log = connect.logger({stream: stream, format: logSpec.format});
    return Q.resolve(function(req, res) {
      log(req, res, next);
    });

  } catch (err) {
    return Q.reject(err);
  }
  return Q.nfcall(fs.open, logSpec.path, 'a', 0660)
  .then(function(fd) {
  })
  .fail(function(err) {
    logger.error('Error creating access log: ' + err.message);
    return null;
  });
}

// On SIGHUP (i.e., initctl reload), reload configuration
process.on('SIGHUP', function() {
  logger.info('SIGHUP received, reloading configuration');
  render.flushCache();
  loadConfig_p().done();
});

// On SIGUSR1, write worker registry contents to log
process.on('SIGUSR1', function() {
  schedulerRegistry.dump();
});

// Clean up worker processes on shutdown

var needsCleanup = true;
function gracefulShutdown() {
  // Sometimes the signal gets sent twice. No idea why.
  if (!needsCleanup)
    return;

  // On SIGINT/SIGTERM (i.e. normal termination) we wait a second before
  // exiting so the clients can all be notified
  shutdown.shuttingDown = true;
  try {
    server.destroy();
  } catch (err) {
    logger.error('Error while attempting to stop server: ' + err.message);
  }
  logger.info('Shutting down worker processes (with notification)');
  schedulerRegistry.shutdown();
  needsCleanup = false;
  setTimeout(process.exit, 500);
}

function lastDitchShutdown() {
  if (!needsCleanup)
    return;
  // More-violent shutdown (e.g. uncaught exception), no chance to notify
  // workers as timers won't be scheduled
  shutdown.shuttingDown = true;
  logger.info('Shutting down worker processes');
  schedulerRegistry.shutdown();
}

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
process.on('SIGABRT', gracefulShutdown);
process.on('uncaughtException2', gracefulShutdown);
process.on('uncaughtException', function(err) {
  logger.error('Uncaught exception: ' + err);
  logger.error(err.stack);
  process.emit('uncaughtException2', err);
  throw err;
});
process.on('exit', lastDitchShutdown);

if (optimist.argv.agent || process.env.DEBUG_PORT) {
  // NOTE: At the time of this writing, webkit-devtools-agent only supports
  // Node 0.8.x, not 0.10.x
  require('webkit-devtools-agent');
} else {
  // Ignore SIGUSR2, which would otherwise cause exit
  process.on('SIGUSR2', function() {
    logger.info('Ignoring SIGUSR2 (env variable DEBUG_PORT is not set)');
  });
}

if (optimist.argv.agent)
  setTimeout(function(){
    process.kill(process.pid, 'SIGUSR2');
  }, 1000);

if (optimist.argv.memlog) {
  var memstatsPath = 'mem-' + process.pid + '.csv';
  logger.info('Writing memory log to ' + memstatsPath);
  var memstatsStream = fs.createWriteStream(memstatsPath, {
    encoding: 'utf-8',
    mode: 0664
  });
  memstatsStream.write('rss,heapTotal,heapUsed\n');
  setInterval(function() {
    var snapshot = process.memoryUsage();
    memstatsStream.write(snapshot.rss + ',' + snapshot.heapTotal + ',' + snapshot.heapUsed + '\n');
  }, 2000);
}
<|endoftext|>


<commit_before>'use babel'

import _ from 'underscore-plus'
import {CompositeDisposable, Disposable} from 'atom'
import SelectListView from 'atom-select-list'
import StatusBarItem from './status-bar-item'
import helpers from './helpers'

const LineEndingRegExp = /\r\n|\n/g
const LFRegExp = /(^|[^\r])\n/g
const CRLFRegExp = /\r\n/g

let disposables = null
let modalPanel = null
let lineEndingListView = null

export function activate () {
  disposables = new CompositeDisposable()

  disposables.add(atom.commands.add('atom-text-editor', {
    'line-ending-selector:show': (event) => {
      if (!modalPanel) {
        lineEndingListView = new SelectListView({
          items: [{name: 'LF', value: '\n'}, {name: 'CRLF', value: '\r\n'}],
          filterKeyForItem: (lineEnding) => lineEnding.name,
          didConfirmSelection: (lineEnding) => {
            setLineEnding(atom.workspace.getActivePaneItem(), lineEnding.value)
            modalPanel.hide()
          },
          didCancelSelection: () => {
            modalPanel.hide()
          },
          elementForItem: (lineEnding) => {
            const element = document.createElement('li')
            element.textContent = lineEnding.name
            return element
          }
        })
        modalPanel = atom.workspace.addModalPanel({item: lineEndingListView})
        disposables.add(new Disposable(() => {
          lineEndingListView.destroy()
          modalPanel.destroy()
          modalPanel = null
        }))
      }

      lineEndingListView.reset()
      modalPanel.show()
      lineEndingListView.focus()
    },

    'line-ending-selector:convert-to-LF': (event) => {
      const editorElement = event.target.closest('atom-text-editor')
      setLineEnding(editorElement.getModel(), '\n')
    },

    'line-ending-selector:convert-to-CRLF': (event) => {
      const editorElement = event.target.closest('atom-text-editor')
      setLineEnding(editorElement.getModel(), '\r\n')
    }
  }))
}

export function deactivate () {
  disposables.dispose()
}

export function consumeStatusBar (statusBar) {
  let statusBarItem = new StatusBarItem()
  let currentBufferDisposable = null
  let tooltipDisposable = null

  const updateTile = _.debounce((buffer) => {
    getLineEndings(buffer).then((lineEndings) => {
      if (lineEndings.size === 0) {
        let defaultLineEnding = getDefaultLineEnding()
        buffer.setPreferredLineEnding(defaultLineEnding)
        lineEndings = new Set().add(defaultLineEnding)
      }
      statusBarItem.setLineEndings(lineEndings)
    })
  }, 0)

  const observeActiveItem = function (item) {
    if (currentBufferDisposable) currentBufferDisposable.dispose()

    if (item && item.getBuffer) {
      let buffer = item.getBuffer()
      updateTile(buffer)
      currentBufferDisposable = buffer.onDidChange(({oldText, newText}) => {
        if (!statusBarItem.hasLineEnding('\n')) {
          if (newText.indexOf('\n') >= 0) {
            updateTile(buffer)
          }
        } else if (!statusBarItem.hasLineEnding('\r\n')) {
          if (newText.indexOf('\r\n') >= 0) {
            updateTile(buffer)
          }
        } else if (oldText.indexOf('\n')) {
          updateTile(buffer)
        }
      })
    } else {
      statusBarItem.setLineEndings(new Set())
      currentBufferDisposable = null
    }

    if (tooltipDisposable) {
      disposables.remove(tooltipDisposable)
      tooltipDisposable.dispose()
    }
    tooltipDisposable = atom.tooltips.add(statusBarItem.element,
      {title: `File uses ${statusBarItem.description()} line endings`})
    disposables.add(tooltipDisposable)
  }

  // TODO[v1.19]: Remove conditional once atom.workspace.observeActiveTextEditor ships in Atom v1.19
  if (atom.workspace.observeActiveTextEditor) {
    disposables.add(atom.workspace.observeActiveTextEditor(observeActiveItem))
  } else {
    disposables.add(atom.workspace.observeActivePaneItem(observeActiveItem))
  }

  disposables.add(new Disposable(() => {
    if (currentBufferDisposable) currentBufferDisposable.dispose()
  }))

  statusBarItem.onClick(() => {
    atom.commands.dispatch(
      atom.views.getView(atom.workspace.getActivePaneItem()),
      'line-ending-selector:show'
    )
  })

  let tile = statusBar.addRightTile({item: statusBarItem, priority: 200})
  disposables.add(new Disposable(() => tile.destroy()))
}

function getDefaultLineEnding () {
  switch (atom.config.get('line-ending-selector.defaultLineEnding')) {
    case 'LF':
      return '\n'
    case 'CRLF':
      return '\r\n'
    case 'OS Default':
    default:
      return (helpers.getProcessPlatform() === 'win32') ? '\r\n' : '\n'
  }
}

function getLineEndings (buffer) {
  if (typeof buffer.search === 'function') {
    return Promise.all([
      buffer.search(LFRegExp),
      buffer.search(CRLFRegExp)
    ]).then(([hasLF, hasCRLF]) => {
      const result = new Set()
      if (hasLF) result.add('\n')
      if (hasCRLF) result.add('\r\n')
      return result
    })
  } else {
    return new Promise((resolve) => {
      const result = new Set()
      for (let i = 0; i < buffer.getLineCount() - 1; i++) {
        result.add(buffer.lineEndingForRow(i))
      }
      resolve(result)
    })
  }
}

function setLineEnding (item, lineEnding) {
  if (item && item.getBuffer) {
    let buffer = item.getBuffer()
    buffer.setPreferredLineEnding(lineEnding)
    buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding))
  }
}
<commit_msg>:bug: Restore ability to select line ending for pending editor<commit_after>'use babel'

import _ from 'underscore-plus'
import {CompositeDisposable, Disposable} from 'atom'
import SelectListView from 'atom-select-list'
import StatusBarItem from './status-bar-item'
import helpers from './helpers'

const LineEndingRegExp = /\r\n|\n/g
const LFRegExp = /(^|[^\r])\n/g
const CRLFRegExp = /\r\n/g

let disposables = null
let modalPanel = null
let lineEndingListView = null

export function activate () {
  disposables = new CompositeDisposable()

  disposables.add(atom.commands.add('atom-text-editor', {
    'line-ending-selector:show': (event) => {
      if (!modalPanel) {
        lineEndingListView = new SelectListView({
          items: [{name: 'LF', value: '\n'}, {name: 'CRLF', value: '\r\n'}],
          filterKeyForItem: (lineEnding) => lineEnding.name,
          didConfirmSelection: (lineEnding) => {
            // TODO[v1.19]: Remove conditional once atom.workspace.getActiveTextEditor ships in Atom v1.19
            if (atom.workspace.getActiveTextEditor) {
              setLineEnding(atom.workspace.getActiveTextEditor(), lineEnding.value)
            } else {
              setLineEnding(atom.workspace.getActivePaneItem(), lineEnding.value)
            }
            modalPanel.hide()
          },
          didCancelSelection: () => {
            modalPanel.hide()
          },
          elementForItem: (lineEnding) => {
            const element = document.createElement('li')
            element.textContent = lineEnding.name
            return element
          }
        })
        modalPanel = atom.workspace.addModalPanel({item: lineEndingListView})
        disposables.add(new Disposable(() => {
          lineEndingListView.destroy()
          modalPanel.destroy()
          modalPanel = null
        }))
      }

      lineEndingListView.reset()
      modalPanel.show()
      lineEndingListView.focus()
    },

    'line-ending-selector:convert-to-LF': (event) => {
      const editorElement = event.target.closest('atom-text-editor')
      setLineEnding(editorElement.getModel(), '\n')
    },

    'line-ending-selector:convert-to-CRLF': (event) => {
      const editorElement = event.target.closest('atom-text-editor')
      setLineEnding(editorElement.getModel(), '\r\n')
    }
  }))
}

export function deactivate () {
  disposables.dispose()
}

export function consumeStatusBar (statusBar) {
  let statusBarItem = new StatusBarItem()
  let currentBufferDisposable = null
  let tooltipDisposable = null

  const updateTile = _.debounce((buffer) => {
    getLineEndings(buffer).then((lineEndings) => {
      if (lineEndings.size === 0) {
        let defaultLineEnding = getDefaultLineEnding()
        buffer.setPreferredLineEnding(defaultLineEnding)
        lineEndings = new Set().add(defaultLineEnding)
      }
      statusBarItem.setLineEndings(lineEndings)
    })
  }, 0)

  const observeActiveItem = function (item) {
    if (currentBufferDisposable) currentBufferDisposable.dispose()

    if (item && item.getBuffer) {
      let buffer = item.getBuffer()
      updateTile(buffer)
      currentBufferDisposable = buffer.onDidChange(({oldText, newText}) => {
        if (!statusBarItem.hasLineEnding('\n')) {
          if (newText.indexOf('\n') >= 0) {
            updateTile(buffer)
          }
        } else if (!statusBarItem.hasLineEnding('\r\n')) {
          if (newText.indexOf('\r\n') >= 0) {
            updateTile(buffer)
          }
        } else if (oldText.indexOf('\n')) {
          updateTile(buffer)
        }
      })
    } else {
      statusBarItem.setLineEndings(new Set())
      currentBufferDisposable = null
    }

    if (tooltipDisposable) {
      disposables.remove(tooltipDisposable)
      tooltipDisposable.dispose()
    }
    tooltipDisposable = atom.tooltips.add(statusBarItem.element,
      {title: `File uses ${statusBarItem.description()} line endings`})
    disposables.add(tooltipDisposable)
  }

  // TODO[v1.19]: Remove conditional once atom.workspace.observeActiveTextEditor ships in Atom v1.19
  if (atom.workspace.observeActiveTextEditor) {
    disposables.add(atom.workspace.observeActiveTextEditor(observeActiveItem))
  } else {
    disposables.add(atom.workspace.observeActivePaneItem(observeActiveItem))
  }

  disposables.add(new Disposable(() => {
    if (currentBufferDisposable) currentBufferDisposable.dispose()
  }))

  statusBarItem.onClick(() => {
    let editor

    // TODO[v1.19]: Remove conditional once atom.workspace.getActiveTextEditor ships in Atom v1.19
    if (atom.workspace.getActiveTextEditor) {
      editor = atom.workspace.getActiveTextEditor()
    } else {
      editor = atom.workspace.getActivePaneItem()
    }

    atom.commands.dispatch(
      atom.views.getView(editor),
      'line-ending-selector:show'
    )
  })

  let tile = statusBar.addRightTile({item: statusBarItem, priority: 200})
  disposables.add(new Disposable(() => tile.destroy()))
}

function getDefaultLineEnding () {
  switch (atom.config.get('line-ending-selector.defaultLineEnding')) {
    case 'LF':
      return '\n'
    case 'CRLF':
      return '\r\n'
    case 'OS Default':
    default:
      return (helpers.getProcessPlatform() === 'win32') ? '\r\n' : '\n'
  }
}

function getLineEndings (buffer) {
  if (typeof buffer.search === 'function') {
    return Promise.all([
      buffer.search(LFRegExp),
      buffer.search(CRLFRegExp)
    ]).then(([hasLF, hasCRLF]) => {
      const result = new Set()
      if (hasLF) result.add('\n')
      if (hasCRLF) result.add('\r\n')
      return result
    })
  } else {
    return new Promise((resolve) => {
      const result = new Set()
      for (let i = 0; i < buffer.getLineCount() - 1; i++) {
        result.add(buffer.lineEndingForRow(i))
      }
      resolve(result)
    })
  }
}

function setLineEnding (item, lineEnding) {
  if (item && item.getBuffer) {
    let buffer = item.getBuffer()
    buffer.setPreferredLineEnding(lineEnding)
    buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding))
  }
}
<|endoftext|>


<commit_before>const cp = require("child_process");
const { shell } = require("electron");
const { AutoLanguageClient } = require("atom-languageclient");
const { detectVirtualEnv } = require("./utils");

// Ref: https://github.com/nteract/hydrogen/blob/master/lib/autocomplete-provider.js#L33
// adapted from http://stackoverflow.com/q/5474008
const PYTHON_REGEX = /(([^\d\W]|[\u00A0-\uFFFF])[\w.\u00A0-\uFFFF]*)|\.$/;

class PythonLanguageClient extends AutoLanguageClient {
  getGrammarScopes() {
    return ["source.python"];
  }

  getLanguageName() {
    return "Python";
  }

  getServerName() {
    return "pyls";
  }

  getRootConfigurationKey() {
    return "ide-python";
  }

  mapConfigurationObject(configuration) {
    return {
      pyls: {
        configurationSources: configuration['pylsConfigurationSources'],
        plugins: configuration['pylsPlugins']
      }
    };
  }

  async startServerProcess(projectPath) {
    await new Promise(resolve => atom.whenShellEnvironmentLoaded(resolve));

    const venvPath = await detectVirtualEnv(projectPath);

    const pylsEnvironment = Object.assign({}, process.env);

    if (venvPath) {
      pylsEnvironment["VIRTUAL_ENV"] = venvPath;
    }

    const childProcess = cp.spawn(atom.config.get("ide-python.pylsPath"), {
      cwd: projectPath,
      env: pylsEnvironment
    });
    childProcess.on("error", err =>
      atom.notifications.addError("Unable to start the Python language server.", {
        dismissable: true,
        buttons: [
          {
            text: "Install Instructions",
            onDidClick: () => atom.workspace.open("atom://config/packages/ide-python")
          },
          {
            text: "Download Python",
            onDidClick: () => shell.openExternal("https://www.python.org/downloads/")
          }
        ],
        description:
          "This can occur if you do not have Python installed or if it is not in your path.\n\n Make sure to install `pyls` by running:\n```\npip install python-language-server\n```"
      })
    );
    return childProcess;
  }

  async getSuggestions(request) {
    if (!PYTHON_REGEX.test(request.prefix)) return null;
    return super.getSuggestions(request);
  }

  deactivate() {
    return Promise.race([super.deactivate(), this.createTimeoutPromise(2000)]);
  }

  createTimeoutPromise(milliseconds) {
    return new Promise((resolve, reject) => {
      let timeout = setTimeout(() => {
        clearTimeout(timeout);
        this.logger.error(`Server failed to shutdown in ${milliseconds}ms, forcing termination`);
        resolve();
      }, milliseconds);
    });
  }
}

module.exports = new PythonLanguageClient();
<commit_msg>Cleanup mapConfigurationObject<commit_after>const cp = require("child_process");
const { shell } = require("electron");
const { AutoLanguageClient } = require("atom-languageclient");
const { detectVirtualEnv } = require("./utils");

// Ref: https://github.com/nteract/hydrogen/blob/master/lib/autocomplete-provider.js#L33
// adapted from http://stackoverflow.com/q/5474008
const PYTHON_REGEX = /(([^\d\W]|[\u00A0-\uFFFF])[\w.\u00A0-\uFFFF]*)|\.$/;

class PythonLanguageClient extends AutoLanguageClient {
  getGrammarScopes() {
    return ["source.python"];
  }

  getLanguageName() {
    return "Python";
  }

  getServerName() {
    return "pyls";
  }

  getRootConfigurationKey() {
    return "ide-python";
  }

  mapConfigurationObject(configuration) {
    return {
      pyls: {
        configurationSources: configuration.pylsConfigurationSources,
        plugins: configuration.pylsPlugins
      }
    };
  }

  async startServerProcess(projectPath) {
    await new Promise(resolve => atom.whenShellEnvironmentLoaded(resolve));

    const venvPath = await detectVirtualEnv(projectPath);

    const pylsEnvironment = Object.assign({}, process.env);

    if (venvPath) {
      pylsEnvironment["VIRTUAL_ENV"] = venvPath;
    }

    const childProcess = cp.spawn(atom.config.get("ide-python.pylsPath"), {
      cwd: projectPath,
      env: pylsEnvironment
    });
    childProcess.on("error", err =>
      atom.notifications.addError("Unable to start the Python language server.", {
        dismissable: true,
        buttons: [
          {
            text: "Install Instructions",
            onDidClick: () => atom.workspace.open("atom://config/packages/ide-python")
          },
          {
            text: "Download Python",
            onDidClick: () => shell.openExternal("https://www.python.org/downloads/")
          }
        ],
        description:
          "This can occur if you do not have Python installed or if it is not in your path.\n\n Make sure to install `pyls` by running:\n```\npip install python-language-server\n```"
      })
    );
    return childProcess;
  }

  async getSuggestions(request) {
    if (!PYTHON_REGEX.test(request.prefix)) return null;
    return super.getSuggestions(request);
  }

  deactivate() {
    return Promise.race([super.deactivate(), this.createTimeoutPromise(2000)]);
  }

  createTimeoutPromise(milliseconds) {
    return new Promise((resolve, reject) => {
      let timeout = setTimeout(() => {
        clearTimeout(timeout);
        this.logger.error(`Server failed to shutdown in ${milliseconds}ms, forcing termination`);
        resolve();
      }, milliseconds);
    });
  }
}

module.exports = new PythonLanguageClient();
<|endoftext|>


<commit_before>// The main module of the bobos Add-on.
var oldToggle;
var oldHandleGcliCommand;

var filters = [
    {name: "Achromatopsia", filter: "grey"},
    {name: "Protanopia", filter: "proto"},
    {name: "Deuteranopia", filter: "deutero"},
    {name: "Tritanopia", filter: "trito"},
];

exports.main = function() {
    var data = require('sdk/self').data;
    var { Cu } = require("chrome");
    Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm");

    var checkRemove = function(aTab) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        if (rui) {
            // Remove my things
            rui.stack.firstElementChild.removeAttribute("style");
        }
    }

    var checkAdd = function(aTab) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        if (rui) {
            // Add my things
            rui.menulist2 = rui.chromeDoc.createElement("menulist");
            rui.menulist2.className = "devtools-responsiveui-menulist";
            rui.menulist2.addEventListener("select", function() {
                let currentIdx = rui.menulist2.selectedIndex;
                if (currentIdx > 0) {
                    let filter = filters[currentIdx - 1];
                    rui.stack.firstElementChild.setAttribute("style", "filter:url(" + data.url("filters.svg") + "#" + filter.filter + ")");
                } else {
                    rui.stack.firstElementChild.removeAttribute("style");
                }
            }, true);

            let menupopup = rui.chromeDoc.createElement("menupopup");
            // Fill menupopup
            let menuitem = rui.chromeDoc.createElement("menuitem");
            menuitem.setAttribute("selected", "true");
            menuitem.setAttribute("label", "No Filter");
            menupopup.appendChild(menuitem);

            for (let i = 0; i < filters.length; i++) {
                menuitem = rui.chromeDoc.createElement("menuitem");
                menuitem.setAttribute("label", filters[i].name);
                menupopup.appendChild(menuitem);
                rui.menulist2.appendChild(menupopup);
            }

            rui.toolbar.insertBefore(rui.menulist2, rui.rotatebutton);

            rui.my_close = (function() {
                checkRemove(aTab);
                this.close();
            }).bind(rui);
            rui.closebutton.removeEventListener("command", rui.bound_close, true);
            rui.closebutton.addEventListener("command", rui.my_close, true);
        }
    }

    oldHandleGcliCommand = ResponsiveUIManager.handleGcliCommand;
    ResponsiveUIManager.handleGcliCommand = function (aWindow, aTab, aCommand, aArgs) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        switch (aCommand) {
            case "resize to":
                if (!rui) {
                    oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                    checkAdd(aTab);
                }
                else {
                    rui.setSize(aArgs.width, aArgs.height);
                }
                break;
            case "resize on":
                if (!rui) {
                    oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                    checkAdd(aTab);
                }
                break;
            case "resize off":
                checkRemove(aTab);
                oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                break;
            case "resize toggle":
                this.toggle(aWindow, aTab);
            default:
        }
    }

    oldToggle = ResponsiveUIManager.toggle;
    ResponsiveUIManager.toggle = function (aWindow, aTab) {
        checkRemove(aTab);
        oldToggle.call(this, aWindow, aTab);
        checkAdd(aTab);
    }
};

exports.onUnload = function (reason) {
    var { Cu } = require("chrome");
    Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm");
    ResponsiveUIManager.toggle = oldToggle;
    ResponsiveUIManager.handleGcliCommand = oldHandleGcliCommand;
};
<commit_msg>Fix an issue with the close responsive UI button<commit_after>// The main module of the bobos Add-on.
var oldToggle;
var oldHandleGcliCommand;

var filters = [
    {name: "Achromatopsia", filter: "grey"},
    {name: "Protanopia", filter: "proto"},
    {name: "Deuteranopia", filter: "deutero"},
    {name: "Tritanopia", filter: "trito"},
];

exports.main = function() {
    var data = require('sdk/self').data;
    var { Cu } = require("chrome");
    Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm");

    var checkRemove = function(aTab) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        if (rui) {
            // Remove my things
            rui.stack.firstElementChild.removeAttribute("style");
            rui.closebutton.removeEventListener("command", rui.my_close, true);
            rui.closebutton.addEventListener("command", rui.bound_close, true);
        }
    }

    var checkAdd = function(aTab) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        if (rui) {
            // Add my things
            rui.menulist2 = rui.chromeDoc.createElement("menulist");
            rui.menulist2.className = "devtools-responsiveui-menulist";
            rui.menulist2.addEventListener("select", function() {
                let currentIdx = rui.menulist2.selectedIndex;
                if (currentIdx > 0) {
                    let filter = filters[currentIdx - 1];
                    rui.stack.firstElementChild.setAttribute("style", "filter:url(" + data.url("filters.svg") + "#" + filter.filter + ")");
                } else {
                    rui.stack.firstElementChild.removeAttribute("style");
                }
            }, true);

            let menupopup = rui.chromeDoc.createElement("menupopup");
            // Fill menupopup
            let menuitem = rui.chromeDoc.createElement("menuitem");
            menuitem.setAttribute("selected", "true");
            menuitem.setAttribute("label", "No Filter");
            menupopup.appendChild(menuitem);

            for (let i = 0; i < filters.length; i++) {
                menuitem = rui.chromeDoc.createElement("menuitem");
                menuitem.setAttribute("label", filters[i].name);
                menupopup.appendChild(menuitem);
                rui.menulist2.appendChild(menupopup);
            }

            rui.toolbar.insertBefore(rui.menulist2, rui.rotatebutton);

            rui.my_close = (function() {
                checkRemove(aTab);
                this.close();
            }).bind(rui);
            rui.closebutton.removeEventListener("command", rui.bound_close, true);
            rui.closebutton.addEventListener("command", rui.my_close, true);
        }
    }

    oldHandleGcliCommand = ResponsiveUIManager.handleGcliCommand;
    ResponsiveUIManager.handleGcliCommand = function (aWindow, aTab, aCommand, aArgs) {
        let rui = ResponsiveUIManager.getResponsiveUIForTab(aTab);
        switch (aCommand) {
            case "resize to":
                if (!rui) {
                    oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                    checkAdd(aTab);
                }
                else {
                    rui.setSize(aArgs.width, aArgs.height);
                }
                break;
            case "resize on":
                if (!rui) {
                    oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                    checkAdd(aTab);
                }
                break;
            case "resize off":
                checkRemove(aTab);
                oldHandleGcliCommand.call(this, aWindow, aTab, aCommand, aArgs);
                break;
            case "resize toggle":
                this.toggle(aWindow, aTab);
            default:
        }
    }

    oldToggle = ResponsiveUIManager.toggle;
    ResponsiveUIManager.toggle = function (aWindow, aTab) {
        checkRemove(aTab);
        oldToggle.call(this, aWindow, aTab);
        checkAdd(aTab);
    }
};

exports.onUnload = function (reason) {
    var { Cu } = require("chrome");
    Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm");
    ResponsiveUIManager.toggle = oldToggle;
    ResponsiveUIManager.handleGcliCommand = oldHandleGcliCommand;
};
<|endoftext|>


<commit_before>'use strict';

var selfModule = require('sdk/self');
var windowUtils = require('window/utils');

var panelModule = require('sdk/panel');
var pageWorkerModule = require('sdk/page-worker');
var hotkeysModule = require('sdk/hotkeys');
var widgetModule = require('sdk/widget');

var DEBUG = false;
var debug = function debug() {
  if (!DEBUG)
    return;

  console.log.apply(console, arguments);
};

var IMEFrontend = {
  active: false,
  inputActive: false,

  focusedElement: null,

  candidateSelectionMode: false,

  pendingsymbols: '',
  candidates: [],
  candidatePage: 0,

  // XXX: make this configable
  hotkeyCombo: 'accel-alt-1',

  init: function if_init() {
    debug('[if/init]');
    // Show an widget
    this.widget = widgetModule.Widget({
      id: 'jszhuyin-indicator',
      label: 'JSZhuyin IME',
      content: 'A'
    });

    // Register a hot key for toggling
    hotkeysModule.Hotkey({
      combo: this.hotkeyCombo,
      onPress: this.toggle.bind(this)
    });
  },

  toggle: function if_toggle() {
    debug('[if/toggle]');
    if (!this.active) {
      this.start();
    } else {
      this.stop();
    }
  },

  start: function if_start() {
    if (this.active)
      return;

    debug('[if/start]');
    this.active = true;

    this.widget.content = 'ㄅ';

    KeyMonitor.start();
    IMEWorkerManager.start();
  },

  stop: function if_stop() {
    if (!this.active)
      return;

    debug('[if/stop]');
    this.active = false;

    this.widget.content = 'A';
    this.stopInput();
    KeyMonitor.stop();
  },

  updateInput: function if_updateInput() {
    // Don't update focusedElement if panel is present
    if (IMEDisplayManager.panel.isShowing)
      return;

    debug('[if/updateInput]');
    var focusedElement = windowUtils.getFocusedElement();
    if (!IMEOutput.canHandle(focusedElement)) {
      if (this.inputActive) {
        this.stopInput();
      }

      return;
    }

    this.startInput(focusedElement);
  },

  startInput: function if_startInput(focusedElement) {
    debug('[if/startInput]');
    // XXX: We kept the reference of focused element even after
    // the focus is taken away. The reference will not get deleted/updated
    // until next keypress (by updateInput() call in handleKeyEvent().)
    this.focusedElement = focusedElement;
    this.inputActive = true;

    IMEOutput.setFocusedElement(focusedElement);
  },

  stopInput: function if_stopInput() {
    debug('[if/stopInput]');

    this.focusedElement = null;
    this.inputActive = false;

    this.candidateSelectionMode = false;

    this.pendingsymbols = '';
    this.candidates = [];
    this.candidatePage = 0;
    this.toggleIMEDisplay();

    IMEOutput.setFocusedElement(null);
    IMEWorkerManager.sendMessage('empty');
    // IMEWorkerManager.stop();
  },

  setCandidateSelectionMode: function if_setCandidateSelectionMode(mode) {
    IMEDisplayManager.sendMessage('candidateselectionmode', mode);
    this.candidateSelectionMode = mode;
  },

  toggleIMEDisplay: function if_toggleIMEDisplay() {
    if (this.pendingsymbols || this.candidates.length) {
      IMEDisplayManager.start(this.focusedElement);
    } else {
      IMEDisplayManager.stop();
      IMEOutput.regainFocus();
    }
  },

  handleWorkerMessage: function if_handleWorkerResult(name, data) {
    switch (name) {
      case 'pendingsymbols':
        if (!this.active)
          return; // Panel not displayed, nothing to show.

        this.pendingsymbols = data;
        IMEDisplayManager.sendMessage(name, data);
        this.toggleIMEDisplay();
        break;

      case 'candidates':
        if (!this.active)
          return; // Panel not displayed, nothing to show.

        this.candidates = data;
        this.candidatePage = 0;
        IMEDisplayManager.sendMessage(name, data);
        this.toggleIMEDisplay();
        if (!this.candidates.length && this.candidateSelectionMode) {
          this.setCandidateSelectionMode(false);
        }
        break;

      case 'key':
        IMEOutput.appendKey(data);
        break;

      case 'string':
        IMEOutput.appendText(data);
        break;
    }
  },

  handleDisplayMessage: function if_handleDisplayMessage(name, data) {
    switch (name) {
      case 'select':
        IMEWorkerManager.sendMessage(name, data);
        break;
    }
  },

  handleKeyEvent: function if_handleKeyEvent(keyCode, charCode, symbolCode) {
    this.updateInput();
    if (!this.inputActive)
      return false;

    // escape key: stop input mode immediately
    if (keyCode === 27) {
      this.stopInput();
      return true;
    }

    if (this.candidateSelectionMode) {
      // A non-printable character will come with charCode = 0,
      // let's squeeze these two code into one variable to simplify the logic.
      var charKeyCode = charCode || keyCode;

      var page = this.candidatePage;

      switch (charKeyCode) {
        case 8: // Backspace key: leave selection mode
          this.setCandidateSelectionMode(false);

          // IME engine will need to handle this bug too
          if (this.pendingsymbols) {
            // Let engine handles backspace
            IMEWorkerManager.sendMessage('click', keyCode);
          } else {
            // Let default handing to run instead.
            // return false;
            // ... or, remove the selection panel altogether w/o deleting
            // text that is sent?
            this.stopInput();
          }
          break;

        case 13: // Enter key: select 1st selection of the page
          if (!this.candidates[page * 9])
            break;

          IMEWorkerManager.sendMessage('select', this.candidates[page * 9]);
          break;

        case 37: // Left arrow key: flip candidate selection page
          if (page === 0)
            break;

          page = this.candidatePage -= 1;
          IMEDisplayManager.sendMessage('candidateselectionpage', page);
          break;

        case 38: // Up arrow key: leaving candidate selection mode
          this.setCandidateSelectionMode(false);
          break;

        case 39: // Right arrow key: flip candidate selection page
          if ((page + 1) * 9 >= this.candidates.length)
            break;

          page = this.candidatePage += 1;
          IMEDisplayManager.sendMessage('candidateselectionpage', page);
          break;

        default:
          // Number keys: select selection
          if (charKeyCode >= 49 && charKeyCode <= 57) {
            var n = charCode - 49;
            if (!this.candidates[page * 9 + n])
              break;

            IMEWorkerManager.sendMessage('select',
                                         this.candidates[page * 9 + n]);
          }
          break;
      }

      // No key should be handled by others in this mode.
      return true;
    }

    // Entering candidate selection mode with down arrow
    if (keyCode === 40 && this.candidates.length) {
      this.setCandidateSelectionMode(true);
      return true;
    }

    // non-printable keys shouldn't be handled by IME engine
    // if nothing is displayed presently.
    if (!charCode && !this.candidates.length && !this.pendingsymbols)
      return false;

    var code = symbolCode || charCode;
    // IME engine is only responsive to backspace and enter keyCodes,
    if (keyCode === 8 || keyCode === 13) {
      code = keyCode;
    }

    // other non-printable keys (charCode = 0) is meaningless to IME engine.
    if (!code)
      return false;

    IMEWorkerManager.sendMessage('click', code);

    return true;
  }
};
IMEFrontend.init();

var KeyMonitor = {
  focusedWindow: null,

  start: function km_start() {
    this.focusedWindow = windowUtils.getMostRecentBrowserWindow();
    this.focusedWindow.addEventListener('keypress', this, true);
  },

  stop: function km_stop() {
    this.focusedWindow.removeEventListener('keypress', this, true);
    this.focusedWindow = null;
  },

  handleEvent: function km_handleEvent(evt) {
    if (evt.metaKey || evt.ctrlKey || evt.altKey)
      return;

    var symbolCode =
      KeyMapper.getSymbolCodeFromCode(evt.charCode, evt.shiftKey);

    debug('[main/km/handleEvent]', evt.keyCode, evt.charCode, symbolCode);

    var handled =
      IMEFrontend.handleKeyEvent(evt.keyCode, evt.charCode, symbolCode);
    if (handled) {
      evt.stopImmediatePropagation();
      evt.preventDefault();
    }
  }
};

var KeyMapper = {
  shiftMapping: {
    '<': '，',
    '>': '。',
    '?': '？',
    '"': '、'
  },

  zhuyinMapping: {
    ',': 'ㄝ',
    '-': 'ㄦ',
    '.': 'ㄡ',
    '/': 'ㄥ',
    '0': 'ㄢ',
    '1': 'ㄅ',
    '2': 'ㄉ',
    '3': 'ˇ',
    '4': 'ˋ',
    '5': 'ㄓ',
    '6': 'ˊ',
    '7': '˙',
    '8': 'ㄚ',
    '9': 'ㄞ',
    ';': 'ㄤ',
    'a': 'ㄇ',
    'b': 'ㄖ',
    'c': 'ㄏ',
    'd': 'ㄎ',
    'e': 'ㄍ',
    'f': 'ㄑ',
    'g': 'ㄕ',
    'h': 'ㄘ',
    'i': 'ㄛ',
    'j': 'ㄨ',
    'k': 'ㄜ',
    'l': 'ㄠ',
    'm': 'ㄩ',
    'n': 'ㄙ',
    'o': 'ㄟ',
    'p': 'ㄣ',
    'q': 'ㄆ',
    'r': 'ㄐ',
    's': 'ㄋ',
    't': 'ㄔ',
    'u': 'ㄧ',
    'v': 'ㄒ',
    'w': 'ㄊ',
    'x': 'ㄌ',
    'y': 'ㄗ',
    'z': 'ㄈ'
  },

  getSymbolCodeFromCode: function km_getSymbolCodeFromCode(code, shiftKey) {
    var chr = String.fromCharCode(code);

    if (shiftKey) {
      if (this.shiftMapping[chr]) {
        return this.shiftMapping[chr].charCodeAt(0);
      } else {
        return chr.toLowerCase().charCodeAt(0);
      }
    }

    if (this.zhuyinMapping[chr]) {
      return this.zhuyinMapping[chr].charCodeAt(0);
    } else {
      return code;
    }
  }
};

/*
 * IMEOutput is poor man's solution for inserting characters into
 * the nsIDOMElement.
 * It does not handle cursor position nor file key/composition
 * events. To do this right we will have to re-implement/copy
 * half of mozKeyboard API in B2G instead. Not something worth doing, yet.
 */
var IMEOutput = {
  // XXX: allow more types.
  inputTagNames: ['html:input', 'INPUT'],

  focusedElement: null,

  setFocusedElement: function io_setFocusedElement(focusedElement) {
    this.focusedElement = focusedElement;
  },

  canHandle: function io_canHandle(element) {
    if (!element)
      return false;

    if (this.inputTagNames.indexOf(element.tagName) === -1)
      return false;

    return true;
  },

  regainFocus: function io_regainFocus() {
    if (!this.focusedElement)
      return;

    this.focusedElement.focus();
    var pos = this.focusedElement.value.length;
    this.focusedElement.setSelectionRange(pos, pos);
  },

  appendKey: function io_appendKey(code) {
    if (!this.focusedElement) {
      throw 'IMEOutput has no focused element.';
    }

    if (code === 8) {
      var val = this.focusedElement.value;
      return (this.focusedElement.value =
        (val.substr(0, val.length - 1)));
    }
    return this.appendText(String.fromCharCode(code));
  },

  appendText: function io_appendText(text) {
    if (!this.focusedElement) {
      throw 'IMEOutput has no focused element.';
    }

    return this.focusedElement.value += text;
  }
};

var IMEDisplayManager = {
  panel: null,

  panelReady: false,

  init: function idm_init() {
    this.panel = panelModule.Panel({
      width: 600,
      height: 74,
      contentURL: selfModule.data.url('panel.html'),
      contentScriptFile: selfModule.data.url('message_proxy.js'),
      contentScriptWhen: 'start',
      onMessage: this.handleMessage.bind(this)
    });
  },

  start: function idm_start(anchor) {
    if (this.panel.isShowing)
      return;

    this.panel.show(anchor);
  },

  stop: function idm_stop() {
    if (!this.panel.isShowing)
      return;

    this.sendMessage('reset');
    this.panel.hide();
  },

  handleMessage: function idm_handleMessage(msg) {
    debug('[main/idm/handleMessage]', msg.name);

    switch (msg.name) {
      case 'init':
        this.panelReady = true;
        break;

      case 'select':
        IMEFrontend.handleDisplayMessage(msg.name, msg.data);
        break;
    }
  },

  sendMessage: function idm_sendMessage(name, data) {
    if (!this.panel) {
      throw 'IMEDisplayManager.init() needs to be run first.';
    }

    if (!this.panelReady) {
      throw 'IMEDisplayManager have not ready yet.';
    }

    this.panel.postMessage({name: name, data: data, to: 'page'});
  }
};
// Init the panel when Firefox starts.
// XXX: this would slow down Firefox start-up and memory usage.
IMEDisplayManager.init();

var IMEWorkerManager = {
  worker: null,
  workerReady: false,
  start: function iwm_start() {
    if (this.worker) {
      if (this.workerReady) {
        this.sendMessage('empty');
      }
      return;
    }

    this.worker = pageWorkerModule.Page({
      contentURL: selfModule.data.url('worker.html'),
      contentScriptFile: selfModule.data.url('message_proxy.js'),
      contentScriptWhen: 'start',
      onMessage: this.handleMessage.bind(this)
    });
  },
  stop: function iwm_stop() {
    if (!this.worker)
      return;

    this.worker.destroy();
    this.worker = null;
    this.workerReady = false;
  },
  handleMessage: function iwm_handleMessage(msg) {

    debug('[main/iwm/handleMessage]', msg.name);

    switch (msg.name) {
      case 'init':
        this.workerReady = true;
        break;

      case 'pendingsymbols':
      case 'candidates':
      case 'key':
      case 'string':
        IMEFrontend.handleWorkerMessage(msg.name, msg.data);
        break;
    }
  },
  sendMessage: function iwm_sendMessage(name, data) {
    if (!this.worker) {
      throw 'IMEWorkerManager.start() needs to be run first.';
    }

    if (!this.workerReady) {
      throw 'IMEWorker have not ready yet.';
    }

    this.worker.postMessage({name: name, data: data, to: 'page'});
  }
};

// Start the walker when Firefox starts.
// XXX: this would slow down Firefox start-up and memory usage.
IMEWorkerManager.start();
<commit_msg>Update punctuation table<commit_after>'use strict';

var selfModule = require('sdk/self');
var windowUtils = require('window/utils');

var panelModule = require('sdk/panel');
var pageWorkerModule = require('sdk/page-worker');
var hotkeysModule = require('sdk/hotkeys');
var widgetModule = require('sdk/widget');

var DEBUG = false;
var debug = function debug() {
  if (!DEBUG)
    return;

  console.log.apply(console, arguments);
};

var IMEFrontend = {
  active: false,
  inputActive: false,

  focusedElement: null,

  candidateSelectionMode: false,

  pendingsymbols: '',
  candidates: [],
  candidatePage: 0,

  // XXX: make this configable
  hotkeyCombo: 'accel-alt-1',

  init: function if_init() {
    debug('[if/init]');
    // Show an widget
    this.widget = widgetModule.Widget({
      id: 'jszhuyin-indicator',
      label: 'JSZhuyin IME',
      content: 'A'
    });

    // Register a hot key for toggling
    hotkeysModule.Hotkey({
      combo: this.hotkeyCombo,
      onPress: this.toggle.bind(this)
    });
  },

  toggle: function if_toggle() {
    debug('[if/toggle]');
    if (!this.active) {
      this.start();
    } else {
      this.stop();
    }
  },

  start: function if_start() {
    if (this.active)
      return;

    debug('[if/start]');
    this.active = true;

    this.widget.content = 'ㄅ';

    KeyMonitor.start();
    IMEWorkerManager.start();
  },

  stop: function if_stop() {
    if (!this.active)
      return;

    debug('[if/stop]');
    this.active = false;

    this.widget.content = 'A';
    this.stopInput();
    KeyMonitor.stop();
  },

  updateInput: function if_updateInput() {
    // Don't update focusedElement if panel is present
    if (IMEDisplayManager.panel.isShowing)
      return;

    debug('[if/updateInput]');
    var focusedElement = windowUtils.getFocusedElement();
    if (!IMEOutput.canHandle(focusedElement)) {
      if (this.inputActive) {
        this.stopInput();
      }

      return;
    }

    this.startInput(focusedElement);
  },

  startInput: function if_startInput(focusedElement) {
    debug('[if/startInput]');
    // XXX: We kept the reference of focused element even after
    // the focus is taken away. The reference will not get deleted/updated
    // until next keypress (by updateInput() call in handleKeyEvent().)
    this.focusedElement = focusedElement;
    this.inputActive = true;

    IMEOutput.setFocusedElement(focusedElement);
  },

  stopInput: function if_stopInput() {
    debug('[if/stopInput]');

    this.focusedElement = null;
    this.inputActive = false;

    this.candidateSelectionMode = false;

    this.pendingsymbols = '';
    this.candidates = [];
    this.candidatePage = 0;
    this.toggleIMEDisplay();

    IMEOutput.setFocusedElement(null);
    IMEWorkerManager.sendMessage('empty');
    // IMEWorkerManager.stop();
  },

  setCandidateSelectionMode: function if_setCandidateSelectionMode(mode) {
    IMEDisplayManager.sendMessage('candidateselectionmode', mode);
    this.candidateSelectionMode = mode;
  },

  toggleIMEDisplay: function if_toggleIMEDisplay() {
    if (this.pendingsymbols || this.candidates.length) {
      IMEDisplayManager.start(this.focusedElement);
    } else {
      IMEDisplayManager.stop();
      IMEOutput.regainFocus();
    }
  },

  handleWorkerMessage: function if_handleWorkerResult(name, data) {
    switch (name) {
      case 'pendingsymbols':
        if (!this.active)
          return; // Panel not displayed, nothing to show.

        this.pendingsymbols = data;
        IMEDisplayManager.sendMessage(name, data);
        this.toggleIMEDisplay();
        break;

      case 'candidates':
        if (!this.active)
          return; // Panel not displayed, nothing to show.

        this.candidates = data;
        this.candidatePage = 0;
        IMEDisplayManager.sendMessage(name, data);
        this.toggleIMEDisplay();
        if (!this.candidates.length && this.candidateSelectionMode) {
          this.setCandidateSelectionMode(false);
        }
        break;

      case 'key':
        IMEOutput.appendKey(data);
        break;

      case 'string':
        IMEOutput.appendText(data);
        break;
    }
  },

  handleDisplayMessage: function if_handleDisplayMessage(name, data) {
    switch (name) {
      case 'select':
        IMEWorkerManager.sendMessage(name, data);
        break;
    }
  },

  handleKeyEvent: function if_handleKeyEvent(keyCode, charCode, symbolCode) {
    this.updateInput();
    if (!this.inputActive)
      return false;

    // escape key: stop input mode immediately
    if (keyCode === 27) {
      this.stopInput();
      return true;
    }

    if (this.candidateSelectionMode) {
      // A non-printable character will come with charCode = 0,
      // let's squeeze these two code into one variable to simplify the logic.
      var charKeyCode = charCode || keyCode;

      var page = this.candidatePage;

      switch (charKeyCode) {
        case 8: // Backspace key: leave selection mode
          this.setCandidateSelectionMode(false);

          // IME engine will need to handle this bug too
          if (this.pendingsymbols) {
            // Let engine handles backspace
            IMEWorkerManager.sendMessage('click', keyCode);
          } else {
            // Let default handing to run instead.
            // return false;
            // ... or, remove the selection panel altogether w/o deleting
            // text that is sent?
            this.stopInput();
          }
          break;

        case 13: // Enter key: select 1st selection of the page
          if (!this.candidates[page * 9])
            break;

          IMEWorkerManager.sendMessage('select', this.candidates[page * 9]);
          break;

        case 37: // Left arrow key: flip candidate selection page
          if (page === 0)
            break;

          page = this.candidatePage -= 1;
          IMEDisplayManager.sendMessage('candidateselectionpage', page);
          break;

        case 38: // Up arrow key: leaving candidate selection mode
          this.setCandidateSelectionMode(false);
          break;

        case 39: // Right arrow key: flip candidate selection page
          if ((page + 1) * 9 >= this.candidates.length)
            break;

          page = this.candidatePage += 1;
          IMEDisplayManager.sendMessage('candidateselectionpage', page);
          break;

        default:
          // Number keys: select selection
          if (charKeyCode >= 49 && charKeyCode <= 57) {
            var n = charCode - 49;
            if (!this.candidates[page * 9 + n])
              break;

            IMEWorkerManager.sendMessage('select',
                                         this.candidates[page * 9 + n]);
          }
          break;
      }

      // No key should be handled by others in this mode.
      return true;
    }

    // Entering candidate selection mode with down arrow
    if (keyCode === 40 && this.candidates.length) {
      this.setCandidateSelectionMode(true);
      return true;
    }

    // non-printable keys shouldn't be handled by IME engine
    // if nothing is displayed presently.
    if (!charCode && !this.candidates.length && !this.pendingsymbols)
      return false;

    var code = symbolCode || charCode;
    // IME engine is only responsive to backspace and enter keyCodes,
    if (keyCode === 8 || keyCode === 13) {
      code = keyCode;
    }

    // other non-printable keys (charCode = 0) is meaningless to IME engine.
    if (!code)
      return false;

    IMEWorkerManager.sendMessage('click', code);

    return true;
  }
};
IMEFrontend.init();

var KeyMonitor = {
  focusedWindow: null,

  start: function km_start() {
    this.focusedWindow = windowUtils.getMostRecentBrowserWindow();
    this.focusedWindow.addEventListener('keypress', this, true);
  },

  stop: function km_stop() {
    this.focusedWindow.removeEventListener('keypress', this, true);
    this.focusedWindow = null;
  },

  handleEvent: function km_handleEvent(evt) {
    if (evt.metaKey || evt.ctrlKey || evt.altKey)
      return;

    var symbolCode =
      KeyMapper.getSymbolCodeFromCode(evt.charCode, evt.shiftKey);

    debug('[main/km/handleEvent]', evt.keyCode, evt.charCode, symbolCode);

    var handled =
      IMEFrontend.handleKeyEvent(evt.keyCode, evt.charCode, symbolCode);
    if (handled) {
      evt.stopImmediatePropagation();
      evt.preventDefault();
    }
  }
};

var KeyMapper = {
  shiftMapping: {
    '<': '，', // XXX: Not available: ．《〈【〖〔
    '>': '。', // XXX: Not available: 》〉】〗〕
    '?': '？',
    '"': '、', // XXX: Not available: ＂
    ':': '：',
    '{': '『',
    '}': '』',
    '!': '！',
    '@': '＠',
    '#': '＃',
    '$': '＄',
    '%': '％',
    '^': '︿',
    '&': '＆',
    '*': '＊',
    '(': '（',
    ')': '）',
    '_': '—',
    '+': '＋',
    '~': '～',
    '|': '｜'
  },

  zhuyinMapping: {
    ',': 'ㄝ',
    '-': 'ㄦ',
    '.': 'ㄡ',
    '/': 'ㄥ',
    '0': 'ㄢ',
    '1': 'ㄅ',
    '2': 'ㄉ',
    '3': 'ˇ',
    '4': 'ˋ',
    '5': 'ㄓ',
    '6': 'ˊ',
    '7': '˙',
    '8': 'ㄚ',
    '9': 'ㄞ',
    ';': 'ㄤ',
    'a': 'ㄇ',
    'b': 'ㄖ',
    'c': 'ㄏ',
    'd': 'ㄎ',
    'e': 'ㄍ',
    'f': 'ㄑ',
    'g': 'ㄕ',
    'h': 'ㄘ',
    'i': 'ㄛ',
    'j': 'ㄨ',
    'k': 'ㄜ',
    'l': 'ㄠ',
    'm': 'ㄩ',
    'n': 'ㄙ',
    'o': 'ㄟ',
    'p': 'ㄣ',
    'q': 'ㄆ',
    'r': 'ㄐ',
    's': 'ㄋ',
    't': 'ㄔ',
    'u': 'ㄧ',
    'v': 'ㄒ',
    'w': 'ㄊ',
    'x': 'ㄌ',
    'y': 'ㄗ',
    'z': 'ㄈ',

    // in MS Photonic IME, '`' is the dead-key for all full-width punctuation.
    // such functionality is not available here yet.
    '`': '，', // XXX: Not available: ｀

    '=': '＝',
    '[': '「',
    ']': '」',
    '\\': '＼', // XXX: Not available: ／
    '\'': '＇'
  },

  getSymbolCodeFromCode: function km_getSymbolCodeFromCode(code, shiftKey) {
    var chr = String.fromCharCode(code);

    if (shiftKey) {
      if (this.shiftMapping[chr]) {
        return this.shiftMapping[chr].charCodeAt(0);
      } else {
        return chr.toLowerCase().charCodeAt(0);
      }
    }

    if (this.zhuyinMapping[chr]) {
      return this.zhuyinMapping[chr].charCodeAt(0);
    } else {
      return code;
    }
  }
};

/*
 * IMEOutput is poor man's solution for inserting characters into
 * the nsIDOMElement.
 * It does not handle cursor position nor file key/composition
 * events. To do this right we will have to re-implement/copy
 * half of mozKeyboard API in B2G instead. Not something worth doing, yet.
 */
var IMEOutput = {
  // XXX: allow more types.
  inputTagNames: ['html:input', 'INPUT'],

  focusedElement: null,

  setFocusedElement: function io_setFocusedElement(focusedElement) {
    this.focusedElement = focusedElement;
  },

  canHandle: function io_canHandle(element) {
    if (!element)
      return false;

    if (this.inputTagNames.indexOf(element.tagName) === -1)
      return false;

    return true;
  },

  regainFocus: function io_regainFocus() {
    if (!this.focusedElement)
      return;

    this.focusedElement.focus();
    var pos = this.focusedElement.value.length;
    this.focusedElement.setSelectionRange(pos, pos);
  },

  appendKey: function io_appendKey(code) {
    if (!this.focusedElement) {
      throw 'IMEOutput has no focused element.';
    }

    if (code === 8) {
      var val = this.focusedElement.value;
      return (this.focusedElement.value =
        (val.substr(0, val.length - 1)));
    }
    return this.appendText(String.fromCharCode(code));
  },

  appendText: function io_appendText(text) {
    if (!this.focusedElement) {
      throw 'IMEOutput has no focused element.';
    }

    return this.focusedElement.value += text;
  }
};

var IMEDisplayManager = {
  panel: null,

  panelReady: false,

  init: function idm_init() {
    this.panel = panelModule.Panel({
      width: 600,
      height: 74,
      contentURL: selfModule.data.url('panel.html'),
      contentScriptFile: selfModule.data.url('message_proxy.js'),
      contentScriptWhen: 'start',
      onMessage: this.handleMessage.bind(this)
    });
  },

  start: function idm_start(anchor) {
    if (this.panel.isShowing)
      return;

    this.panel.show(anchor);
  },

  stop: function idm_stop() {
    if (!this.panel.isShowing)
      return;

    this.sendMessage('reset');
    this.panel.hide();
  },

  handleMessage: function idm_handleMessage(msg) {
    debug('[main/idm/handleMessage]', msg.name);

    switch (msg.name) {
      case 'init':
        this.panelReady = true;
        break;

      case 'select':
        IMEFrontend.handleDisplayMessage(msg.name, msg.data);
        break;
    }
  },

  sendMessage: function idm_sendMessage(name, data) {
    if (!this.panel) {
      throw 'IMEDisplayManager.init() needs to be run first.';
    }

    if (!this.panelReady) {
      throw 'IMEDisplayManager have not ready yet.';
    }

    this.panel.postMessage({name: name, data: data, to: 'page'});
  }
};
// Init the panel when Firefox starts.
// XXX: this would slow down Firefox start-up and memory usage.
IMEDisplayManager.init();

var IMEWorkerManager = {
  worker: null,
  workerReady: false,
  start: function iwm_start() {
    if (this.worker) {
      if (this.workerReady) {
        this.sendMessage('empty');
      }
      return;
    }

    this.worker = pageWorkerModule.Page({
      contentURL: selfModule.data.url('worker.html'),
      contentScriptFile: selfModule.data.url('message_proxy.js'),
      contentScriptWhen: 'start',
      onMessage: this.handleMessage.bind(this)
    });
  },
  stop: function iwm_stop() {
    if (!this.worker)
      return;

    this.worker.destroy();
    this.worker = null;
    this.workerReady = false;
  },
  handleMessage: function iwm_handleMessage(msg) {

    debug('[main/iwm/handleMessage]', msg.name);

    switch (msg.name) {
      case 'init':
        this.workerReady = true;
        break;

      case 'pendingsymbols':
      case 'candidates':
      case 'key':
      case 'string':
        IMEFrontend.handleWorkerMessage(msg.name, msg.data);
        break;
    }
  },
  sendMessage: function iwm_sendMessage(name, data) {
    if (!this.worker) {
      throw 'IMEWorkerManager.start() needs to be run first.';
    }

    if (!this.workerReady) {
      throw 'IMEWorker have not ready yet.';
    }

    this.worker.postMessage({name: name, data: data, to: 'page'});
  }
};

// Start the walker when Firefox starts.
// XXX: this would slow down Firefox start-up and memory usage.
IMEWorkerManager.start();
<|endoftext|>