Factor webapp-specific stuff (and keepalive) out of boot.js into webapp package.

Replace type: "bare" with type: "server" in attributes.json.  This now just
means "only make one server process" rather than controlling the boot script.

Programs that are type: "traditional" that want to be a webapp now need to
explicitly depend on the webapp package.
This commit is contained in:
David Glasser
2013-05-08 17:31:52 -07:00
parent 31bd79c54b
commit 2d827e74fc
21 changed files with 502 additions and 527 deletions

View File

@@ -4,6 +4,7 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('webapp', 'server');
api.use('accounts-base', ['client', 'server']);
api.use('routepolicy', 'server');

View File

@@ -3,6 +3,7 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('webapp', 'server');
api.use('reload', 'client');
api.use('routepolicy', 'server');
api.use('startup', 'client');

View File

@@ -3,6 +3,7 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('webapp', 'server');
api.use('underscore', 'server');
// make sure we come after livedata, so we load after the sockjs
// server has been instantiated.

View File

@@ -10,6 +10,7 @@ Package.on_use(function (api) {
});
Package.on_test(function (api) {
api.use('webapp', 'server');
api.use('underscore');
api.use('random');
api.use('jquery', 'client');

View File

@@ -10,6 +10,11 @@ Package.on_use(function (api) {
api.use(['check', 'random', 'ejson', 'json', 'underscore', 'deps', 'logging'],
['client', 'server']);
// XXX we do NOT require webapp here, because it's OK to use this package on a
// server architecture without making a server (in order to do
// server-to-server DDP as a client). So if you want to provide a DDP server,
// you need to use webapp before you use livedata.
// Transport
api.use('reload', 'client');
api.use('routepolicy', 'server');

View File

@@ -22,18 +22,14 @@ _.extend(Meteor._RemoteCollectionDriver.prototype, {
// Create the singleton _RemoteCollectionDriver only on demand, so we
// only require Mongo configuration if it's actually used (eg, not if
// you're only trying to receive data from a remote DDP server.)
var theDriver = null;
Meteor._getRemoteCollectionDriver = function () {
if (! theDriver) {
// XXX kind of hacky
var mongoUrl = Meteor._get(__meteor_bootstrap__.deployConfig,
'packages', 'mongo-livedata', 'url');
// XXX bad error since it could also be set directly in METEOR_DEPLOY_CONFIG
if (! mongoUrl)
throw new Error("MONGO_URL must be set in environment");
Meteor._getRemoteCollectionDriver = _.once(function () {
// XXX kind of hacky
var mongoUrl = (typeof __meteor_bootstrap__ !== 'undefined' &&
Meteor._get(__meteor_bootstrap__.deployConfig,
'packages', 'mongo-livedata', 'url'));
// XXX bad error since it could also be set directly in METEOR_DEPLOY_CONFIG
if (! mongoUrl)
throw new Error("MONGO_URL must be set in environment");
theDriver = new Meteor._RemoteCollectionDriver(mongoUrl);
}
return theDriver;
};
return new Meteor._RemoteCollectionDriver(mongoUrl);
});

View File

@@ -4,6 +4,7 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('webapp', 'server');
api.use('underscore', 'server');
api.add_files('routepolicy.js', 'server');
});

View File

@@ -13,6 +13,7 @@ Package.on_use(function (api) {
});
Package.on_test(function (api) {
api.use('webapp', 'server');
api.use(['tinytest', 'underscore', 'liverange', 'deps', 'domutils',
'minimongo', 'random']);
api.use(['spark', 'test-helpers'], 'client');

View File

@@ -3,6 +3,7 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('webapp', 'server');
api.use(['templating'], 'client');
api.use(['underscore'], ['client', 'server']);

1
packages/webapp/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.build*

1
packages/webapp/.npm/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

41
packages/webapp/.npm/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,41 @@
{
"dependencies": {
"connect": {
"version": "1.9.2",
"from": "connect@1.9.2",
"resolved": "https://registry.npmjs.org/connect/-/connect-1.9.2.tgz",
"dependencies": {
"qs": {
"version": "0.6.4",
"from": "qs@>= 0.4.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-0.6.4.tgz"
},
"mime": {
"version": "1.2.9",
"from": "mime@>= 0.0.1"
},
"formidable": {
"version": "1.0.14",
"from": "formidable@1.0.x",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
}
}
},
"gzippo": {
"version": "0.1.7",
"from": "https://github.com/meteor/gzippo/tarball/1e4b955439abc643879ae264b28a761521818f3b",
"resolved": "https://github.com/meteor/gzippo/tarball/1e4b955439abc643879ae264b28a761521818f3b",
"dependencies": {
"mime": {
"version": "1.2.9",
"from": "mime@>= 1.2"
}
}
},
"useragent": {
"version": "2.0.1",
"from": "useragent@2.0.1",
"resolved": "https://registry.npmjs.org/useragent/-/useragent-2.0.1.tgz"
}
}
}

View File

@@ -0,0 +1,15 @@
Package.describe({
summary: "Serves a Meteor app over HTTP",
internal: true
});
Npm.depends({connect: "1.9.2",
// allow clientMaxAge to be set to 0:
// https://github.com/tomgco/gzippo/pull/49
gzippo: "https://github.com/meteor/gzippo/tarball/1e4b955439abc643879ae264b28a761521818f3b",
useragent: "2.0.1"});
Package.on_use(function (api) {
api.use(['underscore'], 'server');
api.add_files('webapp_server.js', 'server');
});

View File

@@ -0,0 +1,332 @@
////////// Requires //////////
var fs = Npm.require("fs");
var os = Npm.require("os");
var path = Npm.require("path");
var url = Npm.require("url");
var connect = Npm.require('connect');
var gzippo = Npm.require('gzippo');
var optimist = Npm.require('optimist');
var useragent = Npm.require('useragent');
// Keepalives so that when the outer server dies unceremoniously and
// doesn't kill us, we quit ourselves. A little gross, but better than
// pidfiles.
// XXX This should really be part of the boot script, not the webapp package.
// Or we should just get rid of it, and rely on containerization.
var initKeepalive = function () {
var keepaliveCount = 0;
process.stdin.on('data', function (data) {
keepaliveCount = 0;
});
process.stdin.resume();
setInterval(function () {
keepaliveCount ++;
if (keepaliveCount >= 3) {
console.log("Failed to receive keepalive! Exiting.");
process.exit(1);
}
}, 3000);
};
// #BrowserIdentification
//
// We have multiple places that want to identify the browser: the
// unsupported browser page, the appcache package, and, eventually
// delivering browser polyfills only as needed.
//
// To avoid detecting the browser in multiple places ad-hoc, we create a
// Meteor "browser" object. It uses but does not expose the npm
// useragent module (we could choose a different mechanism to identify
// the browser in the future if we wanted to). The browser object
// contains
//
// * `name`: the name of the browser in camel case
// * `major`, `minor`, `patch`: integers describing the browser version
//
// Also here is an early version of a Meteor `request` object, intended
// to be a high-level description of the request without exposing
// details of connect's low-level `req`. Currently it contains:
//
// * `browser`: browser identification object described above
// * `url`: parsed url, including parsed query params
//
// As a temporary hack there is a `categorizeRequest` function on
// __meteor_bootstrap__ which converts a connect `req` to a Meteor
// `request`. This can go away once smart packages such as appcache are
// being passed a `request` object directly when they serve content.
//
// This allows `request` to be used uniformly: it is passed to the html
// attributes hook, and the appcache package can use it when deciding
// whether to generate a 404 for the manifest.
//
// Real routing / server side rendering will probably refactor this
// heavily.
// e.g. "Mobile Safari" => "mobileSafari"
var camelCase = function (name) {
var parts = name.split(' ');
parts[0] = parts[0].toLowerCase();
for (var i = 1; i < parts.length; ++i) {
parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1);
}
return parts.join('');
};
var identifyBrowser = function (req) {
var userAgent = useragent.lookup(req.headers['user-agent']);
return {
name: camelCase(userAgent.family),
major: +userAgent.major,
minor: +userAgent.minor,
patch: +userAgent.patch
};
};
var categorizeRequest = function (req) {
return {
browser: identifyBrowser(req),
url: url.parse(req.url, true)
};
};
var htmlAttributes = function (template, request) {
var attributes = '';
_.each(__meteor_bootstrap__.htmlAttributeHooks || [], function (hook) {
var attribute = hook(request);
if (attribute !== null && attribute !== undefined && attribute !== '')
attributes += ' ' + attribute;
});
return template.replace('##HTML_ATTRIBUTES##', attributes);
};
// Serve app HTML for this URL?
var appUrl = function (url) {
if (url === '/favicon.ico' || url === '/robots.txt')
return false;
// NOTE: app.manifest is not a web standard like favicon.ico and
// robots.txt. It is a file name we have chosen to use for HTML5
// appcache URLs. It is included here to prevent using an appcache
// then removing it from poisoning an app permanently. Eventually,
// once we have server side routing, this won't be needed as
// unknown URLs with return a 404 automatically.
if (url === '/app.manifest')
return false;
// Avoid serving app HTML for declared routes such as /sockjs/.
if (__meteor_bootstrap__._routePolicy &&
__meteor_bootstrap__._routePolicy.classify(url))
return false;
// we currently return app HTML on all URLs by default
return true;
};
var runWebAppServer = function () {
// read the control for the client we'll be serving up
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
__meteor_bootstrap__.configJson.client);
var clientDir = path.dirname(clientJsonPath);
var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));
if (clientJson.format !== "browser-program-pre1")
throw new Error("Unsupported format for client assets: " +
JSON.stringify(clientJson.format));
// XXX change all this config to something more reasonable
var deployConfig =
process.env.METEOR_DEPLOY_CONFIG
? JSON.parse(process.env.METEOR_DEPLOY_CONFIG) : {};
if (!deployConfig.packages)
deployConfig.packages = {};
if (!deployConfig.boot)
deployConfig.boot = {};
if (!deployConfig.boot.bind)
deployConfig.boot.bind = {};
// check environment for legacy env variables.
if (process.env.PORT && !_.has(deployConfig.boot.bind, 'localPort')) {
deployConfig.boot.bind.localPort = parseInt(process.env.PORT);
}
if (process.env.MONGO_URL) {
if (!deployConfig.packages['mongo-livedata'])
deployConfig.packages['mongo-livedata'] = {};
deployConfig.packages['mongo-livedata'].url = process.env.MONGO_URL;
}
// webserver
var app = connect.createServer();
var staticCacheablePath = path.join(clientDir, clientJson.staticCacheable);
if (staticCacheablePath)
// cacheable files are files that should never change. Typically
// named by their hash (eg meteor bundled js and css files).
// cache them ~forever (1yr)
//
// 'root' option is to work around an issue in connect/gzippo.
// See https://github.com/meteor/meteor/pull/852
app.use(gzippo.staticGzip(staticCacheablePath,
{clientMaxAge: 1000 * 60 * 60 * 24 * 365,
root: '/'}));
// cache non-cacheable file anyway. This isn't really correct, as
// users can change the files and changes won't propogate
// immediately. However, if we don't cache them, browsers will
// 'flicker' when rerendering images. Eventually we will probably want
// to rewrite URLs of static assets to include a query parameter to
// bust caches. That way we can both get good caching behavior and
// allow users to change assets without delay.
// https://github.com/meteor/meteor/issues/773
var staticPath = path.join(clientDir, clientJson.static);
if (staticPath)
app.use(gzippo.staticGzip(staticPath,
{clientMaxAge: 1000 * 60 * 60 * 24,
root: '/'}));
// start up app
_.extend(__meteor_bootstrap__, {
app: app,
// metadata about this bundle
// XXX this could use some refactoring to better distinguish
// server and client
bundle: {
manifest: clientJson.manifest,
root: clientDir
},
// function that takes a connect `req` object and returns a summary
// object with information about the request. See
// #BrowserIdentifcation
categorizeRequest: categorizeRequest,
// list of functions to be called to determine any attributes to be
// added to the '<html>' tag. Each function is passed a 'request'
// object (see #BrowserIdentifcation) and should return a string,
htmlAttributeHooks: [],
deployConfig: deployConfig
});
// Let the rest of the packages (and Meteor.startup hooks) insert connect
// middlewares and update __meteor_runtime_config__, then keep going to set up
// actually serving HTML.
// @export main
main = function (argv) {
argv = optimist(argv).boolean('keepalive').argv;
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
var boilerplateHtml =
fs.readFileSync(boilerplateHtmlPath, 'utf8').replace(
"// ##RUNTIME_CONFIG##",
"__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";");
app.use(function (req, res, next) {
if (! appUrl(req.url))
return next();
var request = categorizeRequest(req);
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
var requestSpecificHtml = htmlAttributes(boilerplateHtml, request);
res.write(requestSpecificHtml);
res.end();
return undefined;
});
// Return 404 by default, if no other handlers serve this URL.
app.use(function (req, res) {
res.writeHead(404);
res.end();
});
// only start listening after all the startup code has run.
var bind = deployConfig.boot.bind;
app.listen(bind.localPort || 0, function() {
if (argv.keepalive)
console.log("LISTENING"); // must match run.js
var port = app.address().port;
if (bind.viaProxy) {
bindToProxy(bind.viaProxy);
}
});
if (argv.keepalive)
initKeepalive();
return 'DAEMON';
};
};
var bindToProxy = function (proxyConfig) {
// XXX also support galaxy-based lookup
if (!proxyConfig.proxyEndpoint)
throw new Error("missing proxyEndpoint");
if (!proxyConfig.bindHost)
throw new Error("missing bindHost");
// XXX move these into deployConfig?
if (!process.env.GALAXY_JOB)
throw new Error("missing $GALAXY_JOB");
if (!process.env.LAST_START)
throw new Error("missing $LAST_START");
// XXX rename pid argument to bindTo.
var pid = {
job: process.env.GALAXY_JOB,
lastStarted: process.env.LAST_START
};
var myHost = os.hostname();
var ddpBindTo = proxyConfig.unprivilegedPorts ? {
ddpUrl: 'ddp://' + proxyConfig.bindHost + ':4433/',
insecurePort: 8080
} : {
ddpUrl: 'ddp://' + proxyConfig.bindHost + '/'
};
// This is run after packages are loaded (in main) so we can use
// Meteor.connect.
var proxy = Meteor.connect(proxyConfig.proxyEndpoint);
var route = process.env.ROUTE;
var host = route.split(":")[0];
var port = route.split(":")[1];
proxy.call('bindDdp', {
pid: pid,
bindTo: ddpBindTo,
proxyTo: {
host: host,
port: port,
pathPrefix: '/websocket'
}
});
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: proxyConfig.bindHost,
port: proxyConfig.unprivilegedPorts ? 8080 : 80
},
proxyTo: {
host: host,
port: port
}
});
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: proxyConfig.bindHost,
port: proxyConfig.unprivilegedPorts ? 4433: 443,
ssl: true
},
proxyTo: {
host: host,
port: port
}
});
};
runWebAppServer();

