pipemaster 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -0,0 +1,7 @@
1
+ 0.4.0 (2010-02-19)
2
+ * Added: pipe command (client).
3
+ * Added: Pipemaster::Client gets pipe and capture methods, global address setting.
4
+ * Added: Command line client gets --tty option, when used, captures stdin when running in terminal.
5
+ * Added: Configurator command method now takes proc/method as second argument.
6
+ * Changed: Unicorn is no longer a dependency, code merged instead.
7
+ * Fixed: USR2 signal now properly restarts server.
data/Gemfile CHANGED
@@ -1,5 +1,4 @@
1
1
  source "http://gemcutter.org"
2
2
  gem "gemcutter"
3
3
  gem "rake"
4
- gem "unicorn", "0.96.1"
5
4
  gem "yard"
@@ -48,8 +48,8 @@ Step 3: For a new shell, execute a command:
48
48
 
49
49
  == License
50
50
 
51
- Pipemaster is copyright of Assaf Arkin. It is based on the awesome Unicorn Web
52
- server and therefore uses the same license.
51
+ Pipemaster is copyright of Assaf Arkin. It is heavily based on the awesome
52
+ Unicorn Web server and therefore uses the same license.
53
53
 
54
54
  Unicorn is copyright 2009 by all contributors (see logs in git).
55
55
  It is based on Mongrel and carries the same license.
data/Rakefile CHANGED
@@ -13,9 +13,13 @@ task :push do
13
13
  sh "gem push #{spec.name}-#{spec.version}.gem"
14
14
  end
15
15
 
16
- desc "Install #{spec.name} locally"
17
- task :install do
16
+ desc "Build #{spec.name}-#{spec.version}.gem"
17
+ task :build=>:test do
18
18
  sh "gem build #{spec.name}.gemspec"
19
+ end
20
+
21
+ desc "Install #{spec.name} locally"
22
+ task :install=>:build do
19
23
  sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir])
20
24
  sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem"
21
25
  end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: binary -*-
3
+ require "pipemaster/client"
4
+ require "optparse"
5
+
6
+ ENV["PIPE_ENV"] ||= "development"
7
+ address = nil
8
+ tty = false
9
+
10
+ opts = OptionParser.new("", 24, ' ') do |opts|
11
+ opts.banner = "Usage: #{File.basename($0)} [options] command [args]\n"
12
+
13
+ opts.on("-t", "--tty", "read input from terminal (default: false)") do |t|
14
+ tty = t ? true : false
15
+ end
16
+
17
+ opts.on("-s", "--socket {HOST:PORT|PATH}",
18
+ "communicate on HOST:PORT or PATH",
19
+ "(default: #{Pipemaster::DEFAULT_LISTEN})") do |a|
20
+ address = a
21
+ end
22
+
23
+ opts.on("-h", "--help", "Show this message") do
24
+ puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
25
+ exit
26
+ end
27
+
28
+ opts.on("-v", "--version", "Show version") do
29
+ puts "Pipemaster v#{Pipemaster::VERSION}"
30
+ exit
31
+ end
32
+
33
+ opts.parse! ARGV
34
+ end
35
+
36
+ input = $stdin if tty || !$stdin.isatty
37
+ exit Pipemaster::Client.new(address).capture(input, $stdout).run(*ARGV)
@@ -1,6 +1,7 @@
1
- #!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
1
+ #!/usr/bin/env ruby
2
2
  # -*- encoding: binary -*-
3
- require "pipemaster"
3
+ require "pipemaster/server"
4
+ require "pipemaster/launcher"
4
5
  require "optparse"
5
6
 
6
7
  ENV["PIPE_ENV"] ||= "development"
@@ -8,13 +9,12 @@ server = false
8
9
  daemonize = false
9
10
  listeners = []
10
11
  options = { :listeners => listeners }
12
+ tty = false
11
13
 
12
14
  opts = OptionParser.new("", 24, ' ') do |opts|
13
- opts.banner = "Usage: #{File.basename($0)}\n" \
14
- "[ruby options] [pipemaster options] command [args]\n" \
15
- "[ruby options] [pipemaster options] --server [Pipefile]"
15
+ opts.banner = "Usage: #{File.basename($0)} [ruby options] [pipemaster options] [Pipefile]"
16
16
 
17
- opts.separator "Ruby options:"
17
+ opts.separator "\nRuby options:"
18
18
 
19
19
  lineno = 1
20
20
  opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line|
@@ -40,61 +40,41 @@ opts = OptionParser.new("", 24, ' ') do |opts|
40
40
  require library
41
41
  end
42
42
 
43
- opts.separator "Pipemaster options:"
44
-
45
- opts.on("-s", "--socket {HOST:PORT|PATH}",
46
- "communicate on HOST:PORT or PATH",
47
- "for server, this may be specified multiple times",
48
- "(default: #{Pipemaster::DEFAULT_LISTEN})") do |address|
49
- listeners << address
50
- end
43
+ opts.separator "\nPipemaster options:"
51
44
 
52
45
  opts.on("-E", "--env ENVIRONMENT",
53
46
  "use ENVIRONMENT for defaults (default: development)") do |e|
54
47
  ENV["PIPE_ENV"] = e
55
48
  end
56
49
 
57
- opts.on("-S", "--server", "run as server (default: client)") do |s|
58
- server = s ? true : false
59
- end
60
-
61
50
  opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
62
51
  daemonize = d ? true : false
63
52
  end
64
53
 
65
- opts.separator "Common options:"
54
+ opts.on("-s", "--socket {HOST:PORT|PATH}",
55
+ "communicate on HOST:PORT or PATH",
56
+ "for server, this may be specified multiple times",
57
+ "(default: #{Pipemaster::DEFAULT_LISTEN})") do |address|
58
+ listeners << address
59
+ end
66
60
 
67
- opts.on_tail("-h", "--help", "Show this message") do
61
+ opts.on("-h", "--help", "Show this message") do
68
62
  puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
69
63
  exit
70
64
  end
71
65
 
