forked from basecamp/pow

This commit is contained in:
unstop
2014-06-25 06:51:35 +08:00
commit 6b5facf969
38 changed files with 3437 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/dist
/node_modules/

91
Cakefile Executable file
View File

@@ -0,0 +1,91 @@
async = require 'async'
fs = require 'fs'
{print} = require 'util'
{spawn, exec} = require 'child_process'
build = (watch, callback) ->
if typeof watch is 'function'
callback = watch
watch = false
options = ['-c', '-o', 'lib', 'src']
options.unshift '-w' if watch
coffee = spawn 'node_modules/.bin/coffee', options
coffee.stdout.on 'data', (data) -> print data.toString()
coffee.stderr.on 'data', (data) -> print data.toString()
coffee.on 'exit', (status) -> callback?() if status is 0
buildTemplates = (callback) ->
eco = require 'eco'
compile = (name) ->
(callback) ->
fs.readFile "src/templates/#{name}.eco", "utf8", (err, data) ->
if err then callback err
else
try
fs.mkdirSync "lib/templates/http_server"
fs.mkdirSync "lib/templates/installer"
fs.writeFile "lib/templates/#{name}.js", "module.exports = #{eco.precompile(data)}", callback
async.parallel [
compile("http_server/application_not_found.html")
compile("http_server/layout.html")
compile("http_server/welcome.html")
compile("installer/com.hackplan.power.firewall.plist")
compile("installer/com.hackplan.power.powerd.plist")
compile("installer/resolver")
], callback
task 'build', 'Compile CoffeeScript source files', ->
build()
buildTemplates()
task 'watch', 'Recompile CoffeeScript source files when modified', ->
build true
task 'install', 'Install power configuration files', ->
sh = (command, callback) ->
exec command, (err, stdout, stderr) ->
if err
console.error stderr
callback err
else
callback()
createHostsDirectory = (callback) ->
sh 'mkdir -p "$HOME/Library/Application Support/Power/Hosts"', (err) ->
fs.stat "#{process.env['HOME']}/.power", (err) ->
if err then sh 'ln -s "$HOME/Library/Application Support/Power/Hosts" "$HOME/.power"', callback
else callback()
installLocal = (callback) ->
console.error "*** Installing local configuration files..."
sh "./bin/power --install-local", callback
installSystem = (callback) ->
exec "./bin/power --install-system --dry-run", (needsRoot) ->
if needsRoot
console.error "*** Installing system configuration files as root..."
sh "sudo ./bin/power --install-system", (err) ->
if err
callback err
else
sh "sudo launchctl load /Library/LaunchDaemons/com.hackplan.power.firewall.plist", callback
else
callback()
async.parallel [createHostsDirectory, installLocal, installSystem], (err) ->
throw err if err
console.error "*** Installed"
task 'start', 'Start power server', ->
agent = "#{process.env['HOME']}/Library/LaunchAgents/com.hackplan.power.powerd.plist"
console.error "*** Starting the Power server..."
exec "launchctl load '#{agent}'", (err, stdout, stderr) ->
console.error stderr if err
task 'stop', 'Stop power server', ->
agent = "#{process.env['HOME']}/Library/LaunchAgents/com.hackplan.power.powerd.plist"
console.error "*** Stopping the Power server..."
exec "launchctl unload '#{agent}'", (err, stdout, stderr) ->
console.error stderr if err

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) 2014 Sam Stephenson, Basecamp
Copyright (c) 2014 Sun Liang, HackPlan
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

2
bin/power Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require("../lib/command.js");

36
build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/sh -e
# `./build.sh` generates dist/$VERSION.tar.gz
# `./build.sh --install` installs into ~/Library/Application Support/Power/Current
VERSION=$(node -e 'console.log(JSON.parse(require("fs").readFileSync("package.json","utf8")).version); ""')
ROOT="/tmp/power-build.$$"
DIST="$(pwd)/dist"
cake build
mkdir -p "$ROOT/$VERSION/node_modules"
cp -R package.json bin lib "$ROOT/$VERSION"
cp Cakefile "$ROOT/$VERSION"
cd "$ROOT/$VERSION"
BUNDLE_ONLY=1 npm install --production &>/dev/null
cp `which node` bin
if [ "$1" == "--install" ]; then
POWER_ROOT="$HOME/Library/Application Support/Power"
rm -fr "$POWER_ROOT/Versions/9999.0.0"
mkdir -p "$POWER_ROOT/Versions"
cp -R "$ROOT/$VERSION" "$POWER_ROOT/Versions/9999.0.0"
rm -f "$POWER_ROOT/Current"
cd "$POWER_ROOT"
ln -s Versions/9999.0.0 Current
echo "$POWER_ROOT/Versions/9999.0.0"
else
cd "$ROOT"
tar czf "$VERSION.tar.gz" "$VERSION"
mkdir -p "$DIST"
cd "$DIST"
mv "$ROOT/$VERSION.tar.gz" "$DIST"
echo "$DIST/$VERSION.tar.gz"
fi
rm -fr "$ROOT"

162
install.sh Normal file
View File