View File

@@ -95,12 +95,10 @@ which npm
# you update version numbers.
cd "$DIR/lib/node_modules"
npm install connect@1.9.2 # not 2.x yet. sockjs doesn't work w/ new connect
npm install optimist@0.3.5
npm install semver@1.1.0
npm install handlebars@1.0.7
npm install clean-css@0.8.3
npm install useragent@2.0.1
npm install request@2.12.0
npm install keypress@0.1.0
npm install http-proxy@0.8.5
@@ -110,10 +108,6 @@ npm install tar@0.1.14
npm install kexec@0.1.1
npm install shell-quote@0.0.1
# allow clientMaxAge to be set to 0:
# https://github.com/tomgco/gzippo/pull/49
npm install https://github.com/meteor/gzippo/tarball/1e4b955439
# uglify-js has a bug which drops 'undefined' in arrays:
# https://github.com/mishoo/UglifyJS2/pull/97
npm install https://github.com/meteor/UglifyJS2/tarball/aa5abd14d3

View File

@@ -1036,16 +1036,12 @@ _.extend(JsImageTarget.prototype, {
// options:
// - clientTarget: the ClientTarget to serve up over HTTP as our client
// - releaseStamp: the Meteor release name (for retrieval at runtime)
// - isBare: at startup, just load the JavaScript and call main()
// (rather than running a traditional script that initializes a HTTP
// server and database connection in a hardcoded way)
var ServerTarget = function (options) {
var self = this;
JsImageTarget.apply(this, arguments);
self.clientTarget = options.clientTarget;
self.releaseStamp = options.releaseStamp;
self.isBare = options.isBare;
if (! archinfo.matches(self.arch, "native"))
throw new Error("ServerTarget targeting something that isn't a server?");
@@ -1098,11 +1094,8 @@ _.extend(ServerTarget.prototype, {
var imageControlFile = self.toJsImage().write(builder);
// Server bootstrap
// XXX ideally, if isBare, should ensure that the app exports a
// main() function and complain if not
var bootScript = self.isBare ? 'boot-bare.js' : 'boot.js';
builder.write('boot.js',
{ file: path.join(__dirname, 'server', bootScript) });
{ file: path.join(__dirname, 'server', 'boot.js') });
// Script that fetches the dev_bundle and runs the server bootstrap
var archToPlatform = {
@@ -1405,30 +1398,16 @@ exports.bundle = function (appDir, outputPath, options) {
return client;
};
var makeTraditionalServerTarget = function (app, clientTarget) {
var server = new ServerTarget({
var makeServerTarget = function (app, clientTarget) {
var targetOptions = {
library: library,
arch: archinfo.host(),
clientTarget: clientTarget,
releaseStamp: options.releaseStamp
});
};
if (clientTarget)
targetOptions.clientTarget = clientTarget;
server.make({
packages: [app],
test: options.testPackages || [],
minify: false
});
return server;
};
var makeBareServerTarget = function (app) {
var server = new ServerTarget({
library: library,
arch: archinfo.host(),
releaseStamp: options.releaseStamp,
isBare: true
});
var server = new ServerTarget(targetOptions);
server.make({
packages: [app],
@@ -1452,7 +1431,7 @@ exports.bundle = function (appDir, outputPath, options) {
targets.client = client;
// Server
var server = makeTraditionalServerTarget(app, client);
var server = makeServerTarget(app, client);
targets.server = server;
}
@@ -1506,7 +1485,7 @@ exports.bundle = function (appDir, outputPath, options) {
// Add to list
programs.push({
type: attrsJson.type || "bare",
type: attrsJson.type || "server",
name: item,
path: itemPath,
client: attrsJson.client,
@@ -1532,8 +1511,8 @@ exports.bundle = function (appDir, outputPath, options) {
library.override(p.name, p.path);
var target;
switch (p.type) {
case "bare":
target = makeBareServerTarget(p.name);
case "server":
target = makeServerTarget(p.name);
break;
case "traditional":
var clientTarget;
@@ -1556,7 +1535,7 @@ exports.bundle = function (appDir, outputPath, options) {
// We don't check whether targets[p.client] is actually a
// ClientTarget. If you want to be clever, go ahead.
target = makeTraditionalServerTarget(p.name, clientTarget);
target = makeServerTarget(p.name, clientTarget);
break;
case "client":
// We pass null for appDir because we are a
@@ -1566,7 +1545,7 @@ exports.bundle = function (appDir, outputPath, options) {
break;
default:
buildmessage.error(
"type must be 'bare', 'traditional', or 'client'",
"type must be 'server', 'traditional', or 'client'",
{ file: p.attrsJsonRelPath });
// recover by ignoring target
return;

View File

@@ -1479,7 +1479,7 @@ _.extend(Package.prototype, {
var names = _.union(
// standard client packages for the classic meteor stack.
// XXX remove and make everyone explicitly declare all dependencies
['meteor', 'deps', 'session', 'livedata', 'mongo-livedata',
['meteor', 'webapp', 'deps', 'session', 'livedata', 'mongo-livedata',
'spark', 'templating', 'startup', 'past', 'check'],
project.get_packages(appDir));

View File

@@ -247,8 +247,8 @@ var startServer = function (options) {
if (! options.program) {
var nodeOptions = _.clone(options.nodeOptions);
nodeOptions.push(path.join(options.bundlePath, 'main.js'));
nodeOptions.push('--keepalive');
nodeOptions.push('program.json');
nodeOptions.push('--keepalive');
var child_process = require('child_process');
var proc = child_process.spawn(process.execPath, nodeOptions,

View File

@@ -1,78 +0,0 @@
// XXX this file is copied from boot.js. They should be unified one day.
var Fiber = require("fibers");
var fs = require("fs");
var path = require("path");
var _ = require('underscore');
// read our control files
var serverJson =
JSON.parse(fs.readFileSync(path.join(__dirname, process.argv[2]), 'utf8'));
var configJson =
JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8'));
// Set up environment
__meteor_bootstrap__ = { startup_hooks: [] };
__meteor_runtime_config__ = { meteorRelease: configJson.release };
Fiber(function () {
_.each(serverJson.load, function (fileInfo) {
var code = fs.readFileSync(path.join(__dirname, fileInfo.path));
var Npm = {
require: function (name) {
if (! fileInfo.node_modules) {
return require(name);
}
var nodeModuleDir =
path.join(__dirname, fileInfo.node_modules, name);
if (fs.existsSync(nodeModuleDir)) {
return require(nodeModuleDir);
}
try {
return require(name);
} catch (e) {
// Try to guess the package name so we can print a nice
// error message
var filePathParts = fileInfo.path.split(path.sep);
var packageName = filePathParts[2].replace(/\.js$/, '');
// XXX better message
throw new Error(
"Can't find npm module '" + name +
"'. Did you forget to call 'Npm.depends' in package.js " +
"within the '" + packageName + "' package?");
}
}
};
// \n is necessary in case final line is a //-comment
var wrapped = "(function(Npm){" + code + "\n})";
var func = require('vm').runInThisContext(wrapped, fileInfo.path, true);
func.call(global, Npm); // Coffeescript
});
// run the user startup hooks.
_.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); });
// find and run main()
// XXX hack. we should know the package that contains main.
var mains = [];
if ('main' in global)
mains.push(main);
_.each(Package, function (p) {
if ('main' in p)
mains.push(p.main);
});
if (! mains.length) {
process.stderr.write("Program has no main() function.\n");
process.exit(1);
}
if (mains.length > 1) {
process.stderr.write("Program has more than one main() function?\n");
process.exit(1);
}
process.exit(mains[0].call({}, process.argv.slice(3)));
}).run();

View File

@@ -1,17 +1,6 @@
////////// Requires //////////
var Fiber = require("fibers");
var fs = require("fs");
var os = require("os");
var path = require("path");
var url = require("url");
var connect = require('connect');
var gzippo = require('gzippo');
var argv = require('optimist').boolean('keepalive').argv;
var useragent = require('useragent');
var _ = require('underscore');
// This code is duplicated in tools/server/server.js.
@@ -22,398 +11,84 @@ if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
process.exit(1);
}
// Keepalives so that when the outer server dies unceremoniously and
// doesn't kill us, we quit ourselves. A little gross, but better than
// pidfiles.
var initKeepalive = function () {
var keepaliveCount = 0;
// read our control files
var serverJson =
JSON.parse(fs.readFileSync(path.join(__dirname, process.argv[2]), 'utf8'));
var configJson =
JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8'));
process.stdin.on('data', function (data) {
keepaliveCount = 0;
});
// Set up environment
__meteor_bootstrap__ = {
startup_hooks: [],
serverDir: __dirname,
configJson: configJson };
__meteor_runtime_config__ = { meteorRelease: configJson.release };
process.stdin.resume();
Fiber(function () {
_.each(serverJson.load, function (fileInfo) {
var code = fs.readFileSync(path.join(__dirname, fileInfo.path));
setInterval(function () {
keepaliveCount ++;
if (keepaliveCount >= 3) {
console.log("Failed to receive keepalive! Exiting.");
process.exit(1);
}
}, 3000);
};
// #BrowserIdentification
//
// We have multiple places that want to identify the browser: the
// unsupported browser page, the appcache package, and, eventually
// delivering browser polyfills only as needed.
//
// To avoid detecting the browser in multiple places ad-hoc, we create a
// Meteor "browser" object. It uses but does not expose the npm
// useragent module (we could choose a different mechanism to identify
// the browser in the future if we wanted to). The browser object
// contains
//
// * `name`: the name of the browser in camel case
// * `major`, `minor`, `patch`: integers describing the browser version
//
// Also here is an early version of a Meteor `request` object, intended
// to be a high-level description of the request without exposing
// details of connect's low-level `req`. Currently it contains:
//
// * `browser`: browser identification object described above
// * `url`: parsed url, including parsed query params
//
// As a temporary hack there is a `categorizeRequest` function on
// __meteor_bootstrap__ which converts a connect `req` to a Meteor
// `request`. This can go away once smart packages such as appcache are
// being passed a `request` object directly when they serve content.
//
// This allows `request` to be used uniformly: it is passed to the html
// attributes hook, and the appcache package can use it when deciding
// whether to generate a 404 for the manifest.
//
// Real routing / server side rendering will probably refactor this
// heavily.
// e.g. "Mobile Safari" => "mobileSafari"
var camelCase = function (name) {
var parts = name.split(' ');
parts[0] = parts[0].toLowerCase();
for (var i = 1; i < parts.length; ++i) {
parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1);
}
return parts.join('');
};
var identifyBrowser = function (req) {
var userAgent = useragent.lookup(req.headers['user-agent']);
return {
name: camelCase(userAgent.family),
major: +userAgent.major,
minor: +userAgent.minor,
patch: +userAgent.patch
};
};
var categorizeRequest = function (req) {
return {
browser: identifyBrowser(req),
url: url.parse(req.url, true)
};
};
var htmlAttributes = function (template, request) {
var attributes = '';
_.each(__meteor_bootstrap__.htmlAttributeHooks || [], function (hook) {
var attribute = hook(request);
if (attribute !== null && attribute !== undefined && attribute !== '')
attributes += ' ' + attribute;
});
return template.replace('##HTML_ATTRIBUTES##', attributes);
};
// Serve app HTML for this URL?
var appUrl = function (url) {
if (url === '/favicon.ico' || url === '/robots.txt')
return false;
// NOTE: app.manifest is not a web standard like favicon.ico and
// robots.txt. It is a file name we have chosen to use for HTML5
// appcache URLs. It is included here to prevent using an appcache
// then removing it from poisoning an app permanently. Eventually,
// once we have server side routing, this won't be needed as
// unknown URLs with return a 404 automatically.
if (url === '/app.manifest')
return false;
// Avoid serving app HTML for declared routes such as /sockjs/.
if (__meteor_bootstrap__._routePolicy &&
__meteor_bootstrap__._routePolicy.classify(url))
return false;
// we currently return app HTML on all URLs by default
return true;
}
var run = function () {
var serverDir = __dirname;
// read our control files
var serverJson =
JSON.parse(fs.readFileSync(path.join(serverDir, argv._[0]), 'utf8'));
var configJson =
JSON.parse(fs.readFileSync(path.join(serverDir, 'config.json'), 'utf8'));
// read the control for the client we'll be serving up
var clientJsonPath = path.join(serverDir, configJson.client);
var clientDir = path.dirname(clientJsonPath);
var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));
if (clientJson.format !== "browser-program-pre1")
throw new Error("Unsupported format for client assets: " +
JSON.stringify(clientJson.format));
var deployConfig =
process.env.METEOR_DEPLOY_CONFIG
? JSON.parse(process.env.METEOR_DEPLOY_CONFIG) : {};
if (!deployConfig.packages)
deployConfig.packages = {};
if (!deployConfig.boot)
deployConfig.boot = {};
if (!deployConfig.boot.bind)
deployConfig.boot.bind = {};
// check environment for legacy env variables.
if (process.env.PORT && !_.has(deployConfig.boot.bind, 'localPort')) {
deployConfig.boot.bind.localPort = parseInt(process.env.PORT);
}
// XXX make outer wrapper fail if MONGO_URL not set
if (process.env.MONGO_URL) {
if (!deployConfig.packages['mongo-livedata'])
deployConfig.packages['mongo-livedata'] = {};
deployConfig.packages['mongo-livedata'].url = process.env.MONGO_URL;
}
// webserver
var app = connect.createServer();
var staticCacheablePath = path.join(clientDir, clientJson.staticCacheable);
if (staticCacheablePath)
// cacheable files are files that should never change. Typically
// named by their hash (eg meteor bundled js and css files).
// cache them ~forever (1yr)
//
// 'root' option is to work around an issue in connect/gzippo.
// See https://github.com/meteor/meteor/pull/852
app.use(gzippo.staticGzip(staticCacheablePath,
{clientMaxAge: 1000 * 60 * 60 * 24 * 365,
root: '/'}));
// cache non-cacheable file anyway. This isn't really correct, as
// users can change the files and changes won't propogate
// immediately. However, if we don't cache them, browsers will
// 'flicker' when rerendering images. Eventually we will probably want
// to rewrite URLs of static assets to include a query parameter to
// bust caches. That way we can both get good caching behavior and
// allow users to change assets without delay.
// https://github.com/meteor/meteor/issues/773
var staticPath = path.join(clientDir, clientJson.static);
if (staticPath)
app.use(gzippo.staticGzip(staticPath,
{clientMaxAge: 1000 * 60 * 60 * 24,
root: '/'}));
// start up app
__meteor_bootstrap__ = {
app: app,
// metadata about this bundle
// XXX this could use some refactoring to better distinguish
// server and client
bundle: {
manifest: clientJson.manifest,
root: clientDir
},
// function that takes a connect `req` object and returns a summary
// object with information about the request. See
// #BrowserIdentifcation
categorizeRequest: categorizeRequest,
// list of functions to be called to determine any attributes to be
// added to the '<html>' tag. Each function is passed a 'request'
// object (see #BrowserIdentifcation) and should return a string,
htmlAttributeHooks: [],
// functions to be called after all packages are loaded and we are
// ready to serve HTTP.
startup_hooks: [],
deployConfig: deployConfig
};
__meteor_runtime_config__ = {};
if (configJson.release) {
__meteor_runtime_config__.meteorRelease = configJson.release;
}
Fiber(function () {
// (put in a fiber to let Meteor.db operations happen during loading)
// load app code
_.each(serverJson.load, function (fileInfo) {
var code = fs.readFileSync(path.join(serverDir, fileInfo.path));
var Npm = {
// require an npm module used by your package, or one from the
// dev bundle if you are in an app or your package isn't using
// said npm module
require: function (name) {
if (! fileInfo.node_modules) {
// current no support for npm outside packages. load from
// dev bundle only
return require(name);
}
var nodeModuleDir =
path.join(__dirname, fileInfo.node_modules, name);
if (fs.existsSync(nodeModuleDir)) {
return require(nodeModuleDir);
}
try {
return require(name);
} catch (e) {
// Try to guess the package name so we can print a nice
// error message
var filePathParts = fileInfo.path.split(path.sep);
var packageName = filePathParts[2].replace(/\.js$/, '');
// XXX better message
throw new Error(
"Can't find npm module '" + name +
"'. Did you forget to call 'Npm.depends' in package.js " +
"within the '" + packageName + "' package?");
}
var Npm = {
require: function (name) {
if (! fileInfo.node_modules) {
return require(name);
}
};
// \n is necessary in case final line is a //-comment
var wrapped = "(function(Npm){" + code + "\n})";
// it's tempting to run the code in a new context so we can
// precisely control the enviroment the user code sees. but,
// this is harder than it looks. you get a situation where []
// created in one runInContext invocation fails 'instanceof
// Array' if tested in another (reusing the same context each
// time fixes it for {} and Object, but not [] and Array.) and
// we have no pressing need to do this, so punt.
//
// the final 'true' is an undocumented argument to
// runIn[Foo]Context that causes it to print out a descriptive
// error message on parse error. it's what require() uses to
// generate its errors.
var func = require('vm').runInThisContext(wrapped, fileInfo.path, true);
// Setting `this` to `global` allows you to do a top-level
// "this.foo = " to define global variables when using "use strict"
// (http://es5.github.io/#x15.3.4.4); this is the only way to do
// it in CoffeeScript.
func.call(global, Npm);
});
var nodeModuleDir =
path.join(__dirname, fileInfo.node_modules, name);
if (fs.existsSync(nodeModuleDir)) {
return require(nodeModuleDir);
}
try {
return require(name);
} catch (e) {
// Try to guess the package name so we can print a nice
// error message
var filePathParts = fileInfo.path.split(path.sep);
var packageName = filePathParts[2].replace(/\.js$/, '');
// Actually serve HTML. This happens after user code, so that
// packages can insert connect middlewares and update
// __meteor_runtime_config__
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
var boilerplateHtml =
fs.readFileSync(boilerplateHtmlPath, 'utf8').replace(
"// ##RUNTIME_CONFIG##",
"__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";");
app.use(function (req, res, next) {
if (! appUrl(req.url))
return next();
var request = categorizeRequest(req);
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
var requestSpecificHtml = htmlAttributes(boilerplateHtml, request);
res.write(requestSpecificHtml);
res.end();
});
// Return 404 by default, if no other handlers serve this URL.
app.use(function (req, res) {
res.writeHead(404);
res.end();
});
// run the user startup hooks.
_.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); });
// only start listening after all the startup code has run.
var bind = deployConfig.boot.bind;
app.listen(bind.localPort || 0, function() {
if (argv.keepalive)
console.log("LISTENING"); // must match run.js
var port = app.address().port;
if (bind.viaProxy) {
Fiber(function () {
bindToProxy(port, bind.viaProxy);
}).run();
// XXX better message
throw new Error(
"Can't find npm module '" + name +
"'. Did you forget to call 'Npm.depends' in package.js " +
"within the '" + packageName + "' package?");
}
}
});
};
// \n is necessary in case final line is a //-comment
var wrapped = "(function(Npm){" + code + "\n})";
}).run();
var func = require('vm').runInThisContext(wrapped, fileInfo.path, true);
func.call(global, Npm); // Coffeescript
});
if (argv.keepalive)
initKeepalive();
};
// run the user startup hooks.
_.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); });
var bindToProxy = function (localPort, proxyConfig) {
// XXX also support galaxy-based lookup
if (!proxyConfig.proxyEndpoint)
throw new Error("missing proxyEndpoint");
if (!proxyConfig.bindHost)
throw new Error("missing bindHost");
// XXX move these into deployConfig?
if (!process.env.GALAXY_JOB)
throw new Error("missing $GALAXY_JOB");
if (!process.env.LAST_START)
throw new Error("missing $LAST_START");
// XXX rename pid argument to bindTo.
var pid = {
job: process.env.GALAXY_JOB,
lastStarted: process.env.LAST_START
};
var myHost = os.hostname();
var ddpBindTo = proxyConfig.unprivilegedPorts ? {
ddpUrl: 'ddp://' + proxyConfig.bindHost + ':4433/',
insecurePort: 8080
} : {
ddpUrl: 'ddp://' + proxyConfig.bindHost + '/'
};
var proxy = Package.meteor.Meteor.connect(proxyConfig.proxyEndpoint);
var route = process.env.ROUTE;
var host = route.split(":")[0];
var port = route.split(":")[1];
proxy.call('bindDdp', {
pid: pid,
bindTo: ddpBindTo,
proxyTo: {
host: host,
port: port,
pathPrefix: '/websocket'
// find and run main()
// XXX hack. we should know the package that contains main.
var mains = [];
var globalMain;
if ('main' in global) {
mains.push(main);
globalMain = main;
}
_.each(Package, function (p, n) {
if ('main' in p && p.main !== globalMain) {
mains.push(p.main);
}
});
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: proxyConfig.bindHost,
port: proxyConfig.unprivilegedPorts ? 8080 : 80
},
proxyTo: {
host: host,
port: port
}
});
proxy.call('bindHttp', {
pid: pid,
bindTo: {
host: proxyConfig.bindHost,
port: proxyConfig.unprivilegedPorts ? 4433: 443,
ssl: true
},
proxyTo: {
host: host,
port: port
}
});
};
run();
if (! mains.length) {
process.stderr.write("Program has no main() function.\n");
process.exit(1);
}
if (mains.length > 1) {
process.stderr.write("Program has more than one main() function?\n");
process.exit(1);
}
var exitCode = mains[0].call({}, process.argv.slice(3));
// XXX hack, needs a better way to keep alive
if (exitCode !== 'DAEMON')
process.exit(exitCode);
}).run();