72
- opts.on_tail("-v", "--version", "Show version") do
66
+ opts.on("-v", "--version", "Show version") do
73
67
  puts "Pipemaster v#{Pipemaster::VERSION}"
74
68
  exit
75
69
  end
76
70
 
71
+
77
72
  opts.parse! ARGV
78
73
  end
79
74
 
80
- if server
81
-
82
- gem "unicorn"
83
- require "pipemaster/server"
84
- require "unicorn/launcher"
85
- config = ARGV[0] || "Pipefile"
86
- abort "configuration file #{config} not found" unless File.exist?(config)
87
- options[:config_file] = config
88
- options[:working_directory] = File.dirname(config)
89
- Unicorn::Launcher.daemonize!(options) if daemonize
90
- Pipemaster.run(options)
91
-
92
- else
93
-
94
- require "pipemaster/client"
95
- client = Pipemaster::Client.new(listeners.first)
96
- client.input = $stdin unless $stdin.isatty
97
- client.output = $stdout
98
- exit client.request(*ARGV)
99
-
100
- end
75
+ config = ARGV[0] || "Pipefile"
76
+ abort "configuration file #{config} not found" unless File.exist?(config)
77
+ options[:config_file] = config
78
+ options[:working_directory] = File.dirname(config)
79
+ Pipemaster::Launcher.daemonize!(options)
80
+ Pipemaster::Server.run(options)
@@ -1,24 +1,44 @@
1
1
  require "socket"
2
+ require "pipemaster"
2
3
 
3
4
  module Pipemaster
4
5
  # Pipemaster client. Use this to send commands to the Pipemaster server.
5
6
  #
6
7
  # For example:
7
8
  # c = Pipemaster::Client.new
8
- # c.request "motd"
9
+ # c.run :motd
9
10
  # puts c.output.string
10
11
  # => "Toilet out of order please use floor below."
11
12
  #
12
13
  # c = Pipemaster::Client.new(9988)
13
14
  # c.input = File.open("image.png")
14
15
  # c.output = File.open("thumbnail.png", "w")
15
- # c.request "transform", "thumbnail"
16
+ # c.run :transform, "thumbnail"
16
17
  class Client
17
18
  BUFFER_SIZE = 16 * 1024
18
19
 
19
- # Address can be "x.x.x.x:port", ":port" or UNIX socket file name.
20
- def initialize(address)
21
- @address = expand_addr(address || DEFAULT_LISTEN)
20
+ class << self
21
+ # Pipe stdin/stdout into the command. For example:
22
+ # exit Pipemaster.pipe(:echo)
23
+ def pipe(command, *args)
24
+ new.capture($stdin, $stdout).run(command, *args)
25
+ end
26
+
27
+ # Address to use by default for new clients. Leave as nil to use the
28
+ # default (Pipemaster::DEFAULT_LISTEN).
29
+ attr_accessor :address
30
+ end
31
+
32
+ # Creates a new client. Accepts optional address. If missing, uses the
33
+ # global address (see Pipemaster::Client::address). Address can be port
34
+ # number, hostname (default port), "host:port" or socket file name.
35
+ #
36
+ # Examples:
37
+ # Pipemaster::Client.new 7890
38
+ # Pipemaster::Client.new "localhost:7890"
39
+ # Pipemaster::Client.new
40
+ def initialize(address = nil)
41
+ @address = expand_addr(address || Client.address || DEFAULT_LISTEN)
22
42
  end
23
43
 
24
44
  # Set this to supply an input stream (for commands that read from $stdin).
@@ -28,11 +48,18 @@ module Pipemaster
28
48
  # StringIO).
29
49
  attr_accessor :output
30
50
 
51
+ # Captures input and output. For example:
52
+ # Pipemaster.new(7890).capture($stdin, $stdout).run(:echo)
53
+ def capture(input, output)
54
+ self.input, self.output = input, output
55
+ self
56
+ end
57
+
31
58
  # Make a request. First argument is the command name. All other arguments
32
59
  # are optional. Returns the exit code (usually 0). Will raise IOError if
33
60
  # it can't talk to the server, or the server closed the connection
34
61
  # prematurely.
35
- def request(command, *args)
62
+ def run(command, *args)
36
63
  # Connect and send arguments.
37
64
  socket = connect(@address)
38
65
  socket.sync = true
@@ -1,13 +1,15 @@
1
1
  require 'socket'
2
2
  require 'logger'
3
+ require 'pipemaster/util'
3
4
 
4
5
  module Pipemaster
5
6
 
6
7
  # Implements a simple DSL for configuring a Pipemaster server.
7
- class Configurator < Unicorn::Configurator
8
+ class Configurator < Struct.new(:set, :config_file)
8
9
 
9
10
  # Default settings for Pipemaster
10
11
  DEFAULTS = {
12
+ :timeout => 60,
11
13
  :logger => Logger.new($stderr),
12
14
  :after_fork => lambda { |server, worker|
13
15
  server.logger.info("spawned pid=#{$$}")
@@ -19,8 +21,7 @@ module Pipemaster
19
21
  server.logger.info("forked child re-executing...")
20
22
  },
21
23
  :pid => nil,
22
- :commands => {},
23
- :timeout => 60
24
+ :commands => {}
24
25
  }
25
26
 
26
27
  def initialize(defaults = {}) #:nodoc:
@@ -35,20 +36,52 @@ module Pipemaster
35
36
  reload
36
37
  end
37
38
 
39
+ def reload #:nodoc:
40
+ instance_eval(File.read(config_file), config_file) if config_file
41
+
42
+ # working_directory binds immediately (easier error checking that way),
43
+ # now ensure any paths we changed are correctly set.
44
+ [ :pid, :stderr_path, :stdout_path ].each do |var|
45
+ String === (path = set[var]) or next
46
+ path = File.expand_path(path)
47
+ test(?w, path) || test(?w, File.dirname(path)) or \
48
+ raise ArgumentError, "directory for #{var}=#{path} not writable"
49
+ end
50
+ end
51
+
52
+ def commit!(server, options = {}) #:nodoc:
53
+ skip = options[:skip] || []
54
+ set.each do |key, value|
55
+ value == :unset and next
56
+ skip.include?(key) and next
57
+ server.__send__("#{key}=", value)
58
+ end
59
+ end
60
+
61
+ def [](key) # :nodoc:
62
+ set[key]
63
+ end
64
+
38
65
  # Sets object to the +new+ Logger-like object. The new logger-like
