├── .envrc ├── .gitignore ├── .travis.yml ├── Cakefile ├── LICENSE ├── MANUAL.md ├── README.md ├── bin └── pow ├── build.sh ├── docs ├── command.html ├── configuration.html ├── daemon.html ├── dns_server.html ├── docco.css ├── http_server.html ├── index.html ├── installer.html ├── logger.html ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css ├── rack_application.html └── util.html ├── index.js ├── install.sh ├── lib ├── command.js ├── configuration.js ├── daemon.js ├── dns_server.js ├── http_server.js ├── index.js ├── installer.js ├── logger.js ├── rack_application.js ├── templates │ ├── http_server │ │ ├── application_not_found.html.js │ │ ├── error_starting_application.html.js │ │ ├── layout.html.js │ │ ├── proxy_error.html.js │ │ ├── rackup_file_missing.html.js │ │ ├── rvm_deprecation_notice.html.js │ │ └── welcome.html.js │ └── installer │ │ ├── cx.pow.firewall.plist.js │ │ ├── cx.pow.powd.plist.js │ │ └── resolver.js └── util.js ├── libexec └── pow_rvm_deprecation_notice ├── package.json ├── src ├── command.coffee ├── configuration.coffee ├── daemon.coffee ├── dns_server.coffee ├── http_server.coffee ├── index.coffee ├── installer.coffee ├── logger.coffee ├── rack_application.coffee ├── templates │ ├── http_server │ │ ├── application_not_found.html.eco │ │ ├── error_starting_application.html.eco │ │ ├── layout.html.eco │ │ ├── proxy_error.html.eco │ │ ├── rackup_file_missing.html.eco │ │ ├── rvm_deprecation_notice.html.eco │ │ └── welcome.html.eco │ └── installer │ │ ├── cx.pow.firewall.plist.eco │ │ ├── cx.pow.powd.plist.eco │ │ └── resolver.eco └── util.coffee ├── test ├── fixtures │ ├── apps │ │ ├── Capital │ │ ├── env │ │ │ ├── .powenv │ │ │ ├── .powenv2 │ │ │ ├── .powrc │ │ │ ├── config.ru │ │ │ └── tmp │ │ │ │ └── .gitkeep │ │ ├── error │ │ │ ├── config.ru │ │ │ ├── ok.ru │ │ │ └── tmp │ │ │ │ └── .gitkeep │ │ ├── hello │ │ │ ├── config.ru │ │ │ ├── public │ │ │ │ └── robots.txt │ │ │ └── tmp │ │ │ │ └── .gitkeep │ │ ├── pid │ │ │ ├── config.ru │ │ │ └── tmp │ │ │ │ └── .gitkeep │ │ ├── rails │ │ │ └── config │ │ │ │ └── environment.rb │ │ ├── rc-error │ │ │ ├── .powrc │ │ │ └── config.ru │ │ ├── rvm │ │ │ ├── .rvmrc │ │ │ └── config.ru │ │ └── static │ │ │ └── public │ │ │ └── index.html │ ├── configuration-with-default │ │ ├── default │ │ └── hello │ ├── configuration │ │ ├── directory │ │ │ └── config.ru │ │ ├── plain-file │ │ ├── port-number │ │ ├── recursive-symlink-a │ │ ├── recursive-symlink-b │ │ ├── remote-host │ │ ├── symlink-to-directory │ │ ├── symlink-to-file │ │ ├── symlink-to-nowhere │ │ ├── symlink-to-symlink │ │ └── www.directory │ │ │ └── config.ru │ ├── fake-rvm │ └── proxies │ │ └── port ├── lib │ └── test_helper.coffee ├── test_configuration.coffee ├── test_daemon.coffee ├── test_dns_server.coffee ├── test_http_server.coffee └── test_rack_application.coffee └── uninstall.sh /.envrc: -------------------------------------------------------------------------------- 1 | export PATH=node_modules/.bin:$PATH 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test/fixtures/*/tmp/restart.txt 2 | /test/fixtures/tmp/ 3 | /dist 4 | /node_modules/ 5 | !/node_modules/dnsserver 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | notifications: 6 | disabled: true 7 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | async = require 'async' 2 | fs = require 'fs' 3 | {print} = require 'util' 4 | {spawn, exec} = require 'child_process' 5 | 6 | build = (watch, callback) -> 7 | if typeof watch is 'function' 8 | callback = watch 9 | watch = false 10 | options = ['-c', '-o', 'lib', 'src'] 11 | options.unshift '-w' if watch 12 | 13 | coffee = spawn 'node_modules/.bin/coffee', options 14 | coffee.stdout.on 'data', (data) -> print data.toString() 15 | coffee.stderr.on 'data', (data) -> print data.toString() 16 | coffee.on 'exit', (status) -> callback?() if status is 0 17 | 18 | buildTemplates = (callback) -> 19 | eco = require 'eco' 20 | compile = (name) -> 21 | (callback) -> 22 | fs.readFile "src/templates/#{name}.eco", "utf8", (err, data) -> 23 | if err then callback err 24 | else fs.writeFile "lib/templates/#{name}.js", "module.exports = #{eco.precompile(data)}", callback 25 | 26 | async.parallel [ 27 | compile("http_server/application_not_found.html") 28 | compile("http_server/error_starting_application.html") 29 | compile("http_server/layout.html") 30 | compile("http_server/proxy_error.html") 31 | compile("http_server/rackup_file_missing.html") 32 | compile("http_server/rvm_deprecation_notice.html") 33 | compile("http_server/welcome.html") 34 | compile("installer/cx.pow.firewall.plist") 35 | compile("installer/cx.pow.powd.plist") 36 | compile("installer/resolver") 37 | ], callback 38 | 39 | task 'docs', 'Generate annotated source code with Docco', -> 40 | fs.readdir 'src', (err, contents) -> 41 | files = ("src/#{file}" for file in contents when /\.coffee$/.test file) 42 | docco = spawn 'node_modules/.bin/docco', files 43 | docco.stdout.on 'data', (data) -> print data.toString() 44 | docco.stderr.on 'data', (data) -> print data.toString() 45 | docco.on 'exit', (status) -> callback?() if status is 0 46 | 47 | task 'build', 'Compile CoffeeScript source files', -> 48 | build() 49 | buildTemplates() 50 | 51 | task 'watch', 'Recompile CoffeeScript source files when modified', -> 52 | build true 53 | 54 | task 'pretest', "Install test dependencies", -> 55 | exec 'which ruby gem', (err) -> 56 | throw "ruby not found" if err 57 | 58 | exec 'ruby -rubygems -e \'require "rack"\'', (err) -> 59 | if err 60 | exec 'gem install rack', (err, stdout, stderr) -> 61 | throw err if err 62 | 63 | task 'test', 'Run the Pow test suite', -> 64 | build -> 65 | process.env["RUBYOPT"] = "-rubygems" 66 | process.env["NODE_ENV"] = "test" 67 | 68 | {reporters} = require 'nodeunit' 69 | process.chdir __dirname 70 | reporters.default.run ['test'] 71 | 72 | task 'install', 'Install pow configuration files', -> 73 | sh = (command, callback) -> 74 | exec command, (err, stdout, stderr) -> 75 | if err 76 | console.error stderr 77 | callback err 78 | else 79 | callback() 80 | 81 | createHostsDirectory = (callback) -> 82 | sh 'mkdir -p "$HOME/Library/Application Support/Pow/Hosts"', (err) -> 83 | fs.stat "#{process.env['HOME']}/.pow", (err) -> 84 | if err then sh 'ln -s "$HOME/Library/Application Support/Pow/Hosts" "$HOME/.pow"', callback 85 | else callback() 86 | 87 | installLocal = (callback) -> 88 | console.error "*** Installing local configuration files..." 89 | sh "./bin/pow --install-local", callback 90 | 91 | installSystem = (callback) -> 92 | exec "./bin/pow --install-system --dry-run", (needsRoot) -> 93 | if needsRoot 94 | console.error "*** Installing system configuration files as root..." 95 | sh "sudo ./bin/pow --install-system", (err) -> 96 | if err 97 | callback err 98 | else 99 | sh "sudo launchctl load /Library/LaunchDaemons/cx.pow.firewall.plist", callback 100 | else 101 | callback() 102 | 103 | async.parallel [createHostsDirectory, installLocal, installSystem], (err) -> 104 | throw err if err 105 | console.error "*** Installed" 106 | 107 | task 'start', 'Start pow server', -> 108 | agent = "#{process.env['HOME']}/Library/LaunchAgents/cx.pow.powd.plist" 109 | console.error "*** Starting the Pow server..." 110 | exec "launchctl load '#{agent}'", (err, stdout, stderr) -> 111 | console.error stderr if err 112 | 113 | task 'stop', 'Stop pow server', -> 114 | agent = "#{process.env['HOME']}/Library/LaunchAgents/cx.pow.powd.plist" 115 | console.error "*** Stopping the Pow server..." 116 | exec "launchctl unload '#{agent}'", (err, stdout, stderr) -> 117 | console.error stderr if err 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Sam Stephenson, Basecamp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | W 2 | R RW W. 3 | RW::::RW DR::R 4 | :RRRRRWWWWRt:::::::RRR::::::E jR 5 | R.::::::::::::::::::::::::::Ri jiR:::R 6 | R:::::::.RERRRRWWRERR,::::::Efi:::::::R GjRRR Rj 7 | R::::::.R R:::::::::::::;G RRj WWR RjRRRRj 8 | Rt::::WR RRWR R::::::::::::::::fWR::R; WRW RW R 9 | WWWWRR:::EWR E::W WRRW:::EWRRR::::::::: RRED WR RRW RR 10 | 'R:::::::RRR RR DWW R::::::::RW LRRR WR R 11 | RL:::::WRR GRWRR RR R::WRiGRWW RRR RRR R 12 | Ri:::WWD RWRRRWW WWR LR R W RR RRRR RR R 13 | RRRWWWWRE;,:::WW R:::RW RR:W RR ERE RR RRR RRR R 14 | RR:::::::::::RR tR:::WR Wf:R RW R R RRR RR R 15 | WR::::::::tRR WR::RW ER.R RRR R RRRR RR R 16 | WE:::::RR R:::RR :RW E RR RW; GRRR RR R 17 | R.::::,WR R:::GRW E::RR WiWW RRWR LRRWWRR 18 | WR::::::RRRRWRG::::::RREWDWRj::::RW ,WR::WR iRWWWWWRWW R 19 | LR:::::::::::::::::::::::::::::::::EWRR::::::RRRDi:::W RR R 20 | R:::::::::::::::::::::::::::::::::::::::::::::::::::tRW RRRWWWW 21 | RRRRRRRRRRR::::::::::::::::::::::::::::::::::::,:::DE RRWRWW, 22 | R::::::::::::: RW::::::::R::::::::::RRWRRR 23 | R::::::::::WR. ;R::::;R RWi:::::ER 24 | R::::::.RR Ri:iR RR:,R 25 | E::: RE RW Y 26 | ERRR 27 | G Knock Out Rails & Rack Apps Like A Superhero. 28 | 29 | 30 | ## Pow is a zero-config Rack server for Mac OS X. 31 | 32 | [Pow Web Site](http://pow.cx/) - 33 | [User's Manual](http://pow.cx/manual) - 34 | [Annotated Source Code](http://pow.cx/docs/) - 35 | [2-Minute Screencast](http://get.pow.cx/media/screencast.mov) 36 | 37 | ----- 38 | 39 | Current Version: **0.6.0** 40 | 41 | To install or upgrade Pow, open a terminal and run this command: 42 | 43 | $ curl get.pow.cx | sh 44 | 45 | To set up a Rack app, just symlink it into `~/.pow`: 46 | 47 | $ cd ~/.pow 48 | $ ln -s /path/to/myapp 49 | 50 | That's it! Your app will be up and running at `http://myapp.test/`. 51 | See the [user's manual](http://pow.cx/manual) for more information. 52 | 53 | ----- 54 | 55 | © 2017 Sam Stephenson, Basecamp 56 | -------------------------------------------------------------------------------- /bin/pow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../lib/command.js"); 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # `./build.sh` generates dist/$VERSION.tar.gz 3 | # `./build.sh --install` installs into ~/Library/Application Support/Pow/Current 4 | 5 | VERSION=$(node -e 'console.log(JSON.parse(require("fs").readFileSync("package.json","utf8")).version); ""') 6 | ROOT="/tmp/pow-build.$$" 7 | DIST="$(pwd)/dist" 8 | 9 | cake build 10 | 11 | mkdir -p "$ROOT/$VERSION/node_modules" 12 | cp -R package.json bin lib libexec "$ROOT/$VERSION" 13 | cp Cakefile "$ROOT/$VERSION" 14 | cd "$ROOT/$VERSION" 15 | BUNDLE_ONLY=1 npm install --production &>/dev/null 16 | cp `which node` bin 17 | 18 | if [ "$1" == "--install" ]; then 19 | POW_ROOT="$HOME/Library/Application Support/Pow" 20 | rm -fr "$POW_ROOT/Versions/9999.0.0" 21 | mkdir -p "$POW_ROOT/Versions" 22 | cp -R "$ROOT/$VERSION" "$POW_ROOT/Versions/9999.0.0" 23 | rm -f "$POW_ROOT/Current" 24 | cd "$POW_ROOT" 25 | ln -s Versions/9999.0.0 Current 26 | echo "$POW_ROOT/Versions/9999.0.0" 27 | else 28 | cd "$ROOT" 29 | tar czf "$VERSION.tar.gz" "$VERSION" 30 | mkdir -p "$DIST" 31 | cd "$DIST" 32 | mv "$ROOT/$VERSION.tar.gz" "$DIST" 33 | echo "$DIST/$VERSION.tar.gz" 34 | fi 35 | 36 | rm -fr "$ROOT" 37 | -------------------------------------------------------------------------------- /docs/dns_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dns_server.coffee 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 74 | 75 | 178 |
179 | 180 | 181 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | index.coffee 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 74 | 75 | 243 |
244 | 245 | 246 | -------------------------------------------------------------------------------- /docs/logger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logger.coffee 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 74 | 75 | 226 |
227 | 228 | 229 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basecamp/pow/56d5214055947db83e2f82a9a16175cf63c15cdf/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /install.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 installation script for Pow. 32 | # See the full annotated source: http://pow.cx/docs/ 33 | # 34 | # Install Pow by running this command: 35 | # curl get.pow.cx | sh 36 | # 37 | # Uninstall Pow: :'( 38 | # curl get.pow.cx/uninstall.sh | sh 39 | 40 | 41 | # Set up the environment. 42 | 43 | set -e 44 | POW_ROOT="$HOME/Library/Application Support/Pow" 45 | NODE_BIN="$POW_ROOT/Current/bin/node" 46 | POW_BIN="$POW_ROOT/Current/bin/pow" 47 | LATEST_VERSION="0.6.0" 48 | 49 | if [ -z "$ARCHIVE_URL_ROOT" ]; then 50 | ARCHIVE_URL_ROOT="http://get.pow.cx/versions" 51 | else 52 | ARCHIVE_URL_ROOT="${ARCHIVE_URL_ROOT%/}" 53 | fi 54 | 55 | 56 | # Fail fast if we're not on OS X. 57 | 58 | if [ "$(uname -s)" != "Darwin" ]; then 59 | echo "Sorry, Pow requires Mac OS X to run." >&2 60 | exit 1 61 | fi 62 | 63 | 64 | # Define a function to extract version number components. 65 | 66 | version_component() { 67 | printf "%s" "$1" | 68 | sed -e "s/\./"$'\t'"/g" -e "s/-/"$'\t'"/" | 69 | cut -f "$2" 70 | } 71 | 72 | 73 | # Pow 0.4.3 and earlier require OS X 10.6; 0.5.0 and up require OS X 10.9. 74 | # If $VERSION is unspecified, default to the highest supported version for 75 | # each platform. Otherwise, ensure the platform version is recent enough. 76 | 77 | MAC_OS_VERSION="$(sw_vers -productVersion)" 78 | MAC_OS_MINOR_VERSION="$(version_component "$MAC_OS_VERSION" 2)" 79 | 80 | if [ "$MAC_OS_MINOR_VERSION" -lt 6 ]; then 81 | echo "Pow requires Mac OS X 10.6 or later." >&2 82 | exit 1 83 | 84 | elif [ -z "$VERSION" ]; then 85 | if [ "$MAC_OS_MINOR_VERSION" -lt 9 ]; then 86 | VERSION="0.4.3" 87 | else 88 | VERSION="$LATEST_VERSION" 89 | fi 90 | 91 | else 92 | POW_MAJOR_VERSION="$(version_component "$VERSION" 1)" 93 | POW_MINOR_VERSION="$(version_component "$VERSION" 2)" 94 | 95 | if [ "$MAC_OS_MINOR_VERSION" -gt 9 ]; then 96 | if [ "$POW_MAJOR_VERSION" -le 0 ] && [ "$POW_MINOR_VERSION" -lt 5 ]; then 97 | echo "OS X $MAC_OS_VERSION requires Pow 0.5.0 or later." >&2 98 | exit 1 99 | fi 100 | 101 | elif [ "$MAC_OS_MINOR_VERSION" -lt 9 ]; then 102 | if [ "$POW_MAJOR_VERSION" -gt 0 ] || [ "$POW_MINOR_VERSION" -ge 5 ]; then 103 | echo "Pow $VERSION requires OS X 10.9 or later." >&2 104 | exit 1 105 | fi 106 | fi 107 | fi 108 | 109 | 110 | # Prepare for installation. 111 | 112 | ARCHIVE_URL="$ARCHIVE_URL_ROOT/$VERSION.tar.gz" 113 | echo "*** Installing Pow $VERSION..." 114 | 115 | 116 | # Create the Pow directory structure if it doesn't already exist. 117 | 118 | mkdir -p "$POW_ROOT/Hosts" "$POW_ROOT/Versions" 119 | 120 | 121 | # If the requested version of Pow is already installed, remove it first. 122 | 123 | cd "$POW_ROOT/Versions" 124 | rm -rf "$POW_ROOT/Versions/$VERSION" 125 | 126 | 127 | # Download the requested version of Pow and unpack it. 128 | 129 | curl -sL "$ARCHIVE_URL" | tar xzf - 130 | 131 | 132 | # Update the Current symlink to point to the new version. 133 | 134 | cd "$POW_ROOT" 135 | rm -f Current 136 | ln -s Versions/$VERSION Current 137 | 138 | 139 | # Create the ~/.pow symlink if it doesn't exist. 140 | 141 | cd "$HOME" 142 | [ -a .pow ] || ln -s "$POW_ROOT/Hosts" .pow 143 | 144 | 145 | # Install local configuration files. 146 | 147 | echo "*** Installing local configuration files..." 148 | "$NODE_BIN" "$POW_BIN" --install-local 149 | 150 | 151 | # Check to see whether we need root privileges. 152 | 153 | "$NODE_BIN" "$POW_BIN" --install-system --dry-run >/dev/null && NEEDS_ROOT=0 || NEEDS_ROOT=1 154 | 155 | 156 | # Install system configuration files, if necessary. (Avoid sudo otherwise.) 157 | 158 | if [ $NEEDS_ROOT -eq 1 ]; then 159 | echo "*** Installing system configuration files as root..." 160 | sudo "$NODE_BIN" "$POW_BIN" --install-system 161 | 162 | if [ "$MAC_OS_MINOR_VERSION" -ge 10 ]; then 163 | sudo launchctl bootstrap system /Library/LaunchDaemons/cx.pow.firewall.plist 2>/dev/null 164 | sudo launchctl enable system/cx.pow.firewall 2>/dev/null 165 | sudo launchctl kickstart -k system/cx.pow.firewall 2>/dev/null 166 | else 167 | sudo launchctl load -Fw /Library/LaunchDaemons/cx.pow.firewall.plist 2>/dev/null 168 | fi 169 | fi 170 | 171 | 172 | # Start (or restart) Pow. 173 | 174 | echo "*** Starting the Pow server..." 175 | 176 | if [ "$MAC_OS_MINOR_VERSION" -ge 10 ]; then 177 | launchctl bootstrap gui/"$UID" "$HOME/Library/LaunchAgents/cx.pow.powd.plist" 2>/dev/null 178 | launchctl enable gui/"$UID"/cx.pow.powd 2>/dev/null 179 | launchctl kickstart -k gui/"$UID"/cx.pow.powd 2>/dev/null 180 | else 181 | launchctl unload "$HOME/Library/LaunchAgents/cx.pow.powd.plist" 2>/dev/null || true 182 | launchctl load -Fw "$HOME/Library/LaunchAgents/cx.pow.powd.plist" 2>/dev/null 183 | fi 184 | 185 | 186 | # Show a message about where to go for help. 187 | 188 | function print_troubleshooting_instructions() { 189 | echo 190 | echo "For troubleshooting instructions, please see the Pow wiki:" 191 | echo "https://github.com/basecamp/pow/wiki/Troubleshooting" 192 | echo 193 | echo "To uninstall Pow, \`curl get.pow.cx/uninstall.sh | sh\`" 194 | } 195 | 196 | 197 | # Check to see if the server is running properly. 198 | 199 | # If this version of Pow supports the --print-config option, 200 | # source the configuration and use it to run a self-test. 201 | CONFIG=$("$NODE_BIN" "$POW_BIN" --print-config 2>/dev/null || true) 202 | 203 | if [ -n "$CONFIG" ]; then 204 | eval "$CONFIG" 205 | echo "*** Performing self-test..." 206 | 207 | # Check to see if the server is running at all. 208 | function check_status() { 209 | sleep 1 210 | curl -sH host:pow "127.0.0.1:$POW_HTTP_PORT/status.json" | grep -c "$VERSION" >/dev/null 211 | } 212 | 213 | # Attempt to connect to Pow via each configured domain. If a 214 | # domain is inaccessible, try to force a reload of OS X's 215 | # network configuration. 216 | function check_domains() { 217 | for domain in ${POW_DOMAINS//,/$IFS}; do 218 | echo | nc "${domain}." "$POW_DST_PORT" 2>/dev/null || return 1 219 | done 220 | } 221 | 222 | # Use networksetup(8) to create a temporary network location, 223 | # switch to it, switch back to the original location, then 224 | # delete the temporary location. This forces reloading of the 225 | # system network configuration. 226 | function reload_network_configuration() { 227 | [ "$MAC_OS_MINOR_VERSION" -lt 10 ] || return 228 | echo "*** Reloading system network configuration..." 229 | local location=$(networksetup -getcurrentlocation) 230 | networksetup -createlocation "pow$$" >/dev/null 2>&1 231 | networksetup -switchtolocation "pow$$" >/dev/null 2>&1 232 | networksetup -switchtolocation "$location" >/dev/null 2>&1 233 | networksetup -deletelocation "pow$$" >/dev/null 2>&1 234 | } 235 | 236 | # Try twice to connect to Pow. Bail if it doesn't work. 237 | check_status || check_status || { 238 | echo "!!! Couldn't find a running Pow server on port $POW_HTTP_PORT" 239 | print_troubleshooting_instructions 240 | exit 1 241 | } 242 | 243 | # Try resolving and connecting to each configured domain. If 244 | # it doesn't work, reload the network configuration and try 245 | # again. Bail if it fails the second time. 246 | check_domains || { 247 | { reload_network_configuration && check_domains; } || { 248 | echo "!!! Couldn't resolve configured domains ($POW_DOMAINS)" 249 | print_troubleshooting_instructions 250 | exit 1 251 | } 252 | } 253 | fi 254 | 255 | 256 | # All done! 257 | 258 | echo "*** Installed" 259 | print_troubleshooting_instructions 260 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Configuration, Daemon, Installer, usage, util, _ref; 4 | 5 | _ref = require(".."), Daemon = _ref.Daemon, Configuration = _ref.Configuration, Installer = _ref.Installer; 6 | 7 | util = require("util"); 8 | 9 | process.title = "pow"; 10 | 11 | usage = function() { 12 | console.error("usage: pow [--print-config | --install-local | --install-system [--dry-run]]"); 13 | return process.exit(-1); 14 | }; 15 | 16 | Configuration.getUserConfiguration(function(err, configuration) { 17 | var arg, createInstaller, daemon, dryRun, installer, key, printConfig, shellEscape, underscore, value, _i, _len, _ref1, _ref2, _results; 18 | 19 | if (err) { 20 | throw err; 21 | } 22 | printConfig = false; 23 | createInstaller = null; 24 | dryRun = false; 25 | _ref1 = process.argv.slice(2); 26 | for (_i = 0, _len = _ref1.length; _i < _len; _i++) { 27 | arg = _ref1[_i]; 28 | if (arg === "--print-config") { 29 | printConfig = true; 30 | } else if (arg === "--install-local") { 31 | createInstaller = Installer.getLocalInstaller; 32 | } else if (arg === "--install-system") { 33 | createInstaller = Installer.getSystemInstaller; 34 | } else if (arg === "--dry-run") { 35 | dryRun = true; 36 | } else { 37 | usage(); 38 | } 39 | } 40 | if (dryRun && !createInstaller) { 41 | return usage(); 42 | } else if (printConfig) { 43 | underscore = function(string) { 44 | return string.replace(/(.)([A-Z])/g, function(match, left, right) { 45 | return left + "_" + right.toLowerCase(); 46 | }); 47 | }; 48 | shellEscape = function(string) { 49 | return "'" + string.toString().replace(/'/g, "'\\''") + "'"; 50 | }; 51 | _ref2 = configuration.toJSON(); 52 | _results = []; 53 | for (key in _ref2) { 54 | value = _ref2[key]; 55 | _results.push(util.puts("POW_" + underscore(key).toUpperCase() + "=" + shellEscape(value))); 56 | } 57 | return _results; 58 | } else if (createInstaller) { 59 | installer = createInstaller(configuration); 60 | if (dryRun) { 61 | return installer.needsRootPrivileges(function(needsRoot) { 62 | var exitCode; 63 | 64 | exitCode = needsRoot ? 1 : 0; 65 | return installer.getStaleFiles(function(files) { 66 | var file, _j, _len1; 67 | 68 | for (_j = 0, _len1 = files.length; _j < _len1; _j++) { 69 | file = files[_j]; 70 | util.puts(file.path); 71 | } 72 | return process.exit(exitCode); 73 | }); 74 | }); 75 | } else { 76 | return installer.install(function(err) { 77 | if (err) { 78 | throw err; 79 | } 80 | }); 81 | } 82 | } else { 83 | daemon = new Daemon(configuration); 84 | daemon.on("restart", function() { 85 | return process.exit(); 86 | }); 87 | return daemon.start(); 88 | } 89 | }); 90 | 91 | }).call(this); 92 | -------------------------------------------------------------------------------- /lib/configuration.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Configuration, Logger, async, compilePattern, fs, getFilenamesForHost, getUserEnv, libraryPath, mkdirp, path, rstat, sourceScriptEnv, 4 | __slice = [].slice; 5 | 6 | fs = require("fs"); 7 | 8 | path = require("path"); 9 | 10 | async = require("async"); 11 | 12 | Logger = require("./logger"); 13 | 14 | mkdirp = require("./util").mkdirp; 15 | 16 | sourceScriptEnv = require("./util").sourceScriptEnv; 17 | 18 | getUserEnv = require("./util").getUserEnv; 19 | 20 | module.exports = Configuration = (function() { 21 | Configuration.userConfigurationPath = path.join(process.env.HOME, ".powconfig"); 22 | 23 | Configuration.loadUserConfigurationEnvironment = function(callback) { 24 | var _this = this; 25 | 26 | return getUserEnv(function(err, env) { 27 | var p; 28 | 29 | if (err) { 30 | return callback(err); 31 | } else { 32 | return fs.exists(p = _this.userConfigurationPath, function(exists) { 33 | if (exists) { 34 | return sourceScriptEnv(p, env, callback); 35 | } else { 36 | return callback(null, env); 37 | } 38 | }); 39 | } 40 | }); 41 | }; 42 | 43 | Configuration.getUserConfiguration = function(callback) { 44 | return this.loadUserConfigurationEnvironment(function(err, env) { 45 | if (err) { 46 | return callback(err); 47 | } else { 48 | return callback(null, new Configuration(env)); 49 | } 50 | }); 51 | }; 52 | 53 | Configuration.optionNames = ["bin", "dstPort", "httpPort", "dnsPort", "timeout", "workers", "domains", "extDomains", "hostRoot", "logRoot", "rvmPath"]; 54 | 55 | function Configuration(env) { 56 | if (env == null) { 57 | env = process.env; 58 | } 59 | this.loggers = {}; 60 | this.initialize(env); 61 | } 62 | 63 | Configuration.prototype.initialize = function(env) { 64 | var _base, _base1, _ref, _ref1, _ref10, _ref11, _ref12, _ref13, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9; 65 | 66 | this.env = env; 67 | this.bin = (_ref = env.POW_BIN) != null ? _ref : path.join(__dirname, "../bin/pow"); 68 | this.dstPort = (_ref1 = env.POW_DST_PORT) != null ? _ref1 : 80; 69 | this.httpPort = (_ref2 = env.POW_HTTP_PORT) != null ? _ref2 : 20559; 70 | this.dnsPort = (_ref3 = env.POW_DNS_PORT) != null ? _ref3 : 20560; 71 | this.timeout = (_ref4 = env.POW_TIMEOUT) != null ? _ref4 : 15 * 60; 72 | this.workers = (_ref5 = env.POW_WORKERS) != null ? _ref5 : 2; 73 | this.domains = (_ref6 = (_ref7 = env.POW_DOMAINS) != null ? _ref7 : env.POW_DOMAIN) != null ? _ref6 : "test,dev"; 74 | this.extDomains = (_ref8 = env.POW_EXT_DOMAINS) != null ? _ref8 : []; 75 | this.domains = (_ref9 = typeof (_base = this.domains).split === "function" ? _base.split(",") : void 0) != null ? _ref9 : this.domains; 76 | this.extDomains = (_ref10 = typeof (_base1 = this.extDomains).split === "function" ? _base1.split(",") : void 0) != null ? _ref10 : this.extDomains; 77 | this.allDomains = this.domains.concat(this.extDomains); 78 | this.allDomains.push(/\d+\.\d+\.\d+\.\d+\.xip\.io$/, /[0-9a-z]{1,7}\.xip\.io$/); 79 | this.supportRoot = libraryPath("Application Support", "Pow"); 80 | this.hostRoot = (_ref11 = env.POW_HOST_ROOT) != null ? _ref11 : path.join(this.supportRoot, "Hosts"); 81 | this.logRoot = (_ref12 = env.POW_LOG_ROOT) != null ? _ref12 : libraryPath("Logs", "Pow"); 82 | this.rvmPath = (_ref13 = env.POW_RVM_PATH) != null ? _ref13 : path.join(process.env.HOME, ".rvm/scripts/rvm"); 83 | this.dnsDomainPattern = compilePattern(this.domains); 84 | return this.httpDomainPattern = compilePattern(this.allDomains); 85 | }; 86 | 87 | Configuration.prototype.toJSON = function() { 88 | var key, result, _i, _len, _ref; 89 | 90 | result = {}; 91 | _ref = this.constructor.optionNames; 92 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 93 | key = _ref[_i]; 94 | result[key] = this[key]; 95 | } 96 | return result; 97 | }; 98 | 99 | Configuration.prototype.getLogger = function(name) { 100 | var _base; 101 | 102 | return (_base = this.loggers)[name] || (_base[name] = new Logger(path.join(this.logRoot, name + ".log"))); 103 | }; 104 | 105 | Configuration.prototype.disableRvmDeprecationNotices = function() { 106 | return fs.writeFile(path.join(this.supportRoot, ".disableRvmDeprecationNotices"), ""); 107 | }; 108 | 109 | Configuration.prototype.enableRvmDeprecationNotices = function() { 110 | return fs.unlink(path.join(this.supportRoot, ".disableRvmDeprecationNotices")); 111 | }; 112 | 113 | Configuration.prototype.findHostConfiguration = function(host, callback) { 114 | var _this = this; 115 | 116 | if (host == null) { 117 | host = ""; 118 | } 119 | return this.gatherHostConfigurations(function(err, hosts) { 120 | var config, domain, file, _i, _j, _len, _len1, _ref, _ref1; 121 | 122 | if (err) { 123 | return callback(err); 124 | } 125 | _ref = _this.allDomains; 126 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 127 | domain = _ref[_i]; 128 | _ref1 = getFilenamesForHost(host, domain); 129 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 130 | file = _ref1[_j]; 131 | if (config = hosts[file]) { 132 | return callback(null, domain, config); 133 | } 134 | } 135 | } 136 | if (config = hosts["default"]) { 137 | return callback(null, _this.allDomains[0], config); 138 | } 139 | return callback(null); 140 | }); 141 | }; 142 | 143 | Configuration.prototype.gatherHostConfigurations = function(callback) { 144 | var hosts, 145 | _this = this; 146 | 147 | hosts = {}; 148 | return mkdirp(this.hostRoot, function(err) { 149 | if (err) { 150 | return callback(err); 151 | } 152 | return fs.readdir(_this.hostRoot, function(err, files) { 153 | if (err) { 154 | return callback(err); 155 | } 156 | return async.forEach(files, function(file, next) { 157 | var name, root; 158 | 159 | root = path.join(_this.hostRoot, file); 160 | name = file.toLowerCase(); 161 | return rstat(root, function(err, stats, path) { 162 | if (stats != null ? stats.isDirectory() : void 0) { 163 | hosts[name] = { 164 | root: path 165 | }; 166 | return next(); 167 | } else if (stats != null ? stats.isFile() : void 0) { 168 | return fs.readFile(path, 'utf-8', function(err, data) { 169 | if (err) { 170 | return next(); 171 | } 172 | data = data.trim(); 173 | if (data.length < 10 && !isNaN(parseInt(data))) { 174 | hosts[name] = { 175 | url: "http://localhost:" + (parseInt(data)) 176 | }; 177 | } else if (data.match("https?://")) { 178 | hosts[name] = { 179 | url: data 180 | }; 181 | } 182 | return next(); 183 | }); 184 | } else { 185 | return next(); 186 | } 187 | }); 188 | }, function(err) { 189 | return callback(err, hosts); 190 | }); 191 | }); 192 | }); 193 | }; 194 | 195 | return Configuration; 196 | 197 | })(); 198 | 199 | libraryPath = function() { 200 | var args; 201 | 202 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 203 | return path.join.apply(path, [process.env.HOME, "Library"].concat(__slice.call(args))); 204 | }; 205 | 206 | getFilenamesForHost = function(host, domain) { 207 | var i, length, parts, _i, _ref, _ref1, _results; 208 | 209 | host = host.toLowerCase(); 210 | if (domain.test != null) { 211 | domain = (_ref = (_ref1 = host.match(domain)) != null ? _ref1[0] : void 0) != null ? _ref : ""; 212 | } 213 | if (host.slice(-domain.length - 1) === ("." + domain)) { 214 | parts = host.slice(0, -domain.length - 1).split("."); 215 | length = parts.length; 216 | _results = []; 217 | for (i = _i = 0; 0 <= length ? _i < length : _i > length; i = 0 <= length ? ++_i : --_i) { 218 | _results.push(parts.slice(i, length).join(".")); 219 | } 220 | return _results; 221 | } else { 222 | return []; 223 | } 224 | }; 225 | 226 | rstat = function(path, callback) { 227 | return fs.lstat(path, function(err, stats) { 228 | if (err) { 229 | return callback(err); 230 | } else if (stats != null ? stats.isSymbolicLink() : void 0) { 231 | return fs.realpath(path, function(err, realpath) { 232 | if (err) { 233 | return callback(err); 234 | } else { 235 | return rstat(realpath, callback); 236 | } 237 | }); 238 | } else { 239 | return callback(err, stats, path); 240 | } 241 | }); 242 | }; 243 | 244 | compilePattern = function(domains) { 245 | return RegExp("((^|\\.)(" + (domains.join("|")) + "))\\.?$", "i"); 246 | }; 247 | 248 | }).call(this); 249 | -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Daemon, DnsServer, EventEmitter, HttpServer, fs, path, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 5 | __hasProp = {}.hasOwnProperty, 6 | __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; }; 7 | 8 | EventEmitter = require("events").EventEmitter; 9 | 10 | HttpServer = require("./http_server"); 11 | 12 | DnsServer = require("./dns_server"); 13 | 14 | fs = require("fs"); 15 | 16 | path = require("path"); 17 | 18 | module.exports = Daemon = (function(_super) { 19 | __extends(Daemon, _super); 20 | 21 | function Daemon(configuration) { 22 | var hostRoot, 23 | _this = this; 24 | 25 | this.configuration = configuration; 26 | this.stop = __bind(this.stop, this); 27 | this.hostRootChanged = __bind(this.hostRootChanged, this); 28 | this.httpServer = new HttpServer(this.configuration); 29 | this.dnsServer = new DnsServer(this.configuration); 30 | process.on("SIGINT", this.stop); 31 | process.on("SIGTERM", this.stop); 32 | process.on("SIGQUIT", this.stop); 33 | hostRoot = this.configuration.hostRoot; 34 | this.restartFilename = path.join(hostRoot, "restart.txt"); 35 | this.on("start", function() { 36 | return _this.watcher = fs.watch(hostRoot, { 37 | persistent: false 38 | }, _this.hostRootChanged); 39 | }); 40 | this.on("stop", function() { 41 | var _ref; 42 | 43 | return (_ref = _this.watcher) != null ? _ref.close() : void 0; 44 | }); 45 | } 46 | 47 | Daemon.prototype.hostRootChanged = function() { 48 | var _this = this; 49 | 50 | return fs.exists(this.restartFilename, function(exists) { 51 | if (exists) { 52 | return _this.restart(); 53 | } 54 | }); 55 | }; 56 | 57 | Daemon.prototype.restart = function() { 58 | var _this = this; 59 | 60 | return fs.unlink(this.restartFilename, function(err) { 61 | if (!err) { 62 | return _this.emit("restart"); 63 | } 64 | }); 65 | }; 66 | 67 | Daemon.prototype.start = function() { 68 | var dnsPort, flunk, httpPort, pass, startServer, _ref, 69 | _this = this; 70 | 71 | if (this.starting || this.started) { 72 | return; 73 | } 74 | this.starting = true; 75 | startServer = function(server, port, callback) { 76 | return process.nextTick(function() { 77 | var err; 78 | 79 | try { 80 | server.on('error', callback); 81 | server.once('listening', function() { 82 | server.removeListener('error', callback); 83 | return callback(); 84 | }); 85 | return server.listen(port); 86 | } catch (_error) { 87 | err = _error; 88 | return callback(err); 89 | } 90 | }); 91 | }; 92 | pass = function() { 93 | _this.starting = false; 94 | _this.started = true; 95 | return _this.emit("start"); 96 | }; 97 | flunk = function(err) { 98 | _this.starting = false; 99 | try { 100 | _this.httpServer.close(); 101 | } catch (_error) {} 102 | try { 103 | _this.dnsServer.close(); 104 | } catch (_error) {} 105 | return _this.emit("error", err); 106 | }; 107 | _ref = this.configuration, httpPort = _ref.httpPort, dnsPort = _ref.dnsPort; 108 | return startServer(this.httpServer, httpPort, function(err) { 109 | if (err) { 110 | return flunk(err); 111 | } else { 112 | return startServer(_this.dnsServer, dnsPort, function(err) { 113 | if (err) { 114 | return flunk(err); 115 | } else { 116 | return pass(); 117 | } 118 | }); 119 | } 120 | }); 121 | }; 122 | 123 | Daemon.prototype.stop = function() { 124 | var stopServer, 125 | _this = this; 126 | 127 | if (this.stopping || !this.started) { 128 | return; 129 | } 130 | this.stopping = true; 131 | stopServer = function(server, callback) { 132 | return process.nextTick(function() { 133 | var close, err; 134 | 135 | try { 136 | close = function() { 137 | server.removeListener("close", close); 138 | return callback(null); 139 | }; 140 | server.on("close", close); 141 | return server.close(); 142 | } catch (_error) { 143 | err = _error; 144 | return callback(err); 145 | } 146 | }); 147 | }; 148 | return stopServer(this.httpServer, function() { 149 | return stopServer(_this.dnsServer, function() { 150 | _this.stopping = false; 151 | _this.started = false; 152 | return _this.emit("stop"); 153 | }); 154 | }); 155 | }; 156 | 157 | return Daemon; 158 | 159 | })(EventEmitter); 160 | 161 | }).call(this); 162 | -------------------------------------------------------------------------------- /lib/dns_server.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var DnsServer, NS_C_IN, NS_RCODE_NXDOMAIN, NS_T_A, dnsserver, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 5 | __hasProp = {}.hasOwnProperty, 6 | __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; }; 7 | 8 | dnsserver = require("dnsserver"); 9 | 10 | NS_T_A = 1; 11 | 12 | NS_C_IN = 1; 13 | 14 | NS_RCODE_NXDOMAIN = 3; 15 | 16 | module.exports = DnsServer = (function(_super) { 17 | __extends(DnsServer, _super); 18 | 19 | function DnsServer(configuration) { 20 | this.configuration = configuration; 21 | this.handleRequest = __bind(this.handleRequest, this); 22 | DnsServer.__super__.constructor.apply(this, arguments); 23 | this.on("request", this.handleRequest); 24 | } 25 | 26 | DnsServer.prototype.listen = function(port, callback) { 27 | this.bind(port); 28 | return typeof callback === "function" ? callback() : void 0; 29 | }; 30 | 31 | DnsServer.prototype.handleRequest = function(req, res) { 32 | var pattern, q, _ref; 33 | 34 | pattern = this.configuration.dnsDomainPattern; 35 | q = (_ref = req.question) != null ? _ref : {}; 36 | if (q.type === NS_T_A && q["class"] === NS_C_IN && pattern.test(q.name)) { 37 | res.addRR(q.name, NS_T_A, NS_C_IN, 600, "127.0.0.1"); 38 | } else { 39 | res.header.rcode = NS_RCODE_NXDOMAIN; 40 | } 41 | return res.send(); 42 | }; 43 | 44 | return DnsServer; 45 | 46 | })(dnsserver.Server); 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | module.exports = { 4 | Configuration: require("./configuration"), 5 | Daemon: require("./daemon"), 6 | DnsServer: require("./dns_server"), 7 | HttpServer: require("./http_server"), 8 | Installer: require("./installer"), 9 | Logger: require("./logger"), 10 | RackApplication: require("./rack_application"), 11 | util: require("./util") 12 | }; 13 | 14 | }).call(this); 15 | -------------------------------------------------------------------------------- /lib/installer.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Installer, InstallerFile, async, chown, daemonSource, firewallSource, fs, mkdirp, path, resolverSource, util, 4 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 5 | 6 | async = require("async"); 7 | 8 | fs = require("fs"); 9 | 10 | path = require("path"); 11 | 12 | mkdirp = require("./util").mkdirp; 13 | 14 | chown = require("./util").chown; 15 | 16 | util = require("util"); 17 | 18 | resolverSource = require("./templates/installer/resolver"); 19 | 20 | firewallSource = require("./templates/installer/cx.pow.firewall.plist"); 21 | 22 | daemonSource = require("./templates/installer/cx.pow.powd.plist"); 23 | 24 | InstallerFile = (function() { 25 | function InstallerFile(path, source, root, mode) { 26 | this.path = path; 27 | this.root = root != null ? root : false; 28 | this.mode = mode != null ? mode : 0x1a4; 29 | this.setPermissions = __bind(this.setPermissions, this); 30 | this.setOwnership = __bind(this.setOwnership, this); 31 | this.writeFile = __bind(this.writeFile, this); 32 | this.vivifyPath = __bind(this.vivifyPath, this); 33 | this.source = source.trim(); 34 | } 35 | 36 | InstallerFile.prototype.isStale = function(callback) { 37 | var _this = this; 38 | 39 | return fs.exists(this.path, function(exists) { 40 | if (exists) { 41 | return fs.readFile(_this.path, "utf8", function(err, contents) { 42 | if (err) { 43 | return callback(true); 44 | } else { 45 | return callback(_this.source !== contents.trim()); 46 | } 47 | }); 48 | } else { 49 | return callback(true); 50 | } 51 | }); 52 | }; 53 | 54 | InstallerFile.prototype.vivifyPath = function(callback) { 55 | return mkdirp(path.dirname(this.path), callback); 56 | }; 57 | 58 | InstallerFile.prototype.writeFile = function(callback) { 59 | return fs.writeFile(this.path, this.source, "utf8", callback); 60 | }; 61 | 62 | InstallerFile.prototype.setOwnership = function(callback) { 63 | if (this.root) { 64 | return chown(this.path, "root:wheel", callback); 65 | } else { 66 | return callback(false); 67 | } 68 | }; 69 | 70 | InstallerFile.prototype.setPermissions = function(callback) { 71 | return fs.chmod(this.path, this.mode, callback); 72 | }; 73 | 74 | InstallerFile.prototype.install = function(callback) { 75 | return async.series([this.vivifyPath, this.writeFile, this.setOwnership, this.setPermissions], callback); 76 | }; 77 | 78 | return InstallerFile; 79 | 80 | })(); 81 | 82 | module.exports = Installer = (function() { 83 | Installer.getSystemInstaller = function(configuration) { 84 | var domain, files, _i, _len, _ref; 85 | 86 | this.configuration = configuration; 87 | files = [new InstallerFile("/Library/LaunchDaemons/cx.pow.firewall.plist", firewallSource(this.configuration), true)]; 88 | _ref = this.configuration.domains; 89 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 90 | domain = _ref[_i]; 91 | files.push(new InstallerFile("/etc/resolver/" + domain, resolverSource(this.configuration), true)); 92 | } 93 | return new Installer(files); 94 | }; 95 | 96 | Installer.getLocalInstaller = function(configuration) { 97 | this.configuration = configuration; 98 | return new Installer([new InstallerFile("" + process.env.HOME + "/Library/LaunchAgents/cx.pow.powd.plist", daemonSource(this.configuration))]); 99 | }; 100 | 101 | function Installer(files) { 102 | this.files = files != null ? files : []; 103 | } 104 | 105 | Installer.prototype.getStaleFiles = function(callback) { 106 | return async.select(this.files, function(file, proceed) { 107 | return file.isStale(proceed); 108 | }, callback); 109 | }; 110 | 111 | Installer.prototype.needsRootPrivileges = function(callback) { 112 | return this.getStaleFiles(function(files) { 113 | return async.detect(files, function(file, proceed) { 114 | return proceed(file.root); 115 | }, function(result) { 116 | return callback(result != null); 117 | }); 118 | }); 119 | }; 120 | 121 | Installer.prototype.install = function(callback) { 122 | return this.getStaleFiles(function(files) { 123 | return async.forEach(files, function(file, proceed) { 124 | return file.install(function(err) { 125 | if (!err) { 126 | util.puts(file.path); 127 | } 128 | return proceed(err); 129 | }); 130 | }, callback); 131 | }); 132 | }; 133 | 134 | return Installer; 135 | 136 | })(); 137 | 138 | }).call(this); 139 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Log, Logger, dirname, fs, level, mkdirp, _fn, _i, _len, _ref, 4 | __slice = [].slice; 5 | 6 | fs = require("fs"); 7 | 8 | dirname = require("path").dirname; 9 | 10 | Log = require("log"); 11 | 12 | mkdirp = require("./util").mkdirp; 13 | 14 | module.exports = Logger = (function() { 15 | Logger.LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]; 16 | 17 | function Logger(path, level) { 18 | this.path = path; 19 | this.level = level != null ? level : "debug"; 20 | this.readyCallbacks = []; 21 | } 22 | 23 | Logger.prototype.ready = function(callback) { 24 | var _this = this; 25 | 26 | if (this.state === "ready") { 27 | return callback.call(this); 28 | } else { 29 | this.readyCallbacks.push(callback); 30 | if (!this.state) { 31 | this.state = "initializing"; 32 | return mkdirp(dirname(this.path), function(err) { 33 | if (err) { 34 | return _this.state = null; 35 | } else { 36 | _this.stream = fs.createWriteStream(_this.path, { 37 | flags: "a" 38 | }); 39 | return _this.stream.on("open", function() { 40 | var _i, _len, _ref; 41 | 42 | _this.log = new Log(_this.level, _this.stream); 43 | _this.state = "ready"; 44 | _ref = _this.readyCallbacks; 45 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 46 | callback = _ref[_i]; 47 | callback.call(_this); 48 | } 49 | return _this.readyCallbacks = []; 50 | }); 51 | } 52 | }); 53 | } 54 | } 55 | }; 56 | 57 | return Logger; 58 | 59 | })(); 60 | 61 | _ref = Logger.LEVELS; 62 | _fn = function(level) { 63 | return Logger.prototype[level] = function() { 64 | var args; 65 | 66 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 67 | return this.ready(function() { 68 | return this.log[level].apply(this.log, args); 69 | }); 70 | }; 71 | }; 72 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 73 | level = _ref[_i]; 74 | _fn(level); 75 | } 76 | 77 | }).call(this); 78 | -------------------------------------------------------------------------------- /lib/rack_application.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var RackApplication, async, basename, bufferLines, fs, join, nack, pause, resolve, sourceScriptEnv, _ref, _ref1; 4 | 5 | async = require("async"); 6 | 7 | fs = require("fs"); 8 | 9 | nack = require("nack"); 10 | 11 | _ref = require("./util"), bufferLines = _ref.bufferLines, pause = _ref.pause, sourceScriptEnv = _ref.sourceScriptEnv; 12 | 13 | _ref1 = require("path"), join = _ref1.join, basename = _ref1.basename, resolve = _ref1.resolve; 14 | 15 | module.exports = RackApplication = (function() { 16 | function RackApplication(configuration, root, firstHost) { 17 | this.configuration = configuration; 18 | this.root = root; 19 | this.firstHost = firstHost; 20 | this.logger = this.configuration.getLogger(join("apps", basename(this.root))); 21 | this.readyCallbacks = []; 22 | this.quitCallbacks = []; 23 | this.statCallbacks = []; 24 | } 25 | 26 | RackApplication.prototype.ready = function(callback) { 27 | if (this.state === "ready") { 28 | return callback(); 29 | } else { 30 | this.readyCallbacks.push(callback); 31 | return this.initialize(); 32 | } 33 | }; 34 | 35 | RackApplication.prototype.quit = function(callback) { 36 | if (this.state) { 37 | if (callback) { 38 | this.quitCallbacks.push(callback); 39 | } 40 | return this.terminate(); 41 | } else { 42 | return typeof callback === "function" ? callback() : void 0; 43 | } 44 | }; 45 | 46 | RackApplication.prototype.queryRestartFile = function(callback) { 47 | var _this = this; 48 | 49 | return fs.stat(join(this.root, "tmp/restart.txt"), function(err, stats) { 50 | var lastMtime; 51 | 52 | if (err) { 53 | _this.mtime = null; 54 | return callback(false); 55 | } else { 56 | lastMtime = _this.mtime; 57 | _this.mtime = stats.mtime.getTime(); 58 | return callback(lastMtime !== _this.mtime); 59 | } 60 | }); 61 | }; 62 | 63 | RackApplication.prototype.setPoolRunOnceFlag = function(callback) { 64 | var _this = this; 65 | 66 | if (!this.statCallbacks.length) { 67 | fs.exists(join(this.root, "tmp/always_restart.txt"), function(alwaysRestart) { 68 | var statCallback, _i, _len, _ref2; 69 | 70 | _this.pool.runOnce = alwaysRestart; 71 | _ref2 = _this.statCallbacks; 72 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 73 | statCallback = _ref2[_i]; 74 | statCallback(); 75 | } 76 | return _this.statCallbacks = []; 77 | }); 78 | } 79 | return this.statCallbacks.push(callback); 80 | }; 81 | 82 | RackApplication.prototype.loadScriptEnvironment = function(env, callback) { 83 | var _this = this; 84 | 85 | return async.reduce([".powrc", ".envrc", ".powenv"], env, function(env, filename, callback) { 86 | var script; 87 | 88 | return fs.exists(script = join(_this.root, filename), function(scriptExists) { 89 | if (scriptExists) { 90 | return sourceScriptEnv(script, env, callback); 91 | } else { 92 | return callback(null, env); 93 | } 94 | }); 95 | }, callback); 96 | }; 97 | 98 | RackApplication.prototype.loadRvmEnvironment = function(env, callback) { 99 | var script, 100 | _this = this; 101 | 102 | return fs.exists(script = join(this.root, ".rvmrc"), function(rvmrcExists) { 103 | var rvm; 104 | 105 | if (rvmrcExists) { 106 | return fs.exists(rvm = _this.configuration.rvmPath, function(rvmExists) { 107 | var before, libexecPath; 108 | 109 | if (rvmExists) { 110 | libexecPath = resolve("" + __dirname + "/../libexec"); 111 | before = ("'" + libexecPath + "/pow_rvm_deprecation_notice' '" + [_this.firstHost] + "'\nsource '" + rvm + "' > /dev/null").trim(); 112 | return sourceScriptEnv(script, env, { 113 | before: before 114 | }, callback); 115 | } else { 116 | return callback(null, env); 117 | } 118 | }); 119 | } else { 120 | return callback(null, env); 121 | } 122 | }); 123 | }; 124 | 125 | RackApplication.prototype.loadEnvironment = function(callback) { 126 | var _this = this; 127 | 128 | return this.queryRestartFile(function() { 129 | return _this.loadScriptEnvironment(_this.configuration.env, function(err, env) { 130 | if (err) { 131 | return callback(err); 132 | } else { 133 | return _this.loadRvmEnvironment(env, function(err, env) { 134 | if (err) { 135 | return callback(err); 136 | } else { 137 | return callback(null, env); 138 | } 139 | }); 140 | } 141 | }); 142 | }); 143 | }; 144 | 145 | RackApplication.prototype.initialize = function() { 146 | var _this = this; 147 | 148 | if (this.state) { 149 | if (this.state === "terminating") { 150 | this.quit(function() { 151 | return _this.initialize(); 152 | }); 153 | } 154 | return; 155 | } 156 | this.state = "initializing"; 157 | return this.loadEnvironment(function(err, env) { 158 | var readyCallback, _i, _len, _ref2, _ref3, _ref4; 159 | 160 | if (err) { 161 | _this.state = null; 162 | _this.logger.error(err.message); 163 | _this.logger.error("stdout: " + err.stdout); 164 | _this.logger.error("stderr: " + err.stderr); 165 | } else { 166 | _this.state = "ready"; 167 | _this.pool = nack.createPool(join(_this.root, "config.ru"), { 168 | env: env, 169 | size: (_ref2 = env != null ? env.POW_WORKERS : void 0) != null ? _ref2 : _this.configuration.workers, 170 | idle: ((_ref3 = env != null ? env.POW_TIMEOUT : void 0) != null ? _ref3 : _this.configuration.timeout) * 1000 171 | }); 172 | bufferLines(_this.pool.stdout, function(line) { 173 | return _this.logger.info(line); 174 | }); 175 | bufferLines(_this.pool.stderr, function(line) { 176 | return _this.logger.warning(line); 177 | }); 178 | _this.pool.on("worker:spawn", function(process) { 179 | return _this.logger.debug("nack worker " + process.child.pid + " spawned"); 180 | }); 181 | _this.pool.on("worker:exit", function(process) { 182 | return _this.logger.debug("nack worker exited"); 183 | }); 184 | } 185 | _ref4 = _this.readyCallbacks; 186 | for (_i = 0, _len = _ref4.length; _i < _len; _i++) { 187 | readyCallback = _ref4[_i]; 188 | readyCallback(err); 189 | } 190 | return _this.readyCallbacks = []; 191 | }); 192 | }; 193 | 194 | RackApplication.prototype.terminate = function() { 195 | var _this = this; 196 | 197 | if (this.state === "initializing") { 198 | return this.ready(function() { 199 | return _this.terminate(); 200 | }); 201 | } else if (this.state === "ready") { 202 | this.state = "terminating"; 203 | return this.pool.quit(function() { 204 | var quitCallback, _i, _len, _ref2; 205 | 206 | _this.state = null; 207 | _this.mtime = null; 208 | _this.pool = null; 209 | _ref2 = _this.quitCallbacks; 210 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 211 | quitCallback = _ref2[_i]; 212 | quitCallback(); 213 | } 214 | return _this.quitCallbacks = []; 215 | }); 216 | } 217 | }; 218 | 219 | RackApplication.prototype.handle = function(req, res, next, callback) { 220 | var _this = this; 221 | 222 | return this.ready(function(err) { 223 | if (err) { 224 | return next(err); 225 | } 226 | return _this.setPoolRunOnceFlag(function() { 227 | return _this.restartIfNecessary(function() { 228 | req.proxyMetaVariables = { 229 | SERVER_PORT: _this.configuration.dstPort.toString() 230 | }; 231 | try { 232 | return _this.pool.proxy(req, res, function(err) { 233 | if (err) { 234 | _this.quit(); 235 | } 236 | return next(err); 237 | }); 238 | } finally { 239 | if (typeof callback === "function") { 240 | callback(); 241 | } 242 | } 243 | }); 244 | }); 245 | }); 246 | }; 247 | 248 | RackApplication.prototype.restart = function(callback) { 249 | var _this = this; 250 | 251 | return this.quit(function() { 252 | return _this.ready(callback); 253 | }); 254 | }; 255 | 256 | RackApplication.prototype.restartIfNecessary = function(callback) { 257 | var _this = this; 258 | 259 | return this.queryRestartFile(function(mtimeChanged) { 260 | if (mtimeChanged) { 261 | return _this.restart(callback); 262 | } else { 263 | return callback(); 264 | } 265 | }); 266 | }; 267 | 268 | RackApplication.prototype.writeRvmBoilerplate = function() { 269 | var boilerplate, powrc; 270 | 271 | powrc = join(this.root, ".powrc"); 272 | boilerplate = this.constructor.rvmBoilerplate; 273 | return fs.readFile(powrc, "utf8", function(err, contents) { 274 | if (contents == null) { 275 | contents = ""; 276 | } 277 | if (contents.indexOf(boilerplate) === -1) { 278 | return fs.writeFile(powrc, "" + boilerplate + "\n" + contents); 279 | } 280 | }); 281 | }; 282 | 283 | RackApplication.rvmBoilerplate = "if [ -f \"$rvm_path/scripts/rvm\" ] && [ -f \".rvmrc\" ]; then\n source \"$rvm_path/scripts/rvm\"\n source \".rvmrc\"\nfi"; 284 | 285 | return RackApplication; 286 | 287 | })(); 288 | 289 | }).call(this); 290 | -------------------------------------------------------------------------------- /lib/templates/http_server/application_not_found.html.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 | var _this = this; 41 | 42 | __out.push(this.renderTemplate("layout", { 43 | title: "Application not found" 44 | }, function() { 45 | return __capture(function() { 46 | __out.push('\n

Application not found

\n

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:

\n
$ cd ~/.pow\n$ ln -s /path/to/myapp ');
53 |           __out.push(__sanitize(_this.name));
54 |           __out.push('\n$ open http://');
55 |           __out.push(__sanitize(_this.host));
56 |           return __out.push('/
\n
\n'); 57 | }); 58 | })); 59 | 60 | __out.push('\n'); 61 | 62 | }).call(this); 63 | 64 | }).call(__obj); 65 | __obj.safe = __objSafe, __obj.escape = __escape; 66 | return __out.join(''); 67 | } -------------------------------------------------------------------------------- /lib/templates/http_server/error_starting_application.html.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 | var _this = this; 41 | 42 | __out.push(this.renderTemplate("layout", { 43 | title: "Error starting application", 44 | "class": "big" 45 | }, function() { 46 | return __capture(function() { 47 | __out.push('\n

Error starting application

\n

Your Rack app raised an exception when Pow tried to run it.

\n
\n
');
48 |           __out.push(__sanitize(_this.err));
49 |           __out.push('\n');
50 |           __out.push(__sanitize(_this.stack.join("\n")));
51 |           if (_this.rest) {
52 |             __out.push('\nShow ');
53 |             __out.push(__sanitize(_this.rest.length));
54 |             __out.push(' more lines
'); 55 | __out.push(__sanitize(_this.rest.join("\n"))); 56 | __out.push('
'); 57 | } 58 | return __out.push('
\n
\n'); 59 | }); 60 | })); 61 | 62 | __out.push('\n'); 63 | 64 | }).call(this); 65 | 66 | }).call(__obj); 67 | __obj.safe = __objSafe, __obj.escape = __escape; 68 | return __out.join(''); 69 | } -------------------------------------------------------------------------------- /lib/templates/http_server/layout.html.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 '); 41 | 42 | __out.push(__sanitize(this.title)); 43 | 44 | __out.push('\n \n\n\n
\n '); 49 | 50 | __out.push(__sanitize(this.yieldContents())); 51 | 52 | __out.push('\n \n
\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/http_server/proxy_error.html.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 | var _this = this; 41 | 42 | __out.push(this.renderTemplate("layout", { 43 | title: "Proxy Error" 44 | }, function() { 45 | return __capture(function() { 46 | __out.push('\n

Proxy Error

\n

Couldn\'t proxy request to '); 47 | __out.push(__sanitize(_this.hostname)); 48 | __out.push(':'); 49 | __out.push(__sanitize(_this.port)); 50 | __out.push('.

\n
\n
');
51 |           __out.push(__sanitize(_this.err));
52 |           return __out.push('\n  
\n'); 53 | }); 54 | })); 55 | 56 | __out.push('\n'); 57 | 58 | }).call(this); 59 | 60 | }).call(__obj); 61 | __obj.safe = __objSafe, __obj.escape = __escape; 62 | return __out.join(''); 63 | } -------------------------------------------------------------------------------- /lib/templates/http_server/rackup_file_missing.html.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 | var _this = this; 41 | 42 | __out.push(this.renderTemplate("layout", { 43 | title: "Rackup file missing" 44 | }, function() { 45 | return __capture(function() { 46 | return __out.push('\n

Rackup file missing

\n

Your Rails app needs a config.ru file.

\n
\n

If your app is using Rails 2.3, create a config.ru file in the application root containing the following:

\n
require File.dirname(__FILE__) + \'/config/environment\'\nrun ActionController::Dispatcher.new
\n

If you’re using a version of Rails older than 2.3, you’ll need to upgrade first.

\n
\n'); 47 | }); 48 | })); 49 | 50 | __out.push('\n'); 51 | 52 | }).call(this); 53 | 54 | }).call(__obj); 55 | __obj.safe = __objSafe, __obj.escape = __escape; 56 | return __out.join(''); 57 | } -------------------------------------------------------------------------------- /lib/templates/http_server/rvm_deprecation_notice.html.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 Pow: Automatic RVM support is deprecated\n \n \n \n \n

Automatic RVM support is deprecated

\n\n

We’re showing you this notice because you just accessed\n an application with a per-project .rvmrc file.

\n\n

Support for automatically loading\n per-project .rvmrc files in Pow is deprecated and\n will be removed in the next major release.

\n\n

Ensure your application continues to work with future releases\n of Pow by adding the following code to the\n application’s .powrc file:

\n\n
');
41 |     
42 |       __out.push(__sanitize(this.boilerplate));
43 |     
44 |       __out.push('
\n\n

Add this\n code to .powrc for me

\n\n

We won’t notify you again for this project.

\n\n

Don’t\n notify me about deprecations for any other applications,\n either

\n\n

Thanks for using Pow.

\n \n\n'); 45 | 46 | }).call(this); 47 | 48 | }).call(__obj); 49 | __obj.safe = __objSafe, __obj.escape = __escape; 50 | return __out.join(''); 51 | } -------------------------------------------------------------------------------- /lib/templates/http_server/welcome.html.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 | var _this = this; 41 | 42 | __out.push(this.renderTemplate("layout", { 43 | title: "Pow is installed" 44 | }, function() { 45 | return __capture(function() { 46 | __out.push('\n

Pow is installed

\n

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 %>/
9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /src/templates/http_server/error_starting_application.html.eco: -------------------------------------------------------------------------------- 1 | <%- @renderTemplate "layout", title: "Error starting application", class: "big", => %> 2 |

Error starting application

3 |

Your Rack app raised an exception when Pow tried to run it.

4 |
5 |
<%= @err %>
 6 | <%= @stack.join "\n" %><% if @rest: %>
 7 | Show <%= @rest.length %> more lines
<%= @rest.join "\n" %>
<% end %>
8 |
9 | <% end %> 10 | -------------------------------------------------------------------------------- /src/templates/http_server/layout.html.eco: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= @title %> 6 | 102 | 103 | 104 |
105 | <%= @yieldContents() %> 106 | 112 |
113 | 114 | 115 | -------------------------------------------------------------------------------- /src/templates/http_server/proxy_error.html.eco: -------------------------------------------------------------------------------- 1 | <%- @renderTemplate "layout", title: "Proxy Error", => %> 2 |

Proxy Error

3 |

Couldn't proxy request to <%= @hostname %>:<%= @port %>.

4 |
5 |
<%= @err %>
6 |   
7 | <% end %> 8 | -------------------------------------------------------------------------------- /src/templates/http_server/rackup_file_missing.html.eco: -------------------------------------------------------------------------------- 1 | <%- @renderTemplate "layout", title: "Rackup file missing", => %> 2 |

Rackup file missing

3 |

Your Rails app needs a config.ru file.

4 |
5 |

If your app is using Rails 2.3, create a config.ru file in the application root containing the following:

6 |
require File.dirname(__FILE__) + '/config/environment'
 7 | run ActionController::Dispatcher.new
8 |

If you’re using a version of Rails older than 2.3, you’ll need to upgrade first.

9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /src/templates/http_server/rvm_deprecation_notice.html.eco: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pow: Automatic RVM support is deprecated 5 | 42 | 81 | 82 | 83 |

Automatic RVM support is deprecated

84 | 85 |

We’re showing you this notice because you just accessed 86 | an application with a per-project .rvmrc file.

87 | 88 |

Support for automatically loading 89 | per-project .rvmrc files in Pow is deprecated and 90 | will be removed in the next major release.

91 | 92 |

Ensure your application continues to work with future releases 93 | of Pow by adding the following code to the 94 | application’s .powrc file:

95 | 96 |
<%= @boilerplate %>
97 | 98 |

Add this 99 | code to .powrc for me

100 | 101 |

We won’t notify you again for this project.

102 | 103 |

Don’t 104 | notify me about deprecations for any other applications, 105 | either

106 | 107 |

Thanks for using Pow.

108 | 109 | 110 | -------------------------------------------------------------------------------- /src/templates/http_server/welcome.html.eco: -------------------------------------------------------------------------------- 1 | <%- @renderTemplate "layout", title: "Pow is installed", => %> 2 |

Pow is installed

3 |

You’re running version <%= @version %>.

4 |
5 |

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 | --------------------------------------------------------------------------------