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