39
66
  # object must respond to the following methods:
40
67
  # +debug+, +info+, +warn+, +error+, +fatal+, +close+
41
68
  def logger(new)
42
- super
69
+ %w(debug info warn error fatal close).each do |m|
70
+ new.respond_to?(m) and next
71
+ raise ArgumentError, "logger=#{new} does not respond to method=#{m}"
72
+ end
73
+
74
+ set[:logger] = new
43
75
  end
44
76
 
77
+ # Set commands from a hash (name=>proc).
45
78
  def commands(hash)
46
79
  set[:commands] = hash
47
80
  end
48
81
 
49
82
  # Defines a command.
50
- def command(name, &block)
51
- set[:commands][name.to_sym] = block
83
+ def command(name, a_proc = nil, &block)
84
+ set[:commands][name.to_sym] = a_proc || block
52
85
  end
53
86
 
54
87
  autoload :Etc, 'etc'
@@ -59,7 +92,7 @@ module Pipemaster
59
92
  # capabilities. Let the caller handle any and all errors.
60
93
  uid = Etc.getpwnam(user).uid
61
94
  gid = Etc.getgrnam(group).gid if group
62
- Unicorn::Util.chown_logs(uid, gid)
95
+ Pipemaster::Util.chown_logs(uid, gid)
63
96
  if gid && Process.egid != gid
64
97
  Process.initgroups(user, gid)
65
98
  Process::GID.change_privilege(gid)
@@ -116,10 +149,15 @@ module Pipemaster
116
149
 
117
150
  # Does nothing interesting for now.
118
151
  def timeout(seconds)
152
+ # Not implemented yet.
119
153
  end
120
154
 
121
- # Does nothing interesting for now.
122
- def worker_processes(nr)
155
+ # This is for internal API use only, do not use it in your Pipemaster
156
+ # config file. Use listen instead.
157
+ def listeners(addresses) # :nodoc:
158
+ Array === addresses or addresses = Array(addresses)
159
+ addresses.map! { |addr| expand_addr(addr) }
160
+ set[:listeners] = addresses
123
161
  end
124
162
 
125
163
  # Adds an +address+ to the existing listener set.
@@ -195,7 +233,26 @@ module Pipemaster
195
233
  #
196
234
  # Default: 0 (world read/writable)
197
235
  def listen(address, opt = {})
198
- super
236
+ address = expand_addr(address)
237
+ if String === address
238
+ [ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key|
239
+ value = opt[key] or next
240
+ Integer === value or
241
+ raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
242
+ end
243
+ [ :tcp_nodelay, :tcp_nopush ].each do |key|
244
+ (value = opt[key]).nil? and next
245
+ TrueClass === value || FalseClass === value or
246
+ raise ArgumentError, "not boolean: #{key}=#{value.inspect}"
247
+ end
248
+ unless (value = opt[:delay]).nil?
249
+ Numeric === value or
250
+ raise ArgumentError, "not numeric: delay=#{value.inspect}"
251
+ end
252
+ set[:listener_opts][address].merge!(opt)
253
+ end
254
+
255
+ set[:listeners] << address
199
256
  end
200
257
 
201
258
  # Sets the +path+ for the PID file of the Pipemaster master process
@@ -218,9 +275,54 @@ module Pipemaster
218
275
  set_path(:stdout_path, path)
219
276
  end
220
277
 
278
+ # expands "unix:path/to/foo" to a socket relative to the current path
279
+ # expands pathnames of sockets if relative to "~" or "~username"
280
+ # expands "*:port and ":port" to "0.0.0.0:port"
281
+ def expand_addr(address) #:nodoc
282
+ return "0.0.0.0:#{address}" if Integer === address
283
+ return address unless String === address
284
+
285
+ case address
286
+ when %r{\Aunix:(.*)\z}
287
+ File.expand_path($1)
288
+ when %r{\A~}
289
+ File.expand_path(address)
290
+ when %r{\A(?:\*:)?(\d+)\z}
291
+ "0.0.0.0:#$1"
292
+ when %r{\A(.*):(\d+)\z}
293
+ # canonicalize the name
294
+ packed = Socket.pack_sockaddr_in($2.to_i, $1)
295
+ Socket.unpack_sockaddr_in(packed).reverse!.join(':')
296
+ else
297
+ address
298
+ end
299
+ end
300
+
221
301
  private
222
302
 
223
- def preload_app(bool)
303
+ def set_path(var, path) #:nodoc:
304
+ case path
305
+ when NilClass, String
306
+ set[var] = path
307
+ else
308
+ raise ArgumentError
309
+ end
310
+ end
311
+
312
+ def set_hook(var, my_proc, req_arity = 2) #:nodoc:
313
+ case my_proc
314
+ when Proc
315
+ arity = my_proc.arity
316
+ (arity == req_arity) or \
317
+ raise ArgumentError,
318
+ "#{var}=#{my_proc.inspect} has invalid arity: " \
319
+ "#{arity} (need #{req_arity})"
320
+ when NilClass
321
+ my_proc = DEFAULTS[var]
322
+ else
323
+ raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}"
324
+ end
325
+ set[var] = my_proc
224
326
  end
225
327
 
226
328
  end
