diff --git a/admin/cli-test.sh b/admin/cli-test.sh index 3a4217a8a9..2c15bd0e11 100755 --- a/admin/cli-test.sh +++ b/admin/cli-test.sh @@ -120,7 +120,25 @@ kill $METEOR_PID ps ax | grep -e "$MONGOMARK" | grep -v grep | awk '{print $1}' | xargs kill +echo "... settings" +cat > settings.json < settings.js < /dev/null # XXX more tests here! diff --git a/app/lib/mongo_runner.js b/app/lib/mongo_runner.js index df3f8f14ce..cb9359bbbb 100644 --- a/app/lib/mongo_runner.js +++ b/app/lib/mongo_runner.js @@ -131,17 +131,24 @@ var find_mongo_and_kill_it_dead = function (port, callback) { }; exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callback) { + var handle = {stop: function (callback) { callback(); } }; launch_callback = launch_callback || function () {}; on_exit_callback = on_exit_callback || function () {}; // If we are passed an external mongo, assume it is launched and never // exits. Matches code in run.js:exports.run. + + // Since it is externally managed, asking it to actually stop would be + // impolite, so our stoppable handle is a noop if (process.env.MONGO_URL) { launch_callback(); - return; + return handle; } - var mongod_path = path.join(files.get_dev_bundle(), 'mongodb', 'bin', 'mongod'); + var mongod_path = path.join(files.get_dev_bundle(), + 'mongodb', + 'bin', + 'mongod'); // store data in app_dir var data_path = path.join(app_dir, '.meteor', 'local', 'db'); @@ -161,15 +168,15 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac '--port', port, '--dbpath', data_path ]); + handle.stop = function (callback) { + var tries = 0; + var exited = false; + proc.removeListener('exit', on_exit_callback); + proc.kill('SIGINT'); + callback && callback(err); + }; - proc.on('exit', function (code, signal) { - on_exit_callback(code, signal); - }); - - // proc.stderr.setEncoding('utf8'); - // proc.stderr.on('data', function (data) { - // process.stdout.write(data); - // }); + proc.on('exit', on_exit_callback); proc.stdout.setEncoding('utf8'); proc.stdout.on('data', function (data) { @@ -178,6 +185,5 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac launch_callback(); }); }); - + return handle; }; - diff --git a/app/meteor/deploy.js b/app/meteor/deploy.js index 4110db4afd..4e7dc56c4b 100644 --- a/app/meteor/deploy.js +++ b/app/meteor/deploy.js @@ -33,10 +33,15 @@ if (process.env.EMACS == "t") { // interactively prompt for here. var meteor_rpc = function (rpc_name, method, site, query_params, callback) { - var url = "https://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site; + var url; + if (DEPLOY_HOSTNAME.indexOf("http://") === 0) + url = DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site; + else + url = "https://" + DEPLOY_HOSTNAME + '/' + rpc_name + '/' + site; - if (!_.isEmpty(query_params)) + if (!_.isEmpty(query_params)) { url += '?' + qs.stringify(query_params); + } var r = request({method: method, url: url}, function (error, response, body) { if (error || ((response.statusCode !== 200) @@ -51,26 +56,39 @@ var meteor_rpc = function (rpc_name, method, site, query_params, callback) { }; var deploy_app = function (url, app_dir, opt_debug, opt_tests, - opt_set_password) { + opt_set_password, settings) { var parsed_url = parse_url(url); // a bit contorted here to make sure we ask for the password before // launching the slow bundle process. with_password(parsed_url.hostname, function (password) { + var deployOptions = { + site: parsed_url.hostname, + appDir: app_dir, + debug: opt_debug, + tests: opt_tests, + password: password, + settings: settings + }; if (opt_set_password) get_new_password(function (set_password) { - bundle_and_deploy(parsed_url.hostname, app_dir, opt_debug, opt_tests, - password, set_password); + deployOptions.setPassword = set_password; + bundle_and_deploy(deployOptions); }); else - bundle_and_deploy(parsed_url.hostname, app_dir, opt_debug, opt_tests, - password); + bundle_and_deploy(deployOptions); }); }; -var bundle_and_deploy = function (site, app_dir, opt_debug, opt_tests, - password, set_password) { +var bundle_and_deploy = function (options) { + var site = options.site; + var app_dir = options.appDir; + var opt_debug = options.debug; + var opt_tests = options.tests; + var password = options.password; + var set_password = options.setPassword; + var settings = options.settings; var build_dir = path.join(app_dir, '.meteor', 'local', 'build_tar'); var bundle_path = path.join(build_dir, 'bundle'); var bundle_opts = { skip_dev_bundle: true, no_minify: !!opt_debug, @@ -90,14 +108,17 @@ var bundle_and_deploy = function (site, app_dir, opt_debug, opt_tests, process.stdout.write('uploading ... '); - var opts = {}; - if (password) opts.password = password; - if (set_password) opts.set_password = set_password; + var rpcOptions = {}; + if (password) rpcOptions.password = password; + if (set_password) rpcOptions.set_password = set_password; + + // When it hits the wire, all these opts will be URL-encoded. + if (settings !== undefined) rpcOptions.settings = settings; var tar = child_process.spawn( 'tar', ['czf', '-', 'bundle'], {cwd: build_dir}); - var rpc = meteor_rpc('deploy', 'POST', site, opts, function (err, body) { + var rpc = meteor_rpc('deploy', 'POST', site, rpcOptions, function (err, body) { if (err) { var errorMessage = (body || ("Connection error (" + err.message + ")")); process.stderr.write("\nError deploying application: " + errorMessage + "\n"); diff --git a/app/meteor/meteor.js b/app/meteor/meteor.js index 9190bf3e93..1ce6074292 100644 --- a/app/meteor/meteor.js +++ b/app/meteor/meteor.js @@ -83,6 +83,26 @@ var findCommand = function (name) { process.exit(1); }; +var getSettings = function (filename) { + var str; + try { + str = fs.readFileSync(filename, "utf8"); + } catch (e) { + throw new Error("Could not find settings file " + filename); + } + if (str.length > 0x10000) { + throw new Error("Settings file must be less than 64 KB long"); + } + // Ensure that the string is parseable in JSON, but there's + // no reason to use the object value of it yet. + if (str.match(/\S/)) { + JSON.parse(str); + return str; + } else { + return ""; + } +}; + // XXX when the pass unexpected argument or unrecognized flags, print // an error and fail out @@ -101,6 +121,8 @@ Commands.push({ .describe('debug', 'Run in debug mode for node-inspector') .boolean('debug-brk') .describe('debug-brk', 'Run in debug mode and break on first line') + .describe('settings', 'Set optional data for Meteor.settings on the server') + .boolean('once') .usage( "Usage: meteor run [options]\n" + "\n" + @@ -113,22 +135,26 @@ Commands.push({ "are automatically detected and applied to the running application.\n" + "\n" + "The application's database persists between runs. It's stored under\n" + -"the .meteor directory in the root of the project.\n" -); +"the .meteor directory in the root of the project.\n"); var new_argv = opt.argv; + var settings = ""; if (argv.help) { process.stdout.write(opt.help()); process.exit(1); } + if (new_argv.settings) { + settings = getSettings(new_argv.settings); + } var app_dir = path.resolve(require_project("run", true)); // app or package - var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true}; + + var bundle_opts = { no_minify: !new_argv.production, symlink_dev_bundle: true }; var debugStatus = runner.DebugStatus.OFF; if (new_argv['debug']) debugStatus = runner.DebugStatus.DEBUG; if (new_argv['debug-brk']) debugStatus = runner.DebugStatus.BREAK; - runner.run(app_dir, bundle_opts, new_argv.port, debugStatus); + runner.run(app_dir, bundle_opts, new_argv.port, new_argv.once, settings, debugStatus); } }); @@ -468,7 +494,7 @@ Commands.push({ process.exit(1); } - new_argv = opt.argv; + var new_argv = opt.argv; if (new_argv._.length === 1) { // localhost mode @@ -485,7 +511,7 @@ Commands.push({ var mongo_url = "mongodb://127.0.0.1:" + mongod_port + "/meteor"; if (new_argv.url) - console.log(mongo_url) + console.log(mongo_url); else deploy.run_mongo_shell(mongo_url); }); @@ -518,11 +544,12 @@ Commands.push({ .boolean('debug') .describe('debug', 'deploy in debug mode (don\'t minify, etc)') .boolean('tests') + .describe('settings', 'set optional data for Meteor.settings on the server') // .describe('tests', 'deploy the tests instead of the actual application') .usage( // XXX document --tests in the future, once we publicly // support tests -"Usage: meteor deploy [--password] [--delete] [--debug]\n" + +"Usage: meteor deploy [--password] [--settings settings.json] [--debug] [--delete]\n" + "\n" + "Deploys the project in your current directory to Meteor's servers.\n" + "\n" + @@ -532,6 +559,12 @@ Commands.push({ "'myapp.mydomain.com', then you'll also need to configure your domain's\n" + "DNS records. See the Meteor docs for details.\n" + "\n" + +"The --settings flag can be used to pass deploy-specific information to\n" + +"the application. It will be available at runtime in Meteor.settings, but only\n" + +"on the server. The argument is the name of a file containing the JSON data\n" + +"to use. The settings will persist across deployments until you again specify\n" + +"a settings file. To unset Meteor.settings, pass an empty settings file.\n" + +"\n" + "The --delete flag permanently removes a deployed application, including\n" + "all of its stored data.\n" + "\n" + @@ -550,10 +583,13 @@ Commands.push({ if (new_argv.delete) { deploy.delete_app(new_argv._[1]); } else { + var settings = undefined; + if (new_argv.settings) + settings = getSettings(new_argv.settings); // accept packages iff we're deploying tests var project_dir = path.resolve(require_project("bundle", new_argv.tests)); deploy.deploy_app(new_argv._[1], project_dir, new_argv.debug, - new_argv.tests, new_argv.password); + new_argv.tests, new_argv.password, settings); } } }); diff --git a/app/meteor/run.js b/app/meteor/run.js index 3f41659fa6..1a16ae4722 100644 --- a/app/meteor/run.js +++ b/app/meteor/run.js @@ -14,6 +14,7 @@ var mongo_runner = require(path.join(__dirname, '..', 'lib', 'mongo_runner.js')) var _ = require(path.join(__dirname, '..', 'lib', 'third', 'underscore.js')); ////////// Globals ////////// +//XXX: Refactor to not have globals anymore? // list of log objects from the child process. var server_log = []; @@ -23,18 +24,42 @@ var Status = { crashing: false, // does server crash whenever we start it? listening: false, // do we expect the server to be listening now. counter: 0, // how many crashes in rapid succession + code: 0, // exit code last returned + shouldRestart: true, // true if we should be restarting the server + shuttingDown: false, // true if we're on the way to shutting down the server + exitNow: function () { + var self = this; + log_to_clients({'exit': "Your application is exiting."}); + self.shuttingDown = true; + + self.mongoHandle && self.mongoHandle.stop(function (err) { + if (err) + process.stdout.write(err.reason + "\n"); + process.exit(self.code); + }); + }, reset: function () { this.crashing = false; this.counter = 0; }, hard_crashed: function () { + var self = this; + if (!self.shouldRestart) { + self.exitNow(); + return; + } log_to_clients({'exit': "Your application is crashing. Waiting for file change."}); this.crashing = true; }, soft_crashed: function () { + var self = this; + if (!self.shouldRestart) { + self.exitNow(); + return; + } if (this.counter === 0) setTimeout(function () { this.counter = 0; @@ -48,6 +73,9 @@ var Status = { } }; + + + // List of queued requests. Each item in the list is a function to run // when the inner app is ready to receive connections. var request_queue = []; @@ -167,16 +195,38 @@ var log_to_clients = function (msg) { }; ////////// Launch server process ////////// +// Takes options: +// bundlePath +// outerPort +// innerPort +// mongoURL +// onExit +// [onListen] +// [debugStatus] +// +// [runOnce]: boolean; default false; if true doesn't ever try to restart, and +// forwards server exit code. +// [settings] -var start_server = function (bundle_path, outer_port, inner_port, mongo_url, - on_exit_callback, on_listen_callback, dbg) { +var start_server = function (options) { // environment + options = _.extend({runOnce: false, + debugStatus: exports.DebugStatus.OFF + }, + options); + if (options.runOnce) { + Status.shouldRestart = false; + } + var env = {}; for (var k in process.env) env[k] = process.env[k]; - env.PORT = inner_port; - env.MONGO_URL = mongo_url; - env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + outer_port); + + env.PORT = options.innerPort; + env.MONGO_URL = options.mongoURL; + env.ROOT_URL = env.ROOT_URL || ('http://localhost:' + options.outerPort); + + var dbg = options.debugStatus; var nodeOptions = []; if (dbg === exports.DebugStatus.DEBUG) nodeOptions.push('--debug'); @@ -184,9 +234,13 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url, console.log('Debug will break on the first line'); nodeOptions.push('--debug-brk'); } - //spawn inner server, with debug enabled if requested + + if (options.settings) + env.METEOR_SETTINGS = options.settings; + + var proc = spawn(process.execPath, - nodeOptions.concat([path.join(bundle_path, 'main.js'), '--keepalive']), + nodeOptions.concat([path.join(options.bundlePath, 'main.js'), '--keepalive']), {env: env}); // XXX deal with test server logging differently?! @@ -199,7 +253,7 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url, // string must match server.js data = data.replace(/^LISTENING\s*(?:\n|$)/m, ''); if (data.length != originalLength) - on_listen_callback && on_listen_callback(); + options.onListen && options.onListen(); if (data) log_to_clients({stdout: data}); }); @@ -216,7 +270,7 @@ var start_server = function (bundle_path, outer_port, inner_port, mongo_url, log_to_clients({'exit': 'Exited with code: ' + code}); } - on_exit_callback(); + options.onExit(code); }); // this happens sometimes when we write a keepalive after the app is @@ -317,7 +371,7 @@ _.extend(DependencyWatcher.prototype, { return false; try { - var stats = fs.lstatSync(filepath) + var stats = fs.lstatSync(filepath); } catch (e) { // doesn't exist -- leave stats undefined } @@ -462,9 +516,7 @@ exports.DebugStatus = { // This function never returns and will call process.exit() if it // can't continue. If you change this, remember to call // watcher.destroy() as appropriate. -exports.run = function (app_dir, bundle_opts, port, dbg) { - debug = bundle_opts.debug; - debug_brk = bundle_opts.debug_brk; +exports.run = function (app_dir, bundle_opts, port, once, settings, dbg) { var outer_port = port || 3000; var inner_port = outer_port + 1; var mongo_port = outer_port + 2; @@ -477,6 +529,7 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { var test_mongo_url = "mongodb://127.0.0.1:" + mongo_port + "/meteor_test"; var test_bundle_opts; + if (files.is_app_dir(app_dir)) { // If we're an app, make separate test_bundle_opts to trigger a // separate runner. @@ -498,6 +551,8 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { var watcher; var start_watching = function () { + if (!Status.shouldRestart) + return; if (deps_info) { if (watcher) watcher.destroy(); @@ -561,22 +616,30 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { start_watching(); Status.running = true; - server_handle = start_server( - bundle_path, outer_port, inner_port, mongo_url, - function () { + server_handle = start_server({ + bundlePath: bundle_path, + outerPort: outer_port, + innerPort: inner_port, + mongoURL: mongo_url, + onExit: function (code) { // on server exit Status.running = false; Status.listening = false; + Status.code = code; Status.soft_crashed(); if (!Status.crashing) restart_server(); - }, function () { + }, + onListen: function () { // on listen Status.listening = true; _.each(request_queue, function (f) { f(); }); request_queue = []; }, - dbg); + debugStatus: dbg, + runOnce: once, + settings: settings + }); // launch test bundle and server if needed. @@ -590,12 +653,16 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { }); files.rm_recursive(test_bundle_path); } else { - test_server_handle = start_server( - test_bundle_path, test_port, test_mongo_url, function () { + test_server_handle = start_server({ + bundlePath: test_bundle_path, + outerPort: test_port, + innerPort: test_port, + mongoURL: test_mongo_url, + onExit: function (code) { // No restarting or crash loop prevention on the test server // for now. We'll see how annoying it is. log_to_clients({'system': "Test server crashed."}); - }); + }}); } }; }; @@ -606,7 +673,7 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { var mongo_startup_print_timer; var process_startup_printer; var launch = function () { - mongo_runner.launch_mongo( + Status.mongoHandle = mongo_runner.launch_mongo( app_dir, mongo_port, function () { // On Mongo startup complete @@ -623,6 +690,9 @@ exports.run = function (app_dir, bundle_opts, port, dbg) { restart_server(); }, function (code, signal) { // On Mongo dead + if (Status.shuttingDown) { + return; + } console.log("Unexpected mongo exit code " + code + ". Restarting."); // if mongo dies 3 times with less than 5 seconds between each, diff --git a/docs/client/commandline.html b/docs/client/commandline.html index 7ed2643e89..979e9197f8 100644 --- a/docs/client/commandline.html +++ b/docs/client/commandline.html @@ -90,6 +90,18 @@ domain like myapp.com, you'll need a DNS A record, matching the IP address of origin.meteor.com. {{/warning}} + + +To add deploy-specific information to your application, use the `--settings` +option. This will set the variable `Meteor.settings` in your application, but +only on the server. The `--settings` option takes an argument: the name of a +file containing JSON data to put into `Meteor.settings`. + +The settings you pass will persist on your deployed application across +invocations of `meteor deploy` until you again pass the `--settings` option with +different contents in your settings file. To unset `Meteor.settings`, pass an +empty settings file. +

meteor logs site

Retrieves the server logs for the named Meteor application. diff --git a/packages/meteor/server_environment.js b/packages/meteor/server_environment.js index 57516f3e90..ea1b39b227 100644 --- a/packages/meteor/server_environment.js +++ b/packages/meteor/server_environment.js @@ -2,3 +2,11 @@ Meteor = { isClient: false, isServer: true }; + +try { + if (process.env.METEOR_SETTINGS) + Meteor.settings = JSON.parse(process.env.METEOR_SETTINGS); +} catch (e) { + throw new Error("Settings are not valid JSON"); +} +