├── var ├── .gitkeep └── run │ └── .gitkeep ├── lib ├── pump │ ├── install │ │ ├── unix │ │ │ └── .gitkeep │ │ └── mac │ │ │ ├── rackhttp.rb │ │ │ ├── firewall.rb │ │ │ ├── masqdns.rb │ │ │ └── launchctl.rb │ ├── version.rb │ ├── rvm.rb │ ├── masqdns │ │ ├── names.rb │ │ └── masqdns.rb │ ├── settings.rb │ ├── rackhttp │ │ ├── rackhttp.rb │ │ └── abstract_application.rb │ ├── helpers.rb │ └── install.rb └── pump.rb ├── Rakefile ├── Gemfile ├── config ├── com.github.pump.masqdnsd.plist.erb ├── com.github.pump.rackhttpd.plist.erb ├── masqdns.conf ├── com.github.pump.plist.erb └── com.github.pump.firewall.plist.erb ├── .gitignore ├── Gemfile.lock ├── public └── index.html ├── bin ├── pump └── pumpup ├── pump.gemspec ├── LICENSE └── README.md /var/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/run/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/pump/install/unix/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /config/com.github.pump.masqdnsd.plist.erb: -------------------------------------------------------------------------------- 1 | com.github.pump.plist.erb -------------------------------------------------------------------------------- /config/com.github.pump.rackhttpd.plist.erb: -------------------------------------------------------------------------------- 1 | com.github.pump.plist.erb -------------------------------------------------------------------------------- /lib/pump/version.rb: -------------------------------------------------------------------------------- 1 | module Pump 2 | VERSION = "0.2.1.beta" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .rvmrc 4 | pkg/* 5 | tmp 6 | tags 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /config/masqdns.conf: -------------------------------------------------------------------------------- 1 | # This config was created automatically by pump 2 | # DON'T CHANGE AND REMOVE IT!!! 3 | nameserver 127.0.0.1 4 | port 11253 5 | -------------------------------------------------------------------------------- /lib/pump/install/mac/rackhttp.rb: -------------------------------------------------------------------------------- 1 | module Pump 2 | class Install::RackHTTP < Launchctl 3 | def self.service_name 4 | "rackhttpd" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | pump (0.1.1.beta) 5 | 6 | GEM 7 | remote: http://rubygems.org/ 8 | specs: 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | pump! 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pump - Zero-configuration Rack server 5 | 6 | 7 |

Pump is currently running

8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/pump/rvm.rb: -------------------------------------------------------------------------------- 1 | if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm') 2 | rvm_path = File.dirname(File.dirname(ENV['MY_RUBY_HOME'])) 3 | rvm_lib_path = File.join(rvm_path, 'lib') 4 | $LOAD_PATH.unshift rvm_lib_path 5 | require 'rvm' rescue LoadError 6 | end 7 | 8 | # TODO make a PR to RVM 9 | module RVM 10 | class Environment 11 | def env_path 12 | rvm(:env, "--path", "--", environment_name).stdout.strip 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/pump.rb: -------------------------------------------------------------------------------- 1 | require 'pump/helpers' 2 | require 'pump/settings' 3 | 4 | require 'pump/masqdns/masqdns' 5 | 6 | require 'pump/rackhttp/rackhttp' 7 | require 'pump/rackhttp/abstract_application' 8 | 9 | module Pump 10 | # Run masqdns server 11 | def self.masqdnsd 12 | $0 = "masqdnsd" 13 | MasqDNS.new "127.0.0.1", 11253 14 | end 15 | 16 | # Run rackhttp server 17 | def self.rackhttpd 18 | $0 = "rackhttpd" 19 | RackHTTP.new "127.0.0.1", 11280 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /bin/pump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | case ARGV.first 6 | when "install", "uninstall" 7 | require 'pump/install' 8 | abort "Usage: #{Pump.sudo} pump #{ARGV.first}" unless Pump.superuser_rights? 9 | Pump::Install.send ARGV.first 10 | exit 11 | when "masqdnsd", "rackhttpd" 12 | require 'pump' 13 | Pump.send ARGV.first 14 | when "version" 15 | require 'pump/version' 16 | puts Pump::VERSION; exit 17 | else 18 | puts "Usage: pump #{%w{ install uninstall version }.join(' | ')}" 19 | end 20 | -------------------------------------------------------------------------------- /lib/pump/masqdns/names.rb: -------------------------------------------------------------------------------- 1 | require 'resolv' 2 | 3 | module Pump 4 | class MasqDNS 5 | class Name 6 | def self.create(name) 7 | Resolv::DNS::Name.create(has_domain?(name) ? "#{name}." : "#{name}.pump.") 8 | end 9 | 10 | def self.has_domain?(name) 11 | name.split(".").length >= 2 12 | end 13 | end 14 | 15 | class Names < ::Array 16 | def include?(name) 17 | !exclude?(name) 18 | end 19 | 20 | def exclude?(name) 21 | select { |n| name.eql?(n) || name.subdomain_of?(n) }.empty? 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/com.github.pump.plist.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | <%= SERVICE_NAME_TEMPLATE % service_name %> 7 | ProgramArguments 8 | 9 | sh 10 | -i 11 | -c 12 | $SHELL --login -c "<%= Pump.pump_path %> <%= service_name %>" 13 | 14 | KeepAlive 15 | 16 | RunAtLoad 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /config/com.github.pump.firewall.plist.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.github.pump.firewall 7 | ProgramArguments 8 | 9 | sh 10 | -c 11 | ipfw add fwd 127.0.0.1,11280 tcp from any to me dst-port 80 in && sysctl -w net.inet.ip.forwarding=1 12 | 13 | RunAtLoad 14 | 15 | UserName 16 | root 17 | 18 | 19 | -------------------------------------------------------------------------------- /pump.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "pump/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "pump" 7 | s.version = Pump::VERSION 8 | s.authors = ["Dmitriy Vorotilin"] 9 | s.email = ["d.vorotilin@gmail.com"] 10 | s.homepage = "https://github.com/evrone/pump" 11 | s.summary = "Ruby Rack server for developers" 12 | s.description = "Zero-configuration Rack server written on pure ruby" 13 | 14 | s.rubyforge_project = "pump" 15 | 16 | s.files = Dir["bin/*", "config/*", "lib/**/*", "public/*", "var/**/.*", "Gemfile", "LICENSE", "Rakefile", "README.md", "pump.gemspec"] 17 | s.executables = Dir["bin/*"].map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | end 20 | -------------------------------------------------------------------------------- /lib/pump/install/mac/firewall.rb: -------------------------------------------------------------------------------- 1 | module Pump 2 | class Install::Firewall < Launchctl 3 | PLIST_DIR = "/Library/LaunchDaemons" 4 | PLIST = File.join(PLIST_DIR, "#{SERVICE_NAME_TEMPLATE}.plist") 5 | 6 | def self.service_name 7 | "firewall" 8 | end 9 | 10 | # Create and load plist under root 11 | def self.install 12 | mkdir_plist 13 | create_plist 14 | Base.load(PLIST % service_name) 15 | end 16 | 17 | # Unload and remove plist under root 18 | def self.uninstall 19 | Base.stop(SERVICE_NAME_TEMPLATE % service_name) 20 | Base.unload(PLIST % service_name) 21 | remove_plist 22 | # TODO: Removing ipfw rule, is there more ruby way? 23 | system("ipfw list | grep 127.0.0.1,11280 | awk '{ system(\"sudo ipfw delete \" $1) }'") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/pump/settings.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'pump/masqdns/names' 3 | 4 | module Pump 5 | module Settings 6 | PATTERN = File.join(USER_CONFIG_DIR, "*") 7 | 8 | def self.settings 9 | symlinks.map do |symlink| 10 | name = MasqDNS::Name.create File.basename(symlink) 11 | path = Pathname.new(symlink).realpath 12 | [name, path] 13 | end.compact 14 | end 15 | 16 | def self.symlinks 17 | Dir.glob(PATTERN).select do |file| 18 | File.lstat(file).symlink? && Pathname.new(file).exist? 19 | end 20 | end 21 | 22 | def self.domain_names 23 | MasqDNS::Names.new settings.map(&:first) 24 | end 25 | 26 | def self.first_level_domains 27 | names = domain_names.map { |domain| domain.to_a.last.to_s } 28 | names << "pump" 29 | names.uniq 30 | end 31 | 32 | def self.find_by_domain(name) 33 | settings.find { |domain, path| domain.to_s == name } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/pump/rackhttp/rackhttp.rb: -------------------------------------------------------------------------------- 1 | require 'webrick' 2 | 3 | module Pump 4 | class RackHTTP 5 | @@config = WEBrick::Config::HTTP 6 | 7 | def initialize(address, port) 8 | @server = TCPServer.new(address, port) 9 | handle_requests 10 | end 11 | 12 | def handle_requests 13 | loop do 14 | client = @server.accept 15 | req, res = WEBrick::HTTPRequest.new(@@config), WEBrick::HTTPResponse.new(@@config) 16 | req.parse(client) 17 | 18 | debug "Request: #{req.unparsed_uri}" 19 | 20 | if app = lookup_server(req) 21 | app.service(req, res) 22 | else 23 | res.status = 200 24 | res.body = "Rack server not found." 25 | end 26 | 27 | client.send res.to_s, 0 28 | client.close 29 | 30 | debug "Answer #{res.object_id} sent." 31 | end 32 | end 33 | 34 | def lookup_server(req) 35 | debug "Lookup responsible server" 36 | domain, path = Settings.find_by_domain(req.host) 37 | Pump::AbstractApplication.get_instance(path) if path 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Evrone.com 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/pump/install/mac/masqdns.rb: -------------------------------------------------------------------------------- 1 | require "pump/settings" 2 | 3 | module Pump 4 | class Install::MasqDNS < Launchctl 5 | RESOLVER_PATH = "/etc/resolver" 6 | CONFIG = File.join(PUMP_ROOT, "config", "masqdns.conf") 7 | 8 | def self.service_name 9 | "masqdnsd" 10 | end 11 | 12 | def self.install 13 | create_resolvers 14 | super 15 | end 16 | 17 | def self.uninstall 18 | remove_resolvers 19 | super 20 | end 21 | 22 | def self.resolvers 23 | Settings.first_level_domains.each do |domain| 24 | yield File.join(RESOLVER_PATH, domain) 25 | end 26 | end 27 | 28 | def self.create_resolvers 29 | create_resolver_dir 30 | resolvers do |resolver| 31 | unless File.exist?(resolver) 32 | FileUtils.cp(CONFIG, resolver) 33 | debug "Resolver #{resolver} was created." 34 | end 35 | end 36 | end 37 | 38 | def self.remove_resolvers 39 | resolvers do |resolver| 40 | if File.exist?(resolver) 41 | FileUtils.rm(resolver) 42 | debug "Resolver #{resolver} was removed." 43 | end 44 | end 45 | end 46 | 47 | def self.create_resolver_dir 48 | FileUtils.mkdir(RESOLVER_PATH) unless File.exist?(RESOLVER_PATH) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/pump/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'pump/rvm' 2 | 3 | module Pump 4 | PUMP_ROOT = File.expand_path("../../../", __FILE__) 5 | USER_CONFIG_DIR = File.join(ENV["HOME"], ".pump") 6 | 7 | module Helpers 8 | def mac? 9 | !!(RUBY_PLATFORM =~ /darwin/) 10 | end 11 | 12 | # Get pump path from rvm bin directory if we used rvm wrapper 13 | def pump_path 14 | defined?(RVM) ? wrapper_path : %x(which pump) 15 | end 16 | 17 | # Get pumpup path to run rack application 18 | def pumpup_path 19 | File.join(PUMP_ROOT, "bin", "pumpup") 20 | end 21 | 22 | # Check superuser privileges 23 | def superuser_rights? 24 | [Process.uid, Process.euid] == [0, 0] 25 | end 26 | 27 | # Switch superuser privileges 28 | def switch_privileges 29 | Process.uid, Process.gid = ENV["SUDO_UID"].to_i, ENV["SUDO_GID"].to_i 30 | Process::UID.switch do 31 | Process::GID.switch do 32 | yield 33 | end 34 | end 35 | Process.uid, Process.gid = Process.euid, Process.egid 36 | end 37 | 38 | def wrapper_path 39 | File.join(RVM::Environment.rvm_bin_path, "pump") 40 | end 41 | 42 | def sudo 43 | defined?(RVM) ? "rvmsudo" : "sudo" 44 | end 45 | 46 | def logger(message) 47 | STDERR.puts message 48 | end 49 | end 50 | 51 | extend Helpers 52 | end 53 | 54 | def debug(*options) 55 | Pump.logger(*options) 56 | end 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Pump 2 | 3 | Zero-configuration Rack server written on pure ruby. 4 | 5 | ### Installation 6 | Only Mac OS X is supported now. 7 | 8 | without rvm: 9 | 10 | $ gem install pump 11 | $ sudo pump install 12 | 13 | with rvm: 14 | 15 | We recommend you to use individual gem set for pump. Don't worry pump will be available for all gem sets and rubies. 16 | 17 | $ rvm use 1.9.3@pump --create 18 | $ gem install pump 19 | $ rvmsudo pump install 20 | 21 | NOTE: If you have installed pow and you don't want uninstall it that I have good news. 22 | You can install pump and remove ipfw rule: 23 | 24 | sudo ipfw list | grep 20559 25 | sudo delete NUM 26 | 27 | Where NUM is first column of command output. 28 | Note that if you restart system you should repeat steps again. 29 | 30 | ### Usage 31 | 32 | After `pump install`, you should create symbol link to your project. 33 | 34 | $ cd ~/.pump 35 | $ ln -s /path-to-app/dirname 36 | 37 | By default your project will be available as dirname.pump 38 | You can set up your own domain name when you create a symlink. It's simple, let's see: 39 | 40 | $ cd ~/.pump && ln -s /path-to-app/dirname domain.name 41 | 42 | And that's it, yes. 43 | 44 | ### Uninstall 45 | 46 | You can uninstall it whenever you want. 47 | 48 | without rvm: 49 | 50 | $ sudo pump uninstall 51 | $ gem uninstall pump 52 | 53 | with rvm: 54 | 55 | $ rvmsudo pump uninstall 56 | $ gem uninstall pump 57 | -------------------------------------------------------------------------------- /lib/pump/install.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'pump/helpers' 3 | 4 | module Pump 5 | module Install 6 | def self.install 7 | # TODO: Add linux support 8 | abort("Currently only macs sorry.") unless Pump.mac? 9 | 10 | Pump.switch_privileges do 11 | mkdir_config 12 | create_rvm_wrapper if defined?(RVM) 13 | end 14 | 15 | if Pump.mac? 16 | dependencies(:install) 17 | else 18 | raise "Support only for mac now." 19 | end 20 | end 21 | 22 | def self.uninstall 23 | # TODO: Add linux support 24 | abort("Currently only macs sorry.") unless Pump.mac? 25 | 26 | if Pump.mac? 27 | dependencies(:uninstall) 28 | else 29 | raise "Support only for mac now." 30 | end 31 | 32 | Pump.switch_privileges do 33 | rmdir_config 34 | remove_rvm_wrapper if defined?(RVM) 35 | end 36 | end 37 | 38 | def self.create_rvm_wrapper 39 | RVM.wrapper RVM.current.environment_name, "--no-prefix", "pump" 40 | end 41 | 42 | def self.remove_rvm_wrapper 43 | FileUtils.rm(Pump.pump_path) if File.exist?(Pump.pump_path) 44 | end 45 | 46 | def self.mkdir_config 47 | FileUtils.mkdir(USER_CONFIG_DIR) unless File.exist?(USER_CONFIG_DIR) 48 | end 49 | 50 | def self.rmdir_config 51 | print "Remove config directory #{USER_CONFIG_DIR} (y/n)? " 52 | FileUtils.rm_rf(USER_CONFIG_DIR) if STDIN.gets.strip == 'y' && File.exist?(USER_CONFIG_DIR) 53 | end 54 | 55 | def self.dependencies(action) 56 | platform = Pump.mac? ? "mac" : "unix" 57 | require "pump/install/#{platform}/launchctl" if Pump.mac? 58 | require "pump/install/#{platform}/masqdns" 59 | require "pump/install/#{platform}/rackhttp" 60 | require "pump/install/#{platform}/firewall" 61 | 62 | [MasqDNS, Firewall, RackHTTP].each(&action) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/pump/masqdns/masqdns.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Pump 4 | class MasqDNS 5 | @@resource = { 6 | "A" => Resolv::DNS::Resource::IN::A.new("127.0.0.1"), 7 | "AAAA" => Resolv::DNS::Resource::IN::AAAA.new("::1") 8 | } 9 | @@ttl = 10800 # 3 hours 10 | 11 | def initialize(addr, port) 12 | # Bind port to receive requests 13 | socket = UDPSocket.new 14 | socket.bind(addr, port) 15 | 16 | loop do 17 | # Receive and parse query 18 | data, sender_addrinfo = socket.recvfrom(512) 19 | debug "Incoming request" 20 | 21 | Thread.new(data, sender_addrinfo) do |data, sender_addrinfo| 22 | sender_port, sender_ip = sender_addrinfo[1], sender_addrinfo[2] 23 | query = Resolv::DNS::Message.decode(data) 24 | debug "Query: #{query.inspect}" 25 | answer = setup_answer(query) 26 | socket.send(answer.encode, 0, sender_ip, sender_port) # Send the response 27 | debug "Answer: #{answer.inspect}" 28 | end 29 | end 30 | end 31 | 32 | # Setup answer 33 | def setup_answer(query) 34 | # Standard fields 35 | answer = Resolv::DNS::Message.new(query.id) 36 | answer.qr = 1 # 0 = Query, 1 = Response 37 | answer.opcode = query.opcode # Type of Query; copy from query 38 | answer.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes 39 | answer.rd = query.rd # Is Recursion Desired, copied from query 40 | answer.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes 41 | answer.rcode = 0 # Response code: 0 = No errors 42 | each_question(query, answer) # There may be multiple questions per query 43 | end 44 | 45 | def each_question(query, answer) 46 | query.each_question do |name, typeclass| 47 | type = typeclass.name.split("::").last 48 | if type == "A" || type == "AAAA" # We need only A and AAAA records 49 | if Settings.domain_names.include?(name) 50 | answer.add_answer(name, @@ttl, @@resource[type]) # Setup answer to this name 51 | answer.encode # Don't forget encode it 52 | end 53 | end 54 | end 55 | answer 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/pump/rackhttp/abstract_application.rb: -------------------------------------------------------------------------------- 1 | module Pump 2 | class AbstractApplication 3 | attr_reader :socket_path, :app_path 4 | 5 | def self.get_instance(app_path) 6 | @@apps ||= Hash.new 7 | @@apps[app_path] ||= new(app_path) 8 | debug "Got instance of application #{app_path}" 9 | @@apps[app_path] 10 | end 11 | 12 | def initialize(app_path) 13 | @app_path, @socket_path = app_path, File.join(PUMP_ROOT, "var", "run", "#{File.basename(app_path)}.sock") 14 | fork_app 15 | end 16 | 17 | def service(req, res) 18 | fork_app unless File.exist?(socket_path) 19 | socket = UNIXSocket.open(socket_path) 20 | 21 | env = req.meta_vars 22 | env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] 23 | env["QUERY_STRING"] ||= "" 24 | unless env["PATH_INFO"] == "" 25 | path, n = req.request_uri.path, env["SCRIPT_NAME"].length 26 | env["PATH_INFO"] = path[n, path.length-n] 27 | end 28 | env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env["PATH_INFO"]].join 29 | env["body"] = req.body.to_s 30 | 31 | socket.send Marshal.dump(env), 0 32 | 33 | data = receive_data(socket) 34 | debug "Response size: #{data.size}" 35 | 36 | status, headers, body = Marshal.load(data) 37 | 38 | res.status = status 39 | headers.each do |k, vs| 40 | if k.downcase == "set-cookie" 41 | res.cookies.concat vs.split("\n") 42 | else 43 | # Since WEBrick won't accept repeated headers, 44 | # merge the values per RFC 1945 section 4.2. 45 | res[k] = vs.split("\n").join(", ") 46 | end 47 | end 48 | res.body = body 49 | end 50 | 51 | def fork_app 52 | debug "Creating #{socket_path} socket" 53 | server_socket = UNIXServer.new(socket_path) 54 | 55 | debug "Forking new application" 56 | fork do 57 | args = Pump.pumpup_path, app_path, server_socket.fileno, socket_path 58 | if defined?(RVM) 59 | rvm_string = RVM.tools.path_identifier(app_path) 60 | RVM::Environment.new(rvm_string).exec(args) 61 | else 62 | exec(args) 63 | end 64 | end 65 | 66 | server_socket.close 67 | end 68 | 69 | def receive_data(socket, buffer = "") 70 | loop do 71 | part = socket.recv(1024) 72 | break if part.empty? 73 | buffer << part 74 | end 75 | buffer 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/pump/install/mac/launchctl.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module Pump 4 | class Launchctl 5 | PLIST_DIR = File.join(ENV["HOME"], "Library/LaunchAgents") 6 | SERVICE_NAME_TEMPLATE = "com.github.pump.%s" 7 | PLIST = File.join(PLIST_DIR, "#{SERVICE_NAME_TEMPLATE}.plist") 8 | PLIST_TEMPLATE = File.join(PUMP_ROOT, "config", "#{SERVICE_NAME_TEMPLATE}.plist.erb") 9 | 10 | class Base 11 | class << self 12 | # Load configuration files 13 | def load(plist) 14 | invoke("launchctl", "load", "-w", plist) 15 | end 16 | 17 | # Unload configuration files 18 | def unload(plist) 19 | invoke("launchctl", "unload", "-w", plist) 20 | end 21 | 22 | # Start specified job 23 | def start(name) 24 | invoke("launchctl", "start", name) 25 | end 26 | 27 | # Stop specified job 28 | def stop(name) 29 | invoke("launchctl", "stop", name) 30 | end 31 | 32 | def invoke(*args) 33 | system(*args) 34 | # unless system(*args) 35 | # abort("launchctl(pid = #{$?.pid}) exited with status: #{$?.exitstatus}") 36 | # end 37 | end 38 | end 39 | end 40 | 41 | class << self 42 | def service_name 43 | raise "You should specify service_name in your class" 44 | end 45 | 46 | def install 47 | mkdir_plist 48 | Pump.switch_privileges do 49 | create_plist 50 | Base.load(self::PLIST % service_name) 51 | end 52 | end 53 | 54 | def uninstall 55 | Pump.switch_privileges do 56 | Base.stop(self::SERVICE_NAME_TEMPLATE % service_name) 57 | Base.unload(self::PLIST % service_name) 58 | remove_plist 59 | end 60 | end 61 | 62 | def create_plist 63 | config = ERB.new File.read(self::PLIST_TEMPLATE % service_name) 64 | debug "Open plist template #{self::PLIST_TEMPLATE % service_name}" 65 | template = config.result(binding) 66 | debug "Write plist #{self::PLIST % service_name}" 67 | File.open(self::PLIST % service_name, "w") do |file| 68 | file.write template 69 | end 70 | end 71 | 72 | def remove_plist 73 | if File.exist?(self::PLIST % service_name) 74 | debug "Remove plist #{self::PLIST % service_name}" 75 | FileUtils.rm(self::PLIST % service_name) 76 | end 77 | end 78 | 79 | def mkdir_plist 80 | FileUtils.mkdir_p(self::PLIST_DIR) unless File.exist?(self::PLIST_DIR) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /bin/pumpup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | require 'stringio' 6 | require 'fileutils' 7 | require 'socket' 8 | 9 | require 'rack' 10 | require 'rack/builder' 11 | 12 | module Pump 13 | class Application 14 | IDLE = 1800 # 30 minutes for idle, otherwise process should be killed 15 | TICK = 30 # seconds for periodic selecting data on socket 16 | 17 | def initialize(app_path, socket_fileno, socket_path) 18 | @idle = 0 19 | @server = UNIXServer.for_fd(socket_fileno) 20 | at_exit { @server.close; FileUtils.rm(socket_path) } 21 | @app = Rack::Builder.parse_file(File.join(app_path, "config.ru")).first 22 | handle_requests 23 | end 24 | 25 | def handle_requests 26 | loop do 27 | unless IO.select([@server], nil, nil, TICK).nil? 28 | @idle = 0 # reset idle time 29 | 30 | socket = @server.accept 31 | debug "Incoming request" 32 | 33 | env = Marshal.load receive_data(socket) 34 | response = app_call(env) 35 | 36 | debug "Accepted answer from application" 37 | response = Marshal.dump(response) 38 | debug "Application response size: #{response.size}" 39 | sent = socket.send response, 0 40 | debug "Sent through socket: #{sent}" 41 | 42 | socket.close 43 | else 44 | @idle += TICK 45 | exit if @idle >= IDLE 46 | end 47 | end 48 | end 49 | 50 | def app_call(env) 51 | response_body = "" 52 | env.delete_if { |k, v| v.nil? } 53 | 54 | rack_input = StringIO.new(env["body"]) 55 | rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding) 56 | 57 | env.update({ 58 | "rack.version" => Rack::VERSION, 59 | "rack.input" => rack_input, 60 | "rack.errors" => $stderr, 61 | "rack.multithread" => true, 62 | "rack.multiprocess" => false, 63 | "rack.run_once" => false, 64 | "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" 65 | }) 66 | 67 | status, headers, body = @app.call(env) 68 | 69 | body.each { |part| response_body << part } 70 | body.close if body.respond_to? :close 71 | 72 | [status.to_i, headers.to_hash, response_body] 73 | end 74 | 75 | def receive_data(socket, buffer = "") 76 | while part = socket.recv(1024) 77 | buffer << part unless part.empty? 78 | break if IO.select([socket], nil, nil, 0).nil? || part.empty? 79 | end 80 | buffer 81 | end 82 | 83 | private 84 | 85 | def debug(message) 86 | STDERR.puts message 87 | end 88 | end 89 | end 90 | 91 | Pump::Application.new(ARGV[0], ARGV[1].to_i, ARGV[2]) 92 | --------------------------------------------------------------------------------