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 +7 -0
- data/Gemfile +0 -1
- data/README.rdoc +2 -2
- data/Rakefile +6 -2
- data/bin/pipe +37 -0
- data/bin/pipemaster +22 -42
- data/lib/pipemaster/client.rb +33 -6
- data/lib/pipemaster/configurator.rb +113 -11
- data/lib/pipemaster/launcher.rb +67 -0
- data/lib/pipemaster/server.rb +262 -24
- data/lib/pipemaster/socket_helper.rb +149 -0
- data/lib/pipemaster/util.rb +67 -0
- data/pipemaster.gemspec +3 -4
- data/test/test_helper.rb +3 -17
- data/test/unit/test_server.rb +2 -10
- metadata +15 -19
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
data/README.rdoc
CHANGED
@@ -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
|
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 "
|
17
|
-
task :
|
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
|
data/bin/pipe
ADDED
@@ -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)
|
data/bin/pipemaster
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
#!/
|
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)}
|
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 "
|
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 "
|
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.
|
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.
|
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.
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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)
|
data/lib/pipemaster/client.rb
CHANGED
@@ -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.
|
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.
|
16
|
+
# c.run :transform, "thumbnail"
|
16
17
|
class Client
|
17
18
|
BUFFER_SIZE = 16 * 1024
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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 <
|
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
|
-
|
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
|
-
|
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
|
-
#
|
122
|
-
|
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
|
-
|
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
|
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
|
data/lib/pipemaster/server.rb
CHANGED
@@ -1,16 +1,71 @@
|
|
1
|
-
require "
|
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
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
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
|
-
|
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
|
+
|
data/pipemaster.gemspec
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = "pipemaster"
|
3
|
-
spec.version = "0.
|
3
|
+
spec.version = "0.4.0"
|
4
4
|
spec.author = "Assaf Arkin"
|
5
5
|
spec.email = "assaf@labnotes.org"
|
6
|
-
spec.homepage = "http://
|
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.
|
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
|
data/test/test_helper.rb
CHANGED
@@ -70,7 +70,7 @@ end
|
|
70
70
|
|
71
71
|
def hit(address, *args)
|
72
72
|
client = Pipemaster::Client.new(address)
|
73
|
-
code = client.
|
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 ==
|
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}/
|
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) }
|
data/test/unit/test_server.rb
CHANGED
@@ -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.
|
76
|
-
|
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.
|
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-
|
12
|
+
date: 2010-02-19 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
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://
|
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.
|
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.
|
75
|
+
rubygems_version: 1.3.1
|
80
76
|
signing_key:
|
81
|
-
specification_version:
|
77
|
+
specification_version: 2
|
82
78
|
summary: Use the fork
|
83
79
|
test_files: []
|
84
80
|
|