Pow’s DnsServer is designed to respond to DNS A queries with
92 | 127.0.0.1 for all subdomains of the specified top-level domain.
93 | When used in conjunction with Mac OS X’s /etc/resolver
94 | system,
95 | there’s no configuration needed to add and remove host names for
96 | local web development.
Each incoming DNS request ends up here. If it’s an A query
157 | and the domain name matches the top-level domain specified in our
158 | configuration, we respond with 127.0.0.1. Otherwise, we respond
159 | with NXDOMAIN.
This is the annotated source code for Pow, a
92 | zero-configuration Rack server for Mac OS X. See the user’s
93 | manual for information on installation
94 | and usage.
95 |
The annotated source HTML is generated by
96 | Docco.
Pow’s Logger wraps the
92 | Log.js library in a class
93 | that adds log file autovivification. The log file you specify is
94 | automatically created the first time you call a log method.
Invoke callback if the logger’s state is ready. Otherwise, queue
150 | the callback to be invoked when the logger becomes ready, then
151 | start the initialization process.
Open a write stream for the log file and create the
192 | underlying Log instance. Then set the logger state to
193 | ready and invoke all queued callbacks.
194 |
195 |
196 |
197 |
@stream = fs.createWriteStream @path, flags: "a"
198 | @stream.on"open", =>
199 | @log = new Log @level, @stream
200 | @state = "ready"
201 | for callback in @readyCallbacks
202 | callback.call @
203 | @readyCallbacks = []
Symlink your app to ~/.pow/');
47 | __out.push(__sanitize(_this.name));
48 | __out.push(' first.
\n \n
When you access http://');
49 | __out.push(__sanitize(_this.host));
50 | __out.push('/, Pow looks for a Rack application at ~/.pow/');
51 | __out.push(__sanitize(_this.name));
52 | __out.push('. To run your app at this domain:
You’re running version ');
47 | __out.push(__sanitize(_this.version));
48 | __out.push('.
\n \n
Set up a Rack application by symlinking it into your ~/.pow directory. The name of the symlink determines the hostname you’ll use to access the application.
\n
$ cd ~/.pow\n$ ln -s /path/to/myapp\n$ open http://myapp.');
49 | __out.push(__sanitize(_this.domain));
50 | return __out.push('/
\n \n');
51 | });
52 | }));
53 |
54 | __out.push('\n');
55 |
56 | }).call(this);
57 |
58 | }).call(__obj);
59 | __obj.safe = __objSafe, __obj.escape = __escape;
60 | return __out.join('');
61 | }
--------------------------------------------------------------------------------
/lib/templates/installer/cx.pow.firewall.plist.js:
--------------------------------------------------------------------------------
1 | module.exports = function(__obj) {
2 | if (!__obj) __obj = {};
3 | var __out = [], __capture = function(callback) {
4 | var out = __out, result;
5 | __out = [];
6 | callback.call(this);
7 | result = __out.join('');
8 | __out = out;
9 | return __safe(result);
10 | }, __sanitize = function(value) {
11 | if (value && value.ecoSafe) {
12 | return value;
13 | } else if (typeof value !== 'undefined' && value != null) {
14 | return __escape(value);
15 | } else {
16 | return '';
17 | }
18 | }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
19 | __safe = __obj.safe = function(value) {
20 | if (value && value.ecoSafe) {
21 | return value;
22 | } else {
23 | if (!(typeof value !== 'undefined' && value != null)) value = '';
24 | var result = new String(value);
25 | result.ecoSafe = true;
26 | return result;
27 | }
28 | };
29 | if (!__escape) {
30 | __escape = __obj.escape = function(value) {
31 | return ('' + value)
32 | .replace(/&/g, '&')
33 | .replace(//g, '>')
35 | .replace(/"/g, '"');
36 | };
37 | }
38 | (function() {
39 | (function() {
40 | __out.push('\n\n\n\n Label\n cx.pow.firewall\n ProgramArguments\n \n /bin/sh\n -c\n \n sysctl -w net.inet.ip.forwarding=1;\n echo "rdr pass proto tcp from any to any port {');
41 |
42 | __out.push(__sanitize(this.dstPort));
43 |
44 | __out.push(',');
45 |
46 | __out.push(__sanitize(this.httpPort));
47 |
48 | __out.push('} -> 127.0.0.1 port ');
49 |
50 | __out.push(__sanitize(this.httpPort));
51 |
52 | __out.push('" | pfctl -a "com.apple/250.PowFirewall" -Ef -\n \n \n RunAtLoad\n \n UserName\n root\n\n\n');
53 |
54 | }).call(this);
55 |
56 | }).call(__obj);
57 | __obj.safe = __objSafe, __obj.escape = __escape;
58 | return __out.join('');
59 | }
--------------------------------------------------------------------------------
/lib/templates/installer/cx.pow.powd.plist.js:
--------------------------------------------------------------------------------
1 | module.exports = function(__obj) {
2 | if (!__obj) __obj = {};
3 | var __out = [], __capture = function(callback) {
4 | var out = __out, result;
5 | __out = [];
6 | callback.call(this);
7 | result = __out.join('');
8 | __out = out;
9 | return __safe(result);
10 | }, __sanitize = function(value) {
11 | if (value && value.ecoSafe) {
12 | return value;
13 | } else if (typeof value !== 'undefined' && value != null) {
14 | return __escape(value);
15 | } else {
16 | return '';
17 | }
18 | }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
19 | __safe = __obj.safe = function(value) {
20 | if (value && value.ecoSafe) {
21 | return value;
22 | } else {
23 | if (!(typeof value !== 'undefined' && value != null)) value = '';
24 | var result = new String(value);
25 | result.ecoSafe = true;
26 | return result;
27 | }
28 | };
29 | if (!__escape) {
30 | __escape = __obj.escape = function(value) {
31 | return ('' + value)
32 | .replace(/&/g, '&')
33 | .replace(//g, '>')
35 | .replace(/"/g, '"');
36 | };
37 | }
38 | (function() {
39 | (function() {
40 | __out.push('\n\n\n\n Label\n cx.pow.powd\n ProgramArguments\n \n ');
41 |
42 | __out.push(__sanitize(process.execPath));
43 |
44 | __out.push('\n ');
45 |
46 | __out.push(__sanitize(this.bin));
47 |
48 | __out.push('\n \n KeepAlive\n \n RunAtLoad\n \n\n\n');
49 |
50 | }).call(this);
51 |
52 | }).call(__obj);
53 | __obj.safe = __objSafe, __obj.escape = __escape;
54 | return __out.join('');
55 | }
--------------------------------------------------------------------------------
/lib/templates/installer/resolver.js:
--------------------------------------------------------------------------------
1 | module.exports = function(__obj) {
2 | if (!__obj) __obj = {};
3 | var __out = [], __capture = function(callback) {
4 | var out = __out, result;
5 | __out = [];
6 | callback.call(this);
7 | result = __out.join('');
8 | __out = out;
9 | return __safe(result);
10 | }, __sanitize = function(value) {
11 | if (value && value.ecoSafe) {
12 | return value;
13 | } else if (typeof value !== 'undefined' && value != null) {
14 | return __escape(value);
15 | } else {
16 | return '';
17 | }
18 | }, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
19 | __safe = __obj.safe = function(value) {
20 | if (value && value.ecoSafe) {
21 | return value;
22 | } else {
23 | if (!(typeof value !== 'undefined' && value != null)) value = '';
24 | var result = new String(value);
25 | result.ecoSafe = true;
26 | return result;
27 | }
28 | };
29 | if (!__escape) {
30 | __escape = __obj.escape = function(value) {
31 | return ('' + value)
32 | .replace(/&/g, '&')
33 | .replace(//g, '>')
35 | .replace(/"/g, '"');
36 | };
37 | }
38 | (function() {
39 | (function() {
40 | __out.push('# Lovingly generated by Pow\nnameserver 127.0.0.1\nport ');
41 |
42 | __out.push(__sanitize(this.dnsPort));
43 |
44 | __out.push('\n');
45 |
46 | }).call(this);
47 |
48 | }).call(__obj);
49 | __obj.safe = __objSafe, __obj.escape = __escape;
50 | return __out.join('');
51 | }
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.6.2
2 | (function() {
3 | var LineBuffer, Stream, async, exec, execFile, fs, getUserLocale, getUserShell, loginExec, makeTemporaryFilename, parseEnv, path, quote, readAndUnlink,
4 | __hasProp = {}.hasOwnProperty,
5 | __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; },
6 | __slice = [].slice;
7 |
8 | fs = require("fs");
9 |
10 | path = require("path");
11 |
12 | async = require("async");
13 |
14 | execFile = require("child_process").execFile;
15 |
16 | Stream = require("stream").Stream;
17 |
18 | exports.LineBuffer = LineBuffer = (function(_super) {
19 | __extends(LineBuffer, _super);
20 |
21 | function LineBuffer(stream) {
22 | var self;
23 |
24 | this.stream = stream;
25 | this.readable = true;
26 | this._buffer = "";
27 | self = this;
28 | this.stream.on('data', function() {
29 | var args;
30 |
31 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
32 | return self.write.apply(self, args);
33 | });
34 | this.stream.on('end', function() {
35 | var args;
36 |
37 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
38 | return self.end.apply(self, args);
39 | });
40 | }
41 |
42 | LineBuffer.prototype.write = function(chunk) {
43 | var index, line, _results;
44 |
45 | this._buffer += chunk;
46 | _results = [];
47 | while ((index = this._buffer.indexOf("\n")) !== -1) {
48 | line = this._buffer.slice(0, index);
49 | this._buffer = this._buffer.slice(index + 1, this._buffer.length);
50 | _results.push(this.emit('data', line));
51 | }
52 | return _results;
53 | };
54 |
55 | LineBuffer.prototype.end = function() {
56 | var args;
57 |
58 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
59 | if (args.length > 0) {
60 | this.write.apply(this, args);
61 | }
62 | if (this._buffer.length) {
63 | this.emit('data', this._buffer);
64 | }
65 | return this.emit('end');
66 | };
67 |
68 | return LineBuffer;
69 |
70 | })(Stream);
71 |
72 | exports.bufferLines = function(stream, callback) {
73 | var buffer;
74 |
75 | buffer = new LineBuffer(stream);
76 | buffer.on("data", callback);
77 | return buffer;
78 | };
79 |
80 | exports.mkdirp = function(dirname, callback) {
81 | var p;
82 |
83 | return fs.lstat((p = path.normalize(dirname)), function(err, stats) {
84 | var paths;
85 |
86 | if (err) {
87 | paths = [p].concat((function() {
88 | var _results;
89 |
90 | _results = [];
91 | while (p !== "/" && p !== ".") {
92 | _results.push(p = path.dirname(p));
93 | }
94 | return _results;
95 | })());
96 | return async.forEachSeries(paths.reverse(), function(p, next) {
97 | return fs.exists(p, function(exists) {
98 | if (exists) {
99 | return next();
100 | } else {
101 | return fs.mkdir(p, 0x1ed, function(err) {
102 | if (err) {
103 | return callback(err);
104 | } else {
105 | return next();
106 | }
107 | });
108 | }
109 | });
110 | }, callback);
111 | } else if (stats.isDirectory()) {
112 | return callback();
113 | } else {
114 | return callback("file exists");
115 | }
116 | });
117 | };
118 |
119 | exports.chown = function(path, owner, callback) {
120 | var error;
121 |
122 | error = "";
123 | return exec(["chown", owner, path], function(err, stdout, stderr) {
124 | if (err) {
125 | return callback(err, stderr);
126 | } else {
127 | return callback(null);
128 | }
129 | });
130 | };
131 |
132 | exports.pause = function(stream) {
133 | var onClose, onData, onEnd, queue, removeListeners;
134 |
135 | queue = [];
136 | onData = function() {
137 | var args;
138 |
139 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
140 | return queue.push(['data'].concat(__slice.call(args)));
141 | };
142 | onEnd = function() {
143 | var args;
144 |
145 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
146 | return queue.push(['end'].concat(__slice.call(args)));
147 | };
148 | onClose = function() {
149 | return removeListeners();
150 | };
151 | removeListeners = function() {
152 | stream.removeListener('data', onData);
153 | stream.removeListener('end', onEnd);
154 | return stream.removeListener('close', onClose);
155 | };
156 | stream.on('data', onData);
157 | stream.on('end', onEnd);
158 | stream.on('close', onClose);
159 | return function() {
160 | var args, _i, _len, _results;
161 |
162 | removeListeners();
163 | _results = [];
164 | for (_i = 0, _len = queue.length; _i < _len; _i++) {
165 | args = queue[_i];
166 | _results.push(stream.emit.apply(stream, args));
167 | }
168 | return _results;
169 | };
170 | };
171 |
172 | exports.sourceScriptEnv = function(script, env, options, callback) {
173 | var command, cwd, filename, _ref;
174 |
175 | if (options.call) {
176 | callback = options;
177 | options = {};
178 | } else {
179 | if (options == null) {
180 | options = {};
181 | }
182 | }
183 | cwd = path.dirname(script);
184 | filename = makeTemporaryFilename();
185 | command = "" + ((_ref = options.before) != null ? _ref : "true") + " &&\nsource " + (quote(script)) + " > /dev/null &&\nenv > " + (quote(filename));
186 | return exec(["bash", "-c", command], {
187 | cwd: cwd,
188 | env: env
189 | }, function(err, stdout, stderr) {
190 | if (err) {
191 | err.message = "'" + script + "' failed to load:\n" + command;
192 | err.stdout = stdout;
193 | err.stderr = stderr;
194 | return callback(err);
195 | } else {
196 | return readAndUnlink(filename, function(err, result) {
197 | if (err) {
198 | return callback(err);
199 | } else {
200 | return callback(null, parseEnv(result));
201 | }
202 | });
203 | }
204 | });
205 | };
206 |
207 | exports.getUserEnv = function(callback, defaultEncoding) {
208 | var filename;
209 |
210 | if (defaultEncoding == null) {
211 | defaultEncoding = "UTF-8";
212 | }
213 | filename = makeTemporaryFilename();
214 | return loginExec("exec env > " + (quote(filename)), function(err) {
215 | if (err) {
216 | return callback(err);
217 | } else {
218 | return readAndUnlink(filename, function(err, result) {
219 | if (err) {
220 | return callback(err);
221 | } else {
222 | return getUserLocale(function(locale) {
223 | var env, _ref;
224 |
225 | env = parseEnv(result);
226 | if ((_ref = env.LANG) == null) {
227 | env.LANG = "" + locale + "." + defaultEncoding;
228 | }
229 | return callback(null, env);
230 | });
231 | }
232 | });
233 | }
234 | });
235 | };
236 |
237 | exec = function(command, options, callback) {
238 | if (callback == null) {
239 | callback = options;
240 | options = {};
241 | }
242 | return execFile("/usr/bin/env", command, options, callback);
243 | };
244 |
245 | quote = function(string) {
246 | return "'" + string.replace(/\'/g, "'\\''") + "'";
247 | };
248 |
249 | makeTemporaryFilename = function() {
250 | var filename, random, timestamp, tmpdir, _ref;
251 |
252 | tmpdir = (_ref = process.env.TMPDIR) != null ? _ref : "/tmp";
253 | timestamp = new Date().getTime();
254 | random = parseInt(Math.random() * Math.pow(2, 16));
255 | filename = "pow." + process.pid + "." + timestamp + "." + random;
256 | return path.join(tmpdir, filename);
257 | };
258 |
259 | readAndUnlink = function(filename, callback) {
260 | return fs.readFile(filename, "utf8", function(err, contents) {
261 | if (err) {
262 | return callback(err);
263 | } else {
264 | return fs.unlink(filename, function(err) {
265 | if (err) {
266 | return callback(err);
267 | } else {
268 | return callback(null, contents);
269 | }
270 | });
271 | }
272 | });
273 | };
274 |
275 | loginExec = function(command, callback) {
276 | return getUserShell(function(shell) {
277 | var login;
278 |
279 | login = ["login", "-qf", process.env.LOGNAME, shell];
280 | return exec(__slice.call(login).concat(["-i"], ["-c"], [command]), function(err, stdout, stderr) {
281 | if (err) {
282 | return exec(__slice.call(login).concat(["-c"], [command]), callback);
283 | } else {
284 | return callback(null, stdout, stderr);
285 | }
286 | });
287 | });
288 | };
289 |
290 | getUserShell = function(callback) {
291 | var command;
292 |
293 | command = ["dscl", ".", "-read", "/Users/" + process.env.LOGNAME, "UserShell"];
294 | return exec(command, function(err, stdout, stderr) {
295 | var match, matches, shell;
296 |
297 | if (err) {
298 | return callback(process.env.SHELL);
299 | } else {
300 | if (matches = stdout.trim().match(/^UserShell: (.+)$/)) {
301 | match = matches[0], shell = matches[1];
302 | return callback(shell);
303 | } else {
304 | return callback(process.env.SHELL);
305 | }
306 | }
307 | });
308 | };
309 |
310 | getUserLocale = function(callback) {
311 | return exec(["defaults", "read", "-g", "AppleLocale"], function(err, stdout, stderr) {
312 | var locale, _ref;
313 |
314 | locale = (_ref = stdout != null ? stdout.trim() : void 0) != null ? _ref : "";
315 | if (!locale.match(/^\w+$/)) {
316 | locale = "en_US";
317 | }
318 | return callback(locale);
319 | });
320 | };
321 |
322 | parseEnv = function(stdout) {
323 | var env, line, match, matches, name, value, _i, _len, _ref;
324 |
325 | env = {};
326 | _ref = stdout.split("\n");
327 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
328 | line = _ref[_i];
329 | if (matches = line.match(/([^=]+)=(.+)/)) {
330 | match = matches[0], name = matches[1], value = matches[2];
331 | env[name] = value;
332 | }
333 | }
334 | return env;
335 | };
336 |
337 | }).call(this);
338 |
--------------------------------------------------------------------------------
/libexec/pow_rvm_deprecation_notice:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | HOST="$1"
4 | RVMRC_PATH="$PWD/.rvmrc"
5 | SUPPORT_ROOT="$HOME/Library/Application Support/Pow"
6 | DISABLED_FILE="$SUPPORT_ROOT/.disableRvmDeprecationNotices"
7 | NOTIFIED_FILE="$SUPPORT_ROOT/.rvmDeprecationNotices"
8 |
9 | if [ -z "$HOST" ] || [ -f "$DISABLED_FILE" ]; then
10 | exit
11 | fi
12 |
13 | if [ -f "$NOTIFIED_FILE" ] && grep -xF "$RVMRC_PATH" "$NOTIFIED_FILE"; then
14 | exit
15 | fi
16 |
17 | for file in .powrc .powenv; do
18 | if [ -f "$file" ] && egrep 'source.+rvmrc' "$file"; then
19 | exit
20 | fi
21 | done
22 |
23 | echo "$RVMRC_PATH" >> "$NOTIFIED_FILE"
24 | open "http://$HOST/__pow__/rvm_deprecation"
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pow",
3 | "description": "Zero-configuration Rack server for Mac OS X",
4 | "version": "0.6.0",
5 | "author": "Sam Stephenson",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/sstephenson/pow.git"
9 | },
10 | "bin": {
11 | "pow": "./bin/pow"
12 | },
13 | "main": "./lib/index.js",
14 | "dependencies": {
15 | "async": "0.1.22",
16 | "connect": "~> 1.8.0",
17 | "log": ">= 1.1.1",
18 | "nack": "~> 0.16",
19 | "request": "~> 2.16",
20 | "dnsserver": "https://github.com/sstephenson/dnsserver.js/tarball/4f2c713b2e"
21 | },
22 | "devDependencies": {
23 | "eco": "1.1.0-rc-3",
24 | "nodeunit": "0.8.0",
25 | "coffee-script": "1.6.2",
26 | "docco": "0.6.3"
27 | },
28 | "engines": {
29 | "node": ">= 0.10.0"
30 | },
31 | "scripts": {
32 | "pretest": "cake pretest",
33 | "test": "cake test",
34 | "start": "cake start",
35 | "stop": "cake stop",
36 | "build": "cake build",
37 | "docs": "cake docs"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/command.coffee:
--------------------------------------------------------------------------------
1 | # The `command` module is loaded when the `pow` binary runs. It parses
2 | # any command-line arguments and determines whether to install Pow's
3 | # configuration files or start the daemon itself.
4 |
5 | {Daemon, Configuration, Installer} = require ".."
6 | util = require "util"
7 |
8 | # Set the process's title to `pow` so it's easier to find in `ps`,
9 | # `top`, Activity Monitor, and so on.
10 | process.title = "pow"
11 |
12 | # Print valid command-line arguments and exit with a non-zero exit
13 | # code if invalid arguments are passed to the `pow` binary.
14 | usage = ->
15 | console.error "usage: pow [--print-config | --install-local | --install-system [--dry-run]]"
16 | process.exit -1
17 |
18 | # Start by loading the user configuration from `~/.powconfig`, if it
19 | # exists. The user configuration affects both the installer and the
20 | # daemon.
21 | Configuration.getUserConfiguration (err, configuration) ->
22 | throw err if err
23 |
24 | printConfig = false
25 | createInstaller = null
26 | dryRun = false
27 |
28 | for arg in process.argv.slice(2)
29 | # Set a flag if --print-config is requested.
30 | if arg is "--print-config"
31 | printConfig = true
32 | # Cache the factory method for creating a local or system
33 | # installer if necessary.
34 | else if arg is "--install-local"
35 | createInstaller = Installer.getLocalInstaller
36 | else if arg is "--install-system"
37 | createInstaller = Installer.getSystemInstaller
38 | # Set a flag if a dry run is requested.
39 | else if arg is "--dry-run"
40 | dryRun = true
41 | # Abort if we encounter an unknown argument.
42 | else
43 | usage()
44 |
45 | # Abort if a dry run is requested without installing anything.
46 | if dryRun and not createInstaller
47 | usage()
48 |
49 | # Print out the current configuration in a format that can be
50 | # evaluated by a shell script (`eval $(pow --print-config)`).
51 | else if printConfig
52 | underscore = (string) ->
53 | string.replace /(.)([A-Z])/g, (match, left, right) ->
54 | left + "_" + right.toLowerCase()
55 |
56 | shellEscape = (string) ->
57 | "'" + string.toString().replace(/'/g, "'\\''") + "'" #'
58 |
59 | for key, value of configuration.toJSON()
60 | util.puts "POW_" + underscore(key).toUpperCase() +
61 | "=" + shellEscape(value)
62 |
63 | # Create the installer, passing in our loaded configuration.
64 | else if createInstaller
65 | installer = createInstaller configuration
66 | # If a dry run was requested, check to see whether any files need
67 | # to be installed with root privileges. If yes, exit with a status
68 | # of 1. If no, exit with a status of 0.
69 | if dryRun
70 | installer.needsRootPrivileges (needsRoot) ->
71 | exitCode = if needsRoot then 1 else 0
72 | installer.getStaleFiles (files) ->
73 | util.puts file.path for file in files
74 | process.exit exitCode
75 | # Otherwise, install all the requested files, printing the full
76 | # path of each installed file to stdout.
77 | else
78 | installer.install (err) ->
79 | throw err if err
80 |
81 | # Start up the Pow daemon if no arguments were passed. Terminate the
82 | # process if the daemon requests a restart.
83 | else
84 | daemon = new Daemon configuration
85 | daemon.on "restart", -> process.exit()
86 | daemon.start()
87 |
--------------------------------------------------------------------------------
/src/daemon.coffee:
--------------------------------------------------------------------------------
1 | # A `Daemon` is the root object in a Pow process. It's responsible for
2 | # starting and stopping an `HttpServer` and a `DnsServer` in tandem.
3 |
4 | {EventEmitter} = require "events"
5 | HttpServer = require "./http_server"
6 | DnsServer = require "./dns_server"
7 | fs = require "fs"
8 | path = require "path"
9 |
10 | module.exports = class Daemon extends EventEmitter
11 | # Create a new `Daemon` with the given `Configuration` instance.
12 | constructor: (@configuration) ->
13 | # `HttpServer` and `DnsServer` instances are created accordingly.
14 | @httpServer = new HttpServer @configuration
15 | @dnsServer = new DnsServer @configuration
16 | # The daemon stops in response to `SIGINT`, `SIGTERM` and
17 | # `SIGQUIT` signals.
18 | process.on "SIGINT", @stop
19 | process.on "SIGTERM", @stop
20 | process.on "SIGQUIT", @stop
21 |
22 | # Watch for changes to the host root directory once the daemon has
23 | # started. When the directory changes and the `restart.txt` file
24 | # is present, remove it and emit a `restart` event.
25 | hostRoot = @configuration.hostRoot
26 | @restartFilename = path.join hostRoot, "restart.txt"
27 | @on "start", => @watcher = fs.watch hostRoot, persistent: false, @hostRootChanged
28 | @on "stop", => @watcher?.close()
29 |
30 | hostRootChanged: =>
31 | fs.exists @restartFilename, (exists) =>
32 | @restart() if exists
33 |
34 | # Remove the `~/.pow/restart.txt` file, if present, and emit a
35 | # `restart` event. The `pow` command observes this event and
36 | # terminates the process in response, causing Launch Services to
37 | # restart the server.
38 | restart: ->
39 | fs.unlink @restartFilename, (err) =>
40 | @emit "restart" unless err
41 |
42 | # Start the daemon if it's stopped. The process goes like this:
43 | #
44 | # * First, start the HTTP server. If the HTTP server can't boot,
45 | # emit an `error` event and abort.
46 | # * Next, start the DNS server. If the DNS server can't boot, stop
47 | # the HTTP server, emit an `error` event and abort.
48 | # * If both servers start up successfully, emit a `start` event and
49 | # mark the daemon as started.
50 | start: ->
51 | return if @starting or @started
52 | @starting = true
53 |
54 | startServer = (server, port, callback) -> process.nextTick ->
55 | try
56 | server.on 'error', callback
57 |
58 | server.once 'listening', ->
59 | server.removeListener 'error', callback
60 | callback()
61 |
62 | server.listen port
63 |
64 | catch err
65 | callback err
66 |
67 | pass = =>
68 | @starting = false
69 | @started = true
70 | @emit "start"
71 |
72 | flunk = (err) =>
73 | @starting = false
74 | try @httpServer.close()
75 | try @dnsServer.close()
76 | @emit "error", err
77 |
78 | {httpPort, dnsPort} = @configuration
79 | startServer @httpServer, httpPort, (err) =>
80 | if err then flunk err
81 | else startServer @dnsServer, dnsPort, (err) =>
82 | if err then flunk err
83 | else pass()
84 |
85 | # Stop the daemon if it's started. This means calling `close` on
86 | # both servers in succession, beginning with the HTTP server, and
87 | # waiting for the servers to notify us that they're done. The daemon
88 | # emits a `stop` event when this process is complete.
89 | stop: =>
90 | return if @stopping or !@started
91 | @stopping = true
92 |
93 | stopServer = (server, callback) -> process.nextTick ->
94 | try
95 | close = ->
96 | server.removeListener "close", close
97 | callback null
98 | server.on "close", close
99 | server.close()
100 | catch err
101 | callback err
102 |
103 | stopServer @httpServer, =>
104 | stopServer @dnsServer, =>
105 | @stopping = false
106 | @started = false
107 | @emit "stop"
108 |
--------------------------------------------------------------------------------
/src/dns_server.coffee:
--------------------------------------------------------------------------------
1 | # Pow's `DnsServer` is designed to respond to DNS `A` queries with
2 | # `127.0.0.1` for all subdomains of the specified top-level domain.
3 | # When used in conjunction with Mac OS X's [/etc/resolver
4 | # system](http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/resolver.5.html),
5 | # there's no configuration needed to add and remove host names for
6 | # local web development.
7 |
8 | dnsserver = require "dnsserver"
9 |
10 | NS_T_A = 1
11 | NS_C_IN = 1
12 | NS_RCODE_NXDOMAIN = 3
13 |
14 | module.exports = class DnsServer extends dnsserver.Server
15 | # Create a `DnsServer` with the given `Configuration` instance. The
16 | # server installs a single event handler for responding to DNS
17 | # queries.
18 | constructor: (@configuration) ->
19 | super
20 | @on "request", @handleRequest
21 |
22 | # The `listen` method is just a wrapper around `bind` that makes
23 | # `DnsServer` quack like a `HttpServer` (for initialization, at
24 | # least).
25 | listen: (port, callback) ->
26 | @bind port
27 | callback?()
28 |
29 | # Each incoming DNS request ends up here. If it's an `A` query
30 | # and the domain name matches the top-level domain specified in our
31 | # configuration, we respond with `127.0.0.1`. Otherwise, we respond
32 | # with `NXDOMAIN`.
33 | handleRequest: (req, res) =>
34 | pattern = @configuration.dnsDomainPattern
35 |
36 | q = req.question ? {}
37 |
38 | if q.type is NS_T_A and q.class is NS_C_IN and pattern.test q.name
39 | res.addRR q.name, NS_T_A, NS_C_IN, 600, "127.0.0.1"
40 | else
41 | res.header.rcode = NS_RCODE_NXDOMAIN
42 |
43 | res.send()
44 |
--------------------------------------------------------------------------------
/src/index.coffee:
--------------------------------------------------------------------------------
1 | # This is the annotated source code for [Pow](http://pow.cx/), a
2 | # zero-configuration Rack server for Mac OS X. See the [user's
3 | # manual](http://pow.cx/manual.html) for information on installation
4 | # and usage.
5 | #
6 | # The annotated source HTML is generated by
7 | # [Docco](http://jashkenas.github.com/docco/).
8 |
9 | # ## Table of contents
10 | module.exports =
11 |
12 | # The [Configuration](configuration.html) class stores settings for
13 | # a Pow daemon and is responsible for mapping hostnames to Rack
14 | # applications.
15 | Configuration: require "./configuration"
16 |
17 | # The [Daemon](daemon.html) class represents a running Pow daemon.
18 | Daemon: require "./daemon"
19 |
20 | # [DnsServer](dns_server.html) handles incoming DNS queries.
21 | DnsServer: require "./dns_server"
22 |
23 | # [HttpServer](http_server.html) handles incoming HTTP requests.
24 | HttpServer: require "./http_server"
25 |
26 | # [Installer](installer.html) compiles and installs local and system
27 | # configuration files.
28 | Installer: require "./installer"
29 |
30 | # [Logger](logger.html) instances keep track of everything that
31 | # happens during a Pow daemon's lifecycle.
32 | Logger: require "./logger"
33 |
34 | # [RackApplication](rack_application.html) represents a single
35 | # running application.
36 | RackApplication: require "./rack_application"
37 |
38 | # The [util](util.html) module contains various helper functions.
39 | util: require "./util"
40 |
--------------------------------------------------------------------------------
/src/installer.coffee:
--------------------------------------------------------------------------------
1 | # The `Installer` class, in conjunction with the private
2 | # `InstallerFile` class, creates and installs local and system
3 | # configuration files if they're missing or out of date. It's used by
4 | # the Pow install script to set up the system for local development.
5 |
6 | async = require "async"
7 | fs = require "fs"
8 | path = require "path"
9 | {mkdirp} = require "./util"
10 | {chown} = require "./util"
11 | util = require "util"
12 |
13 | # Import the Eco templates for the `/etc/resolver` and `launchd`
14 | # configuration files.
15 | resolverSource = require "./templates/installer/resolver"
16 | firewallSource = require "./templates/installer/cx.pow.firewall.plist"
17 | daemonSource = require "./templates/installer/cx.pow.powd.plist"
18 |
19 | # `InstallerFile` represents a single file candidate for installation:
20 | # a pathname, a string of the file's source, and optional flags
21 | # indicating whether the file needs to be installed as root and what
22 | # permission bits it should have.
23 | class InstallerFile
24 | constructor: (@path, source, @root = false, @mode = 0o644) ->
25 | @source = source.trim()
26 |
27 | # Check to see whether the file actually needs to be installed. If
28 | # the file exists on the filesystem with the specified path and
29 | # contents, `callback` is invoked with false. Otherwise, `callback`
30 | # is invoked with true.
31 | isStale: (callback) ->
32 | fs.exists @path, (exists) =>
33 | if exists
34 | fs.readFile @path, "utf8", (err, contents) =>
35 | if err
36 | callback true
37 | else
38 | callback @source isnt contents.trim()
39 | else
40 | callback true
41 |
42 | # Create all the parent directories of the file's path, if
43 | # necessary, and then invoke `callback`.
44 | vivifyPath: (callback) =>
45 | mkdirp path.dirname(@path), callback
46 |
47 | # Write the file's source to disk and invoke `callback`.
48 | writeFile: (callback) =>
49 | fs.writeFile @path, @source, "utf8", callback
50 |
51 | # If the root flag is set for this file, change its ownership to the
52 | # `root` user and `wheel` group. Then invoke `callback`.
53 | setOwnership: (callback) =>
54 | if @root
55 | chown @path, "root:wheel", callback
56 | else
57 | callback false
58 |
59 | # Set permissions on the installed file with `chmod`.
60 | setPermissions: (callback) =>
61 | fs.chmod @path, @mode, callback
62 |
63 | # Install a file asynchronously, first by making its parent
64 | # directory, then writing it to disk, and finally setting its
65 | # ownership and permission bits.
66 | install: (callback) ->
67 | async.series [
68 | @vivifyPath,
69 | @writeFile,
70 | @setOwnership,
71 | @setPermissions
72 | ], callback
73 |
74 | # The `Installer` class operates on a set of `InstallerFile` instances.
75 | # It can check to see if any files are stale and whether or not root
76 | # access is necessary for installation. It can also install any stale
77 | # files asynchronously.
78 | module.exports = class Installer
79 | # Factory method that takes a `Configuration` instance and returns
80 | # an `Installer` for system firewall and DNS configuration files.
81 | @getSystemInstaller: (@configuration) ->
82 | files = [
83 | new InstallerFile "/Library/LaunchDaemons/cx.pow.firewall.plist",
84 | firewallSource(@configuration),
85 | true
86 | ]
87 |
88 | for domain in @configuration.domains
89 | files.push new InstallerFile "/etc/resolver/#{domain}",
90 | resolverSource(@configuration),
91 | true
92 |
93 | new Installer files
94 |
95 | # Factory method that takes a `Configuration` instance and returns
96 | # an `Installer` for the Pow `launchctl` daemon configuration file.
97 | @getLocalInstaller: (@configuration) ->
98 | new Installer [
99 | new InstallerFile "#{process.env.HOME}/Library/LaunchAgents/cx.pow.powd.plist",
100 | daemonSource(@configuration)
101 | ]
102 |
103 | # Create an installer for a set of files.
104 | constructor: (@files = []) ->
105 |
106 | # Invoke `callback` with an array of any files that need to be
107 | # installed.
108 | getStaleFiles: (callback) ->
109 | async.select @files, (file, proceed) ->
110 | file.isStale proceed
111 | , callback
112 |
113 | # Invoke `callback` with a boolean argument indicating whether or
114 | # not any files need to be installed as root.
115 | needsRootPrivileges: (callback) ->
116 | @getStaleFiles (files) ->
117 | async.detect files, (file, proceed) ->
118 | proceed file.root
119 | , (result) ->
120 | callback result?
121 |
122 | # Installs any stale files asynchronously and then invokes
123 | # `callback`.
124 | install: (callback) ->
125 | @getStaleFiles (files) ->
126 | async.forEach files, (file, proceed) ->
127 | file.install (err) ->
128 | util.puts file.path unless err
129 | proceed err
130 | , callback
131 |
--------------------------------------------------------------------------------
/src/logger.coffee:
--------------------------------------------------------------------------------
1 | # Pow's `Logger` wraps the
2 | # [Log.js](https://github.com/visionmedia/log.js) library in a class
3 | # that adds log file autovivification. The log file you specify is
4 | # automatically created the first time you call a log method.
5 |
6 | fs = require "fs"
7 | {dirname} = require "path"
8 | Log = require "log"
9 | {mkdirp} = require "./util"
10 |
11 | module.exports = class Logger
12 | # Log level method names that will be forwarded to the underlying
13 | # `Log` instance.
14 | @LEVELS: ["debug", "info", "notice", "warning", "error",
15 | "critical", "alert", "emergency"]
16 |
17 | # Create a `Logger` that writes to the file at the given path and
18 | # log level. The logger begins life in the uninitialized state.
19 | constructor: (@path, @level = "debug") ->
20 | @readyCallbacks = []
21 |
22 | # Invoke `callback` if the logger's state is ready. Otherwise, queue
23 | # the callback to be invoked when the logger becomes ready, then
24 | # start the initialization process.
25 | ready: (callback) ->
26 | if @state is "ready"
27 | callback.call @
28 | else
29 | @readyCallbacks.push callback
30 | unless @state
31 | @state = "initializing"
32 | # Make the log file's directory if it doesn't already
33 | # exist. Reset the logger's state if an error is thrown.
34 | mkdirp dirname(@path), (err) =>
35 | if err
36 | @state = null
37 | else
38 | # Open a write stream for the log file and create the
39 | # underlying `Log` instance. Then set the logger state to
40 | # ready and invoke all queued callbacks.
41 | @stream = fs.createWriteStream @path, flags: "a"
42 | @stream.on "open", =>
43 | @log = new Log @level, @stream
44 | @state = "ready"
45 | for callback in @readyCallbacks
46 | callback.call @
47 | @readyCallbacks = []
48 |
49 | # Define the log level methods as wrappers around the corresponding
50 | # `Log` methods passing through `ready`.
51 | for level in Logger.LEVELS then do (level) ->
52 | Logger::[level] = (args...) ->
53 | @ready -> @log[level].apply @log, args
54 |
--------------------------------------------------------------------------------
/src/rack_application.coffee:
--------------------------------------------------------------------------------
1 | # The `RackApplication` class is responsible for managing a
2 | # [Nack](http://josh.github.com/nack/) pool for a given Rack
3 | # application. Incoming HTTP requests are dispatched to
4 | # `RackApplication` instances by an `HttpServer`, where they are
5 | # subsequently handled by a pool of Nack worker processes. By default,
6 | # Pow tells Nack to use a maximum of two worker processes per
7 | # application, but this can be overridden with the configuration's
8 | # `workers` option.
9 | #
10 | # Before creating the Nack pool, Pow executes the `.powrc` and
11 | # `.powenv` scripts if they're present in the application root,
12 | # captures their environment variables, and passes them along to the
13 | # Nack worker processes. This lets you modify your `RUBYOPT` to use
14 | # different Ruby options, for example.
15 | #
16 | # If [rvm](http://rvm.beginrescueend.com/) is installed and an
17 | # `.rvmrc` file is present in the application's root, Pow will load
18 | # both before creating the Nack pool. This makes it easy to run an
19 | # app with a specific version of Ruby.
20 | #
21 | # Nack workers remain running until they're killed, restarted (by
22 | # touching the `tmp/restart.txt` file in the application root), or
23 | # until the application has not served requests for the length of time
24 | # specified in the configuration's `timeout` option (15 minutes by
25 | # default).
26 |
27 | async = require "async"
28 | fs = require "fs"
29 | nack = require "nack"
30 |
31 | {bufferLines, pause, sourceScriptEnv} = require "./util"
32 | {join, basename, resolve} = require "path"
33 |
34 | module.exports = class RackApplication
35 | # Create a `RackApplication` for the given configuration and
36 | # root path. The application begins life in the uninitialized
37 | # state.
38 | constructor: (@configuration, @root, @firstHost) ->
39 | @logger = @configuration.getLogger join "apps", basename @root
40 | @readyCallbacks = []
41 | @quitCallbacks = []
42 | @statCallbacks = []
43 |
44 | # Queue `callback` to be invoked when the application becomes ready,
45 | # then start the initialization process. If the application's state
46 | # is ready, the callback is invoked immediately.
47 | ready: (callback) ->
48 | if @state is "ready"
49 | callback()
50 | else
51 | @readyCallbacks.push callback
52 | @initialize()
53 |
54 | # Tell the application to quit and queue `callback` to be invoked
55 | # when all workers have exited. If the application has already quit,
56 | # the callback is invoked immediately.
57 | quit: (callback) ->
58 | if @state
59 | @quitCallbacks.push callback if callback
60 | @terminate()
61 | else
62 | callback?()
63 |
64 | # Stat `tmp/restart.txt` in the application root and invoke the
65 | # given callback with a single argument indicating whether or not
66 | # the file has been touched since the last call to
67 | # `queryRestartFile`.
68 | queryRestartFile: (callback) ->
69 | fs.stat join(@root, "tmp/restart.txt"), (err, stats) =>
70 | if err
71 | @mtime = null
72 | callback false
73 | else
74 | lastMtime = @mtime
75 | @mtime = stats.mtime.getTime()
76 | callback lastMtime isnt @mtime
77 |
78 | # Check to see if `tmp/always_restart.txt` is present in the
79 | # application root, and set the pool's `runOnce` option
80 | # accordingly. Invoke `callback` when the existence check has
81 | # finished. (Multiple calls to this method are aggregated.)
82 | setPoolRunOnceFlag: (callback) ->
83 | unless @statCallbacks.length
84 | fs.exists join(@root, "tmp/always_restart.txt"), (alwaysRestart) =>
85 | @pool.runOnce = alwaysRestart
86 | statCallback() for statCallback in @statCallbacks
87 | @statCallbacks = []
88 |
89 | @statCallbacks.push callback
90 |
91 | # Collect environment variables from `.powrc` and `.powenv`, in that
92 | # order, if present. The idea is that `.powrc` files can be checked
93 | # into a source code repository for global configuration, leaving
94 | # `.powenv` free for any necessary local overrides.
95 | loadScriptEnvironment: (env, callback) ->
96 | async.reduce [".powrc", ".envrc", ".powenv"], env, (env, filename, callback) =>
97 | fs.exists script = join(@root, filename), (scriptExists) ->
98 | if scriptExists
99 | sourceScriptEnv script, env, callback
100 | else
101 | callback null, env
102 | , callback
103 |
104 | # If `.rvmrc` and `$HOME/.rvm/scripts/rvm` are present, load rvm,
105 | # source `.rvmrc`, and invoke `callback` with the resulting
106 | # environment variables. If `.rvmrc` is present but rvm is not
107 | # installed, invoke `callback` without sourcing `.rvmrc`.
108 | # Before loading rvm, Pow invokes a helper script that shows a
109 | # deprecation notice if it has not yet been displayed.
110 | loadRvmEnvironment: (env, callback) ->
111 | fs.exists script = join(@root, ".rvmrc"), (rvmrcExists) =>
112 | if rvmrcExists
113 | fs.exists rvm = @configuration.rvmPath, (rvmExists) =>
114 | if rvmExists
115 | libexecPath = resolve "#{__dirname}/../libexec"
116 | before = """
117 | '#{libexecPath}/pow_rvm_deprecation_notice' '#{[@firstHost]}'
118 | source '#{rvm}' > /dev/null
119 | """.trim()
120 | sourceScriptEnv script, env, {before}, callback
121 | else
122 | callback null, env
123 | else
124 | callback null, env
125 |
126 | # Stat `tmp/restart.txt` to cache its mtime, then load the
127 | # application's full environment from `.powrc`, `.powenv`, and
128 | # `.rvmrc`.
129 | loadEnvironment: (callback) ->
130 | @queryRestartFile =>
131 | @loadScriptEnvironment @configuration.env, (err, env) =>
132 | if err then callback err
133 | else @loadRvmEnvironment env, (err, env) =>
134 | if err then callback err
135 | else callback null, env
136 |
137 | # Begin the initialization process if the application is in the
138 | # uninitialized state. (If the application is terminating, queue a
139 | # call to `initialize` after all workers have exited.)
140 | initialize: ->
141 | if @state
142 | if @state is "terminating"
143 | @quit => @initialize()
144 | return
145 |
146 | @state = "initializing"
147 |
148 | # Load the application's environment. If an error is raised or
149 | # either of the environment scripts exits with a non-zero status,
150 | # reset the application's state and log the error.
151 | @loadEnvironment (err, env) =>
152 | if err
153 | @state = null
154 | @logger.error err.message
155 | @logger.error "stdout: #{err.stdout}"
156 | @logger.error "stderr: #{err.stderr}"
157 |
158 | # Set the application's state to ready. Then create the Nack
159 | # pool instance using the `workers` and `timeout` options from
160 | # the application's environment or the global configuration.
161 | else
162 | @state = "ready"
163 |
164 | @pool = nack.createPool join(@root, "config.ru"),
165 | env: env
166 | size: env?.POW_WORKERS ? @configuration.workers
167 | idle: (env?.POW_TIMEOUT ? @configuration.timeout) * 1000
168 |
169 | # Log the workers' stderr and stdout, and log each worker's
170 | # PID as it spawns and exits.
171 | bufferLines @pool.stdout, (line) => @logger.info line
172 | bufferLines @pool.stderr, (line) => @logger.warning line
173 |
174 | @pool.on "worker:spawn", (process) =>
175 | @logger.debug "nack worker #{process.child.pid} spawned"
176 |
177 | @pool.on "worker:exit", (process) =>
178 | @logger.debug "nack worker exited"
179 |
180 | # Invoke and remove all queued callbacks, passing along the
181 | # error, if any.
182 | readyCallback err for readyCallback in @readyCallbacks
183 | @readyCallbacks = []
184 |
185 | # Begin the termination process. (If the application is initializing,
186 | # wait until it is ready before shutting down.)
187 | terminate: ->
188 | if @state is "initializing"
189 | @ready => @terminate()
190 |
191 | else if @state is "ready"
192 | @state = "terminating"
193 |
194 | # Instruct all workers to exit. After the processes have
195 | # terminated, reset the application's state, then invoke and
196 | # remove all queued callbacks.
197 | @pool.quit =>
198 | @state = null
199 | @mtime = null
200 | @pool = null
201 |
202 | quitCallback() for quitCallback in @quitCallbacks
203 | @quitCallbacks = []
204 |
205 | # Handle an incoming HTTP request. Wait until the application is in
206 | # the ready state, restart the workers if necessary, then pass the
207 | # request along to the Nack pool. If the Nack worker raises an
208 | # exception handling the request, reset the application.
209 | handle: (req, res, next, callback) ->
210 | @ready (err) =>
211 | return next err if err
212 | @setPoolRunOnceFlag =>
213 | @restartIfNecessary =>
214 | req.proxyMetaVariables =
215 | SERVER_PORT: @configuration.dstPort.toString()
216 | try
217 | @pool.proxy req, res, (err) =>
218 | @quit() if err
219 | next err
220 | finally
221 | callback?()
222 |
223 | # Terminate the application, re-initialize it, and invoke the given
224 | # callback when the application's state becomes ready.
225 | restart: (callback) ->
226 | @quit =>
227 | @ready callback
228 |
229 | # Restart the application if `tmp/restart.txt` has been touched
230 | # since the last call to this function.
231 | restartIfNecessary: (callback) ->
232 | @queryRestartFile (mtimeChanged) =>
233 | if mtimeChanged
234 | @restart callback
235 | else
236 | callback()
237 |
238 | # Append RVM autoload boilerplate to the application's `.powrc`
239 | # file. This is called by the RVM deprecation notice mini-app.
240 | writeRvmBoilerplate: ->
241 | powrc = join @root, ".powrc"
242 | boilerplate = @constructor.rvmBoilerplate
243 |
244 | fs.readFile powrc, "utf8", (err, contents) ->
245 | contents ?= ""
246 | if contents.indexOf(boilerplate) is -1
247 | fs.writeFile powrc, "#{boilerplate}\n#{contents}"
248 |
249 | @rvmBoilerplate: """
250 | if [ -f "$rvm_path/scripts/rvm" ] && [ -f ".rvmrc" ]; then
251 | source "$rvm_path/scripts/rvm"
252 | source ".rvmrc"
253 | fi
254 | """
255 |
--------------------------------------------------------------------------------
/src/templates/http_server/application_not_found.html.eco:
--------------------------------------------------------------------------------
1 | <%- @renderTemplate "layout", title: "Application not found", => %>
2 |
Application not found
3 |
Symlink your app to ~/.pow/<%= @name %> first.
4 |
5 |
When you access http://<%= @host %>/, Pow looks for a Rack application at ~/.pow/<%= @name %>. To run your app at this domain:
6 |
$ cd ~/.pow
7 | $ ln -s /path/to/myapp <%= @name %>
8 | $ open http://<%= @host %>/
Set up a Rack application by symlinking it into your ~/.pow directory. The name of the symlink determines the hostname you’ll use to access the application.
6 |
$ cd ~/.pow
7 | $ ln -s /path/to/myapp
8 | $ open http://myapp.<%= @domain %>/
9 |
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/src/templates/installer/cx.pow.firewall.plist.eco:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | cx.pow.firewall
7 | ProgramArguments
8 |
9 | /bin/sh
10 | -c
11 |
12 | sysctl -w net.inet.ip.forwarding=1;
13 | echo "rdr pass proto tcp from any to any port {<%= @dstPort %>,<%= @httpPort %>} -> 127.0.0.1 port <%= @httpPort %>" | pfctl -a "com.apple/250.PowFirewall" -Ef -
14 |
15 |
16 | RunAtLoad
17 |
18 | UserName
19 | root
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/templates/installer/cx.pow.powd.plist.eco:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | cx.pow.powd
7 | ProgramArguments
8 |
9 | <%= process.execPath %>
10 | <%= @bin %>
11 |
12 | KeepAlive
13 |
14 | RunAtLoad
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/templates/installer/resolver.eco:
--------------------------------------------------------------------------------
1 | # Lovingly generated by Pow
2 | nameserver 127.0.0.1
3 | port <%= @dnsPort %>
4 |
--------------------------------------------------------------------------------
/src/util.coffee:
--------------------------------------------------------------------------------
1 | # The `util` module houses a number of utility functions used
2 | # throughout Pow.
3 |
4 | fs = require "fs"
5 | path = require "path"
6 | async = require "async"
7 | {execFile} = require "child_process"
8 | {Stream} = require "stream"
9 |
10 | # The `LineBuffer` class is a `Stream` that emits a `data` event for
11 | # each line in the stream.
12 | exports.LineBuffer = class LineBuffer extends Stream
13 | # Create a `LineBuffer` around the given stream.
14 | constructor: (@stream) ->
15 | @readable = true
16 | @_buffer = ""
17 |
18 | # Install handlers for the underlying stream's `data` and `end`
19 | # events.
20 | self = this
21 | @stream.on 'data', (args...) -> self.write args...
22 | @stream.on 'end', (args...) -> self.end args...
23 |
24 | # Write a chunk of data read from the stream to the internal buffer.
25 | write: (chunk) ->
26 | @_buffer += chunk
27 |
28 | # If there's a newline in the buffer, slice the line from the
29 | # buffer and emit it. Repeat until there are no more newlines.
30 | while (index = @_buffer.indexOf("\n")) != -1
31 | line = @_buffer[0...index]
32 | @_buffer = @_buffer[index+1...@_buffer.length]
33 | @emit 'data', line
34 |
35 | # Process any final lines from the underlying stream's `end`
36 | # event. If there is trailing data in the buffer, emit it.
37 | end: (args...) ->
38 | if args.length > 0
39 | @write args...
40 | @emit 'data', @_buffer if @_buffer.length
41 | @emit 'end'
42 |
43 | # Read lines from `stream` and invoke `callback` on each line.
44 | exports.bufferLines = (stream, callback) ->
45 | buffer = new LineBuffer stream
46 | buffer.on "data", callback
47 | buffer
48 |
49 | # ---
50 |
51 | # Asynchronously and recursively create a directory if it does not
52 | # already exist. Then invoke the given callback.
53 | exports.mkdirp = (dirname, callback) ->
54 | fs.lstat (p = path.normalize dirname), (err, stats) ->
55 | if err
56 | paths = [p].concat(p = path.dirname p until p in ["/", "."])
57 | async.forEachSeries paths.reverse(), (p, next) ->
58 | fs.exists p, (exists) ->
59 | if exists then next()
60 | else fs.mkdir p, 0o755, (err) ->
61 | if err then callback err
62 | else next()
63 | , callback
64 | else if stats.isDirectory()
65 | callback()
66 | else
67 | callback "file exists"
68 |
69 | # A wrapper around `chown(8)` for taking ownership of a given path
70 | # with the specified owner string (such as `"root:wheel"`). Invokes
71 | # `callback` with the error string, if any, and a boolean value
72 | # indicating whether or not the operation succeeded.
73 | exports.chown = (path, owner, callback) ->
74 | error = ""
75 | exec ["chown", owner, path], (err, stdout, stderr) ->
76 | if err then callback err, stderr
77 | else callback null
78 |
79 | # Capture all `data` events on the given stream and return a function
80 | # that, when invoked, replays the captured events on the stream in
81 | # order.
82 | exports.pause = (stream) ->
83 | queue = []
84 |
85 | onData = (args...) -> queue.push ['data', args...]
86 | onEnd = (args...) -> queue.push ['end', args...]
87 | onClose = -> removeListeners()
88 |
89 | removeListeners = ->
90 | stream.removeListener 'data', onData
91 | stream.removeListener 'end', onEnd
92 | stream.removeListener 'close', onClose
93 |
94 | stream.on 'data', onData
95 | stream.on 'end', onEnd
96 | stream.on 'close', onClose
97 |
98 | ->
99 | removeListeners()
100 |
101 | for args in queue
102 | stream.emit args...
103 |
104 | # Spawn a Bash shell with the given `env` and source the named
105 | # `script`. Then collect its resulting environment variables and pass
106 | # them to `callback` as the second argument. If the script returns a
107 | # non-zero exit code, call `callback` with the error as its first
108 | # argument, and annotate the error with the captured `stdout` and
109 | # `stderr`.
110 | exports.sourceScriptEnv = (script, env, options, callback) ->
111 | if options.call
112 | callback = options
113 | options = {}
114 | else
115 | options ?= {}
116 |
117 | # Build up the command to execute, starting with the `before`
118 | # option, if any. Then source the given script, swallowing any
119 | # output written to stderr. Finally, dump the current environment to
120 | # a temporary file.
121 | cwd = path.dirname script
122 | filename = makeTemporaryFilename()
123 | command = """
124 | #{options.before ? "true"} &&
125 | source #{quote script} > /dev/null &&
126 | env > #{quote filename}
127 | """
128 |
129 | # Run our command through Bash in the directory of the script. If an
130 | # error occurs, rewrite the error to a more descriptive
131 | # message. Otherwise, read and parse the environment from the
132 | # temporary file and pass it along to the callback.
133 | exec ["bash", "-c", command], {cwd, env}, (err, stdout, stderr) ->
134 | if err
135 | err.message = "'#{script}' failed to load:\n#{command}"
136 | err.stdout = stdout
137 | err.stderr = stderr
138 | callback err
139 | else readAndUnlink filename, (err, result) ->
140 | if err then callback err
141 | else callback null, parseEnv result
142 |
143 | # Get the user's login environment by spawning a login shell and
144 | # collecting its environment variables via the `env` command. (In case
145 | # the user's shell profile script prints output to stdout or stderr,
146 | # we must redirect `env` output to a temporary file and read that.)
147 | #
148 | # The returned environment will include a default `LANG` variable if
149 | # one is not set by the user's shell. This default value of `LANG` is
150 | # determined by joining the user's current locale with the value of
151 | # the `defaultEncoding` parameter, or `UTF-8` if it is not set.
152 | exports.getUserEnv = (callback, defaultEncoding = "UTF-8") ->
153 | filename = makeTemporaryFilename()
154 | loginExec "exec env > #{quote filename}", (err) ->
155 | if err then callback err
156 | else readAndUnlink filename, (err, result) ->
157 | if err then callback err
158 | else getUserLocale (locale) ->
159 | env = parseEnv result
160 | env.LANG ?= "#{locale}.#{defaultEncoding}"
161 | callback null, env
162 |
163 | # Execute a command without spawning a subshell. The command argument
164 | # is an array of program name and arguments.
165 | exec = (command, options, callback) ->
166 | unless callback?
167 | callback = options
168 | options = {}
169 | execFile "/usr/bin/env", command, options, callback
170 |
171 | # Single-quote a string for command line execution.
172 | quote = (string) -> "'" + string.replace(/\'/g, "'\\''") + "'"
173 |
174 | # Generate and return a unique temporary filename based on the
175 | # current process's PID, the number of milliseconds elapsed since the
176 | # UNIX epoch, and a random integer.
177 | makeTemporaryFilename = ->
178 | tmpdir = process.env.TMPDIR ? "/tmp"
179 | timestamp = new Date().getTime()
180 | random = parseInt Math.random() * Math.pow(2, 16)
181 | filename = "pow.#{process.pid}.#{timestamp}.#{random}"
182 | path.join tmpdir, filename
183 |
184 | # Read the contents of a file, unlink the file, then invoke the
185 | # callback with the contents of the file.
186 | readAndUnlink = (filename, callback) ->
187 | fs.readFile filename, "utf8", (err, contents) ->
188 | if err then callback err
189 | else fs.unlink filename, (err) ->
190 | if err then callback err
191 | else callback null, contents
192 |
193 | # Execute the given command through a login shell and pass the
194 | # contents of its stdout and stderr streams to the callback. In order
195 | # to spawn a login shell, first spawn the user's shell with the `-l`
196 | # option. If that fails, retry without `-l`; some shells, like tcsh,
197 | # cannot be started as non-interactive login shells. If that fails,
198 | # bubble the error up to the callback.
199 | loginExec = (command, callback) ->
200 | getUserShell (shell) ->
201 | login = ["login", "-qf", process.env.LOGNAME, shell]
202 | exec [login..., "-i", "-c", command], (err, stdout, stderr) ->
203 | if err
204 | exec [login..., "-c", command], callback
205 | else
206 | callback null, stdout, stderr
207 |
208 | # Invoke `dscl(1)` to find out what shell the user prefers. We cannot
209 | # rely on `process.env.SHELL` because it always seems to be
210 | # `/bin/bash` when spawned from `launchctl`, regardless of what the
211 | # user has set.
212 | getUserShell = (callback) ->
213 | command = ["dscl", ".", "-read", "/Users/#{process.env.LOGNAME}", "UserShell"]
214 | exec command, (err, stdout, stderr) ->
215 | if err
216 | callback process.env.SHELL
217 | else
218 | if matches = stdout.trim().match /^UserShell: (.+)$/
219 | [match, shell] = matches
220 | callback shell
221 | else
222 | callback process.env.SHELL
223 |
224 | # Read the user's current locale preference from the OS X defaults
225 | # database. Fall back to `en_US` if it can't be determined.
226 | getUserLocale = (callback) ->
227 | exec ["defaults", "read", "-g", "AppleLocale"], (err, stdout, stderr) ->
228 | locale = stdout?.trim() ? ""
229 | locale = "en_US" unless locale.match /^\w+$/
230 | callback locale
231 |
232 | # Parse the output of the `env` command into a JavaScript object.
233 | parseEnv = (stdout) ->
234 | env = {}
235 | for line in stdout.split "\n"
236 | if matches = line.match /([^=]+)=(.+)/
237 | [match, name, value] = matches
238 | env[name] = value
239 | env
240 |
--------------------------------------------------------------------------------
/test/fixtures/apps/Capital:
--------------------------------------------------------------------------------
1 | hello
--------------------------------------------------------------------------------
/test/fixtures/apps/env/.powenv:
--------------------------------------------------------------------------------
1 | export POW_TEST2="Overridden by .powenv"
2 | export POW_TEST3="Hello!"
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/apps/env/.powenv2:
--------------------------------------------------------------------------------
1 | export POW_TEST3="Goodbye!"
2 | export POW_WORKERS=3
3 | export POW_TIMEOUT=500
4 |
--------------------------------------------------------------------------------
/test/fixtures/apps/env/.powrc:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | export POW_TEST="Hello Pow"
3 | export POW_TEST2="This should be overridden by .powenv"
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/apps/env/config.ru:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | run lambda { |env|
4 | body = ENV.keys.grep(/^POW/).inject({}) { |e, k| e.merge(k => ENV[k]) }.to_json
5 | [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/apps/env/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/apps/env/tmp/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/apps/error/config.ru:
--------------------------------------------------------------------------------
1 | raise ArgumentError, "copy goes here"
2 |
--------------------------------------------------------------------------------
/test/fixtures/apps/error/ok.ru:
--------------------------------------------------------------------------------
1 | run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
2 |
--------------------------------------------------------------------------------
/test/fixtures/apps/error/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/apps/error/tmp/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/apps/hello/config.ru:
--------------------------------------------------------------------------------
1 | run lambda { |env|
2 | case env['PATH_INFO']
3 | when '/post'
4 | [200, {'Content-Type' => 'text/plain'}, [env['rack.input'].read]]
5 | when '/..'
6 | [200, {'Content-Type' => 'text/plain'}, ['..']]
7 | when '/redirect'
8 | require 'rack/request'
9 | request = Rack::Request.new(env)
10 | location = "#{request.scheme}://#{request.host}"
11 | location << ":#{request.port}" if request.port != 80
12 | location << "/"
13 | [302, {'Location' => location, 'Content-Type' => 'text/plain', 'Content-Length' => '0'}, ['']]
14 | else
15 | [200, {'Content-Type' => 'text/plain', 'Content-Length' => '5'}, ['Hello']]
16 | end
17 | }
18 |
--------------------------------------------------------------------------------
/test/fixtures/apps/hello/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/test/fixtures/apps/hello/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/apps/hello/tmp/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/apps/pid/config.ru:
--------------------------------------------------------------------------------
1 | run lambda { |env|
2 | body = Process.pid.to_s
3 | [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
4 | }
5 |
--------------------------------------------------------------------------------
/test/fixtures/apps/pid/tmp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/apps/pid/tmp/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/apps/rails/config/environment.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/apps/rails/config/environment.rb
--------------------------------------------------------------------------------
/test/fixtures/apps/rc-error/.powrc:
--------------------------------------------------------------------------------
1 | exit 1
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/apps/rc-error/config.ru:
--------------------------------------------------------------------------------
1 | run lambda { |env|
2 | [200, {'Content-Type' => 'text/plain', 'Content-Length' => '5'}, ['Hello']]
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/apps/rvm/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm 1.9.2
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/apps/rvm/config.ru:
--------------------------------------------------------------------------------
1 | run lambda { |env|
2 | [200, {'Content-Type' => 'text/plain'}, [ENV['RVM_VERSION']]]
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/apps/static/public/index.html:
--------------------------------------------------------------------------------
1 |
2 | hello world!
3 |
--------------------------------------------------------------------------------
/test/fixtures/configuration-with-default/default:
--------------------------------------------------------------------------------
1 | ../apps/hello
--------------------------------------------------------------------------------
/test/fixtures/configuration-with-default/hello:
--------------------------------------------------------------------------------
1 | ../apps/hello
--------------------------------------------------------------------------------
/test/fixtures/configuration/directory/config.ru:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/configuration/directory/config.ru
--------------------------------------------------------------------------------
/test/fixtures/configuration/plain-file:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/configuration/plain-file
--------------------------------------------------------------------------------
/test/fixtures/configuration/port-number:
--------------------------------------------------------------------------------
1 | 3333
2 |
--------------------------------------------------------------------------------
/test/fixtures/configuration/recursive-symlink-a:
--------------------------------------------------------------------------------
1 | recursive-symlink-b
--------------------------------------------------------------------------------
/test/fixtures/configuration/recursive-symlink-b:
--------------------------------------------------------------------------------
1 | recursive-symlink-a
--------------------------------------------------------------------------------
/test/fixtures/configuration/remote-host:
--------------------------------------------------------------------------------
1 | http://pow.cx/
2 |
--------------------------------------------------------------------------------
/test/fixtures/configuration/symlink-to-directory:
--------------------------------------------------------------------------------
1 | ../apps/hello
--------------------------------------------------------------------------------
/test/fixtures/configuration/symlink-to-file:
--------------------------------------------------------------------------------
1 | ../apps/hello/config.ru
--------------------------------------------------------------------------------
/test/fixtures/configuration/symlink-to-nowhere:
--------------------------------------------------------------------------------
1 | ../nonexistent
--------------------------------------------------------------------------------
/test/fixtures/configuration/symlink-to-symlink:
--------------------------------------------------------------------------------
1 | symlink-to-directory
--------------------------------------------------------------------------------
/test/fixtures/configuration/www.directory/config.ru:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/test/fixtures/configuration/www.directory/config.ru
--------------------------------------------------------------------------------
/test/fixtures/fake-rvm:
--------------------------------------------------------------------------------
1 | rvm() {
2 | export RVM_VERSION=$1
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/proxies/port:
--------------------------------------------------------------------------------
1 | 14136
2 |
--------------------------------------------------------------------------------
/test/lib/test_helper.coffee:
--------------------------------------------------------------------------------
1 | fs = require "fs"
2 | http = require "http"
3 | {exec} = require "child_process"
4 | {join} = require "path"
5 |
6 | {Configuration} = require "../.."
7 |
8 | exports.merge = merge = (objects...) ->
9 | result = {}
10 | for object in objects
11 | for key, value of object
12 | result[key] = value
13 | result
14 |
15 | exports.fixturePath = fixturePath = (path) ->
16 | join fs.realpathSync(join __dirname, ".."), "fixtures", path
17 |
18 | defaultEnvironment =
19 | POW_HOST_ROOT: fixturePath "tmp"
20 | POW_LOG_ROOT: fixturePath "tmp/logs"
21 |
22 | exports.createConfiguration = (env = {}) ->
23 | new Configuration merge defaultEnvironment, env
24 |
25 | exports.prepareFixtures = (callback) ->
26 | rm_rf fixturePath("tmp"), ->
27 | mkdirp fixturePath("tmp"), ->
28 | callback()
29 |
30 | exports.rm_rf = rm_rf = (path, callback) ->
31 | exec "rm -rf #{path}", callback
32 |
33 | exports.mkdirp = mkdirp = (path, callback) ->
34 | exec "mkdir -p #{path}", callback
35 |
36 | exports.touch = touch = (path, callback) ->
37 | exec "touch #{path}", callback
38 |
39 | exports.swap = swap = (path1, path2, callback) ->
40 | unswap = (callback) ->
41 | swap path2, path1, callback
42 |
43 | exec """
44 | mv #{path1} #{path1}.swap;
45 | mv #{path2} #{path1};
46 | mv #{path1}.swap #{path2}
47 | """, (err) ->
48 | callback err, unswap
49 |
50 | exports.debug = debug = ->
51 | if process.env.DEBUG
52 | console.error.apply console, arguments
53 |
54 | exports.serve = serve = (server, callback) ->
55 | server.listen 0, ->
56 | port = server.address().port
57 | debug "server listening on port #{port}"
58 | request = createRequester server.address().port
59 | callback request, (callback) ->
60 | debug "server on port #{port} is closing"
61 | server.close()
62 | callback()
63 | , server
64 |
65 | exports.createRequester = createRequester = (port) ->
66 | (method, path, headers, callback) ->
67 | callback = headers unless callback
68 | request = http.request {method, path, port, headers}
69 |
70 | if data = headers.data
71 | delete headers.data
72 | request.write data
73 |
74 | request.end()
75 |
76 | debug "client requesting #{method} #{path} on port #{port}"
77 | request.on "response", (response) ->
78 | body = ""
79 | response.on "data", (chunk) ->
80 | debug "client received #{chunk.length} bytes from server on port #{port}"
81 | body += chunk.toString "utf8"
82 | response.on "end", ->
83 | debug "client disconnected from server on port #{port}"
84 | callback body, response
85 |
--------------------------------------------------------------------------------
/test/test_configuration.coffee:
--------------------------------------------------------------------------------
1 | async = require "async"
2 | fs = require "fs"
3 | {testCase} = require "nodeunit"
4 |
5 | {prepareFixtures, fixturePath, createConfiguration} = require "./lib/test_helper"
6 |
7 | module.exports = testCase
8 | setUp: (proceed) ->
9 | prepareFixtures proceed
10 |
11 | "gatherHostConfigurations returns directories and symlinks to directories": (test) ->
12 | test.expect 1
13 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration")
14 | configuration.gatherHostConfigurations (err, hosts) ->
15 | test.same hosts,
16 | "directory": { root: fixturePath("configuration/directory") }
17 | "www.directory": { root: fixturePath("configuration/www.directory") }
18 | "symlink-to-directory": { root: fixturePath("apps/hello") }
19 | "symlink-to-symlink": { root: fixturePath("apps/hello") }
20 | "port-number": { url: "http://localhost:3333" }
21 | "remote-host": { url: "http://pow.cx/" }
22 | test.done()
23 |
24 | "gatherHostConfigurations with non-existent host": (test) ->
25 | test.expect 2
26 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("tmp/pow")
27 | configuration.gatherHostConfigurations (err, hosts) ->
28 | test.same {}, hosts
29 | fs.lstat fixturePath("tmp/pow"), (err, stat) ->
30 | test.ok stat.isDirectory()
31 | test.done()
32 |
33 | "findHostConfiguration matches hostnames to application roots": (test) ->
34 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration")
35 | matchHostToRoot = (host, fixtureRoot) -> (proceed) ->
36 | configuration.findHostConfiguration host, (err, domain, conf) ->
37 | if fixtureRoot
38 | test.same "dev", domain
39 | test.same { root: fixturePath(fixtureRoot) }, conf
40 | else
41 | test.ok !conf
42 | proceed()
43 |
44 | test.expect 14
45 | async.parallel [
46 | matchHostToRoot "directory.dev", "configuration/directory"
47 | matchHostToRoot "sub.directory.dev", "configuration/directory"
48 | matchHostToRoot "www.directory.dev", "configuration/www.directory"
49 | matchHostToRoot "asset0.www.directory.dev", "configuration/www.directory"
50 | matchHostToRoot "symlink-to-directory.dev", "apps/hello"
51 | matchHostToRoot "symlink-to-symlink.dev", "apps/hello"
52 | matchHostToRoot "directory"
53 | matchHostToRoot "nonexistent.dev"
54 | ], test.done
55 |
56 | "findHostConfiguration with alternate domain": (test) ->
57 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration"), POW_DOMAINS: "dev.local"
58 | test.expect 3
59 | configuration.findHostConfiguration "directory.dev.local", (err, domain, conf) ->
60 | test.same "dev.local", domain
61 | test.same fixturePath("configuration/directory"), conf.root
62 | configuration.findHostConfiguration "directory.dev", (err, domain, conf) ->
63 | test.ok !conf
64 | test.done()
65 |
66 | "findHostConfiguration with multiple domains": (test) ->
67 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration"), POW_DOMAINS: ["test", "dev"]
68 | test.expect 4
69 | configuration.findHostConfiguration "directory.dev", (err, domain, conf) ->
70 | test.same "dev", domain
71 | test.same fixturePath("configuration/directory"), conf.root
72 | configuration.findHostConfiguration "directory.dev", (err, domain, conf) ->
73 | test.same "dev", domain
74 | test.same fixturePath("configuration/directory"), conf.root
75 | test.done()
76 |
77 | "findHostConfiguration with default host": (test) ->
78 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration-with-default")
79 | test.expect 2
80 | configuration.findHostConfiguration "missing.dev", (err, domain, conf) ->
81 | test.same "dev", domain
82 | test.same fixturePath("apps/hello"), conf.root
83 | test.done()
84 |
85 | "findHostConfiguration with ext domain": (test) ->
86 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration"), POW_DOMAINS: ["dev"], POW_EXT_DOMAINS: ["me"]
87 | test.expect 2
88 | configuration.findHostConfiguration "directory.me", (err, domain, conf) ->
89 | test.same "me", domain
90 | test.same fixturePath("configuration/directory"), conf.root
91 | test.done()
92 |
93 | "findHostConfiguration matches regex domains": (test) ->
94 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration"), POW_DOMAINS: ["dev"], POW_EXT_DOMAINS: [/foo\d$/]
95 | test.expect 2
96 | configuration.findHostConfiguration "directory.foo3", (err, domain, conf) ->
97 | test.ok domain.test?
98 | test.same fixturePath("configuration/directory"), conf.root
99 | test.done()
100 |
101 | "findHostConfiguration matches xip.io domains": (test) ->
102 | configuration = createConfiguration POW_HOST_ROOT: fixturePath("configuration")
103 | matchHostToRoot = (host, fixtureRoot) -> (proceed) ->
104 | configuration.findHostConfiguration host, (err, domain, conf) ->
105 | if fixtureRoot
106 | test.ok domain.test?
107 | test.same { root: fixturePath(fixtureRoot) }, conf
108 | else
109 | test.ok !conf
110 | proceed()
111 |
112 | test.expect 16
113 | async.parallel [
114 | matchHostToRoot "directory.127.0.0.1.xip.io", "configuration/directory"
115 | matchHostToRoot "directory.10.0.1.43.xip.io", "configuration/directory"
116 | matchHostToRoot "directory.9zlhb.xip.io", "configuration/directory"
117 | matchHostToRoot "directory.bxjy16.xip.io", "configuration/directory"
118 | matchHostToRoot "sub.directory.9zlhb.xip.io", "configuration/directory"
119 | matchHostToRoot "www.directory.9zlhb.xip.io", "configuration/www.directory"
120 | matchHostToRoot "www.directory.127.0.0.1.xip.io", "configuration/www.directory"
121 | matchHostToRoot "127.0.0.1.xip.io"
122 | matchHostToRoot "nonexistent.127.0.0.1.xip.io"
123 | ], test.done
124 |
125 | "getLogger returns the same logger instance": (test) ->
126 | configuration = createConfiguration()
127 | logger = configuration.getLogger "test"
128 | test.expect 2
129 | test.ok logger is configuration.getLogger "test"
130 | test.ok logger isnt configuration.getLogger "test2"
131 | test.done()
132 |
133 | "getLogger returns a logger with the specified log root": (test) ->
134 | test.expect 2
135 |
136 | configuration = createConfiguration()
137 | logger = configuration.getLogger "test"
138 | test.same fixturePath("tmp/logs/test.log"), logger.path
139 |
140 | configuration = createConfiguration POW_LOG_ROOT: fixturePath("tmp/log2")
141 | logger = configuration.getLogger "test"
142 | test.same fixturePath("tmp/log2/test.log"), logger.path
143 |
144 | test.done()
145 |
--------------------------------------------------------------------------------
/test/test_daemon.coffee:
--------------------------------------------------------------------------------
1 | net = require "net"
2 | fs = require "fs"
3 | path = require "path"
4 | {testCase} = require "nodeunit"
5 | {Configuration, Daemon} = require ".."
6 | {prepareFixtures, fixturePath, touch} = require "./lib/test_helper"
7 |
8 | module.exports = testCase
9 | setUp: (proceed) ->
10 | prepareFixtures proceed
11 |
12 | "start and stop": (test) ->
13 | test.expect 2
14 |
15 | configuration = new Configuration POW_HOST_ROOT: fixturePath("tmp"), POW_HTTP_PORT: 0, POW_DNS_PORT: 0
16 | daemon = new Daemon configuration
17 |
18 | daemon.start()
19 | daemon.on "start", ->
20 | test.ok daemon.started
21 | daemon.stop()
22 | daemon.on "stop", ->
23 | test.ok !daemon.started
24 | test.done()
25 |
26 | "start rolls back when it can't boot a server": (test) ->
27 | test.expect 2
28 |
29 | server = net.createServer()
30 | server.listen 0, ->
31 | port = server.address().port
32 | configuration = new Configuration POW_HOST_ROOT: fixturePath("tmp"), POW_HTTP_PORT: port
33 | daemon = new Daemon configuration
34 |
35 | daemon.start()
36 | daemon.on "error", (err) ->
37 | test.ok err
38 | test.ok !daemon.started
39 | server.close()
40 | test.done()
41 |
42 | "touching restart.txt removes the file and emits a restart event": (test) ->
43 | test.expect 1
44 |
45 | restartFilename = path.join fixturePath("tmp"), "restart.txt"
46 | configuration = new Configuration POW_HOST_ROOT: fixturePath("tmp"), POW_HTTP_PORT: 0, POW_DNS_PORT: 0
47 | daemon = new Daemon configuration
48 |
49 | daemon.start()
50 |
51 | daemon.once "restart", ->
52 | fs.exists restartFilename, (exists) ->
53 | test.ok !exists
54 | daemon.stop()
55 | test.done()
56 |
57 | touch restartFilename
58 |
--------------------------------------------------------------------------------
/test/test_dns_server.coffee:
--------------------------------------------------------------------------------
1 | {DnsServer} = require ".."
2 | async = require "async"
3 | {exec} = require "child_process"
4 | {testCase} = require "nodeunit"
5 |
6 | {prepareFixtures, createConfiguration} = require "./lib/test_helper"
7 |
8 | module.exports = testCase
9 | setUp: (proceed) ->
10 | prepareFixtures proceed
11 |
12 | "responds to all A queries for the configured domain": (test) ->
13 | test.expect 12
14 |
15 | exec "which dig", (err) ->
16 | if err
17 | console.warn "Skipping test, system is missing `dig`"
18 | test.expect 0
19 | test.done()
20 | else
21 | configuration = createConfiguration POW_DOMAINS: "powtest,powdev"
22 | dnsServer = new DnsServer configuration
23 | address = "0.0.0.0"
24 | port = 20561
25 |
26 | dnsServer.listen port, ->
27 | resolve = (domain, callback) ->
28 | cmd = "dig -p #{port} @#{address} #{domain} +noall +answer +comments"
29 | exec cmd, (err, stdout, stderr) ->
30 | status = stdout.match(/status: (.*?),/)?[1]
31 | answer = stdout.match(/IN\tA\t([\d.]+)/)?[1]
32 | callback err, status, answer
33 |
34 | testResolves = (host, expectedStatus, expectedAnswer) ->
35 | (callback) -> resolve host, (err, status, answer) ->
36 | test.ifError err
37 | test.same [expectedStatus, expectedAnswer], [status, answer]
38 | callback()
39 |
40 | async.parallel [
41 | testResolves "hello.powtest", "NOERROR", "127.0.0.1"
42 | testResolves "hello.powdev", "NOERROR", "127.0.0.1"
43 | testResolves "a.b.c.powtest", "NOERROR", "127.0.0.1"
44 | testResolves "powtest.", "NOERROR", "127.0.0.1"
45 | testResolves "powdev.", "NOERROR", "127.0.0.1"
46 | testResolves "foo.", "NXDOMAIN"
47 | ], ->
48 | dnsServer.close()
49 | test.done()
50 |
--------------------------------------------------------------------------------
/test/test_rack_application.coffee:
--------------------------------------------------------------------------------
1 | async = require "async"
2 | connect = require "connect"
3 | fs = require "fs"
4 | http = require "http"
5 | {testCase} = require "nodeunit"
6 | {RackApplication} = require ".."
7 |
8 | {prepareFixtures, fixturePath, createConfiguration, touch, swap, serve} = require "./lib/test_helper"
9 |
10 | serveApp = (path, callback) ->
11 | configuration = createConfiguration
12 | POW_HOST_ROOT: fixturePath("apps")
13 | POW_RVM_PATH: fixturePath("fake-rvm")
14 | POW_WORKERS: 1
15 |
16 | @application = new RackApplication configuration, fixturePath(path)
17 | server = connect.createServer()
18 |
19 | server.use (req, res, next) ->
20 | if req.url is "/"
21 | application.handle req, res, next
22 | else
23 | next()
24 |
25 | serve server, (request, done) ->
26 | callback request, (callback) ->
27 | done -> application.quit callback
28 | , application
29 |
30 | module.exports = testCase
31 | setUp: (proceed) ->
32 | prepareFixtures proceed
33 |
34 | "handling a request": (test) ->
35 | test.expect 1
36 | serveApp "apps/hello", (request, done) ->
37 | request "GET", "/", (body) ->
38 | test.same "Hello", body
39 | done -> test.done()
40 |
41 | "handling multiple requests": (test) ->
42 | test.expect 2
43 | serveApp "apps/pid", (request, done) ->
44 | request "GET", "/", (body) ->
45 | test.ok pid = parseInt body
46 | request "GET", "/", (body) ->
47 | test.same pid, parseInt body
48 | done -> test.done()
49 |
50 | "handling a request, restart, request": (test) ->
51 | test.expect 3
52 | restart = fixturePath("apps/pid/tmp/restart.txt")
53 | serveApp "apps/pid", (request, done) ->
54 | fs.unlink restart, ->
55 | request "GET", "/", (body) ->
56 | test.ok pid = parseInt body
57 | touch restart, ->
58 | request "GET", "/", (body) ->
59 | test.ok newpid = parseInt body
60 | test.ok pid isnt newpid
61 | done -> fs.unlink restart, -> test.done()
62 |
63 | "handling the initial request when restart.txt is present": (test) ->
64 | test.expect 3
65 | touch restart = fixturePath("apps/pid/tmp/restart.txt"), ->
66 | serveApp "apps/pid", (request, done) ->
67 | request "GET", "/", (body) ->
68 | test.ok pid = parseInt body
69 | request "GET", "/", (body) ->
70 | test.ok newpid = parseInt body
71 | test.same pid, newpid
72 | done -> fs.unlink restart, -> test.done()
73 |
74 | "handling a request when restart.txt is present and the worker has timed out": (test) ->
75 | serveApp "apps/pid", (request, done, app) ->
76 | request "GET", "/", (body) ->
77 | test.ok pid = parseInt body
78 | app.pool.quit ->
79 | touch restart = fixturePath("apps/pid/tmp/restart.txt"), ->
80 | request "GET", "/", (body) ->
81 | test.ok newpid = parseInt body
82 | test.ok pid isnt newpid
83 | done -> fs.unlink restart, -> test.done()
84 |
85 | "handling a request, always_restart.txt present, request": (test) ->
86 | test.expect 3
87 | always_restart = fixturePath("apps/pid/tmp/always_restart.txt")
88 | serveApp "apps/pid", (request, done) ->
89 | fs.unlink always_restart, ->
90 | request "GET", "/", (body) ->
91 | test.ok pid = parseInt body
92 | touch always_restart, ->
93 | request "GET", "/", (body) ->
94 | test.ok newpid = parseInt body
95 | test.ok pid isnt newpid
96 | done -> fs.unlink always_restart, -> test.done()
97 |
98 | "always_restart.txt present, handling a request, request": (test) ->
99 | test.expect 3
100 | touch always_restart = fixturePath("apps/pid/tmp/always_restart.txt"), ->
101 | serveApp "apps/pid", (request, done) ->
102 | request "GET", "/", (body) ->
103 | test.ok pid = parseInt body
104 | request "GET", "/", (body) ->
105 | test.ok newpid = parseInt body
106 | test.ok pid isnt newpid
107 | done -> fs.unlink always_restart, -> test.done()
108 |
109 | "always_restart.txt present, handling a request, touch restart.txt, request": (test) ->
110 | test.expect 3
111 | touch always_restart = fixturePath("apps/pid/tmp/always_restart.txt"), ->
112 | serveApp "apps/pid", (request, done) ->
113 | request "GET", "/", (body) ->
114 | test.ok pid = parseInt body
115 | touch restart = fixturePath("apps/pid/tmp/restart.txt"), ->
116 | request "GET", "/", (body) ->
117 | test.ok newpid = parseInt body
118 | test.ok pid isnt newpid
119 | done -> fs.unlink always_restart, fs.unlink restart, -> test.done()
120 |
121 | "handling the initial request when restart.txt and always_restart.txt is present": (test) ->
122 | test.expect 3
123 | touch always_restart = fixturePath("apps/pid/tmp/always_restart.txt"), ->
124 | touch restart = fixturePath("apps/pid/tmp/restart.txt"), ->
125 | serveApp "apps/pid", (request, done) ->
126 | request "GET", "/", (body) ->
127 | test.ok pid = parseInt body
128 | request "GET", "/", (body) ->
129 | test.ok newpid = parseInt body
130 | test.ok pid isnt newpid
131 | done -> fs.unlink restart, fs.unlink always_restart, -> test.done()
132 |
133 | "always_restart.txt present, handling a request, request, request": (test) ->
134 | test.expect 5
135 | touch always_restart = fixturePath("apps/pid/tmp/always_restart.txt"), ->
136 | serveApp "apps/pid", (request, done) ->
137 | request "GET", "/", (body) ->
138 | test.ok pid = parseInt body
139 | request "GET", "/", (body) ->
140 | test.ok newpid = parseInt body
141 | test.ok pid isnt newpid
142 | request "GET", "/", (body) ->
143 | test.ok newerpid = parseInt body
144 | test.ok newpid isnt newerpid
145 | done -> fs.unlink always_restart, -> test.done()
146 |
147 | "custom environment": (test) ->
148 | test.expect 3
149 | serveApp "apps/env", (request, done) ->
150 | request "GET", "/", (body) ->
151 | env = JSON.parse body
152 | test.same "Hello Pow", env.POW_TEST
153 | test.same "Overridden by .powenv", env.POW_TEST2
154 | test.same "Hello!", env.POW_TEST3
155 | done -> test.done()
156 |
157 | "custom environments are reloaded after a restart": (test) ->
158 | serveApp "apps/env", (request, done) ->
159 | request "GET", "/", (body) ->
160 | test.same "Hello!", JSON.parse(body).POW_TEST3
161 | powenv1 = fixturePath("apps/env/.powenv")
162 | powenv2 = fixturePath("apps/env/.powenv2")
163 | swap powenv1, powenv2, (err, unswap) ->
164 | touch restart = fixturePath("apps/env/tmp/restart.txt"), ->
165 | request "GET", "/", (body) ->
166 | test.same "Goodbye!", JSON.parse(body).POW_TEST3
167 | done -> unswap -> fs.unlink restart, -> test.done()
168 |
169 | "custom worker/timeout values are loaded": (test) ->
170 | serveApp "apps/env", (request, done) ->
171 | request "GET", "/", (body) ->
172 | test.same @application.pool.processOptions.idle, 900 * 1000
173 | test.same @application.pool.workers.length, 1
174 | powenv1 = fixturePath("apps/env/.powenv")
175 | powenv2 = fixturePath("apps/env/.powenv2")
176 | swap powenv1, powenv2, (err, unswap) ->
177 | touch restart = fixturePath("apps/env/tmp/restart.txt"), ->
178 | request "GET", "/", (body) ->
179 | test.same @application.pool.processOptions.idle, 500 * 1000
180 | test.same @application.pool.workers.length, 3
181 | done -> unswap -> fs.unlink restart, -> test.done()
182 |
183 | "handling an error in .powrc": (test) ->
184 | test.expect 2
185 | serveApp "apps/rc-error", (request, done, application) ->
186 | request "GET", "/", (body, response) ->
187 | test.same 500, response.statusCode
188 | test.ok !application.state
189 | done -> test.done()
190 |
191 | "loading rvm and .rvmrc": (test) ->
192 | test.expect 2
193 | serveApp "apps/rvm", (request, done, application) ->
194 | request "GET", "/", (body, response) ->
195 | test.same 200, response.statusCode
196 | test.same "1.9.2", body
197 | done -> test.done()
198 |
199 |
--------------------------------------------------------------------------------
/uninstall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # W
3 | # R RW W.
4 | # RW::::RW DR::R
5 | # :RRRRRWWWWRt:::::::RRR::::::E jR
6 | # R.::::::::::::::::::::::::::Ri jiR:::R
7 | # R:::::::.RERRRRWWRERR,::::::Efi:::::::R GjRRR Rj
8 | # R::::::.R R:::::::::::::;G RRj WWR RjRRRRj
9 | # Rt::::WR RRWR R::::::::::::::::fWR::R; WRW RW R
10 | # WWWWRR:::EWR E::W WRRW:::EWRRR::::::::: RRED WR RRW RR
11 | # 'R:::::::RRR RR DWW R::::::::RW LRRR WR R
12 | # RL:::::WRR GRWRR RR R::WRiGRWW RRR RRR R
13 | # Ri:::WWD RWRRRWW WWR LR R W RR RRRR RR R
14 | # RRRWWWWRE;,:::WW R:::RW RR:W RR ERE RR RRR RRR R
15 | # RR:::::::::::RR tR:::WR Wf:R RW R R RRR RR R
16 | # WR::::::::tRR WR::RW ER.R RRR R RRRR RR R
17 | # WE:::::RR R:::RR :RW E RR RW; GRRR RR R
18 | # R.::::,WR R:::GRW E::RR WiWW RRWR LRRWWRR
19 | # WR::::::RRRRWRG::::::RREWDWRj::::RW ,WR::WR iRWWWWWRWW R
20 | # LR:::::::::::::::::::::::::::::::::EWRR::::::RRRDi:::W RR R
21 | # R:::::::::::::::::::::::::::::::::::::::::::::::::::tRW RRRWWWW
22 | # RRRRRRRRRRR::::::::::::::::::::::::::::::::::::,:::DE RRWRWW,
23 | # R::::::::::::: RW::::::::R::::::::::RRWRRR
24 | # R::::::::::WR. ;R::::;R RWi:::::ER
25 | # R::::::.RR Ri:iR RR:,R
26 | # E::: RE RW Y
27 | # ERRR
28 | # G Zero-configuration Rack server for Mac OS X
29 | # http://pow.cx/
30 | #
31 | # This is the uninstallation script for Pow.
32 | # See the full annotated source: http://pow.cx/docs/
33 | #
34 | # Uninstall Pow by running this command:
35 | # curl get.pow.cx/uninstall.sh | sh
36 |
37 |
38 | # Set up the environment.
39 |
40 | set -e
41 | POW_ROOT="$HOME/Library/Application Support/Pow"
42 | POW_CURRENT_PATH="$POW_ROOT/Current"
43 | POW_VERSIONS_PATH="$POW_ROOT/Versions"
44 | POWD_PLIST_PATH="$HOME/Library/LaunchAgents/cx.pow.powd.plist"
45 | FIREWALL_PLIST_PATH="/Library/LaunchDaemons/cx.pow.firewall.plist"
46 | POW_CONFIG_PATH="$HOME/.powconfig"
47 |
48 |
49 | # Fail fast if Pow isn't present.
50 |
51 | if [ ! -d "$POW_CURRENT_PATH" ] && [ ! -a "$POWD_PLIST_PATH" ] && [ ! -a "$FIREWALL_PLIST_PATH" ]; then
52 | echo "error: can't find Pow" >&2
53 | exit 1
54 | fi
55 |
56 |
57 | # Find the tty so we can prompt for confirmation even if we're being piped from curl.
58 |
59 | TTY="/dev/$( ps -p$$ -o tty | tail -1 | awk '{print$1}' )"
60 |
61 |
62 | # Make sure we really want to uninstall.
63 |
64 | read -p "Sorry to see you go. Uninstall Pow [y/n]? " ANSWER < $TTY
65 | [ $ANSWER == "y" ] || exit 1
66 | echo "*** Uninstalling Pow..."
67 |
68 |
69 | # Remove the Versions directory and the Current symlink.
70 |
71 | rm -fr "$POW_VERSIONS_PATH"
72 | rm -f "$POW_CURRENT_PATH"
73 |
74 |
75 | # Unload cx.pow.powd from launchctl and remove the plist.
76 |
77 | launchctl unload "$POWD_PLIST_PATH" 2>/dev/null || true
78 | rm -f "$POWD_PLIST_PATH"
79 |
80 |
81 | # Determine if the firewall uses ipfw or pf.
82 |
83 | if grep ipfw "$FIREWALL_PLIST_PATH" >/dev/null; then
84 | FIREWALL_TYPE=ipfw
85 | elif grep pfctl "$FIREWALL_PLIST_PATH" >/dev/null; then
86 | FIREWALL_TYPE=pf
87 | fi
88 |
89 |
90 | # If ipfw, extract the port numbers from the plist.
91 |
92 | if [ "$FIREWALL_TYPE" = "ipfw" ]; then
93 | ports=( $(ruby -e'puts $<.read.scan(/fwd .*?,([\d]+).*?dst-port ([\d]+)/)' "$FIREWALL_PLIST_PATH") )
94 |
95 | HTTP_PORT="${ports[0]:-80}"
96 | DST_PORT="${ports[1]:-20559}"
97 | fi
98 |
99 |
100 | # Try to find the ipfw rule and delete it.
101 |
102 | if [ "$FIREWALL_TYPE" = "ipfw" ] && [ -x /sbin/ipfw ]; then
103 | RULE=$(sudo ipfw show | (grep ",$HTTP_PORT .* dst-port $DST_PORT in" || true) | cut -f 1 -d " ")
104 | [ -z "$RULE" ] || sudo ipfw del "$RULE"
105 | fi
106 |
107 |
108 | # If pf, just flush all rules from the Pow anchor.
109 |
110 | if [ "$FIREWALL_TYPE" = "pf" ]; then
111 | sudo pfctl -a "com.apple/250.PowFirewall" -F all 2>/dev/null || true
112 | fi
113 |
114 |
115 | # Unload the firewall plist and remove it.
116 |
117 | sudo launchctl unload "$FIREWALL_PLIST_PATH" 2>/dev/null || true
118 | sudo rm -f "$FIREWALL_PLIST_PATH"
119 |
120 |
121 | # Remove /etc/resolver files that belong to us
122 | grep -Rl 'generated by Pow' /etc/resolver/ | sudo xargs rm
123 |
124 | echo "*** Uninstalled"
125 |
--------------------------------------------------------------------------------