mirror of
https://github.com/HackPlan/power.git
synced 2026-01-12 15:04:56 +08:00
forked from basecamp/pow
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules/
|
||||
91
Cakefile
Executable file
91
Cakefile
Executable 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
21
LICENSE
Normal 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
2
bin/power
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require("../lib/command.js");
|
||||
36
build.sh
Executable file
36
build.sh
Executable 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
162
install.sh
Normal 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
91
lib/command.js
Normal 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
237
lib/configuration.js
Normal 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
161
lib/daemon.js
Normal 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
48
lib/dns_server.js
Normal 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
245
lib/http_server.js
Normal 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
13
lib/index.js
Normal 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
138
lib/installer.js
Normal 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
77
lib/logger.js
Normal 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);
|
||||
67
lib/templates/http_server/application_not_found.html.js
Normal file
67
lib/templates/http_server/application_not_found.html.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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('');
|
||||
}
|
||||
59
lib/templates/http_server/layout.html.js
Normal file
59
lib/templates/http_server/layout.html.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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('');
|
||||
}
|
||||
61
lib/templates/http_server/welcome.html.js
Normal file
61
lib/templates/http_server/welcome.html.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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’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’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('');
|
||||
}
|
||||
55
lib/templates/installer/com.hackplan.power.firewall.plist.js
Normal file
55
lib/templates/installer/com.hackplan.power.firewall.plist.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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 && sysctl -w net.inet.ip.forwarding=1 && 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('');
|
||||
}
|
||||
55
lib/templates/installer/com.hackplan.power.powerd.plist.js
Normal file
55
lib/templates/installer/com.hackplan.power.powerd.plist.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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('');
|
||||
}
|
||||
51
lib/templates/installer/resolver.js
Normal file
51
lib/templates/installer/resolver.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
};
|
||||
}
|
||||
(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
337
lib/util.js
Normal 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
36
package.json
Executable 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
86
src/command.coffee
Executable 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
235
src/configuration.coffee
Executable 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
107
src/daemon.coffee
Executable 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
43
src/dns_server.coffee
Executable 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
212
src/http_server.coffee
Executable 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
35
src/index.coffee
Executable 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
130
src/installer.coffee
Executable 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
53
src/logger.coffee
Executable 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
|
||||
10
src/templates/http_server/application_not_found.html.eco
Normal file
10
src/templates/http_server/application_not_found.html.eco
Normal 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 %>
|
||||
108
src/templates/http_server/layout.html.eco
Normal file
108
src/templates/http_server/layout.html.eco
Normal 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>
|
||||
10
src/templates/http_server/welcome.html.eco
Normal file
10
src/templates/http_server/welcome.html.eco
Normal file
@@ -0,0 +1,10 @@
|
||||
<%- @renderTemplate "layout", title: "Power is installed", => %>
|
||||
<h1 class="ok">Power is installed</h1>
|
||||
<h2>You’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’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 %>
|
||||
18
src/templates/installer/com.hackplan.power.firewall.plist.eco
Executable file
18
src/templates/installer/com.hackplan.power.firewall.plist.eco
Executable 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 && sysctl -w net.inet.ip.forwarding=1 && sysctl -w net.inet.ip.fw.enable=1</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>UserName</key>
|
||||
<string>root</string>
|
||||
</dict>
|
||||
</plist>
|
||||
17
src/templates/installer/com.hackplan.power.powerd.plist.eco
Executable file
17
src/templates/installer/com.hackplan.power.powerd.plist.eco
Executable 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>
|
||||
3
src/templates/installer/resolver.eco
Executable file
3
src/templates/installer/resolver.eco
Executable file
@@ -0,0 +1,3 @@
|
||||
# Lovingly generated by Power
|
||||
nameserver 127.0.0.1
|
||||
port <%= @dnsPort %>
|
||||
239
src/util.coffee
Executable file
239
src/util.coffee
Executable 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
86
uninstall.sh
Normal 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"
|
||||
Reference in New Issue
Block a user