@@ -0,0 +1,162 @@
#!/bin/sh
#
# This is the installation script for Power.
# See the full annotated source: http://power.hackplan.com
#
# Install Power by running this command:
# curl power.hackplan.com/install.sh | sh
#
# Uninstall Power: :'(
# curl power.hackplan.com/uninstall.sh | sh
# Set up the environment. Respect $VERSION if it's set.
set -e
POWER_ROOT="$HOME/Library/Application Support/Power"
NODE_BIN="$POWER_ROOT/Current/bin/node"
POWER_BIN="$POWER_ROOT/Current/bin/power"
[[ -z "$VERSION" ]] && VERSION=0.0.1
# Fail fast if we're not on OS X >= 10.6.0.
if [ "$(uname -s)" != "Darwin" ]; then
echo "Sorry, Power requires Mac OS X to run." >&2
exit 1
elif [ "$(expr "$(sw_vers -productVersion | cut -f 2 -d .)" \>= 6)" = 0 ]; then
echo "Power requires Mac OS X 10.6 or later." >&2
exit 1
fi
echo "*** Installing Power $VERSION..."
# Create the Power directory structure if it doesn't already exist.
mkdir -p "$POWER_ROOT/Hosts" "$POWER_ROOT/Versions"
# If the requested version of Power is already installed, remove it first.
cd "$POWER_ROOT/Versions"
rm -rf "$POWER_ROOT/Versions/$VERSION"
# Download the requested version of Power and unpack it.
curl -s http://power.hackplan.com/versions/$VERSION.tar.gz | tar xzf -
# Update the Current symlink to point to the new version.
cd "$POWER_ROOT"
rm -f Current
ln -s Versions/$VERSION Current
# Create the ~/.power symlink if it doesn't exist.
cd "$HOME"
[[ -a .power ]] || ln -s "$POWER_ROOT/Hosts" .power
# Install local configuration files.
echo "*** Installing local configuration files..."
"$NODE_BIN" "$POWER_BIN" --install-local
# Check to see whether we need root privileges.
"$NODE_BIN" "$POWER_BIN" --install-system --dry-run >/dev/null && NEEDS_ROOT=0 || NEEDS_ROOT=1
# Install system configuration files, if necessary. (Avoid sudo otherwise.)
if [ $NEEDS_ROOT -eq 1 ]; then
echo "*** Installing system configuration files as root..."
sudo "$NODE_BIN" "$POWER_BIN" --install-system
sudo launchctl load -Fw /Library/LaunchDaemons/com.hackplan.power.firewall.plist 2>/dev/null
fi
# Start (or restart) Power.
echo "*** Starting the Power server..."
launchctl unload "$HOME/Library/LaunchAgents/com.hackplan.power.powerd.plist" 2>/dev/null || true
launchctl load -Fw "$HOME/Library/LaunchAgents/com.hackplan.power.powerd.plist" 2>/dev/null
# Show a message about where to go for help.
function print_troubleshooting_instructions() {
echo
echo "For troubleshooting instructions, please see the Power wiki:"
echo "https://github.com/hackplan/power/wiki/Troubleshooting"
echo
echo "To uninstall Power, \`curl power.hackplan.com/uninstall.sh | sh\`"
}
# Check to see if the server is running properly.
# If this version of Power supports the --print-config option,
# source the configuration and use it to run a self-test.
CONFIG=$("$NODE_BIN" "$POWER_BIN" --print-config 2>/dev/null || true)
if [[ -n "$CONFIG" ]]; then
eval "$CONFIG"
echo "*** Performing self-test..."
# Check to see if the server is running at all.
function check_status() {
sleep 1
curl -sH host:power "localhost:$POWER_HTTP_PORT/status.json" | grep -c "$VERSION" >/dev/null
}
# Attempt to connect to Power via each configured domain. If a
# domain is inaccessible, try to force a reload of OS X's
# network configuration.
function check_domains() {
for domain in ${POWER_DOMAINS//,/$IFS}; do
echo | nc "${domain}." "$POWER_DST_PORT" 2>/dev/null || return 1
done
}
# Use networksetup(8) to create a temporary network location,
# switch to it, switch back to the original location, then
# delete the temporary location. This forces reloading of the
# system network configuration.
function reload_network_configuration() {
echo "*** Reloading system network configuration..."
local location=$(networksetup -getcurrentlocation)
networksetup -createlocation "power$$" >/dev/null 2>&1
networksetup -switchtolocation "power$$" >/dev/null 2>&1
networksetup -switchtolocation "$location" >/dev/null 2>&1
networksetup -deletelocation "power$$" >/dev/null 2>&1
}
# Try twice to connect to Power. Bail if it doesn't work.
check_status || check_status || {
echo "!!! Couldn't find a running Power server on port $POWER_HTTP_PORT"
print_troubleshooting_instructions
exit 1
}
# Try resolving and connecting to each configured domain. If
# it doesn't work, reload the network configuration and try
# again. Bail if it fails the second time.
check_domains || {
{ reload_network_configuration && check_domains; } || {
echo "!!! Couldn't resolve configured domains ($POWER_DOMAINS)"
print_troubleshooting_instructions
exit 1
}
}
fi
# All done!
echo "*** Installed"
print_troubleshooting_instructions

91
lib/command.js Normal file
View File

@@ -0,0 +1,91 @@
// Generated by CoffeeScript 1.6.2
(function() {
var Configuration, Daemon, Installer, usage, util, _ref;
_ref = require(".."), Daemon = _ref.Daemon, Configuration = _ref.Configuration, Installer = _ref.Installer;
util = require("util");
process.title = "power";
usage = function() {
console.error("usage: power [--print-config | --install-local | --install-system [--dry-run]]");
return process.exit(-1);
};
Configuration.getUserConfiguration(function(err, configuration) {
var arg, createInstaller, daemon, dryRun, installer, key, printConfig, shellEscape, underscore, value, _i, _len, _ref1, _ref2, _results;
if (err) {
throw err;
}
printConfig = false;
createInstaller = null;
dryRun = false;
_ref1 = process.argv.slice(2);
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
arg = _ref1[_i];
if (arg === "--print-config") {
printConfig = true;
} else if (arg === "--install-local") {
createInstaller = Installer.getLocalInstaller;
} else if (arg === "--install-system") {
createInstaller = Installer.getSystemInstaller;
} else if (arg === "--dry-run") {
dryRun = true;
} else {
usage();
}
}
if (dryRun && !createInstaller) {
return usage();
} else if (printConfig) {
underscore = function(string) {
return string.replace(/(.)([A-Z])/g, function(match, left, right) {
return left + "_" + right.toLowerCase();
});
};
shellEscape = function(string) {
return "'" + string.toString().replace(/'/g, "'\\''") + "'";
};
_ref2 = configuration.toJSON();
_results = [];
for (key in _ref2) {
value = _ref2[key];
_results.push(util.puts("POWER_" + underscore(key).toUpperCase() + "=" + shellEscape(value)));
}
return _results;
} else if (createInstaller) {
installer = createInstaller(configuration);
if (dryRun) {
return installer.needsRootPrivileges(function(needsRoot) {
var exitCode;
exitCode = needsRoot ? 1 : 0;
return installer.getStaleFiles(function(files) {
var file, _j, _len1;
for (_j = 0, _len1 = files.length; _j < _len1; _j++) {
file = files[_j];
util.puts(file.path);
}
return process.exit(exitCode);
});
});
} else {
return installer.install(function(err) {
if (err) {
throw err;
}
});
}
} else {
daemon = new Daemon(configuration);
daemon.on("restart", function() {
return process.exit();
});
return daemon.start();
}
});
}).call(this);

237
lib/configuration.js Normal file
View File

@@ -0,0 +1,237 @@
// Generated by CoffeeScript 1.6.2
(function() {
var Configuration, Logger, async, compilePattern, fs, getFilenamesForHost, getUserEnv, libraryPath, mkdirp, path, rstat, sourceScriptEnv,
__slice = [].slice;
fs = require("fs");
path = require("path");
async = require("async");
Logger = require("./logger");
mkdirp = require("./util").mkdirp;
sourceScriptEnv = require("./util").sourceScriptEnv;
getUserEnv = require("./util").getUserEnv;
module.exports = Configuration = (function() {
Configuration.userConfigurationPath = path.join(process.env.HOME, ".powerconfig");
Configuration.loadUserConfigurationEnvironment = function(callback) {
var _this = this;
return getUserEnv(function(err, env) {
var p;
if (err) {
return callback(err);
} else {
return fs.exists(p = _this.userConfigurationPath, function(exists) {
if (exists) {
return sourceScriptEnv(p, env, callback);
} else {
return callback(null, env);
}
});
}
});
};
Configuration.getUserConfiguration = function(callback) {
return this.loadUserConfigurationEnvironment(function(err, env) {
if (err) {
return callback(err);
} else {
return callback(null, new Configuration(env));
}
});
};
Configuration.optionNames = ["bin", "dstPort", "httpPort", "dnsPort", "domains", "extDomains", "hostRoot", "logRoot"];
function Configuration(env) {
if (env == null) {
env = process.env;
}
this.loggers = {};
this.initialize(env);
}
Configuration.prototype.initialize = function(env) {
var _base, _base1, _ref, _ref1, _ref10, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
this.env = env;
this.bin = (_ref = env.POWER_BIN) != null ? _ref : path.join(__dirname, "../bin/power");
this.dstPort = (_ref1 = env.POWER_DST_PORT) != null ? _ref1 : 80;
this.httpPort = (_ref2 = env.POWER_HTTP_PORT) != null ? _ref2 : 20559;
this.dnsPort = (_ref3 = env.POWER_DNS_PORT) != null ? _ref3 : 20560;
this.domains = (_ref4 = (_ref5 = env.POWER_DOMAINS) != null ? _ref5 : env.POWER_DOMAIN) != null ? _ref4 : "dev";
this.extDomains = (_ref6 = env.POWER_EXT_DOMAINS) != null ? _ref6 : [];
this.domains = (_ref7 = typeof (_base = this.domains).split === "function" ? _base.split(",") : void 0) != null ? _ref7 : this.domains;
this.extDomains = (_ref8 = typeof (_base1 = this.extDomains).split === "function" ? _base1.split(",") : void 0) != null ? _ref8 : this.extDomains;
this.allDomains = this.domains.concat(this.extDomains);
this.allDomains.push(/\d+\.\d+\.\d+\.\d+\.xip\.io$/, /[0-9a-z]{1,7}\.xip\.io$/);
this.supportRoot = libraryPath("Application Support", "Power");
this.hostRoot = (_ref9 = env.POWER_HOST_ROOT) != null ? _ref9 : path.join(this.supportRoot, "Hosts");
this.logRoot = (_ref10 = env.POWER_LOG_ROOT) != null ? _ref10 : libraryPath("Logs", "Power");
this.dnsDomainPattern = compilePattern(this.domains);
return this.httpDomainPattern = compilePattern(this.allDomains);
};
Configuration.prototype.toJSON = function() {
var key, result, _i, _len, _ref;
result = {};
_ref = this.constructor.optionNames;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
key = _ref[_i];
result[key] = this[key];
}
return result;
};
Configuration.prototype.getLogger = function(name) {
var _base;
return (_base = this.loggers)[name] || (_base[name] = new Logger(path.join(this.logRoot, name + ".log")));
};
Configuration.prototype.findHostConfiguration = function(host, callback) {
var _this = this;
if (host == null) {
host = "";
}
return this.gatherHostConfigurations(function(err, hosts) {
var config, domain, file, _i, _j, _len, _len1, _ref, _ref1;
if (err) {
return callback(err);
}
_ref = _this.allDomains;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
domain = _ref[_i];
_ref1 = getFilenamesForHost(host, domain);
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
file = _ref1[_j];
if (config = hosts[file]) {
return callback(null, domain, config);
}
}
}
if (config = hosts["default"]) {
return callback(null, _this.allDomains[0], config);
}
return callback(null);
});
};
Configuration.prototype.gatherHostConfigurations = function(callback) {
var hosts,
_this = this;
hosts = {};
return mkdirp(this.hostRoot, function(err) {
if (err) {
return callback(err);
}
return fs.readdir(_this.hostRoot, function(err, files) {
if (err) {
return callback(err);
}
return async.forEach(files, function(file, next) {
var name, root;
root = path.join(_this.hostRoot, file);
name = file.toLowerCase();
return rstat(root, function(err, stats, path) {
if (stats != null ? stats.isDirectory() : void 0) {
hosts[name] = {
root: path
};
return next();
} else if (stats != null ? stats.isFile() : void 0) {
return fs.readFile(path, 'utf-8', function(err, data) {
if (err) {
return next();
}
data = data.trim();
if (data.length < 10 && !isNaN(parseInt(data))) {
hosts[name] = {
url: "http://localhost:" + (parseInt(data))
};
} else if (data.match("https?://")) {
hosts[name] = {
url: data
};
}
return next();
});
} else {
return next();
}
});
}, function(err) {
return callback(err, hosts);
});
});
});
};
return Configuration;
})();
libraryPath = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return path.join.apply(path, [process.env.HOME, "Library"].concat(__slice.call(args)));
};
getFilenamesForHost = function(host, domain) {
var i, length, parts, _i, _ref, _ref1, _results;
host = host.toLowerCase();
if (domain.test != null) {
domain = (_ref = (_ref1 = host.match(domain)) != null ? _ref1[0] : void 0) != null ? _ref : "";
}
if (host.slice(-domain.length - 1) === ("." + domain)) {
parts = host.slice(0, -domain.length - 1).split(".");
length = parts.length;
_results = [];
for (i = _i = 0; 0 <= length ? _i < length : _i > length; i = 0 <= length ? ++_i : --_i) {
_results.push(parts.slice(i, length).join("."));
}
return _results;
} else {
return [];
}
};
rstat = function(path, callback) {
return fs.lstat(path, function(err, stats) {
if (err) {
return callback(err);
} else if (stats != null ? stats.isSymbolicLink() : void 0) {
return fs.realpath(path, function(err, realpath) {
if (err) {
return callback(err);
} else {
return rstat(realpath, callback);
}
});
} else {
return callback(err, stats, path);
}
});
};
compilePattern = function(domains) {
return RegExp("((^|\\.)(" + (domains.join("|")) + "))\\.?$", "i");
};
}).call(this);

161
lib/daemon.js Normal file
View File

@@ -0,0 +1,161 @@
// Generated by CoffeeScript 1.6.2
(function() {
var Daemon, DnsServer, EventEmitter, HttpServer, fs, path,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
EventEmitter = require("events").EventEmitter;
HttpServer = require("./http_server");
DnsServer = require("./dns_server");
fs = require("fs");
path = require("path");
module.exports = Daemon = (function(_super) {
__extends(Daemon, _super);
function Daemon(configuration) {
var hostRoot,
_this = this;
this.configuration = configuration;
this.stop = __bind(this.stop, this);
this.hostRootChanged = __bind(this.hostRootChanged, this);
this.httpServer = new HttpServer(this.configuration);
this.dnsServer = new DnsServer(this.configuration);
process.on("SIGINT", this.stop);
process.on("SIGTERM", this.stop);
process.on("SIGQUIT", this.stop);
hostRoot = this.configuration.hostRoot;
this.restartFilename = path.join(hostRoot, "restart");
this.on("start", function() {
return _this.watcher = fs.watch(hostRoot, {
persistent: false
}, _this.hostRootChanged);
});
this.on("stop", function() {
var _ref;
return (_ref = _this.watcher) != null ? _ref.close() : void 0;
});
}
Daemon.prototype.hostRootChanged = function() {
var _this = this;
return fs.exists(this.restartFilename, function(exists) {
if (exists) {
return _this.restart();
}
});
};
Daemon.prototype.restart = function() {
var _this = this;
return fs.unlink(this.restartFilename, function(err) {
if (!err) {
return _this.emit("restart");
}
});
};
Daemon.prototype.start = function() {
var dnsPort, flunk, httpPort, pass, startServer, _ref,
_this = this;
if (this.starting || this.started) {
return;
}
this.starting = true;
startServer = function(server, port, callback) {
return process.nextTick(function() {
var err;
try {
server.on('error', callback);
server.once('listening', function() {
server.removeListener('error', callback);
return callback();
});
return server.listen(port);
} catch (_error) {
err = _error;
return callback(err);
}
});
};
pass = function() {
_this.starting = false;
_this.started = true;
return _this.emit("start");
};
flunk = function(err) {
_this.starting = false;
try {
_this.httpServer.close();
} catch (_error) {}
try {
_this.dnsServer.close();
} catch (_error) {}
return _this.emit("error", err);
};
_ref = this.configuration, httpPort = _ref.httpPort, dnsPort = _ref.dnsPort;
return startServer(this.httpServer, httpPort, function(err) {
if (err) {
return flunk(err);
} else {
return startServer(_this.dnsServer, dnsPort, function(err) {
if (err) {
return flunk(err);
} else {
return pass();
}
});
}
});
};
Daemon.prototype.stop = function() {
var stopServer,
_this = this;
if (this.stopping || !this.started) {
return;
}
this.stopping = true;
stopServer = function(server, callback) {
return process.nextTick(function() {
var close, err;
try {
close = function() {
server.removeListener("close", close);
return callback(null);
};
server.on("close", close);
return server.close();
} catch (_error) {
err = _error;
return callback(err);
}
});
};
return stopServer(this.httpServer, function() {
return stopServer(_this.dnsServer, function() {
_this.stopping = false;
_this.started = false;
return _this.emit("stop");
});
});
};
return Daemon;
})(EventEmitter);
}).call(this);

48
lib/dns_server.js Normal file
View File

@@ -0,0 +1,48 @@
// Generated by CoffeeScript 1.6.2
(function() {
var DnsServer, NS_C_IN, NS_RCODE_NXDOMAIN, NS_T_A, dnsserver,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
dnsserver = require("dnsserver");
NS_T_A = 1;
NS_C_IN = 1;
NS_RCODE_NXDOMAIN = 3;
module.exports = DnsServer = (function(_super) {
__extends(DnsServer, _super);
function DnsServer(configuration) {
this.configuration = configuration;
this.handleRequest = __bind(this.handleRequest, this);
DnsServer.__super__.constructor.apply(this, arguments);
this.on("request", this.handleRequest);
}
DnsServer.prototype.listen = function(port, callback) {
this.bind(port);
return typeof callback === "function" ? callback() : void 0;
};
DnsServer.prototype.handleRequest = function(req, res) {
var pattern, q, _ref;
pattern = this.configuration.dnsDomainPattern;
q = (_ref = req.question) != null ? _ref : {};
if (q.type === NS_T_A && q["class"] === NS_C_IN && pattern.test(q.name)) {
res.addRR(q.name, NS_T_A, NS_C_IN, 600, "127.0.0.1");
} else {
res.header.rcode = NS_RCODE_NXDOMAIN;
}
return res.send();
};
return DnsServer;
})(dnsserver.Server);
}).call(this);