@@ -0,0 +1,67 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ $stdout.sync = $stderr.sync = true
4
+ $stdin.binmode
5
+ $stdout.binmode
6
+ $stderr.binmode
7
+
8
+ require "pipemaster/configurator"
9
+
10
+ module Pipemaster
11
+ class Launcher
12
+
13
+ # We don't do a lot of standard daemonization stuff:
14
+ # * umask is whatever was set by the parent process at startup
15
+ # and can be set in config.ru and config_file, so making it
16
+ # 0000 and potentially exposing sensitive log data can be bad
17
+ # policy.
18
+ # * don't bother to chdir("/") here since unicorn is designed to
19
+ # run inside APP_ROOT. Pipemaster will also re-chdir() to
20
+ # the directory it was started in when being re-executed
21
+ # to pickup code changes if the original deployment directory
22
+ # is a symlink or otherwise got replaced.
23
+ def self.daemonize!(options = nil)
24
+ $stdin.reopen("/dev/null")
25
+
26
+ # We only start a new process group if we're not being reexecuted
27
+ # and inheriting file descriptors from our parent
28
+ unless ENV['PIPEMASTER_FD']
29
+ if options
30
+ # grandparent - reads pipe, exits when master is ready
31
+ # \_ parent - exits immediately ASAP
32
+ # \_ unicorn master - writes to pipe when ready
33
+
34
+ rd, wr = IO.pipe
35
+ grandparent = $$
36
+ if fork
37
+ wr.close # grandparent does not write
38
+ else
39
+ rd.close # unicorn master does not read
40
+ Process.setsid
41
+ exit if fork # parent dies now
42
+ end
43
+
44
+ if grandparent == $$
45
+ # this will block until HttpServer#join runs (or it dies)
46
+ master_pid = (rd.readpartial(16) rescue nil).to_i
47
+ unless master_pid > 1
48
+ warn "master failed to start, check stderr log for details"
49
+ exit!(1)
50
+ end
51
+ exit 0
52
+ else # unicorn master process
53
+ options[:ready_pipe] = wr
54
+ end
55
+ else # backwards compat
56
+ exit if fork
57
+ Process.setsid
58
+ exit if fork
59
+ end
60
+ # $stderr/$stderr can/will be redirected separately in the Pipemaster config
61
+ Pipemaster::Configurator::DEFAULTS[:stderr_path] = "/dev/null"
62
+ Pipemaster::Configurator::DEFAULTS[:stdout_path] = "/dev/null"
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -1,16 +1,71 @@
1
- require "unicorn"
1
+ require "pipemaster"
2
2
  require "pipemaster/worker"
3
+ require 'pipemaster/util'
3
4
  require "pipemaster/configurator"
5
+ require "pipemaster/socket_helper"
4
6
 
5
7
  module Pipemaster
6
8
 
7
- class << self
8
- def run(options = {})
9
- Server.new(options).start.join
10
- end
11
- end
9
+ class Server < Struct.new(:listener_opts, :timeout, :logger,
10
+ :before_fork, :after_fork, :before_exec,
11
+ :pid, :reexec_pid, :init_listeners,
12
+ :master_pid, :config, :ready_pipe)
13
+
14
+ include SocketHelper
15
+
16
+ # prevents IO objects in here from being GC-ed
17
+ IO_PURGATORY = []
18
+
19
+ # all bound listener sockets
20
+ LISTENERS = []
21
+
22
+ # This hash maps PIDs to Workers
23
+ WORKERS = {}
24
+
25
+ # We populate this at startup so we can figure out how to reexecute
26
+ # and upgrade the currently running instance of Pipemaster
27
+ # This Hash is considered a stable interface and changing its contents
28
+ # will allow you to switch between different installations of Pipemaster
29
+ # or even different installations of the same applications without
30
+ # downtime. Keys of this constant Hash are described as follows:
31
+ #
32
+ # * 0 - the path to the pipemaster executable
33
+ # * :argv - a deep copy of the ARGV array the executable originally saw
34
+ # * :cwd - the working directory of the application, this is where
35
+ # you originally started Pipemaster.
36
+ #
37
+ # The following example may be used in your Pipemaster config file to
38
+ # change your working directory during a config reload (HUP) without
39
+ # upgrading or restarting:
40
+ #
41
+ # Dir.chdir(Pipemaster::Server::START_CTX[:cwd] = path)
42
+ #
43
+ # To change your Pipemaster executable to a different path without downtime,
44
+ # you can set the following in your Pipemaster config file, HUP and then
45
+ # continue with the traditional USR2 + QUIT upgrade steps:
46
+ #
47
+ # Pipemaster::Server::START_CTX[0] = "/home/bofh/1.9.2/bin/pipemaster"
48
+ START_CTX = {
49
+ :argv => ARGV.map { |arg| arg.dup },
50
+ :cwd => lambda {
51
+ # favor ENV['PWD'] since it is (usually) symlink aware for
52
+ # Capistrano and like systems
53
+ begin
54
+ a = File.stat(pwd = ENV['PWD'])
55
+ b = File.stat(Dir.pwd)
56
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
57
+ rescue
58
+ Dir.pwd
59
+ end
60
+ }.call,
61
+ 0 => $0.dup,
62
+ }
12
63
 
13
- class Server < Unicorn::HttpServer
64
+ class << self
65
+ def run(options = {})
66
+ Server.new(options).start.join
67
+ end
68
+ end
14
69
 
15
70
  def initialize(options = {})
16
71
  self.reexec_pid = 0
@@ -38,22 +93,22 @@ module Pipemaster
38
93
  trap(:CHLD) { reap_all_workers }
39
94
  trap :USR1 do
40
95
  logger.info "master reopening logs..."
41
- Unicorn::Util.reopen_logs
96
+ Pipemaster::Util.reopen_logs
42
97
  logger.info "master done reopening logs"
43
98
  end
44
99
  reloaded = nil
45
100
  trap(:HUP) { reloaded = true ; load_config! }
46
101
  trap(:USR2) { reexec }
47
102
 
48
- begin
49
- config_listeners = config[:listeners].dup
50
- if config_listeners.empty? && LISTENERS.empty?
51
- config_listeners << DEFAULT_LISTEN
52
- init_listeners << DEFAULT_LISTEN
53
- START_CTX[:argv] << "-s#{DEFAULT_LISTEN}"
54
- end
55
- config_listeners.each { |addr| listen(addr) }
103
+ config_listeners = config[:listeners].dup
104
+ if config_listeners.empty? && LISTENERS.empty?
105
+ config_listeners << DEFAULT_LISTEN
106
+ init_listeners << DEFAULT_LISTEN
107
+ START_CTX[:argv] << "-s#{DEFAULT_LISTEN}"
108
+ end
109
+ config_listeners.each { |addr| listen(addr) }
56
110
 
