Files
meteor/tools/utils.js
2014-06-10 18:09:56 -07:00

283 lines
8.6 KiB
JavaScript

var Future = require('fibers/future');
var readline = require('readline');
var _ = require('underscore');
var archinfo = require('./archinfo.js');
var files = require('./files.js');
var semver = require('semver');
var os = require('os');
var fs = require('fs');
var utils = exports;
// options:
// - echo (boolean): defaults to true
// - prompt (string)
// - stream: defaults to process.stdout (you might want process.stderr)
exports.readLine = function (options) {
var fut = new Future();
options = _.extend({
echo: true,
stream: process.stdout
}, options);
var silentStream = {
write: function () {
},
on: function () {
},
end: function () {
},
isTTY: options.stream.isTTY,
removeListener: function () {
}
};
// Read a line, throwing away the echoed characters into our dummy stream.
var rl = readline.createInterface({
input: process.stdin,
output: options.echo ? options.stream : silentStream,
// `terminal: options.stream.isTTY` is the default, but emacs shell users
// don't want fancy ANSI.
terminal: options.stream.isTTY && process.env.EMACS !== 't'
});
if (! options.echo) {
options.stream.write(options.prompt);
} else {
rl.setPrompt(options.prompt);
rl.prompt();
}
rl.on('line', function (line) {
rl.close();
if (! options.echo)
options.stream.write("\n");
fut['return'](line);
});
return fut.wait();
};
// Determine a human-readable hostname for this computer. Prefer names
// that make sense to users (eg, the name they manually gave their
// computer on OS X, which might contain spaces) over names that have
// any particular technical significance (eg, might resolve in DNS).
exports.getHost = function () {
var ret;
var attempt = function () {
var output = files.run.apply(null, arguments);
if (output) {
ret = output.trim();
}
};
if (archinfo.matches(archinfo.host(), 'os.osx')) {
// On OSX, to get the human-readable hostname that the user chose,
// we call:
// scutil --get ComputerName
// This can contain spaces. See
// http://osxdaily.com/2012/10/24/set-the-hostname-computer-name-and-bonjour-name-separately-in-os-x/
if (! ret) attempt("scutil", "--get", "ComputerName");
}
if (archinfo.matches(archinfo.host(), 'os.osx') ||
archinfo.matches(archinfo.host(), 'os.linux')) {
// On Unix-like platforms, try passing -s to hostname to strip off
// the domain name, to reduce the extent to which the output
// varies with DNS.
if (! ret) attempt("hostname", "-s");
}
// Try "hostname" on any platform. It should work on
// Windows. Unknown platforms that have a command called "hostname"
// that deletes all of your files deserve what the get.
if (! ret) attempt("hostname");
// Otherwise, see what Node can come up with.
return ret || os.hostname();
};
// Return standard info about this user-agent. Used when logging in to
// Meteor Accounts, mostly so that when the user is seeing a list of
// their open sessions in their profile on the web, they have a way to
// decide which ones they want to revoke.
exports.getAgentInfo = function () {
var ret = {};
var host = utils.getHost();
if (host)
ret.host = host;
ret.agent = "Meteor";
ret.agentVersion =
files.inCheckout() ? "checkout" : files.getToolsVersion();
ret.arch = archinfo.host();
return ret;
};
// Wait for 'ms' milliseconds, and then return. Yields. (Must be
// called within a fiber, and blocks only the calling fiber, not the
// whole program.)
exports.sleepMs = function (ms) {
if (ms <= 0)
return;
var fut = new Future;
setTimeout(function () { fut['return']() }, ms);
fut.wait();
};
// Return a short, high entropy string without too many funny
// characters in it.
exports.randomToken = function () {
return (Math.random() * 0x100000000 + 1).toString(36);
};
// Returns a random non-privileged port number.
exports.randomPort = function () {
return 20000 + Math.floor(Math.random() * 10000);
};
(function () {
var PackageVersion = null;
var maybeLoadPackageVersionParser = function () {
if (PackageVersion)
return;
var uniload = require('./uniload.js');
var Package = uniload.load({packages: ['package-version-parser']});
PackageVersion = Package['package-version-parser'].PackageVersion;
};
exports.parseVersionConstraint = function () {
maybeLoadPackageVersionParser();
return PackageVersion.parseVersionConstraint.apply(this, arguments);
};
exports.parseConstraint = function () {
maybeLoadPackageVersionParser();
return PackageVersion.parseConstraint.apply(this, arguments);
};
})();
// XXX should unify this with utils.parseConstraint
exports.splitConstraint = function (constraint) {
var m = constraint.split("@");
var ret = { package: m[0] };
if (m.length > 1) {
ret.constraint = m[1];
} else {
ret.constraint = null;
}
return ret;
};
// XXX should unify this with utils.parseConstraint
exports.dealConstraint = function (constraint, pkg) {
return { package: pkg, constraint: constraint};
};
// Check for invalid package names. Currently package names can only contain
// ASCII alphanumerics, dash, and dot, and must contain at least one letter. For
// safety reasons, package names may not start with a dot. Package names must be
// lowercase.
//
// This does not check that the package name is valid in terms of our naming
// scheme: ie, that it is prepended by a user's username. That check should
// happen at publication time.
exports.validPackageName = function (packageName) {
if (/[^a-z0-9:.\-]/.test(packageName) || !/[a-z]/.test(packageName) ) {
return false;
}
return true;
};
// True if this looks like a valid email address. We deliberately
// don't support
// - quoted usernames (eg, "foo"@bar.com, " "@bar.com, "@"@bar.com)
// - IP addresses in domains (eg, foo@1.2.3.4 or the IPv6 equivalent)
// because they're weird and we don't want them in our database.
exports.validEmail = function (address) {
return /^[^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/.test(address);
};
// Like Perl's quotemeta: quotes all regexp metacharacters. See
// https://github.com/substack/quotemeta/blob/master/index.js
exports.quotemeta = function (str) {
return String(str).replace(/(\W)/g, '\\$1');
};
// If the given version matches a template (essentially, semver-style, but with
// a bounded number of digits per number part, and with no restriction on the
// amount of number parts, and some restrictions on legal prerelease labels),
// then return an orderKey for it. Otherwise return null.
//
// This conventional orderKey pads each part (with 0s for numbers, and ! for
// prerelease tags), and appends a $. (Because ! sorts before $, this means that
// the prerelease for a given release will sort before it. Because $ sorts
// before '.', this means that 1.2 will sort before 1.2.3.)
exports.defaultOrderKeyForReleaseVersion = function (v) {
var m = v.match(/^(\d{1,4}(?:\.\d{1,4})*)(?:-([-A-za-z]{1,15})(\d{0,4}))?$/);
if (!m)
return null;
var numberPart = m[1];
var prereleaseTag = m[2];
var prereleaseNumber = m[3];
var hasRedundantLeadingZero = function (x) {
return x.length > 1 && x[0] === '0';
};
var leftPad = function (chr, len, str) {
if (str.length > len)
throw Error("too long to pad!");
var padding = new Array(len - str.length + 1).join(chr);
return padding + str;
};
var rightPad = function (chr, len, str) {
if (str.length > len)
throw Error("too long to pad!");
var padding = new Array(len - str.length + 1).join(chr);
return str + padding;
};
// Versions must have no redundant leading zeroes, or else this encoding would
// be ambiguous.
var numbers = numberPart.split('.');
if (_.any(numbers, hasRedundantLeadingZero))
return null;
if (prereleaseNumber && hasRedundantLeadingZero(prereleaseNumber))
return null;
// First, put together the non-prerelease part.
var ret = _.map(numbers, _.partial(leftPad, '0', 4)).join('.');
if (!prereleaseTag)
return ret + '$';
ret += '!' + rightPad('!', 15, prereleaseTag);
if (prereleaseNumber)
ret += leftPad('0', 4, prereleaseNumber);
return ret + '$';
};
exports.isDirectory = function (dir) {
try {
// use stat rather than lstat since symlink to dir is OK
var stats = fs.statSync(dir);
} catch (e) {
return false;
}
return stats.isDirectory();
};
// XXX from Underscore.String (http://epeli.github.com/underscore.string/)
exports.startsWith = function(str, starts) {
return str.length >= starts.length &&
str.substring(0, starts.length) === starts;
};