mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Includes the following changes to Console.js:
- Console.info, Console.warn, Console.debug and Console.error now automatically
line-wrap the output to 80 characters, or the width of the terminal screen (if
known). This is in line with our current style guide on how things should be wrapped!
- Sometimes, there are parts of text that we don't want to line-wrap. For example, if we are
telling the user to run 'meteor long-command --with --options' we don't want to
have a newline in the middle of that! Wrap those commands in Console.command, like
this:
Console.info("something and then run", Console.command(command), "and then");
This also makes them bold if chalk is on, as a nice bonus. So, if we ever turn
chalk back on, the bolding of commands will be more consistent.
- Sometimes, there is bulkier output that we don't want to format at all, including
line-wrapping: log snippets, stack traces, JSON output, etc. In that case, we can use
Console.rawInfo, Console.rawError, Console.rawWarn and Console.rawDebug. Don't use
Console.command inside the raw* functions! It won't be processed (at all).
- There are fancier things that we can do, other than just simply wrapping things.
We can indent:
" Start here and then when wrapping
continue over here".
We frequently do this for commands, for example. In the past, we did this manually --
but we can't do this for long messages that might get wrapped, and anyway, it is
good to codify this instead of counting spaces. Allows us to be better about consistency,
for example.
- We can also add a bulletPoint, which is a small notice in the beginning that looks like
this:
" => Start here and then when wrapping
continue below the bulletPoint".
Since it is a elss intuitive option, I have wrapped most of the time that we use a
bulletPoint into helper functions on the Console.js.
- Some common bulletpoints that we use are:
ASCII Checkboxes (Console.success)
ASCII X-s (Console.failWarn and Console.failInfo)
=> (Console.arrowError, Console.arrowWarn, Console.arrowInfo)
WARNING (Console.labelWarn)
The => are sometimes indented, so they take an optional indent argument, showing how
many spaces to indent by.
The wrapper interface would be less complicated, if there was a more unified conceit behind our
terminal messages. If there is one, it is not documented. My hope is that, in many cases,
moving these to Console will make it easier for someone with great product sense to
clean up our terminal messages. It will also make it easier to write such messages, since
it will be easier to follow an accepted standard.
In the codebase outside of Console:
- Went through and looked at our use of Console.error/info/etc, replacing with rawError/etc
whenever approporiate.
- Went through and modified most of 'stdout' and 'stderr' calls to use the new functions.
I made an exception for stuff that doesn't want a new line at the end, or otherwise does
weird things (ex: print user logs directly), on the basis that, at this juncture, it is
better to be safe than to be sorry.
- Long messages no longer need to break the code style guide by ignoring indentation rules.
Fixed that where approporiate.
- Fixed the tests! A number of our stock messages are actually longer than 80 chars.
- Personal favourite: The Android license agreement is now line wrapped! Much better experience.
- There is some more work to do on:
- longform help (currently comes with built-in linebreaks, would have to change the entire
mechanism for how that works)
- Buildmessage sometimes has headers that start with =>, but they are short. I didn't want to
pass wrapper options all the way to main.captureOrExit before merging the rest of this and
making sure that we like it. Since these messages are fairly short, I don't think that's
likely to be a serious problem.
I hope that this makes life easier for us in the future! No more counting chars, no more breaking
the style guide. Better experience for users with wider terminals (or even shorter terminals!).
Let's give this a try.
423 lines
14 KiB
JavaScript
423 lines
14 KiB
JavaScript
var Future = require('fibers/future');
|
|
var files = require('./files.js');
|
|
var config = require('./config.js');
|
|
var path = require('path');
|
|
var fs = require('fs');
|
|
var isopackets = require("./isopackets.js");
|
|
var fiberHelpers = require('./fiber-helpers.js');
|
|
var httpHelpers = require('./http-helpers.js');
|
|
var auth = require('./auth.js');
|
|
var release = require('./release.js');
|
|
var url = require('url');
|
|
var _ = require('underscore');
|
|
var buildmessage = require('./buildmessage.js');
|
|
var ServiceConnection = require('./service-connection.js');
|
|
var stats = require('./stats.js');
|
|
var Console = require('./console.js').Console;
|
|
|
|
// If 'error' is an exception that we know how to report in a
|
|
// user-friendly way, print an approprite message to stderr and return
|
|
// an appropriate exit status for a command. Else rethrow error.
|
|
//
|
|
// galaxyName should be the name of the galaxy that we're talking to.
|
|
// If messages is provided, it is a map from DDP error names to
|
|
// human-readable explanation to use.
|
|
var handleError = function (error, galaxyName, messages) {
|
|
messages = messages || {};
|
|
|
|
if (error.errorType === "Meteor.Error") {
|
|
var msg = messages[error.error];
|
|
if (msg)
|
|
Console.error(msg);
|
|
else if (error.message)
|
|
Console.error("Denied: " + error.message);
|
|
return 1;
|
|
} else if (error.errorType === "DDP.ConnectionError") {
|
|
// If we have an http/https URL for a galaxyName instead of a
|
|
// proper galaxyName (which is what the code in this file
|
|
// currently passes), strip off the scheme and trailing slash.
|
|
var m = galaxyName.match(/^https?:\/\/(.*[^\/])\/?$/);
|
|
if (m)
|
|
galaxyName = m[1];
|
|
|
|
Console.error(galaxyName + ": connection failed");
|
|
return 1;
|
|
} else {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Returns a ServiceConnection to a galaxy service that is authenticated
|
|
// from the credential cache.
|
|
//
|
|
// - galaxy: the name of the galaxy to connect to, as returned by
|
|
// discoverGalaxy (as described there, should probably be a galaxy
|
|
// name, but currently is a https or http URL)
|
|
// - service: the service to connect to within the Galaxy, such as
|
|
// 'ultraworld' or 'log-reader'.
|
|
var galaxyServiceConnection = function (galaxy, service) {
|
|
var endpointUrl = galaxy + "/" + service;
|
|
var parsedEndpoint = url.parse(endpointUrl);
|
|
var authToken = auth.getSessionToken(parsedEndpoint.hostname);
|
|
|
|
// XXX XXX higher up on the stack, we need to get the galaxy name
|
|
// from the hostname of endpointUrl, and run the login command for
|
|
// that galaxy.
|
|
if (! authToken)
|
|
throw new Error("not logged in to galaxy?");
|
|
|
|
return new ServiceConnection(endpointUrl, {
|
|
headers: {
|
|
cookie: "GALAXY_AUTH=" + authToken
|
|
}
|
|
});
|
|
};
|
|
|
|
// Determine if a particular site is hosted by Galaxy, and if so, by
|
|
// which Galaxy. 'app' should be a hostname, like 'myapp.meteor.com'
|
|
// or 'mysite.com'. Returns the base URL for the Galaxy
|
|
// (https://[galaxyname], or possibly http://[galaxyname] if running
|
|
// locally). The URL will not have a trailing slash. Returns null if
|
|
// the site is not hosted by Galaxy.
|
|
//
|
|
// The result is cached, so there is no penality for calling this
|
|
// function multiple times (during the same run of the
|
|
// tool). (Assuming you wait for the first call to complete before
|
|
// making the subsequent calls. The caching doesn't kick in until the
|
|
// first call returns.)
|
|
//
|
|
// XXX in the future, should probably return the name of the Galaxy,
|
|
// rather than a URL.
|
|
//
|
|
// XXX at many places in this file we call discoverGalaxy and don't
|
|
// check its return value. This is safe because we expect that
|
|
// command.js will have already called discoverGalaxy on the same app
|
|
// before we get here, and gotten a satisfactory value, which is now
|
|
// cached. But it's not great -- add better error handling.
|
|
var discoveryCache = {};
|
|
exports.discoverGalaxy = function (app) {
|
|
var cacheKey = app;
|
|
if (_.has(discoveryCache, cacheKey))
|
|
return discoveryCache[cacheKey];
|
|
|
|
app = app + ":" + config.getDiscoveryPort();
|
|
var discoveryUrl = "https://" + app + "/_GALAXY_";
|
|
var fut = new Future();
|
|
|
|
if (process.env.GALAXY)
|
|
return process.env.GALAXY;
|
|
|
|
// At some point we may want to send a version in the request so that galaxy
|
|
// can respond differently to different versions of meteor.
|
|
httpHelpers.request({
|
|
url: discoveryUrl,
|
|
json: true,
|
|
strictSSL: true,
|
|
// We don't want to be confused by, eg, a non-Galaxy-hosted site which
|
|
// redirects to a Galaxy-hosted site.
|
|
followRedirect: false
|
|
}, function (err, resp, body) {
|
|
if (! err &&
|
|
resp.statusCode === 200 &&
|
|
body &&
|
|
_.has(body, "galaxyDiscoveryVersion") &&
|
|
_.has(body, "galaxyUrl") &&
|
|
(body.galaxyDiscoveryVersion === "galaxy-discovery-pre0")) {
|
|
var result = body.galaxyUrl;
|
|
|
|
if (result.indexOf("https://") === -1)
|
|
result = "https://" + result;
|
|
|
|
if (result[result.length - 1] === "/")
|
|
result = result.substring(0, result.length - 1);
|
|
|
|
fut.return(result);
|
|
} else {
|
|
fut.return(null);
|
|
}
|
|
});
|
|
|
|
var result = fut.wait();
|
|
discoveryCache[cacheKey] = result;
|
|
return result;
|
|
};
|
|
|
|
exports.deleteApp = function (app) {
|
|
var galaxy = exports.discoverGalaxy(app);
|
|
var conn = galaxyServiceConnection(galaxy, "ultraworld");
|
|
|
|
try {
|
|
conn.call("destroyApp", app);
|
|
Console.info("Deleted.");
|
|
} catch (e) {
|
|
return handleError(e, galaxy);
|
|
} finally {
|
|
conn.close();
|
|
}
|
|
};
|
|
|
|
// Returns exit code for deploy command.
|
|
//
|
|
// options:
|
|
// - app
|
|
// - appDir
|
|
// - settingsFile
|
|
// - buildOptions
|
|
// - starball
|
|
// XXX refactor this to separate the "maybe bundle" part from "actually deploy"
|
|
// so we can be careful to not rely on any of the app dir context when
|
|
// in --star mode.
|
|
exports.deploy = function (options) {
|
|
var conn = null;
|
|
|
|
try {
|
|
var tmpdir = files.mkdtemp('deploy');
|
|
var buildDir = path.join(tmpdir, 'build');
|
|
var topLevelDirName = path.basename(options.appDir);
|
|
var bundlePath = path.join(buildDir, topLevelDirName);
|
|
var bundler = require('./bundler.js');
|
|
var starball;
|
|
|
|
var settings = null;
|
|
var messages = buildmessage.capture({
|
|
title: "preparing to deploy",
|
|
rootPath: process.cwd()
|
|
}, function () {
|
|
if (options.settingsFile)
|
|
settings = files.getSettings(options.settingsFile);
|
|
});
|
|
|
|
// Don't try to connect to galaxy before the bundle is
|
|
// done. Because bundling doesn't yield, this will cause the
|
|
// connection to time out. Eventually we'd like to have bundle
|
|
// yield, so that we can connect (and make sure auth works)
|
|
// concurrent with bundling.
|
|
|
|
if (! options.starball && ! messages.hasMessages()) {
|
|
Console.info('Deploying ' + options.app + '. Bundling...');
|
|
var bundleResult = bundler.bundle({
|
|
projectContext: options.projectContext,
|
|
outputPath: bundlePath,
|
|
buildOptions: options.buildOptions,
|
|
requireControlProgram: true
|
|
});
|
|
|
|
if (bundleResult.errors) {
|
|
messages = bundleResult.errors;
|
|
} else {
|
|
stats.recordPackages({
|
|
what: "sdk.deploy",
|
|
projectContext: options.projectContext,
|
|
site: options.app
|
|
});
|
|
|
|
// S3 (which is what's likely on the other end our upload)
|
|
// requires a content-length header for HTTP PUT uploads. That
|
|
// means that we have to actually tgz up the bundle before we
|
|
// can start the upload rather than streaming it. S3 has an
|
|
// alternate API for doing chunked uploads, but (a) it has a
|
|
// minimum chunk size of 5 MB, so it doesn't help us much
|
|
// (many/most stars will be smaller than that), and (b) it's
|
|
// nonstandard, so we'd have to bake in S3's specific
|
|
// scheme. Doesn't seem worthwhile for now, so just tar to a
|
|
// temporary directory. If stars get radically bigger then it
|
|
// might be worthwhile to tar to memory and spill to S3 every
|
|
// 5MB.
|
|
starball = path.join(tmpdir, topLevelDirName + ".tar.gz");
|
|
files.createTarball(bundlePath, starball);
|
|
}
|
|
} else {
|
|
starball = options.starball;
|
|
}
|
|
|
|
if (messages.hasMessages()) {
|
|
Console.info("\nErrors prevented deploying:");
|
|
Console.info(messages.formatMessages());
|
|
return 1;
|
|
}
|
|
|
|
Console.info('Uploading...');
|
|
|
|
var galaxy = exports.discoverGalaxy(options.app);
|
|
conn = galaxyServiceConnection(galaxy, "ultraworld");
|
|
|
|
var created = true;
|
|
var appConfig = {};
|
|
if (settings !== null)
|
|
appConfig.settings = settings;
|
|
|
|
if (options.admin)
|
|
appConfig.admin = true;
|
|
|
|
try {
|
|
conn.call('createApp', options.app, appConfig);
|
|
} catch (e) {
|
|
if (e.errorType === 'Meteor.Error' && e.error === 'already-exists') {
|
|
// Cool, it already exists. No problem. Just set the settings
|
|
// if they were passed. We explicitly check for undefined
|
|
// because we want to allow you to unset settings by passing
|
|
// an empty file.
|
|
if (appConfig.settings !== undefined) {
|
|
conn.call('updateAppConfiguration', options.app, appConfig);
|
|
}
|
|
created = false;
|
|
} else {
|
|
return handleError(e, galaxy);
|
|
}
|
|
}
|
|
|
|
// Get the upload information from Galaxy. It's a surprise if this
|
|
// fails (we already know the app exists).
|
|
try {
|
|
var info = conn.call('beginUploadStar', options.app,
|
|
bundleResult.starManifest);
|
|
} catch (e) {
|
|
return handleError(e, galaxy);
|
|
}
|
|
|
|
// Upload
|
|
// XXX copied from galaxy/tool/galaxy.js
|
|
var fileSize = fs.statSync(starball).size;
|
|
var fileStream = fs.createReadStream(starball);
|
|
var future = new Future;
|
|
var req = httpHelpers.request({
|
|
method: "PUT",
|
|
url: info.put,
|
|
headers: { 'content-length': fileSize,
|
|
'content-type': 'application/octet-stream' },
|
|
strictSSL: true
|
|
}, function (error, response, body) {
|
|
if (error || ((response.statusCode !== 200)
|
|
&& (response.statusCode !== 201))) {
|
|
if (error && error.message)
|
|
Console.error("Upload failed: " + error.message);
|
|
else
|
|
Console.error("Upload failed" +
|
|
(response.statusCode ?
|
|
" (" + response.statusCode + ")" : ""));
|
|
future['return'](false);
|
|
} else
|
|
future['return'](true);
|
|
});
|
|
|
|
fileStream.pipe(req);
|
|
var uploadSucceeded = future.wait();
|
|
if (! uploadSucceeded)
|
|
return 1;
|
|
|
|
try {
|
|
var result = conn.call('completeUploadStar', info.id);
|
|
} catch (e) {
|
|
return handleError(e, galaxy, {
|
|
'no-such-upload': 'Upload request expired. Try again.'
|
|
});
|
|
}
|
|
|
|
if (created)
|
|
Console.error(options.app + ": created app\n");
|
|
|
|
Console.error(options.app + ": " +
|
|
"pushed revision " + result.serial);
|
|
return 0;
|
|
} finally {
|
|
// Close the connection to Galaxy (otherwise Node will continue running).
|
|
conn && conn.close();
|
|
}
|
|
};
|
|
|
|
// options:
|
|
// - app
|
|
// - streaming (BOOL)
|
|
//
|
|
// The log messages are printed. Returns a command exit code, or if
|
|
// streaming is true and streaming was successfully started, returns
|
|
// null.
|
|
exports.logs = function (options) {
|
|
var galaxy = exports.discoverGalaxy(options.app);
|
|
var logReader = galaxyServiceConnection(galaxy, "log-reader");
|
|
|
|
try {
|
|
var lastLogId = null;
|
|
var Log = isopackets.load('logging').logging.Log;
|
|
|
|
// XXX we're cheating a bit here, relying on the server sending
|
|
// the log messages in order
|
|
var ok = logReader.connection.registerStore('logs', {
|
|
update: function (msg) {
|
|
// Ignore all messages but 'changed'
|
|
if (msg.msg !== 'changed')
|
|
return;
|
|
var obj = msg.fields.obj;
|
|
lastLogId = msg.fields.id;
|
|
obj = Log.parse(obj);
|
|
obj && console.log(Log.format(obj, {color: true}));
|
|
}
|
|
});
|
|
|
|
if (! ok)
|
|
throw new Error("Can't listen to messages on the logs collection");
|
|
|
|
var logsSubscription = null;
|
|
try {
|
|
logsSubscription =
|
|
logReader.subscribeAndWait("logsForApp", options.app,
|
|
{ streaming: options.streaming });
|
|
} catch (e) {
|
|
return handleError(e, galaxy, {
|
|
"no-such-app": "No such app: " + options.app
|
|
});
|
|
}
|
|
|
|
// In case of reconnect recover the state so user sees only new logs.
|
|
// Only set up the onReconnect handler after the subscribe and wait
|
|
// has returned; if we set it up before, then we'll end up with two
|
|
// subscriptions, because the onReconnect handler will run for the
|
|
// first time before the subscribeAndWait returns.
|
|
logReader.connection.onReconnect = function () {
|
|
logsSubscription && logsSubscription.stop();
|
|
var opts = { streaming: options.streaming };
|
|
if (lastLogId)
|
|
opts.resumeAfterId = lastLogId;
|
|
// Don't use subscribeAndWait here; it'll deadlock. We can't
|
|
// process the sub messages until `onReconnect` returns, and
|
|
// `onReconnect` won't return unless the sub messages have been
|
|
// processed. There's no reason we need to wait for the sub to be
|
|
// ready here anyway.
|
|
// XXX correctly handle errors on resubscribe
|
|
logsSubscription = logReader.connection.subscribe(
|
|
"logsForApp",
|
|
options.app,
|
|
opts
|
|
);
|
|
};
|
|
|
|
return options.streaming ? null : 0;
|
|
} finally {
|
|
// If not streaming, close the connection to log-reader so that
|
|
// Node can exit cleanly. If streaming, leave the connection open
|
|
// so that we continue to get logs.
|
|
if (! options.streaming) {
|
|
logReader.close();
|
|
}
|
|
}
|
|
};
|
|
|
|
// On failure, prints a message to stderr and returns null. Otherwise,
|
|
// returns a temporary authenticated Mongo URL allowing access to this
|
|
// site's database.
|
|
exports.temporaryMongoUrl = function (app) {
|
|
var galaxy = exports.discoverGalaxy(app);
|
|
var conn = galaxyServiceConnection(galaxy, "ultraworld");
|
|
|
|
try {
|
|
var mongoUrl = conn.call('getTemporaryMongoUrl', app);
|
|
} catch (e) {
|
|
handleError(e, galaxy);
|
|
return null;
|
|
} finally {
|
|
conn.close();
|
|
}
|
|
|
|
return mongoUrl;
|
|
};
|