245
lib/http_server.js Normal file
View File

@@ -0,0 +1,245 @@
// Generated by CoffeeScript 1.6.2
(function() {
var HttpServer, connect, dirname, fs, harp, join, pause, request, url, version, _ref,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
fs = require("fs");
url = require("url");
connect = require("connect");
harp = require("harp");
request = require("request");
pause = require("./util").pause;
_ref = require("path"), dirname = _ref.dirname, join = _ref.join;
version = JSON.parse(fs.readFileSync(__dirname + "/../package.json", "utf8")).version;
module.exports = HttpServer = (function(_super) {
var o, renderResponse, renderTemplate, x;
__extends(HttpServer, _super);
o = function(fn) {
return function(req, res, next) {
return fn(req, res, next);
};
};
x = function(fn) {
return function(err, req, res, next) {
return fn(err, req, res, next);
};
};
renderTemplate = function(templateName, renderContext, yieldContents) {
var context, key, template, value;
template = require("./templates/http_server/" + templateName + ".html");
context = {
renderTemplate: renderTemplate,
yieldContents: yieldContents
};
for (key in renderContext) {
value = renderContext[key];
context[key] = value;
}
return template(context);
};
renderResponse = function(res, status, templateName, context) {
if (context == null) {
context = {};
}
res.writeHead(status, {
"Content-Type": "text/html; charset=utf8",
"X-Power-Template": templateName
});
return res.end(renderTemplate(templateName, context));
};
function HttpServer(configuration) {
this.configuration = configuration;
this.handleWelcomeRequest = __bind(this.handleWelcomeRequest, this);
this.handleApplicationNotFound = __bind(this.handleApplicationNotFound, this);
this.handleProxyRequest = __bind(this.handleProxyRequest, this);
this.handleStaticRequest = __bind(this.handleStaticRequest, this);
this.findHostConfiguration = __bind(this.findHostConfiguration, this);
this.handlePowerRequest = __bind(this.handlePowerRequest, this);
this.logRequest = __bind(this.logRequest, this);
HttpServer.__super__.constructor.call(this, [o(this.logRequest), o(this.annotateRequest), o(this.handlePowerRequest), o(this.findHostConfiguration), o(this.handleStaticRequest), o(this.handleProxyRequest), o(this.handleApplicationNotFound), o(this.handleWelcomeRequest), o(this.handleLocationNotFound)]);
this.staticHandlers = {};
this.requestCount = 0;
this.accessLog = this.configuration.getLogger("access");
}
HttpServer.prototype.toJSON = function() {
return {
pid: process.pid,
version: version,
requestCount: this.requestCount
};
};
HttpServer.prototype.logRequest = function(req, res, next) {
this.accessLog.info("[" + req.socket.remoteAddress + "] " + req.method + " " + req.headers.host + " " + req.url);
this.requestCount++;
return next();
};
HttpServer.prototype.annotateRequest = function(req, res, next) {
var host, _ref1;
host = (_ref1 = req.headers.host) != null ? _ref1.replace(/(\.$)|(\.?:.*)/, "") : void 0;
req.power = {
host: host
};
return next();
};
HttpServer.prototype.handlePowerRequest = function(req, res, next) {
if (req.power.host !== "power") {
return next();
}
switch (req.url) {
case "/config.json":
res.writeHead(200);
return res.end(JSON.stringify(this.configuration));
case "/env.json":
res.writeHead(200);
return res.end(JSON.stringify(this.configuration.env));
case "/status.json":
res.writeHead(200);
return res.end(JSON.stringify(this));
default:
return this.handleLocationNotFound(req, res, next);
}
};
HttpServer.prototype.findHostConfiguration = function(req, res, next) {
var resume,
_this = this;
resume = pause(req);
return this.configuration.findHostConfiguration(req.power.host, function(err, domain, config) {
if (config) {
if (config.root) {
req.power.root = config.root;
}
if (config.url) {
req.power.url = config.url;
}
req.power.domain = domain;
req.power.resume = resume;
} else {
resume();
}
return next(err);
});
};
HttpServer.prototype.handleStaticRequest = function(req, res, next) {
var handler, root, _base, _ref1, _ref2;
if ((_ref1 = req.method) !== "GET" && _ref1 !== "HEAD") {
return next();
}
if (!((root = req.power.root) && typeof root === "string")) {
return next();
}
if (req.url.match(/\.\./)) {
return next();
}
handler = (_ref2 = (_base = this.staticHandlers)[root]) != null ? _ref2 : _base[root] = harp.mount(root);
return handler(req, res, next);
};
HttpServer.prototype.handleProxyRequest = function(req, res, next) {
var headers, hostname, key, port, proxy, value, _ref1, _ref2;
if (!req.power.url) {
return next();
}
_ref1 = url.parse(req.power.url), hostname = _ref1.hostname, port = _ref1.port;
headers = {};
_ref2 = req.headers;
for (key in _ref2) {
value = _ref2[key];
headers[key] = value;
}
headers['X-Forwarded-For'] = req.connection.address().address;
headers['X-Forwarded-Host'] = req.power.host;
headers['X-Forwarded-Server'] = req.power.host;
proxy = request({
method: req.method,
url: "" + req.power.url + req.url,
headers: headers,
jar: false,
followRedirect: false
});
req.pipe(proxy);
proxy.pipe(res);
proxy.on('error', function(err) {
return renderResponse(res, 500, "proxy_error", {
err: err,
hostname: hostname,
port: port
});
});
return req.power.resume();
};
HttpServer.prototype.handleApplicationNotFound = function(req, res, next) {
var domain, host, name, pattern, _ref1;
if (req.power.root) {
return next();
}
host = req.power.host;
pattern = this.configuration.httpDomainPattern;
if (!(domain = host != null ? (_ref1 = host.match(pattern)) != null ? _ref1[1] : void 0 : void 0)) {
return next();
}
name = host.slice(0, host.length - domain.length);
if (!name.length) {
return next();
}
return renderResponse(res, 503, "application_not_found", {
name: name,
host: host
});
};
HttpServer.prototype.handleWelcomeRequest = function(req, res, next) {
var domain, domains;
if (req.power.root || req.url !== "/") {
return next();
}
domains = this.configuration.domains;
domain = __indexOf.call(domains, "dev") >= 0 ? "dev" : domains[0];
return renderResponse(res, 200, "welcome", {
version: version,
domain: domain
});
};
HttpServer.prototype.handleLocationNotFound = function(req, res, next) {
res.writeHead(404, {
"Content-Type": "text/html"
});
return res.end("<!doctype html><html><body><h1>404 Not Found</h1>");
};
return HttpServer;
})(connect.HTTPServer);
}).call(this);

13
lib/index.js Normal file
View File

@@ -0,0 +1,13 @@
// Generated by CoffeeScript 1.6.2
(function() {
module.exports = {
Configuration: require("./configuration"),
Daemon: require("./daemon"),
DnsServer: require("./dns_server"),
HttpServer: require("./http_server"),
Installer: require("./installer"),
Logger: require("./logger"),
util: require("./util")
};
}).call(this);

138
lib/installer.js Normal file
View File

@@ -0,0 +1,138 @@
// Generated by CoffeeScript 1.6.2
(function() {
var Installer, InstallerFile, async, chown, daemonSource, firewallSource, fs, mkdirp, path, resolverSource, util,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
async = require("async");
fs = require("fs");
path = require("path");
mkdirp = require("./util").mkdirp;
chown = require("./util").chown;
util = require("util");
resolverSource = require("./templates/installer/resolver");
firewallSource = require("./templates/installer/com.hackplan.power.firewall.plist");
daemonSource = require("./templates/installer/com.hackplan.power.powerd.plist");
InstallerFile = (function() {
function InstallerFile(path, source, root, mode) {
this.path = path;
this.root = root != null ? root : false;
this.mode = mode != null ? mode : 0x1a4;
this.setPermissions = __bind(this.setPermissions, this);
this.setOwnership = __bind(this.setOwnership, this);
this.writeFile = __bind(this.writeFile, this);
this.vivifyPath = __bind(this.vivifyPath, this);
this.source = source.trim();
}
InstallerFile.prototype.isStale = function(callback) {
var _this = this;
return fs.exists(this.path, function(exists) {
if (exists) {
return fs.readFile(_this.path, "utf8", function(err, contents) {
if (err) {
return callback(true);
} else {
return callback(_this.source !== contents.trim());
}
});
} else {
return callback(true);
}
});
};
InstallerFile.prototype.vivifyPath = function(callback) {
return mkdirp(path.dirname(this.path), callback);
};
InstallerFile.prototype.writeFile = function(callback) {
return fs.writeFile(this.path, this.source, "utf8", callback);
};
InstallerFile.prototype.setOwnership = function(callback) {
if (this.root) {
return chown(this.path, "root:wheel", callback);
} else {
return callback(false);
}
};
InstallerFile.prototype.setPermissions = function(callback) {
return fs.chmod(this.path, this.mode, callback);
};
InstallerFile.prototype.install = function(callback) {
return async.series([this.vivifyPath, this.writeFile, this.setOwnership, this.setPermissions], callback);
};
return InstallerFile;
})();
module.exports = Installer = (function() {
Installer.getSystemInstaller = function(configuration) {
var domain, files, _i, _len, _ref;
this.configuration = configuration;
files = [new InstallerFile("/Library/LaunchDaemons/com.hackplan.power.firewall.plist", firewallSource(this.configuration), true)];
_ref = this.configuration.domains;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
domain = _ref[_i];
files.push(new InstallerFile("/etc/resolver/" + domain, resolverSource(this.configuration), true));
}
return new Installer(files);
};
Installer.getLocalInstaller = function(configuration) {
this.configuration = configuration;
return new Installer([new InstallerFile("" + process.env.HOME + "/Library/LaunchAgents/com.hackplan.power.powerd.plist", daemonSource(this.configuration))]);
};
function Installer(files) {
this.files = files != null ? files : [];
}
Installer.prototype.getStaleFiles = function(callback) {
return async.select(this.files, function(file, proceed) {
return file.isStale(proceed);
}, callback);
};
Installer.prototype.needsRootPrivileges = function(callback) {
return this.getStaleFiles(function(files) {
return async.detect(files, function(file, proceed) {
return proceed(file.root);
}, function(result) {
return callback(result != null);
});
});
};
Installer.prototype.install = function(callback) {
return this.getStaleFiles(function(files) {
return async.forEach(files, function(file, proceed) {
return file.install(function(err) {
if (!err) {
util.puts(file.path);
}
return proceed(err);
});
}, callback);
});
};
return Installer;
})();
}).call(this);

77
lib/logger.js Normal file
View File