111
+ begin
57
112
  reloaded = false
58
113
  while selected = Kernel.select(LISTENERS)
59
114
  selected.first.each do |socket|
@@ -81,6 +136,139 @@ module Pipemaster
81
136
  unlink_pid_safe(pid) if pid
82
137
  end
83
138
 
139
+ # replaces current listener set with +listeners+. This will
140
+ # close the socket if it will not exist in the new listener set
141
+ def listeners=(listeners)
142
+ cur_names, dead_names = [], []
143
+ listener_names.each do |name|
144
+ if ?/ == name[0]
145
+ # mark unlinked sockets as dead so we can rebind them
146
+ (File.socket?(name) ? cur_names : dead_names) << name
147
+ else
148
+ cur_names << name
149
+ end
150
+ end
151
+ set_names = listener_names(listeners)
152
+ dead_names.concat(cur_names - set_names).uniq!
153
+
154
+ LISTENERS.delete_if do |io|
155
+ if dead_names.include?(sock_name(io))
156
+ IO_PURGATORY.delete_if do |pio|
157
+ pio.fileno == io.fileno && (pio.close rescue nil).nil? # true
158
+ end
159
+ (io.close rescue nil).nil? # true
160
+ else
161
+ set_server_sockopt(io, listener_opts[sock_name(io)])
162
+ false
163
+ end
164
+ end
165
+
166
+ (set_names - cur_names).each { |addr| listen(addr) }
167
+ end
168
+
169
+ # returns an array of string names for the given listener array
170
+ def listener_names(listeners = LISTENERS)
171
+ listeners.map { |io| sock_name(io) }
172
+ end
173
+
174
+ def stdout_path=(path); redirect_io($stdout, path); end
175
+ def stderr_path=(path); redirect_io($stderr, path); end
176
+
177
+ def redirect_io(io, path)
178
+ File.open(path, 'ab') { |fp| io.reopen(fp) } if path
179
+ io.sync = true
180
+ end
181
+
182
+ # sets the path for the PID file of the master process
183
+ def pid=(path)
184
+ if path
185
+ if x = valid_pid?(path)
186
+ return path if pid && path == pid && x == $$
187
+ raise ArgumentError, "Already running on PID:#{x} " \
188
+ "(or pid=#{path} is stale)"
189
+ end
190
+ end
191
+ unlink_pid_safe(pid) if pid
192
+
193
+ if path
194
+ fp = begin
195
+ tmp = "#{File.dirname(path)}/#{rand}.#$$"
196
+ File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
197
+ rescue Errno::EEXIST
198
+ retry
199
+ end
200
+ fp.syswrite("#$$\n")
201
+ File.rename(fp.path, path)
202
+ fp.close
203
+ end
204
+ super(path)
205
+ end
206
+
207
+ # unlinks a PID file at given +path+ if it contains the current PID
208
+ # still potentially racy without locking the directory (which is
209
+ # non-portable and may interact badly with other programs), but the
210
+ # window for hitting the race condition is small
211
+ def unlink_pid_safe(path)
212
+ (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
213
+ end
214
+
215
+ # returns a PID if a given path contains a non-stale PID file,
216
+ # nil otherwise.
217
+ def valid_pid?(path)
218
+ wpid = File.read(path).to_i
219
+ wpid <= 0 and return nil
220
+ begin
221
+ Process.kill(0, wpid)
222
+ wpid
223
+ rescue Errno::ESRCH
224
+ # don't unlink stale pid files, racy without non-portable locking...
225
+ end
226
+ rescue Errno::ENOENT
227
+ end
228
+
229
+ def proc_name(tag)
230
+ $0 = ([ File.basename(START_CTX[0]), tag
231
+ ]).concat(START_CTX[:argv]).join(' ')
232
+ end
233
+
234
+ # add a given address to the +listeners+ set, idempotently
235
+ # Allows workers to add a private, per-process listener via the
236
+ # after_fork hook. Very useful for debugging and testing.
237
+ # +:tries+ may be specified as an option for the number of times
238
+ # to retry, and +:delay+ may be specified as the time in seconds
239
+ # to delay between retries.
240
+ # A negative value for +:tries+ indicates the listen will be
241
+ # retried indefinitely, this is useful when workers belonging to
242
+ # different masters are spawned during a transparent upgrade.
243
+ def listen(address, opt = {}.merge(listener_opts[address] || {}))
244
+ address = config.expand_addr(address)
245
+ return if String === address && listener_names.include?(address)
246
+
247
+ delay = opt[:delay] || 0.5
248
+ tries = opt[:tries] || 5
249
+ begin
250
+ io = bind_listen(address, opt)
251
+ unless TCPServer === io || UNIXServer === io
252
+ IO_PURGATORY << io
253
+ io = server_cast(io)
254
+ end
255
+ logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
256
+ LISTENERS << io
257
+ io
258
+ rescue Errno::EADDRINUSE => err
259
+ logger.error "adding listener failed addr=#{address} (in use)"
260
+ raise err if tries == 0
261
+ tries -= 1
262
+ logger.error "retrying in #{delay} seconds " \
263
+ "(#{tries < 0 ? 'infinite' : tries} tries left)"
264
+ sleep(delay)
265
+ retry
266
+ rescue => err
267
+ logger.fatal "error adding listener addr=#{address}"
268
+ raise err
269
+ end
270
+ end
271
+
84
272
  def kill_worker(signal, wpid)
85
273
  begin
86
274
  Process.kill(signal, wpid)
@@ -94,15 +282,15 @@ module Pipemaster
94
282
  loop do
95
283
  wpid, status = Process.waitpid2(-1, Process::WNOHANG)
96
284
  wpid or break
97
- #if reexec_pid == wpid
98
- # logger.error "reaped #{status.inspect} exec()-ed"
99
- # self.reexec_pid = 0
100
- # self.pid = pid.chomp('.oldbin') if pid
101
- # proc_name 'master'
102
- #else
285
+ if reexec_pid == wpid
286
+ logger.error "reaped #{status.inspect} exec()-ed"
287
+ self.reexec_pid = 0
288
+ self.pid = pid.chomp('.oldbin') if pid
289
+ proc_name 'master'
290
+ else
103
291
  worker = WORKERS.delete(wpid) rescue nil
104
292
  logger.info "reaped #{status.inspect} "
105
- #end
293
+ end
106
294
  end
107
295
  rescue Errno::ECHILD
108
296
  end
@@ -114,7 +302,7 @@ module Pipemaster
114
302
  config[:listeners].replace(init_listeners)
115
303
  config.reload
116
304
  config.commit!(self)
117
- Unicorn::Util.reopen_logs
305
+ Pipemaster::Util.reopen_logs
118
306
  logger.info "done reloading pipefile=#{config.config_file}"
119
307
  rescue => e
120
308
  logger.error "error reloading pipefile=#{config.config_file}: " \
@@ -122,6 +310,56 @@ module Pipemaster
122
310
  end
123
311
  end
124
312
 
313
+ def reexec
314
+ if reexec_pid > 0
315
+ begin
316
+ Process.kill(0, reexec_pid)
317
+ logger.error "reexec-ed child already running PID:#{reexec_pid}"
318
+ return
319
+ rescue Errno::ESRCH
320
+ self.reexec_pid = 0
321
+ end
322
+ end
323
+
324
+ if pid
325
+ old_pid = "#{pid}.oldbin"
326
+ prev_pid = pid.dup
327
+ begin
328
+ self.pid = old_pid # clear the path for a new pid file
329
+ rescue ArgumentError
330
+ logger.error "old PID:#{valid_pid?(old_pid)} running with " \
331
+ "existing pid=#{old_pid}, refusing rexec"
332
+ return
333
+ rescue => e
334
+ logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
335
+ return
336
+ end
337
+ end
338
+
339
+ listener_fds = LISTENERS.map { |sock| sock.fileno }
340
+ LISTENERS.delete_if { |s| s.close rescue nil ; true }
341
+ self.reexec_pid = fork do
342
+ ENV['PIPEMASTER_FD'] = listener_fds.join(',')
343
+ Dir.chdir(START_CTX[:cwd])
344
+ cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
345
+
346
+ # avoid leaking FDs we don't know about, but let before_exec
347
+ # unset FD_CLOEXEC, if anything else in the app eventually
348
+ # relies on FD inheritence.
349
+ (3..1024).each do |io|
350
+ next if listener_fds.include?(io)
351
+ io = IO.for_fd(io) rescue nil
352
+ io or next
353
+ IO_PURGATORY << io
354
+ io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
355
+ end
356
+ logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
357
+ before_exec.call(self)
358
+ exec(*cmd)
359
+ end
360
+ proc_name 'master (old)'
361
+ end
362
+
125
363
  def process_request(socket, worker)
126
364
  trap(:QUIT) { exit }
127
365
  [:TERM, :INT].each { |sig| trap(sig) { exit! } }
@@ -0,0 +1,149 @@
1
+ require 'socket'
2
+
3
+ module Pipemaster
4
+ module SocketHelper
5
+ include Socket::Constants
6
+
7
+ # configure platform-specific options (only tested on Linux 2.6 so far)
8
+ case RUBY_PLATFORM
9
+ when /linux/
10
+ # from /usr/include/linux/tcp.h
11
+ TCP_DEFER_ACCEPT = 9 unless defined?(TCP_DEFER_ACCEPT)
12
+
13
+ # do not send out partial frames (Linux)
14
+ TCP_CORK = 3 unless defined?(TCP_CORK)
15
+ when /freebsd(([1-4]\..{1,2})|5\.[0-4])/
16
+ # Do nothing for httpready, just closing a bug when freebsd <= 5.4
17
+ TCP_NOPUSH = 4 unless defined?(TCP_NOPUSH) # :nodoc:
18
+ when /freebsd/
19
+ # do not send out partial frames (FreeBSD)
20
+ TCP_NOPUSH = 4 unless defined?(TCP_NOPUSH)
21
+
22
+ # Use the HTTP accept filter if available.
23
+ # The struct made by pack() is defined in /usr/include/sys/socket.h
24
+ # as accept_filter_arg
25
+ unless `/sbin/sysctl -nq net.inet.accf.http`.empty?
26
+ # set set the "httpready" accept filter in FreeBSD if available
27
+ # if other protocols are to be supported, this may be
28
+ # String#replace-d with "dataready" arguments instead
29
+ FILTER_ARG = ['httpready', nil].pack('a16a240')
30
+ end
31
+ end
32
+
33
+ def set_tcp_sockopt(sock, opt)
34
+
35
+ # highly portable, but off by default because we don't do keepalive
36
+ if defined?(TCP_NODELAY) && ! (val = opt[:tcp_nodelay]).nil?
37
+ sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, val ? 1 : 0)
38
+ end
39
+
40
+ unless (val = opt[:tcp_nopush]).nil?
41
+ val = val ? 1 : 0
42
+ if defined?(TCP_CORK) # Linux
43
+ sock.setsockopt(IPPROTO_TCP, TCP_CORK, val)
44
+ elsif defined?(TCP_NOPUSH) # TCP_NOPUSH is untested (FreeBSD)
45
+ sock.setsockopt(IPPROTO_TCP, TCP_NOPUSH, val)
46
+ end
47
+ end
48
+
49
+ # No good reason to ever have deferred accepts off
50
+ if defined?(TCP_DEFER_ACCEPT)
51
+ sock.setsockopt(SOL_TCP, TCP_DEFER_ACCEPT, 1)
52
+ elsif defined?(SO_ACCEPTFILTER) && defined?(FILTER_ARG)
53
+ sock.setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, FILTER_ARG)
54
+ end
55
+ end
56
+
57
+ def set_server_sockopt(sock, opt)
58
+ opt ||= {}
59
+
60
+ TCPSocket === sock and set_tcp_sockopt(sock, opt)
61
+
62
+ if opt[:rcvbuf] || opt[:sndbuf]
63
+ log_buffer_sizes(sock, "before: ")
64
+ sock.setsockopt(SOL_SOCKET, SO_RCVBUF, opt[:rcvbuf]) if opt[:rcvbuf]
65
+ sock.setsockopt(SOL_SOCKET, SO_SNDBUF, opt[:sndbuf]) if opt[:sndbuf]
66
+ log_buffer_sizes(sock, " after: ")
67
+ end
68
+ sock.listen(opt[:backlog] || 1024)
69
+ rescue => e
70
+ if respond_to?(:logger)
71
+ logger.error "error setting socket options: #{e.inspect}"
72
+ logger.error e.backtrace.join("\n")
73
+ end
74
+ end
75
+
76
+ def log_buffer_sizes(sock, pfx = '')
77
+ respond_to?(:logger) or return
78
+ rcvbuf = sock.getsockopt(SOL_SOCKET, SO_RCVBUF).unpack('i')
79
+ sndbuf = sock.getsockopt(SOL_SOCKET, SO_SNDBUF).unpack('i')
80
+ logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}"
81
+ end
82
+
83
+ # creates a new server, socket. address may be a HOST:PORT or
84
+ # an absolute path to a UNIX socket. address can even be a Socket
85
+ # object in which case it is immediately returned
86
+ def bind_listen(address = '0.0.0.0:8080', opt = {})
87
+ return address unless String === address
88
+
89
+ sock = if address[0] == ?/
90
+ if File.exist?(address)
91
+ if File.socket?(address)
92
+ if self.respond_to?(:logger)
93
+ logger.info "unlinking existing socket=#{address}"
94
+ end
95
+ File.unlink(address)
96
+ else
97
+ raise ArgumentError,
98
+ "socket=#{address} specified but it is not a socket!"
99
+ end
100
+ end
101
+ old_umask = File.umask(opt[:umask] || 0)
102
+ begin
103
+ UNIXServer.new(address)
104
+ ensure
105
+ File.umask(old_umask)
106
+ end
107
+ elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
108
+ TCPServer.new($1, $2.to_i)
109
+ else
110
+ raise ArgumentError, "Don't know how to bind: #{address}"
111
+ end
112
+ set_server_sockopt(sock, opt)
113
+ sock
114
+ end
115
+
116
+ # Returns the configuration name of a socket as a string. sock may
117
+ # be a string value, in which case it is returned as-is
118
+ # Warning: TCP sockets may not always return the name given to it.
119
+ def sock_name(sock)
120
+ case sock
121
+ when String then sock
122
+ when UNIXServer
123
+ Socket.unpack_sockaddr_un(sock.getsockname)
124
+ when TCPServer
125
+ Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':')
126
+ when Socket
127
+ begin
128
+ Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':')
129
+ rescue ArgumentError
130
+ Socket.unpack_sockaddr_un(sock.getsockname)
131
+ end
132
+ else
133
+ raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}"
134
+ end
135
+ end
136
+
137
+ # casts a given Socket to be a TCPServer or UNIXServer
138
+ def server_cast(sock)
139
+ begin
140
+ Socket.unpack_sockaddr_in(sock.getsockname)
141
+ TCPServer.for_fd(sock.fileno)
142
+ rescue ArgumentError
143
+ UNIXServer.for_fd(sock.fileno)
144
+ end
145
+ end
146
+
147
+ end # module SocketHelper
148
+ end # module Pipemaster
149
+
@@ -0,0 +1,67 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'fcntl'
4
+ require 'tmpdir'
5
+
6
+ module Pipemaster
7
+
8
+ class Util
9
+ class << self
10
+
11
+ def is_log?(fp)
12
+ append_flags = File::WRONLY | File::APPEND
13
+
14
+ ! fp.closed? &&
15
+ fp.sync &&
16
+ fp.path[0] == ?/ &&
17
+ (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
18
+ end
19
+
20
+ def chown_logs(uid, gid)
21
+ ObjectSpace.each_object(File) do |fp|
22
+ is_log?(fp) or next
23
+ fp.chown(uid, gid)
24
+ end
25
+ end
26
+
27
+ # This reopens ALL logfiles in the process that have been rotated
28
+ # using logrotate(8) (without copytruncate) or similar tools.
29
+ # A +File+ object is considered for reopening if it is:
30
+ # 1) opened with the O_APPEND and O_WRONLY flags
31
+ # 2) opened with an absolute path (starts with "/")
32
+ # 3) the current open file handle does not match its original open path
33
+ # 4) unbuffered (as far as userspace buffering goes, not O_SYNC)
34
+ # Returns the number of files reopened
35
+ def reopen_logs
36
+ nr = 0
37
+
38
+ ObjectSpace.each_object(File) do |fp|
39
+ is_log?(fp) or next
40
+ orig_st = fp.stat
41
+ begin
42
+ b = File.stat(fp.path)
43
+ next if orig_st.ino == b.ino && orig_st.dev == b.dev
44
+ rescue Errno::ENOENT
45
+ end
46
+
47
+ open_arg = 'a'
48
+ if fp.respond_to?(:external_encoding) && enc = fp.external_encoding
49
+ open_arg << ":#{enc.to_s}"
50
+ enc = fp.internal_encoding and open_arg << ":#{enc.to_s}"
51
+ end
52
+ fp.reopen(fp.path, open_arg)
53
+ fp.sync = true
54
+ new_st = fp.stat
55
+ if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
56
+ fp.chown(orig_st.uid, orig_st.gid)
57
+ end
58
+ nr += 1
59
+ end # each_object
60
+ nr
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
67
+
@@ -1,18 +1,17 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "pipemaster"
3
- spec.version = "0.3.1"
3
+ spec.version = "0.4.0"
4
4
  spec.author = "Assaf Arkin"
5
5
  spec.email = "assaf@labnotes.org"
6
- spec.homepage = "http://labnotes.org"
6
+ spec.homepage = "http://github.com/assaf/pipemaster"
7
7
  spec.summary = "Use the fork"
8
8
  spec.post_install_message = "To get started run pipemaster --help"
9
9
 
10
10
  spec.files = Dir["{bin,lib,test}/**/*", "CHANGELOG", "LICENSE", "README.rdoc", "Rakefile", "Gemfile", "pipemaster.gemspec"]
11
- spec.executable = "pipemaster"
11
+ spec.executables = %w{pipe pipemaster}
12
12
 
13
13
  spec.has_rdoc = true
14
14
  spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
15
15
  spec.rdoc_options = "--title", "Pipemaster #{spec.version}", "--main", "README.rdoc",
16
16
  "--webcvs", "http://github.com/assaf/#{spec.name}"
17
- spec.add_dependency("unicorn", ["~> 0.96"])
18
17
  end
@@ -70,7 +70,7 @@ end
70
70
 
71
71
  def hit(address, *args)
72
72
  client = Pipemaster::Client.new(address)
73
- code = client.request(*args)
73
+ code = client.run(*args)
74
74
  [code, client.output.string]
75
75
  end
76
76
 
@@ -90,7 +90,7 @@ def unused_port(addr = '127.0.0.1')
90
90
  begin
91
91
  begin
92
92
  port = base + rand(32768 - base)
93
- while port == Unicorn::Const::DEFAULT_PORT
93
+ while port == Pipemaster::DEFAULT_PORT
94
94
  port = base + rand(32768 - base)
95
95
  end
96
96
 
@@ -106,7 +106,7 @@ def unused_port(addr = '127.0.0.1')
106
106
  # condition could allow the random port we just chose to reselect itself
107
107
  # when running tests in parallel with gmake. Create a lock file while
108
108
  # we have the port here to ensure that does not happen .
109
- lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
109
+ lock_path = "#{Dir::tmpdir}/pipemaster_test.#{addr}:#{port}.lock"
110
110
  lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
111
111
  at_exit { File.unlink(lock_path) rescue nil }
112
112
  rescue Errno::EEXIST
@@ -126,20 +126,6 @@ def try_require(lib)
126
126
  end
127
127
  end
128
128
 
129
- # sometimes the server may not come up right away
130
- def retry_hit(uris = [])
131
- tries = DEFAULT_TRIES
132
- begin
133
- hit(uris)
134
- rescue Errno::EINVAL, Errno::ECONNREFUSED => err
135
- if (tries -= 1) > 0
136
- sleep DEFAULT_RES
137
- retry
138
- end
139
- raise err
140
- end
141
- end
142
-
143
129
  def assert_shutdown(pid)
144
130
  wait_master_ready("test_stderr.#{pid}.log")
145
131
  assert_nothing_raised { Process.kill(:QUIT, pid) }
@@ -1,11 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
2
 
3
- # Copyright (c) 2005 Zed A. Shaw
4
- # You can redistribute it and/or modify it under the same terms as Ruby.
5
- #
6
- # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
7
- # for more information.
8
-
9
3
  require 'test/test_helper'
10
4
 
11
5
  class ServerTest < Test::Unit::TestCase
@@ -72,10 +66,8 @@ class ServerTest < Test::Unit::TestCase
72
66
  tmp.write "foo bar"
73
67
  tmp.flush
74
68
  tmp.rewind
75
- client.input = tmp
76
- output = StringIO.new
77
- client.output = output
78
- client.request :reverse
69
+ client.capture tmp, StringIO.new
70
+ client.run :reverse
79
71
  assert_equal "rab oof", client.output.string
80
72
  ensure
81
73
  tmp.close!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipemaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assaf Arkin
@@ -9,22 +9,14 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-18 00:00:00 -08:00
12
+ date: 2010-02-19 00:00:00 -08:00
13
13
  default_executable:
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: unicorn
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
20
- requirements:
21
- - - ~>
22
- - !ruby/object:Gem::Version
23
- version: "0.96"
24
- version:
14
+ dependencies: []
15
+
25
16
  description:
26
17
  email: assaf@labnotes.org
27
18
  executables:
19
+ - pipe
28
20
  - pipemaster
29
21
  extensions: []
30
22
 
@@ -32,13 +24,19 @@ extra_rdoc_files:
32
24
  - README.rdoc
33
25
  - CHANGELOG
34
26
  files:
27
+ - bin/pipe
35
28
  - bin/pipemaster
29
+ - lib/pipemaster
36
30
  - lib/pipemaster/client.rb
37
31
  - lib/pipemaster/configurator.rb
32
+ - lib/pipemaster/launcher.rb
38
33
  - lib/pipemaster/server.rb
34
+ - lib/pipemaster/socket_helper.rb
35
+ - lib/pipemaster/util.rb
39
36
  - lib/pipemaster/worker.rb
40
37
  - lib/pipemaster.rb
41
38
  - test/test_helper.rb
39
+ - test/unit
42
40
  - test/unit/test_configurator.rb
43
41
  - test/unit/test_server.rb
44
42
  - CHANGELOG
@@ -48,13 +46,11 @@ files:
48
46
  - Gemfile
49
47
  - pipemaster.gemspec
50
48
  has_rdoc: true
51
- homepage: http://labnotes.org
52
- licenses: []
53
-
49
+ homepage: http://github.com/assaf/pipemaster
54
50
  post_install_message: To get started run pipemaster --help
55
51
  rdoc_options:
56
52
  - --title
57
- - Pipemaster 0.3.1
53
+ - Pipemaster 0.4.0
58
54
  - --main
59
55
  - README.rdoc
60
56
  - --webcvs
@@ -76,9 +72,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
72
  requirements: []
77
73
 
78
74
  rubyforge_project:
79
- rubygems_version: 1.3.5
75
+ rubygems_version: 1.3.1
80
76
  signing_key:
81
- specification_version: 3
77
+ specification_version: 2
82
78
  summary: Use the fork
83
79
  test_files: []
84
80