@@ -0,0 +1,77 @@
// Generated by CoffeeScript 1.6.2
(function() {
var Log, Logger, dirname, fs, level, mkdirp, _fn, _i, _len, _ref,
__slice = [].slice;
fs = require("fs");
dirname = require("path").dirname;
Log = require("log");
mkdirp = require("./util").mkdirp;
module.exports = Logger = (function() {
Logger.LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"];
function Logger(path, level) {
this.path = path;
this.level = level != null ? level : "debug";
this.readyCallbacks = [];
}
Logger.prototype.ready = function(callback) {
var _this = this;
if (this.state === "ready") {
return callback.call(this);
} else {
this.readyCallbacks.push(callback);
if (!this.state) {
this.state = "initializing";
return mkdirp(dirname(this.path), function(err) {
if (err) {
return _this.state = null;
} else {
_this.stream = fs.createWriteStream(_this.path, {
flags: "a"
});
return _this.stream.on("open", function() {
var _i, _len, _ref;
_this.log = new Log(_this.level, _this.stream);
_this.state = "ready";
_ref = _this.readyCallbacks;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
callback = _ref[_i];
callback.call(_this);
}
return _this.readyCallbacks = [];
});
}
});
}
}
};
return Logger;
})();
_ref = Logger.LEVELS;
_fn = function(level) {
return Logger.prototype[level] = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return this.ready(function() {
return this.log[level].apply(this.log, args);
});
};
};
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
level = _ref[_i];
_fn(level);
}
}).call(this);

View File

@@ -0,0 +1,67 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
var _this = this;
__out.push(this.renderTemplate("layout", {
title: "Application not found"
}, function() {
return __capture(function() {
__out.push('\n <h1 class="err">Application not found</h1>\n <h2>Symlink your app to <code>~/.power/');
__out.push(__sanitize(_this.name));
__out.push('</code> first.</h2>\n <section>\n <p>When you access <code>http://');
__out.push(__sanitize(_this.host));
__out.push('/</code>, Power looks for a Rack application at <code>~/.power/');
__out.push(__sanitize(_this.name));
__out.push('</code>. To run your app at this domain:</p>\n <pre><span>$</span> cd ~/.pow\n<span>$</span> ln -s /path/to/myapp ');
__out.push(__sanitize(_this.name));
__out.push('\n<span>$</span> open http://');
__out.push(__sanitize(_this.host));
return __out.push('/</pre>\n </section>\n');
});
}));
__out.push('\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

View File

@@ -0,0 +1,59 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<!doctype html>\n<html>\n<head>\n <meta charset="utf-8">\n <title>');
__out.push(__sanitize(this.title));
__out.push('</title>\n <style>\n body {\n margin: 0;\n padding: 0;\n background: #e0e0d8;\n line-height: 18px;\n }\n div.page {\n margin: 72px auto;\n margin: 36px auto;\n background: #fff;\n border-radius: 18px;\n -webkit-box-shadow: 0px 2px 7px #999;\n -moz-box-shadow: 0px 2px 7px #999;\n padding: 36px 90px;\n width: 480px;\n position: relative;\n }\n .big div.page {\n width: 720px;\n }\n h1, h2, p, li {\n font-family: Helvetica, sans-serif;\n font-size: 13px;\n }\n h1 {\n line-height: 45px;\n font-size: 36px;\n margin: 0;\n }\n h1:before {\n font-size: 66px;\n line-height: 42px;\n position: absolute;\n right: 576px;\n }\n .big h1:before {\n right: 819px;\n }\n h1.ok {\n color: #060;\n }\n h1.ok:before {\n content: "✓";\n color: #090;\n }\n h1.err {\n color: #600;\n }\n h1.err:before {\n content: "✗";\n color: #900;\n }\n h2 {\n line-height: 27px;\n font-size: 18px;\n font-weight: normal;\n margin: 0;\n }\n a, pre span {\n color: #776;\n }\n h2, p, pre {\n color: #222;\n }\n pre {\n white-space: pre-wrap;\n font-size: 13px;\n }\n pre, code {\n font-family: Menlo, Monaco, monospace;\n }\n p code {\n font-size: 12px;\n }\n pre.breakout {\n border-top: 1px solid #ddd;\n border-bottom: 1px solid #ddd;\n background: #fafcf4;\n margin-left: -90px;\n margin-right: -90px;\n padding: 8px 0 8px 90px;\n }\n pre.small_text {\n font-size: 10px;\n }\n pre.small_text strong {\n font-size: 13px;\n }\n ul {\n padding: 0;\n }\n li {\n list-style-type: none;\n }\n </style>\n</head>\n<body class="');
__out.push(__sanitize(this["class"]));
__out.push('">\n <div class="page">\n ');
__out.push(__sanitize(this.yieldContents()));
__out.push('\n </div>\n</body>\n</html>\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

View File

@@ -0,0 +1,61 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
var _this = this;
__out.push(this.renderTemplate("layout", {
title: "Power is installed"
}, function() {
return __capture(function() {
__out.push('\n <h1 class="ok">Power is installed</h1>\n <h2>You&rsquo;re running version ');
__out.push(__sanitize(_this.version));
__out.push('.</h2>\n <section>\n <p>Set up a application by symlinking it into your <code>~/.power</code> directory. The name of the symlink determines the hostname you&rsquo;ll use to access the application.</p>\n <pre><span>$</span> cd ~/.power\n<span>$</span> ln -s /path/to/myapp\n<span>$</span> open http://myapp.');
__out.push(__sanitize(_this.domain));
return __out.push('/</pre>\n </section>\n');
});
}));
__out.push('\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

View File

@@ -0,0 +1,55 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n\t<key>Label</key>\n\t<string>com.hackplan.power.firewall</string>\n\t<key>ProgramArguments</key>\n\t<array>\n\t\t<string>sh</string>\n\t\t<string>-c</string>\n\t\t<string>ipfw add fwd 127.0.0.1,');
__out.push(__sanitize(this.httpPort));
__out.push(' tcp from any to me dst-port ');
__out.push(__sanitize(this.dstPort));
__out.push(' in &amp;&amp; sysctl -w net.inet.ip.forwarding=1 &amp;&amp; sysctl -w net.inet.ip.fw.enable=1</string>\n\t</array>\n\t<key>RunAtLoad</key>\n\t<true/>\n\t<key>UserName</key>\n\t<string>root</string>\n</dict>\n</plist>\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

View File

@@ -0,0 +1,55 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>com.hackplan.power.powerd</string>\n <key>ProgramArguments</key>\n <array>\n <string>');
__out.push(__sanitize(process.execPath));
__out.push('</string>\n <string>');
__out.push(__sanitize(this.bin));
__out.push('</string>\n </array>\n <key>KeepAlive</key>\n <true/>\n <key>RunAtLoad</key>\n <true/>\n</dict>\n</plist>\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

View File

@@ -0,0 +1,51 @@
module.exports = function(__obj) {
if (!__obj) __obj = {};
var __out = [], __capture = function(callback) {
var out = __out, result;
__out = [];
callback.call(this);
result = __out.join('');
__out = out;
return __safe(result);
}, __sanitize = function(value) {
if (value && value.ecoSafe) {
return value;
} else if (typeof value !== 'undefined' && value != null) {
return __escape(value);
} else {
return '';
}
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
__safe = __obj.safe = function(value) {
if (value && value.ecoSafe) {
return value;
} else {
if (!(typeof value !== 'undefined' && value != null)) value = '';
var result = new String(value);
result.ecoSafe = true;
return result;
}
};
if (!__escape) {
__escape = __obj.escape = function(value) {
return ('' + value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
}
(function() {
(function() {
__out.push('# Lovingly generated by Power\nnameserver 127.0.0.1\nport ');
__out.push(__sanitize(this.dnsPort));
__out.push('\n');
}).call(this);
}).call(__obj);
__obj.safe = __objSafe, __obj.escape = __escape;
return __out.join('');
}

337
lib/util.js Normal file
View File

@@ -0,0 +1,337 @@
// Generated by CoffeeScript 1.6.2
(function() {
var LineBuffer, Stream, async, exec, execFile, fs, getUserLocale, getUserShell, loginExec, makeTemporaryFilename, parseEnv, path, quote, readAndUnlink,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__slice = [].slice;
fs = require("fs");
path = require("path");
async = require("async");
execFile = require("child_process").execFile;
Stream = require("stream").Stream;
exports.LineBuffer = LineBuffer = (function(_super) {
__extends(LineBuffer, _super);
function LineBuffer(stream) {
var self;
this.stream = stream;
this.readable = true;
this._buffer = "";
self = this;
this.stream.on('data', function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return self.write.apply(self, args);
});
this.stream.on('end', function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return self.end.apply(self, args);
});
}
LineBuffer.prototype.write = function(chunk) {
var index, line, _results;
this._buffer += chunk;
_results = [];
while ((index = this._buffer.indexOf("\n")) !== -1) {
line = this._buffer.slice(0, index);
this._buffer = this._buffer.slice(index + 1, this._buffer.length);
_results.push(this.emit('data', line));
}
return _results;
};
LineBuffer.prototype.end = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
if (args.length > 0) {
this.write.apply(this, args);
}
if (this._buffer.length) {
this.emit('data', this._buffer);
}
return this.emit('end');
};
return LineBuffer;
})(Stream);
exports.bufferLines = function(stream, callback) {
var buffer;
buffer = new LineBuffer(stream);
buffer.on("data", callback);
return buffer;
};
exports.mkdirp = function(dirname, callback) {
var p;
return fs.lstat((p = path.normalize(dirname)), function(err, stats) {
var paths;
if (err) {
paths = [p].concat((function() {
var _results;
_results = [];
while (p !== "/" && p !== ".") {
_results.push(p = path.dirname(p));
}
return _results;
})());
return async.forEachSeries(paths.reverse(), function(p, next) {
return fs.exists(p, function(exists) {
if (exists) {
return next();
} else {
return fs.mkdir(p, 0x1ed, function(err) {
if (err) {
return callback(err);
} else {
return next();
}
});
}
});
}, callback);
} else if (stats.isDirectory()) {
return callback();
} else {
return callback("file exists");
}
});
};
exports.chown = function(path, owner, callback) {
var error;
error = "";
return exec(["chown", owner, path], function(err, stdout, stderr) {
if (err) {
return callback(err, stderr);
} else {
return callback(null);
}
});
};
exports.pause = function(stream) {
var onClose, onData, onEnd, queue, removeListeners;
queue = [];
onData = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return queue.push(['data'].concat(__slice.call(args)));
};
onEnd = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return queue.push(['end'].concat(__slice.call(args)));
};
onClose = function() {
return removeListeners();
};
removeListeners = function() {
stream.removeListener('data', onData);
stream.removeListener('end', onEnd);
return stream.removeListener('close', onClose);
};
stream.on('data', onData);
stream.on('end', onEnd);
stream.on('close', onClose);
return function() {
var args, _i, _len, _results;
removeListeners();
_results = [];
for (_i = 0, _len = queue.length; _i < _len; _i++) {
args = queue[_i];
_results.push(stream.emit.apply(stream, args));
}
return _results;
};
};
exports.sourceScriptEnv = function(script, env, options, callback) {
var command, cwd, filename, _ref;
if (options.call) {
callback = options;
options = {};
} else {
if (options == null) {
options = {};
}
}
cwd = path.dirname(script);
filename = makeTemporaryFilename();
command = "" + ((_ref = options.before) != null ? _ref : "true") + " &&\nsource " + (quote(script)) + " > /dev/null &&\nenv > " + (quote(filename));
return exec(["bash", "-c", command], {
cwd: cwd,
env: env
}, function(err, stdout, stderr) {
if (err) {
err.message = "'" + script + "' failed to load:\n" + command;
err.stdout = stdout;
err.stderr = stderr;
return callback(err);
} else {
return readAndUnlink(filename, function(err, result) {
if (err) {
return callback(err);
} else {
return callback(null, parseEnv(result));
}
});
}
});
};
exports.getUserEnv = function(callback, defaultEncoding) {
var filename;
if (defaultEncoding == null) {
defaultEncoding = "UTF-8";
}
filename = makeTemporaryFilename();
return loginExec("exec env > " + (quote(filename)), function(err) {
if (err) {
return callback(err);
} else {
return readAndUnlink(filename, function(err, result) {
if (err) {
return callback(err);
} else {
return getUserLocale(function(locale) {
var env, _ref;
env = parseEnv(result);
if ((_ref = env.LANG) == null) {
env.LANG = "" + locale + "." + defaultEncoding;
}
return callback(null, env);
});
}
});
}
});
};
exec = function(command, options, callback) {
if (callback == null) {
callback = options;
options = {};
}
return execFile("/usr/bin/env", command, options, callback);
};
quote = function(string) {
return "'" + string.replace(/\'/g, "'\\''") + "'";
};
makeTemporaryFilename = function() {
var filename, random, timestamp, tmpdir, _ref;
tmpdir = (_ref = process.env.TMPDIR) != null ? _ref : "/tmp";
timestamp = new Date().getTime();
random = parseInt(Math.random() * Math.pow(2, 16));
filename = "power." + process.pid + "." + timestamp + "." + random;
return path.join(tmpdir, filename);
};
readAndUnlink = function(filename, callback) {
return fs.readFile(filename, "utf8", function(err, contents) {
if (err) {
return callback(err);
} else {
return fs.unlink(filename, function(err) {
if (err) {
return callback(err);
} else {
return callback(null, contents);
}
});
}
});
};
loginExec = function(command, callback) {
return getUserShell(function(shell) {
var login;
login = ["login", "-qf", process.env.LOGNAME, shell];
return exec(__slice.call(login).concat(["-i"], ["-c"], [command]), function(err, stdout, stderr) {
if (err) {
return exec(__slice.call(login).concat(["-c"], [command]), callback);
} else {
return callback(null, stdout, stderr);
}
});
});
};
getUserShell = function(callback) {
var command;
command = ["dscl", ".", "-read", "/Users/" + process.env.LOGNAME, "UserShell"];
return exec(command, function(err, stdout, stderr) {
var match, matches, shell;
if (err) {
return callback(process.env.SHELL);
} else {
if (matches = stdout.trim().match(/^UserShell: (.+)$/)) {
match = matches[0], shell = matches[1];
return callback(shell);
} else {
return callback(process.env.SHELL);
}
}
});
};
getUserLocale = function(callback) {
return exec(["defaults", "read", "-g", "AppleLocale"], function(err, stdout, stderr) {
var locale, _ref;
locale = (_ref = stdout != null ? stdout.trim() : void 0) != null ? _ref : "";
if (!locale.match(/^\w+$/)) {
locale = "en_US";
}
return callback(locale);
});
};
parseEnv = function(stdout) {
var env, line, match, matches, name, value, _i, _len, _ref;
env = {};
_ref = stdout.split("\n");
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
line = _ref[_i];
if (matches = line.match(/([^=]+)=(.+)/)) {
match = matches[0], name = matches[1], value = matches[2];
env[name] = value;
}
}
return env;
};
}).call(this);

36
package.json Executable file
View File

@@ -0,0 +1,36 @@
{
"name": "power",
"description": "Zero-configuration Static server for Mac OS X",
"version": "0.0.1",
"author": "Unstop",
"repository": {
"type": "git",
"url": "http://github.com/hackplan/pow.git"
},
"bin": {
"power": "./bin/power"
},
"main": "./lib/index.js",
"dependencies": {
"async": "0.1.22",
"connect": "^1.8.7",
"dnsserver": "https://github.com/sstephenson/dnsserver.js/tarball/4f2c713b2e",
"harp": "^0.12.1",
"log": ">= 1.1.1",
"nack": "~> 0.16",
"request": "~> 2.16"
},
"devDependencies": {
"eco": "~> 1.1.0",
"nodeunit": "0.8.0",
"coffee-script": "1.6.2",
"docco": "0.6.3"
},
"engines": {
"node": ">= 0.8.0"
},
"scripts": {
"start": "cake start",
"stop": "cake stop"
}
}

86
src/command.coffee Executable file
View File

@@ -0,0 +1,86 @@
# The `command` module is loaded when the `power` binary runs. It parses
# any command-line arguments and determines whether to install Pow's
# configuration files or start the daemon itself.
{Daemon, Configuration, Installer} = require ".."
util = require "util"
# Set the process's title to `power` so it's easier to find in `ps`,
# `top`, Activity Monitor, and so on.
process.title = "power"
# Print valid command-line arguments and exit with a non-zero exit
# code if invalid arguments are passed to the `power` binary.
usage = ->
console.error "usage: power [--print-config | --install-local | --install-system [--dry-run]]"
process.exit -1
# Start by loading the user configuration from `~/.powerconfig`, if it
# exists. The user configuration affects both the installer and the
# daemon.
Configuration.getUserConfiguration (err, configuration) ->
throw err if err
printConfig = false
createInstaller = null
dryRun = false
for arg in process.argv.slice(2)
# Set a flag if --print-config is requested.
if arg is "--print-config"
printConfig = true
# Cache the factory method for creating a local or system
# installer if necessary.
else if arg is "--install-local"
createInstaller = Installer.getLocalInstaller
else if arg is "--install-system"
createInstaller = Installer.getSystemInstaller
# Set a flag if a dry run is requested.
else if arg is "--dry-run"
dryRun = true
# Abort if we encounter an unknown argument.
else
usage()
# Abort if a dry run is requested without installing anything.
if dryRun and not createInstaller
usage()
# Print out the current configuration in a format that can be
# evaluated by a shell script (`eval $(power --print-config)`).
else if printConfig
underscore = (string) ->
string.replace /(.)([A-Z])/g, (match, left, right) ->
left + "_" + right.toLowerCase()
shellEscape = (string) ->
"'" + string.toString().replace(/'/g, "'\\''") + "'" #'
for key, value of configuration.toJSON()
util.puts "POWER_" + underscore(key).toUpperCase() +
"=" + shellEscape(value)
# Create the installer, passing in our loaded configuration.
else if createInstaller
installer = createInstaller configuration
# If a dry run was requested, check to see whether any files need
# to be installed with root privileges. If yes, exit with a status
# of 1. If no, exit with a status of 0.
if dryRun
installer.needsRootPrivileges (needsRoot) ->
exitCode = if needsRoot then 1 else 0
installer.getStaleFiles (files) ->
util.puts file.path for file in files
process.exit exitCode
# Otherwise, install all the requested files, printing the full
# path of each installed file to stdout.
else
installer.install (err) ->
throw err if err
# Start up the Pow daemon if no arguments were passed. Terminate the
# process if the daemon requests a restart.
else
daemon = new Daemon configuration
daemon.on "restart", -> process.exit()
daemon.start()

235
src/configuration.coffee Executable file
View File

@@ -0,0 +1,235 @@
# The `Configuration` class encapsulates various options for a Pow
# daemon (port numbers, directories, etc.). It's also responsible for
# creating `Logger` instances and mapping hostnames to application
# root paths.
fs = require "fs"
path = require "path"
async = require "async"
Logger = require "./logger"
{mkdirp} = require "./util"
{sourceScriptEnv} = require "./util"
{getUserEnv} = require "./util"
module.exports = class Configuration
# The user configuration file, `~/.powerconfig`, is evaluated on
# boot. You can configure options such as the top-level domain,
# number of workers, the worker idle timeout, and listening ports.
#
# export POWER_DOMAINS=dev,test
# export POWER_WORKERS=3
#
# See the `Configuration` constructor for a complete list of
# environment options.
@userConfigurationPath: path.join process.env.HOME, ".powerconfig"
# Evaluates the user configuration script and calls the `callback`
# with the environment variables if the config file exists. Any
# script errors are passed along in the first argument. (No error
# occurs if the file does not exist.)
@loadUserConfigurationEnvironment: (callback) ->
getUserEnv (err, env) =>
if err
callback err
else
fs.exists p = @userConfigurationPath, (exists) ->
if exists
sourceScriptEnv p, env, callback
else
callback null, env
# Creates a Configuration object after evaluating the user
# configuration file. Any environment variables in `~/.powerconfig`
# affect the process environment and will be copied to spawned
# subprocesses.
@getUserConfiguration: (callback) ->
@loadUserConfigurationEnvironment (err, env) ->
if err
callback err
else
callback null, new Configuration env
# A list of option names accessible on `Configuration` instances.
@optionNames: [
"bin", "dstPort", "httpPort", "dnsPort",
"domains", "extDomains", "hostRoot", "logRoot"
]
# Pass in any environment variables you'd like to override when
# creating a `Configuration` instance.
constructor: (env = process.env) ->
@loggers = {}
@initialize env
# Valid environment variables and their defaults:
initialize: (@env) ->
# `POWER_BIN`: the path to the `power` binary. (This should be
# correctly configured for you.)
@bin = env.POWER_BIN ? path.join __dirname, "../bin/power"
# `POWER_DST_PORT`: the public port Power expects to be forwarded or
# otherwise proxied for incoming HTTP requests. Defaults to `80`.
@dstPort = env.POWER_DST_PORT ? 80
# `POWER_HTTP_PORT`: the TCP port Pow opens for accepting incoming
# HTTP requests. Defaults to `20559`.
@httpPort = env.POWER_HTTP_PORT ? 20559
# `POWER_DNS_PORT`: the UDP port Pow listens on for incoming DNS
# queries. Defaults to `20560`.
@dnsPort = env.POWER_DNS_PORT ? 20560
# `POWER_DOMAINS`: the top-level domains for which Pow will respond
# to DNS `A` queries with `127.0.0.1`. Defaults to `dev`. If you
# configure this in your `~/.powerconfig` you will need to re-run
# `sudo power --install-system` to make `/etc/resolver` aware of
# the new TLDs.
@domains = env.POWER_DOMAINS ? env.POWER_DOMAIN ? "dev"
# `POWER_EXT_DOMAINS`: additional top-level domains for which Pow
# will serve HTTP requests (but not DNS requests -- hence the
# "ext").
@extDomains = env.POWER_EXT_DOMAINS ? []
# Allow for comma-separated domain lists, e.g. `POWER_DOMAINS=dev,test`
@domains = @domains.split?(",") ? @domains
@extDomains = @extDomains.split?(",") ? @extDomains
@allDomains = @domains.concat @extDomains
# Support *.xip.io top-level domains.
@allDomains.push /\d+\.\d+\.\d+\.\d+\.xip\.io$/, /[0-9a-z]{1,7}\.xip\.io$/
# Runtime support files live in `~/Library/Application Support/Power`.
@supportRoot = libraryPath "Application Support", "Power"
# `POWER_HOST_ROOT`: path to the directory containing symlinks to
# applications that will be served by Pow. Defaults to
# `~/Library/Application Support/Pow/Hosts`.
@hostRoot = env.POWER_HOST_ROOT ? path.join @supportRoot, "Hosts"
# `POWER_LOG_ROOT`: path to the directory that Pow will use to store
# its log files. Defaults to `~/Library/Logs/Pow`.
@logRoot = env.POWER_LOG_ROOT ? libraryPath "Logs", "Power"
# ---
# Precompile regular expressions for matching domain names to be
# served by the DNS server and hosts to be served by the HTTP
# server.
@dnsDomainPattern = compilePattern @domains
@httpDomainPattern = compilePattern @allDomains
# Gets an object of the `Configuration` instance's options that can
# be passed to `JSON.stringify`.
toJSON: ->
result = {}
result[key] = @[key] for key in @constructor.optionNames
result
# Retrieve a `Logger` instance with the given `name`.
getLogger: (name) ->
@loggers[name] ||= new Logger path.join @logRoot, name + ".log"
# Search `hostRoot` for files, symlinks or directories matching the
# domain specified by `host`. If a match is found, the matching domain
# name and its configuration are passed as second and third arguments
# to `callback`. The configuration will either have a `root` or
# a `port` property. If no match is found, `callback` is called
# without any arguments. If an error is raised, `callback` is called
# with the error as its first argument.
findHostConfiguration: (host = "", callback) ->
@gatherHostConfigurations (err, hosts) =>
return callback err if err
for domain in @allDomains
for file in getFilenamesForHost host, domain
if config = hosts[file]
return callback null, domain, config
if config = hosts["default"]
return callback null, @allDomains[0], config
callback null
# Asynchronously build a mapping of entries in `hostRoot` to
# application root paths and proxy ports. For each symlink, store the
# symlink's name and the real path of the application it points to.
# For each directory, store the directory's name and its full path.
# For each file that contains a port number, store the file's name and
# the port. The mapping is passed as an object to the second argument
# of `callback`. If an error is raised, `callback` is called with the
# error as its first argument.
#
# The mapping object will look something like this:
#
# {
# "basecamp": { "root": "/Volumes/37signals/basecamp" },
# "launchpad": { "root": "/Volumes/37signals/launchpad" },
# "37img": { "root": "/Volumes/37signals/portfolio" },
# "couchdb": { "url": "http://localhost:5984" }
# }
gatherHostConfigurations: (callback) ->
hosts = {}
mkdirp @hostRoot, (err) =>
return callback err if err
fs.readdir @hostRoot, (err, files) =>
return callback err if err
async.forEach files, (file, next) =>
root = path.join @hostRoot, file
name = file.toLowerCase()
rstat root, (err, stats, path) ->
if stats?.isDirectory()
hosts[name] = root: path
next()
else if stats?.isFile()
fs.readFile path, 'utf-8', (err, data) ->
return next() if err
data = data.trim()
if data.length < 10 and not isNaN(parseInt(data))
hosts[name] = {url: "http://localhost:#{parseInt(data)}"}
else if data.match("https?://")
hosts[name] = {url: data}
next()
else
next()
, (err) ->
callback err, hosts
# Convenience wrapper for constructing paths to subdirectories of
# `~/Library`.
libraryPath = (args...) ->
path.join process.env.HOME, "Library", args...
# Strip a trailing `domain` from the given `host`, then generate a
# sorted array of possible entry names for finding which application
# should serve the host. For example, a `host` of
# `asset0.37s.basecamp.dev` will produce `["asset0.37s.basecamp",
# "37s.basecamp", "basecamp"]`, and `basecamp.dev` will produce
# `["basecamp"]`.
getFilenamesForHost = (host, domain) ->
host = host.toLowerCase()
domain = host.match(domain)?[0] ? "" if domain.test?
if host.slice(-domain.length - 1) is ".#{domain}"
parts = host.slice(0, -domain.length - 1).split "."
length = parts.length
for i in [0...length]
parts.slice(i, length).join "."
else
[]
# Similar to `fs.stat`, but passes the realpath of the file as the
# third argument to the callback.
rstat = (path, callback) ->
fs.lstat path, (err, stats) ->
if err
callback err
else if stats?.isSymbolicLink()
fs.realpath path, (err, realpath) ->
if err then callback err
else rstat realpath, callback
else
callback err, stats, path
# Helper function for compiling a list of top-level domains into a
# regular expression for matching purposes.
compilePattern = (domains) ->
/// ( (^|\.) (#{domains.join("|")}) ) \.? $ ///i

107
src/daemon.coffee Executable file
View File

@@ -0,0 +1,107 @@
# A `Daemon` is the root object in a Power process. It's responsible for
# starting and stopping an `HttpServer` and a `DnsServer` in tandem.
{EventEmitter} = require "events"
HttpServer = require "./http_server"
DnsServer = require "./dns_server"
fs = require "fs"
path = require "path"
module.exports = class Daemon extends EventEmitter
# Create a new `Daemon` with the given `Configuration` instance.
constructor: (@configuration) ->
# `HttpServer` and `DnsServer` instances are created accordingly.
@httpServer = new HttpServer @configuration
@dnsServer = new DnsServer @configuration
# The daemon stops in response to `SIGINT`, `SIGTERM` and
# `SIGQUIT` signals.
process.on "SIGINT", @stop
process.on "SIGTERM", @stop
process.on "SIGQUIT", @stop
# Watch for changes to the host root directory once the daemon has
# started. When the directory changes and the `restart` file
# is present, remove it and emit a `restart` event.
hostRoot = @configuration.hostRoot
@restartFilename = path.join hostRoot, "restart"
@on "start", => @watcher = fs.watch hostRoot, persistent: false, @hostRootChanged
@on "stop", => @watcher?.close()
hostRootChanged: =>
fs.exists @restartFilename, (exists) =>
@restart() if exists
# Remove the `~/.power/restart` file, if present, and emit a
# `restart` event. The `pow` command observes this event and
# terminates the process in response, causing Launch Services to
# restart the server.
restart: ->
fs.unlink @restartFilename, (err) =>
@emit "restart" unless err
# Start the daemon if it's stopped. The process goes like this:
#
# * First, start the HTTP server. If the HTTP server can't boot,
# emit an `error` event and abort.
# * Next, start the DNS server. If the DNS server can't boot, stop
# the HTTP server, emit an `error` event and abort.
# * If both servers start up successfully, emit a `start` event and
# mark the daemon as started.
start: ->
return if @starting or @started
@starting = true
startServer = (server, port, callback) -> process.nextTick ->
try
server.on 'error', callback
server.once 'listening', ->
server.removeListener 'error', callback
callback()
server.listen port
catch err
callback err
pass = =>
@starting = false
@started = true
@emit "start"
flunk = (err) =>
@starting = false
try @httpServer.close()
try @dnsServer.close()
@emit "error", err
{httpPort, dnsPort} = @configuration
startServer @httpServer, httpPort, (err) =>
if err then flunk err
else startServer @dnsServer, dnsPort, (err) =>
if err then flunk err
else pass()
# Stop the daemon if it's started. This means calling `close` on
# both servers in succession, beginning with the HTTP server, and
# waiting for the servers to notify us that they're done. The daemon
# emits a `stop` event when this process is complete.
stop: =>
return if @stopping or !@started
@stopping = true
stopServer = (server, callback) -> process.nextTick ->
try
close = ->
server.removeListener "close", close
callback null
server.on "close", close
server.close()
catch err
callback err
stopServer @httpServer, =>
stopServer @dnsServer, =>
@stopping = false
@started = false
@emit "stop"

43
src/dns_server.coffee Executable file
View File

@@ -0,0 +1,43 @@
# Pow's `DnsServer` is designed to respond to DNS `A` queries with
# `127.0.0.1` for all subdomains of the specified top-level domain.
# When used in conjunction with Mac OS X's [/etc/resolver
# system](http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/resolver.5.html),
# there's no configuration needed to add and remove host names for
# local web development.
dnsserver = require "dnsserver"
NS_T_A = 1
NS_C_IN = 1
NS_RCODE_NXDOMAIN = 3
module.exports = class DnsServer extends dnsserver.Server
# Create a `DnsServer` with the given `Configuration` instance. The
# server installs a single event handler for responding to DNS
# queries.
constructor: (@configuration) ->
super
@on "request", @handleRequest
# The `listen` method is just a wrapper around `bind` that makes
# `DnsServer` quack like a `HttpServer` (for initialization, at
# least).
listen: (port, callback) ->
@bind port
callback?()
# Each incoming DNS request ends up here. If it's an `A` query
# and the domain name matches the top-level domain specified in our
# configuration, we respond with `127.0.0.1`. Otherwise, we respond
# with `NXDOMAIN`.
handleRequest: (req, res) =>
pattern = @configuration.dnsDomainPattern
q = req.question ? {}
if q.type is NS_T_A and q.class is NS_C_IN and pattern.test q.name
res.addRR q.name, NS_T_A, NS_C_IN, 600, "127.0.0.1"
else
res.header.rcode = NS_RCODE_NXDOMAIN
res.send()

212
src/http_server.coffee Executable file
View File

@@ -0,0 +1,212 @@
# Where the magic happens.
#
# Pow's `HttpServer` runs as your user and listens on a high port
# (20559 by default) for HTTP requests. (An `ipfw` rule forwards
# incoming requests on port 80 to your Pow instance.) Requests work
# their way through a middleware stack and are served to your browser
# as static assets, Rack requests, or error pages.
fs = require "fs"
url = require "url"
connect = require "connect"
harp = require "harp"
request = require "request"
{pause} = require "./util"
{dirname, join} = require "path"
{version} = JSON.parse fs.readFileSync __dirname + "/../package.json", "utf8"
# `HttpServer` is a subclass of
# [Connect](http://senchalabs.github.com/connect/)'s `HTTPServer` with
# a custom set of middleware and a reference to a Pow `Configuration`.
module.exports = class HttpServer extends connect.HTTPServer
# Connect depends on Function.prototype.length to determine
# whether a given middleware is an error handler. These wrappers
# provide compatibility with bound instance methods.
o = (fn) -> (req, res, next) -> fn req, res, next
x = (fn) -> (err, req, res, next) -> fn err, req, res, next
# Helper that loads the named template, creates a new context from
# the given context with itself and an optional `yieldContents`
# block, and passes that to the template for rendering.
renderTemplate = (templateName, renderContext, yieldContents) ->
template = require "./templates/http_server/#{templateName}.html"
context = {renderTemplate, yieldContents}
context[key] = value for key, value of renderContext
template context
# Helper to render `templateName` to the given `res` response with
# the given `status` code and `context` values.
renderResponse = (res, status, templateName, context = {}) ->
res.writeHead status, "Content-Type": "text/html; charset=utf8", "X-Power-Template": templateName
res.end renderTemplate templateName, context
# Create an HTTP server for the given configuration. This sets up
# the middleware stack, gets a `Logger` instace for the global
# access log, and registers a handler to close any running
# applications when the server shuts down.
constructor: (@configuration) ->
super [
o @logRequest
o @annotateRequest
o @handlePowerRequest
o @findHostConfiguration
o @handleStaticRequest
o @handleProxyRequest
o @handleApplicationNotFound
o @handleWelcomeRequest
o @handleLocationNotFound
]
@staticHandlers = {}
@requestCount = 0
@accessLog = @configuration.getLogger "access"
# Gets an object describing the server's current status that can be
# passed to `JSON.stringify`.
toJSON: ->
pid: process.pid
version: version
requestCount: @requestCount
# The first middleware in the stack logs each incoming request's
# source address, method, hostname, and path to the access log
# (`~/Library/Logs/Pow/access.log` by default).
logRequest: (req, res, next) =>
@accessLog.info "[#{req.socket.remoteAddress}] #{req.method} #{req.headers.host} #{req.url}"
@requestCount++
next()
# Annotate the request object with a `pow` property whose value is
# an object that will hold the request's normalized hostname, root
# path, and application, if any. (Only the `pow.host` property is
# set here.)
annotateRequest: (req, res, next) ->
host = req.headers.host?.replace /(\.$)|(\.?:.*)/, ""
req.power = {host}
next()
# Serve requests for status information at `http://power/`. The status
# endpoints are:
#
# * `/config.json`: Returns a JSON representation of the server's
# `Configuration` instance.
# * `/env.json`: Returns the environment variables that all spawned
# applications inherit.
# * `/status.json`: Returns information about the current server
# version, number of requests handled, and process ID.
#
# Third-party utilities may use these endpoints to inspect a running
# Pow server.
handlePowerRequest: (req, res, next) =>
return next() unless req.power.host is "power"
switch req.url
when "/config.json"
res.writeHead 200
res.end JSON.stringify @configuration
when "/env.json"
res.writeHead 200
res.end JSON.stringify @configuration.env
when "/status.json"
res.writeHead 200
res.end JSON.stringify this
else
@handleLocationNotFound req, res, next
# After the request has been annotated, attempt to match its hostname
# using the server's configuration. If a host configuration is found,
# annotate the request object with the application's root path or the
# port number so we can use it further down the stack.
findHostConfiguration: (req, res, next) =>
resume = pause req
@configuration.findHostConfiguration req.power.host, (err, domain, config) =>
if config
req.power.root = config.root if config.root
req.power.url = config.url if config.url
req.power.domain = domain
req.power.resume = resume
else
resume()
next err
# If this is a `GET` or `HEAD` request matching a file in the
# application's `public/` directory, serve the file directly.
handleStaticRequest: (req, res, next) =>
unless req.method in ["GET", "HEAD"]
return next()
unless (root = req.power.root) and typeof root is "string"
return next()
if req.url.match /\.\./
return next()
handler = @staticHandlers[root] ?= harp.mount(root)
handler req, res, next
# If the request object is annotated with a url, proxy the
# request off to the hostname and port.
handleProxyRequest: (req, res, next) =>
return next() unless req.power.url
{hostname, port} = url.parse req.power.url
headers = {}
for key, value of req.headers
headers[key] = value
headers['X-Forwarded-For'] = req.connection.address().address
headers['X-Forwarded-Host'] = req.power.host
headers['X-Forwarded-Server'] = req.power.host
proxy = request
method: req.method
url: "#{req.power.url}#{req.url}"
headers: headers
jar: false
followRedirect: false
req.pipe proxy
proxy.pipe res
proxy.on 'error', (err) ->
renderResponse res, 500, "proxy_error",
{err, hostname, port}
req.power.resume()
# Show a friendly message when accessing a hostname that hasn't been
# set up with Power yet (but only for hosts that the server is
# configured to handle).
handleApplicationNotFound: (req, res, next) =>
return next() if req.power.root
host = req.power.host
pattern = @configuration.httpDomainPattern
return next() unless domain = host?.match(pattern)?[1]
name = host.slice 0, host.length - domain.length
return next() unless name.length
renderResponse res, 503, "application_not_found", {name, host}
# If the request is for `/` on an unsupported domain (like
# `http://localhost/` or `http://127.0.0.1/`), show a page
# confirming that Power is installed and running, with instructions on
# how to set up an app.
handleWelcomeRequest: (req, res, next) =>
return next() if req.power.root or req.url isnt "/"
{domains} = @configuration
domain = if "dev" in domains then "dev" else domains[0]
renderResponse res, 200, "welcome", {version, domain}
# If the request ends up here, it's for a static site, but the
# requested file doesn't exist. Show a basic 404 message.
handleLocationNotFound: (req, res, next) ->
res.writeHead 404, "Content-Type": "text/html"
res.end "<!doctype html><html><body><h1>404 Not Found</h1>"

35
src/index.coffee Executable file
View File

@@ -0,0 +1,35 @@
# This is the annotated source code for [Pow](http://pow.cx/), a
# zero-configuration Rack server for Mac OS X. See the [user's
# manual](http://pow.cx/manual.html) for information on installation
# and usage.
#
# The annotated source HTML is generated by
# [Docco](http://jashkenas.github.com/docco/).
# ## Table of contents
module.exports =
# The [Configuration] class stores settings for
# a Power daemon and is responsible for mapping hostnames to Rack
# applications.
Configuration: require "./configuration"
# The [Daemon] class represents a running Pow daemon.
Daemon: require "./daemon"
# [DnsServer] handles incoming DNS queries.
DnsServer: require "./dns_server"
# [HttpServer] handles incoming HTTP requests.
HttpServer: require "./http_server"
# [Installer] compiles and installs local and system
# configuration files.
Installer: require "./installer"
# [Logger] instances keep track of everything that
# happens during a Power daemon's lifecycle.
Logger: require "./logger"
# The [util] module contains various helper functions.
util: require "./util"

130
src/installer.coffee Executable file
View File

@@ -0,0 +1,130 @@
# The `Installer` class, in conjunction with the private
# `InstallerFile` class, creates and installs local and system
# configuration files if they're missing or out of date. It's used by
# the Pow install script to set up the system for local development.
async = require "async"
fs = require "fs"
path = require "path"
{mkdirp} = require "./util"
{chown} = require "./util"
util = require "util"
# Import the Eco templates for the `/etc/resolver` and `launchd`
# configuration files.
resolverSource = require "./templates/installer/resolver"
firewallSource = require "./templates/installer/com.hackplan.power.firewall.plist"
daemonSource = require "./templates/installer/com.hackplan.power.powerd.plist"
# `InstallerFile` represents a single file candidate for installation:
# a pathname, a string of the file's source, and optional flags
# indicating whether the file needs to be installed as root and what
# permission bits it should have.
class InstallerFile
constructor: (@path, source, @root = false, @mode = 0o644) ->
@source = source.trim()
# Check to see whether the file actually needs to be installed. If
# the file exists on the filesystem with the specified path and
# contents, `callback` is invoked with false. Otherwise, `callback`
# is invoked with true.
isStale: (callback) ->
fs.exists @path, (exists) =>
if exists
fs.readFile @path, "utf8", (err, contents) =>
if err
callback true
else
callback @source isnt contents.trim()
else
callback true
# Create all the parent directories of the file's path, if
# necessary, and then invoke `callback`.
vivifyPath: (callback) =>
mkdirp path.dirname(@path), callback
# Write the file's source to disk and invoke `callback`.
writeFile: (callback) =>
fs.writeFile @path, @source, "utf8", callback
# If the root flag is set for this file, change its ownership to the
# `root` user and `wheel` group. Then invoke `callback`.
setOwnership: (callback) =>
if @root
chown @path, "root:wheel", callback
else
callback false
# Set permissions on the installed file with `chmod`.
setPermissions: (callback) =>
fs.chmod @path, @mode, callback
# Install a file asynchronously, first by making its parent
# directory, then writing it to disk, and finally setting its
# ownership and permission bits.
install: (callback) ->
async.series [
@vivifyPath,
@writeFile,
@setOwnership,
@setPermissions
], callback
# The `Installer` class operates on a set of `InstallerFile` instances.
# It can check to see if any files are stale and whether or not root
# access is necessary for installation. It can also install any stale
# files asynchronously.
module.exports = class Installer
# Factory method that takes a `Configuration` instance and returns
# an `Installer` for system firewall and DNS configuration files.
@getSystemInstaller: (@configuration) ->
files = [
new InstallerFile "/Library/LaunchDaemons/com.hackplan.power.firewall.plist",
firewallSource(@configuration),
true
]
for domain in @configuration.domains
files.push new InstallerFile "/etc/resolver/#{domain}",
resolverSource(@configuration),
true
new Installer files
# Factory method that takes a `Configuration` instance and returns
# an `Installer` for the Pow `launchctl` daemon configuration file.
@getLocalInstaller: (@configuration) ->
new Installer [
new InstallerFile "#{process.env.HOME}/Library/LaunchAgents/com.hackplan.power.powerd.plist",
daemonSource(@configuration)
]
# Create an installer for a set of files.
constructor: (@files = []) ->
# Invoke `callback` with an array of any files that need to be
# installed.
getStaleFiles: (callback) ->
async.select @files, (file, proceed) ->
file.isStale proceed
, callback
# Invoke `callback` with a boolean argument indicating whether or
# not any files need to be installed as root.
needsRootPrivileges: (callback) ->
@getStaleFiles (files) ->
async.detect files, (file, proceed) ->
proceed file.root
, (result) ->
callback result?
# Installs any stale files asynchronously and then invokes
# `callback`.
install: (callback) ->
@getStaleFiles (files) ->
async.forEach files, (file, proceed) ->
file.install (err) ->
util.puts file.path unless err
proceed err
, callback

53
src/logger.coffee Executable file
View File

@@ -0,0 +1,53 @@
# Pow's `Logger` wraps the
# [Log.js](https://github.com/visionmedia/log.js) library in a class
# that adds log file autovivification. The log file you specify is
# automatically created the first time you call a log method.
fs = require "fs"
{dirname} = require "path"
Log = require "log"
{mkdirp} = require "./util"
module.exports = class Logger
# Log level method names that will be forwarded to the underlying
# `Log` instance.
@LEVELS: ["debug", "info", "notice", "warning", "error",
"critical", "alert", "emergency"]
# Create a `Logger` that writes to the file at the given path and
# log level. The logger begins life in the uninitialized state.
constructor: (@path, @level = "debug") ->
@readyCallbacks = []
# Invoke `callback` if the logger's state is ready. Otherwise, queue
# the callback to be invoked when the logger becomes ready, then
# start the initialization process.
ready: (callback) ->
if @state is "ready"
callback.call @
else
@readyCallbacks.push callback
unless @state
@state = "initializing"
# Make the log file's directory if it doesn't already
# exist. Reset the logger's state if an error is thrown.
mkdirp dirname(@path), (err) =>
if err
@state = null
else
# Open a write stream for the log file and create the
# underlying `Log` instance. Then set the logger state to
# ready and invoke all queued callbacks.
@stream = fs.createWriteStream @path, flags: "a"
@stream.on "open", =>
@log = new Log @level, @stream
@state = "ready"
for callback in @readyCallbacks
callback.call @
@readyCallbacks = []
# Define the log level methods as wrappers around the corresponding
# `Log` methods passing through `ready`.
for level in Logger.LEVELS then do (level) ->
Logger::[level] = (args...) ->
@ready -> @log[level].apply @log, args

View File

@@ -0,0 +1,10 @@
<%- @renderTemplate "layout", title: "Application not found", => %>
<h1 class="err">Application not found</h1>
<h2>Symlink your app to <code>~/.power/<%= @name %></code> first.</h2>
<section>
<p>When you access <code>http://<%= @host %>/</code>, Power looks for a Rack application at <code>~/.power/<%= @name %></code>. To run your app at this domain:</p>
<pre><span>$</span> cd ~/.pow
<span>$</span> ln -s /path/to/myapp <%= @name %>
<span>$</span> open http://<%= @host %>/</pre>
</section>
<% end %>

View File

@@ -0,0 +1,108 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title><%= @title %></title>
<style>
body {
margin: 0;
padding: 0;
background: #e0e0d8;
line-height: 18px;
}
div.page {
margin: 72px auto;
margin: 36px auto;
background: #fff;
border-radius: 18px;
-webkit-box-shadow: 0px 2px 7px #999;
-moz-box-shadow: 0px 2px 7px #999;
padding: 36px 90px;
width: 480px;
position: relative;
}
.big div.page {
width: 720px;
}
h1, h2, p, li {
font-family: Helvetica, sans-serif;
font-size: 13px;
}
h1 {
line-height: 45px;
font-size: 36px;
margin: 0;
}
h1:before {
font-size: 66px;
line-height: 42px;
position: absolute;
right: 576px;
}
.big h1:before {
right: 819px;
}
h1.ok {
color: #060;
}
h1.ok:before {
content: "✓";
color: #090;
}
h1.err {
color: #600;
}
h1.err:before {
content: "✗";
color: #900;
}
h2 {
line-height: 27px;
font-size: 18px;
font-weight: normal;
margin: 0;
}
a, pre span {
color: #776;
}
h2, p, pre {
color: #222;
}
pre {
white-space: pre-wrap;
font-size: 13px;
}
pre, code {
font-family: Menlo, Monaco, monospace;
}
p code {
font-size: 12px;
}
pre.breakout {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
background: #fafcf4;
margin-left: -90px;
margin-right: -90px;
padding: 8px 0 8px 90px;
}
pre.small_text {
font-size: 10px;
}
pre.small_text strong {
font-size: 13px;
}
ul {
padding: 0;
}
li {
list-style-type: none;
}
</style>
</head>
<body class="<%= @class %>">
<div class="page">
<%= @yieldContents() %>
</div>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<%- @renderTemplate "layout", title: "Power is installed", => %>
<h1 class="ok">Power is installed</h1>
<h2>You&rsquo;re running version <%= @version %>.</h2>
<section>
<p>Set up a application by symlinking it into your <code>~/.power</code> directory. The name of the symlink determines the hostname you&rsquo;ll use to access the application.</p>
<pre><span>$</span> cd ~/.power
<span>$</span> ln -s /path/to/myapp
<span>$</span> open http://myapp.<%= @domain %>/</pre>
</section>
<% end %>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.hackplan.power.firewall</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>ipfw add fwd 127.0.0.1,<%= @httpPort %> tcp from any to me dst-port <%= @dstPort %> in &amp;&amp; sysctl -w net.inet.ip.forwarding=1 &amp;&amp; sysctl -w net.inet.ip.fw.enable=1</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>root</string>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.hackplan.power.powerd</string>
<key>ProgramArguments</key>
<array>
<string><%= process.execPath %></string>
<string><%= @bin %></string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,3 @@
# Lovingly generated by Power
nameserver 127.0.0.1
port <%= @dnsPort %>

239
src/util.coffee Executable file
View File

@@ -0,0 +1,239 @@
# The `util` module houses a number of utility functions used
# throughout Pow.
fs = require "fs"
path = require "path"
async = require "async"
{execFile} = require "child_process"
{Stream} = require "stream"
# The `LineBuffer` class is a `Stream` that emits a `data` event for
# each line in the stream.
exports.LineBuffer = class LineBuffer extends Stream
# Create a `LineBuffer` around the given stream.
constructor: (@stream) ->
@readable = true
@_buffer = ""
# Install handlers for the underlying stream's `data` and `end`
# events.
self = this
@stream.on 'data', (args...) -> self.write args...
@stream.on 'end', (args...) -> self.end args...
# Write a chunk of data read from the stream to the internal buffer.
write: (chunk) ->
@_buffer += chunk
# If there's a newline in the buffer, slice the line from the
# buffer and emit it. Repeat until there are no more newlines.
while (index = @_buffer.indexOf("\n")) != -1
line = @_buffer[0...index]
@_buffer = @_buffer[index+1...@_buffer.length]
@emit 'data', line
# Process any final lines from the underlying stream's `end`
# event. If there is trailing data in the buffer, emit it.
end: (args...) ->
if args.length > 0
@write args...
@emit 'data', @_buffer if @_buffer.length
@emit 'end'
# Read lines from `stream` and invoke `callback` on each line.
exports.bufferLines = (stream, callback) ->
buffer = new LineBuffer stream
buffer.on "data", callback
buffer
# ---
# Asynchronously and recursively create a directory if it does not
# already exist. Then invoke the given callback.
exports.mkdirp = (dirname, callback) ->
fs.lstat (p = path.normalize dirname), (err, stats) ->
if err
paths = [p].concat(p = path.dirname p until p in ["/", "."])
async.forEachSeries paths.reverse(), (p, next) ->
fs.exists p, (exists) ->
if exists then next()
else fs.mkdir p, 0o755, (err) ->
if err then callback err
else next()
, callback
else if stats.isDirectory()
callback()
else
callback "file exists"
# A wrapper around `chown(8)` for taking ownership of a given path
# with the specified owner string (such as `"root:wheel"`). Invokes
# `callback` with the error string, if any, and a boolean value
# indicating whether or not the operation succeeded.
exports.chown = (path, owner, callback) ->
error = ""
exec ["chown", owner, path], (err, stdout, stderr) ->
if err then callback err, stderr
else callback null
# Capture all `data` events on the given stream and return a function
# that, when invoked, replays the captured events on the stream in
# order.
exports.pause = (stream) ->
queue = []
onData = (args...) -> queue.push ['data', args...]
onEnd = (args...) -> queue.push ['end', args...]
onClose = -> removeListeners()
removeListeners = ->
stream.removeListener 'data', onData
stream.removeListener 'end', onEnd
stream.removeListener 'close', onClose
stream.on 'data', onData
stream.on 'end', onEnd
stream.on 'close', onClose
->
removeListeners()
for args in queue
stream.emit args...
# Spawn a Bash shell with the given `env` and source the named
# `script`. Then collect its resulting environment variables and pass
# them to `callback` as the second argument. If the script returns a
# non-zero exit code, call `callback` with the error as its first
# argument, and annotate the error with the captured `stdout` and
# `stderr`.
exports.sourceScriptEnv = (script, env, options, callback) ->
if options.call
callback = options
options = {}
else
options ?= {}
# Build up the command to execute, starting with the `before`
# option, if any. Then source the given script, swallowing any
# output written to stderr. Finally, dump the current environment to
# a temporary file.
cwd = path.dirname script
filename = makeTemporaryFilename()
command = """
#{options.before ? "true"} &&
source #{quote script} > /dev/null &&
env > #{quote filename}
"""
# Run our command through Bash in the directory of the script. If an
# error occurs, rewrite the error to a more descriptive
# message. Otherwise, read and parse the environment from the
# temporary file and pass it along to the callback.
exec ["bash", "-c", command], {cwd, env}, (err, stdout, stderr) ->
if err
err.message = "'#{script}' failed to load:\n#{command}"
err.stdout = stdout
err.stderr = stderr
callback err
else readAndUnlink filename, (err, result) ->
if err then callback err
else callback null, parseEnv result
# Get the user's login environment by spawning a login shell and
# collecting its environment variables via the `env` command. (In case
# the user's shell profile script prints output to stdout or stderr,
# we must redirect `env` output to a temporary file and read that.)
#
# The returned environment will include a default `LANG` variable if
# one is not set by the user's shell. This default value of `LANG` is
# determined by joining the user's current locale with the value of
# the `defaultEncoding` parameter, or `UTF-8` if it is not set.
exports.getUserEnv = (callback, defaultEncoding = "UTF-8") ->
filename = makeTemporaryFilename()
loginExec "exec env > #{quote filename}", (err) ->
if err then callback err
else readAndUnlink filename, (err, result) ->
if err then callback err
else getUserLocale (locale) ->
env = parseEnv result
env.LANG ?= "#{locale}.#{defaultEncoding}"
callback null, env
# Execute a command without spawning a subshell. The command argument
# is an array of program name and arguments.
exec = (command, options, callback) ->
unless callback?
callback = options
options = {}
execFile "/usr/bin/env", command, options, callback
# Single-quote a string for command line execution.
quote = (string) -> "'" + string.replace(/\'/g, "'\\''") + "'"
# Generate and return a unique temporary filename based on the
# current process's PID, the number of milliseconds elapsed since the
# UNIX epoch, and a random integer.
makeTemporaryFilename = ->
tmpdir = process.env.TMPDIR ? "/tmp"
timestamp = new Date().getTime()
random = parseInt Math.random() * Math.pow(2, 16)
filename = "power.#{process.pid}.#{timestamp}.#{random}"
path.join tmpdir, filename
# Read the contents of a file, unlink the file, then invoke the
# callback with the contents of the file.
readAndUnlink = (filename, callback) ->
fs.readFile filename, "utf8", (err, contents) ->
if err then callback err
else fs.unlink filename, (err) ->
if err then callback err
else callback null, contents
# Execute the given command through a login shell and pass the
# contents of its stdout and stderr streams to the callback. In order
# to spawn a login shell, first spawn the user's shell with the `-l`
# option. If that fails, retry without `-l`; some shells, like tcsh,
# cannot be started as non-interactive login shells. If that fails,
# bubble the error up to the callback.
loginExec = (command, callback) ->
getUserShell (shell) ->
login = ["login", "-qf", process.env.LOGNAME, shell]
exec [login..., "-i", "-c", command], (err, stdout, stderr) ->
if err
exec [login..., "-c", command], callback
else
callback null, stdout, stderr
# Invoke `dscl(1)` to find out what shell the user prefers. We cannot
# rely on `process.env.SHELL` because it always seems to be
# `/bin/bash` when spawned from `launchctl`, regardless of what the
# user has set.
getUserShell = (callback) ->
command = ["dscl", ".", "-read", "/Users/#{process.env.LOGNAME}", "UserShell"]
exec command, (err, stdout, stderr) ->
if err
callback process.env.SHELL
else
if matches = stdout.trim().match /^UserShell: (.+)$/
[match, shell] = matches
callback shell
else
callback process.env.SHELL
# Read the user's current locale preference from the OS X defaults
# database. Fall back to `en_US` if it can't be determined.
getUserLocale = (callback) ->
exec ["defaults", "read", "-g", "AppleLocale"], (err, stdout, stderr) ->
locale = stdout?.trim() ? ""
locale = "en_US" unless locale.match /^\w+$/
callback locale
# Parse the output of the `env` command into a JavaScript object.
parseEnv = (stdout) ->
env = {}
for line in stdout.split "\n"
if matches = line.match /([^=]+)=(.+)/
[match, name, value] = matches
env[name] = value
env

86
uninstall.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/bin/sh
#
# This is the installation script for Power.
# See the full annotated source: http://power.hackplan.com
#
# Install Power by running this command:
# curl power.hackplan.com/install.sh | sh
#
# Uninstall Power: :'(
# curl power.hackplan.com/uninstall.sh | sh
# Set up the environment.
set -e
POWER_ROOT="$HOME/Library/Application Support/Power"
POWER_CURRENT_PATH="$POWER_ROOT/Current"
POWER_VERSIONS_PATH="$POWER_ROOT/Versions"
POWERD_PLIST_PATH="$HOME/Library/LaunchAgents/com.hackplan.power.powerd.plist"
FIREWALL_PLIST_PATH="/Library/LaunchDaemons/com.hackplan.power.firewall.plist"
POWER_CONFIG_PATH="$HOME/.powerconfig"
# Fail fast if Power isn't present.
if [[ ! -d "$POWER_CURRENT_PATH" ]] && [[ ! -a "$POWERD_PLIST_PATH" ]] && [[ ! -a "$FIREWALL_PLIST_PATH" ]]; then
echo "error: can't find Power" >&2
exit 1
fi
# Find the tty so we can prompt for confirmation even if we're being piped from curl.
TTY="/dev/$( ps -p$$ -o tty | tail -1 | awk '{print$1}' )"
# Make sure we really want to uninstall.
read -p "Sorry to see you go. Uninstall Power [y/n]? " ANSWER < $TTY
[[ $ANSWER == "y" ]] || exit 1
echo "*** Uninstalling Power..."
# Remove the Versions directory and the Current symlink.
rm -fr "$POWER_VERSIONS_PATH"
rm -f "$POWER_CURRENT_PATH"
# Unload com.hackplan.power.powerd from launchctl and remove the plist.
launchctl unload "$POWERD_PLIST_PATH" 2>/dev/null || true
rm -f "$POWERD_PLIST_PATH"
# Read the firewall plist, if possible, to figure out what ports are in use.
if [[ -a "$FIREWALL_PLIST_PATH" ]]; then
ports=($(ruby -e'puts $<.read.scan(/fwd .*?,([\d]+).*?dst-port ([\d]+)/)' "$FIREWALL_PLIST_PATH"))
HTTP_PORT=${ports[0]}
DST_PORT=${ports[1]}
fi
# Assume reasonable defaults otherwise.
[[ -z "$HTTP_PORT" ]] && HTTP_PORT=20559
[[ -z "$DST_PORT" ]] && DST_PORT=80
# Try to find the ipfw rule and delete it.
RULE=$(sudo ipfw show | (grep ",$HTTP_PORT .* dst-port $DST_PORT in" || true) | cut -f 1 -d " ")
[[ -n "$RULE" ]] && sudo ipfw del "$RULE"
# Unload the firewall plist and remove it.
sudo launchctl unload "$FIREWALL_PLIST_PATH" 2>/dev/null || true
sudo rm -f "$FIREWALL_PLIST_PATH"
# Remove /etc/resolver files that belong to us
grep -Rl 'generated by Power' /etc/resolver/ | sudo xargs rm
echo "*** Uninstalled"