buzzware-buzzcore 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +23 -0
- data/README.rdoc +46 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/buzzcore.gemspec +77 -0
- data/buzzcore.vpj +93 -0
- data/buzzcore.vpw +6 -0
- data/lib/buzzcore.rb +2 -0
- data/lib/buzzcore/cap_utils.rb +181 -0
- data/lib/buzzcore/config.rb +210 -0
- data/lib/buzzcore/database_utils.rb +86 -0
- data/lib/buzzcore/enum.rb +50 -0
- data/lib/buzzcore/extend_base_classes.rb +320 -0
- data/lib/buzzcore/ftp_extra.rb +188 -0
- data/lib/buzzcore/logging.rb +159 -0
- data/lib/buzzcore/misc_utils.rb +382 -0
- data/lib/buzzcore/require_paths.rb +28 -0
- data/lib/buzzcore/shell_extras.rb +80 -0
- data/lib/buzzcore/string_utils.rb +53 -0
- data/lib/buzzcore/text_doc.rb +70 -0
- data/lib/buzzcore/thread_utils.rb +709 -0
- data/lib/buzzcore/xml_utils.rb +184 -0
- data/test/buzzcore_test.rb +7 -0
- data/test/config_test.rb +201 -0
- data/test/credentials_test.rb +71 -0
- data/test/shell_test.rb +54 -0
- data/test/test_helper.rb +10 -0
- metadata +95 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# This sorts out the issues of require'ing files in Ruby
|
2
|
+
# 1) on one line, you specify all the paths you need
|
3
|
+
# 2) Relative paths will be relative to the file you are in, absolute paths also supported
|
4
|
+
# 3) Paths will be expanded
|
5
|
+
# 4) Paths will only be added if they don't already exist
|
6
|
+
#
|
7
|
+
module ::Kernel
|
8
|
+
def require_paths(*aArgs)
|
9
|
+
caller_dir = File.dirname(File.expand_path(caller.first.sub(/:[0-9]+.*/,'')))
|
10
|
+
aArgs.each do |aPath|
|
11
|
+
aPath = File.expand_path(aPath,caller_dir)
|
12
|
+
$LOAD_PATH << aPath unless $LOAD_PATH.include?(aPath)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def require_paths_first(*aArgs)
|
17
|
+
caller_dir = File.dirname(File.expand_path(caller.first.sub(/:[0-9]+.*/,'')))
|
18
|
+
paths = []
|
19
|
+
aArgs.each do |aPath|
|
20
|
+
aPath = File.expand_path(aPath,caller_dir)
|
21
|
+
paths << aPath
|
22
|
+
end
|
23
|
+
paths.each do |p|
|
24
|
+
$LOAD_PATH.insert(0,p)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
gem 'Platform'; require 'platform'
|
2
|
+
gem 'shairontoledo-popen4'; require 'popen4'
|
3
|
+
|
4
|
+
module POpen4
|
5
|
+
|
6
|
+
class ExecuteError < StandardError
|
7
|
+
|
8
|
+
attr_reader :result #,:stderr,:stdout,:exitcode,:pid
|
9
|
+
|
10
|
+
def initialize(aArg)
|
11
|
+
if aArg.is_a? Hash
|
12
|
+
msg = ([aArg[:stderr],aArg[:stdout],"Error #{aArg[:exitcode].to_s}"].find {|i| i && !i.empty?})
|
13
|
+
super(msg)
|
14
|
+
@result = aArg
|
15
|
+
else
|
16
|
+
super(aArg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"#{self.class.to_s}: #{@result.inspect}"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.pump_thread(aIn,aOut)
|
27
|
+
Thread.new do
|
28
|
+
loop { aOut.puts aIn.gets }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Usage :
|
33
|
+
# result = POpen4::shell('somebinary') do |r| # block gives opportunity to adjust result, and avoid exception raised from non-zero exit codes
|
34
|
+
# if r[:exitcode]==254 # eg. say this binary returns 254 to mean something special but not an error
|
35
|
+
# r[:stdout] = 'some correct output'
|
36
|
+
# r[:stderr] = ''
|
37
|
+
# r[:exitcode] = 0
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# OR
|
42
|
+
#
|
43
|
+
# result = POpen4::shell('somebinary');
|
44
|
+
# puts result[:stdout]
|
45
|
+
#
|
46
|
+
# Giving aStdOut,aStdErr causes the command output to be connected to the given stream, and that stream to not be given in the result hash
|
47
|
+
def self.shell(aCommand,aWorkingDir=nil,aTimeout=nil,aStdOut=nil,aStdErr=nil)
|
48
|
+
raise ExecuteError.new('aWorkingDir doesnt exist') unless !aWorkingDir || File.exists?(aWorkingDir)
|
49
|
+
orig_wd = Dir.getwd
|
50
|
+
result = {:command => aCommand, :dir => (aWorkingDir || orig_wd)}
|
51
|
+
status = nil
|
52
|
+
begin
|
53
|
+
Dir.chdir(aWorkingDir) if aWorkingDir
|
54
|
+
Timeout.timeout(aTimeout,ExecuteError) do # nil aTimeout will not time out
|
55
|
+
status = POpen4::popen4(aCommand) do |stdout, stderr, stdin, pid|
|
56
|
+
thrOut = aStdOut ? Thread.new { aStdOut.puts stdout.read } : nil
|
57
|
+
thrErr = aStdErr ? Thread.new { aStdErr.puts stderr.read } : nil
|
58
|
+
thrOut.join if thrOut
|
59
|
+
thrErr.join if thrErr
|
60
|
+
|
61
|
+
result[:stdout] = stdout.read unless aStdOut
|
62
|
+
result[:stderr] = stderr.read unless aStdErr
|
63
|
+
result[:pid] = pid
|
64
|
+
end
|
65
|
+
end
|
66
|
+
ensure
|
67
|
+
Dir.chdir(orig_wd)
|
68
|
+
end
|
69
|
+
result[:exitcode] = (status && status.exitstatus) || 1
|
70
|
+
yield(result) if block_given?
|
71
|
+
raise ExecuteError.new(result) if result[:exitcode] != 0
|
72
|
+
return result
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.shell_out(aCommand,aWorkingDir=nil,aTimeout=nil,&block)
|
76
|
+
block_given? ? POpen4::shell(aCommand,aWorkingDir,aTimeout,STDOUT,STDERR,&block) : POpen4::shell(aCommand,aWorkingDir,aTimeout,STDOUT,STDERR)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module StringUtils
|
2
|
+
def self.crop(aString,aLength,aEllipsis=true,aConvertNil=true)
|
3
|
+
return aConvertNil ? ' '*aLength : nil if !aString
|
4
|
+
|
5
|
+
increase = aLength-aString.length
|
6
|
+
return aString+' '*increase if increase>=0
|
7
|
+
return aEllipsis ? aString[0,aLength-3]+'...' : aString[0,aLength]
|
8
|
+
end
|
9
|
+
|
10
|
+
# aTemplate is a string containing tokens like ${SOME_TOKEN}
|
11
|
+
# aValues is a hash of token names eg. 'SOME_TOKEN' and their values to substitute
|
12
|
+
def self.render_template(aTemplate,aValues)
|
13
|
+
# get positions of tokens
|
14
|
+
result = aTemplate.gsub(/\$\{(.*?)\}/) do |s|
|
15
|
+
key = s[2..-2]
|
16
|
+
rep = (aValues[key] || s)
|
17
|
+
#puts "replacing #{s} with #{rep}"
|
18
|
+
rep
|
19
|
+
end
|
20
|
+
#puts "rendered :\n#{result}"
|
21
|
+
return result
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.clean_number(aString)
|
25
|
+
aString.gsub(/[^0-9.-]/,'')
|
26
|
+
end
|
27
|
+
|
28
|
+
# supply a block with 2 parameters, and it will get called for each char as an integer
|
29
|
+
def self.each_unicode_char(aString)
|
30
|
+
len = 1
|
31
|
+
index = 0
|
32
|
+
char = 0
|
33
|
+
aString.each_byte do |b|
|
34
|
+
if index==0
|
35
|
+
len = 1
|
36
|
+
len = 2 if b & 0b11000000 != 0
|
37
|
+
len = 3 if b & 0b11100000 != 0
|
38
|
+
len = 4 if b & 0b11110000 != 0
|
39
|
+
char = 0
|
40
|
+
end
|
41
|
+
|
42
|
+
char |= b << index*8
|
43
|
+
|
44
|
+
yield(char,len) if index==len-1 # last byte; char is complete
|
45
|
+
|
46
|
+
index += 1
|
47
|
+
index = 0 if index >= len
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# represents a mono-spaced text document with a given width and expandable height.
|
2
|
+
class TextDoc
|
3
|
+
|
4
|
+
attr_reader :width, :height, :lines
|
5
|
+
|
6
|
+
def logger
|
7
|
+
RAILS_DEFAULT_LOGGER
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(aWidth=80,aHeight=66)
|
11
|
+
@width = aWidth
|
12
|
+
@height = aHeight
|
13
|
+
|
14
|
+
@lines = Array.new(@height)
|
15
|
+
line_str = ' '*@width
|
16
|
+
@lines.collect!{|line| line_str.clone }
|
17
|
+
end
|
18
|
+
|
19
|
+
def replace_string(aString,aCol,aSubString)
|
20
|
+
return aString if aSubString==nil || aSubString==''
|
21
|
+
|
22
|
+
aSubString = aSubString.to_s
|
23
|
+
start_col = aCol < 0 ? 0 : aCol
|
24
|
+
end_col = aCol+aSubString.length-1
|
25
|
+
end_col = @width-1 if end_col >= @width
|
26
|
+
source_len = end_col-start_col+1
|
27
|
+
return aString if source_len <= 0 || end_col < 0 || start_col >= @width
|
28
|
+
aString += ' '*((end_col+1) - aString.length) if aString.length < end_col+1
|
29
|
+
aString[start_col,source_len] = aSubString[start_col-aCol,end_col-start_col+1]
|
30
|
+
return aString
|
31
|
+
end
|
32
|
+
|
33
|
+
def replace(aCol,aLine,aString)
|
34
|
+
return if (aLine < 0) || (aLine>=@lines.length)
|
35
|
+
replace_string(@lines[aLine],aCol,aString)
|
36
|
+
end
|
37
|
+
|
38
|
+
def replace_block(aCol,aLine,aLines)
|
39
|
+
aLines = aLines.split(/\n/) if aLines.is_a?(String)
|
40
|
+
aLines = aLines.lines if aLines.is_a?(TextDoc)
|
41
|
+
|
42
|
+
aLines.each_index do |iSource|
|
43
|
+
replace(aCol,aLine+iSource,aLines[iSource])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_block(aLines,aCol=0)
|
48
|
+
aLines = aLines.split(/\n/) if aLines.is_a?(String)
|
49
|
+
aLines = aLines.lines if aLines.is_a?(TextDoc)
|
50
|
+
aLines.each_index do |iSource|
|
51
|
+
@lines << ' '*@width
|
52
|
+
replace(aCol,@lines.length-1,aLines[iSource])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_line(aLine=nil,aCol=0)
|
57
|
+
@lines << ' '*@width and return if !aLine
|
58
|
+
@lines << ' '*@width
|
59
|
+
replace(aCol,@lines.length-1,aLine)
|
60
|
+
end
|
61
|
+
|
62
|
+
def centre_bar(aChar = '-', indent = 6)
|
63
|
+
(' '*indent) + aChar*(@width-(indent*2)) + (' '*indent)
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
return @lines.join("\n")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,709 @@
|
|
1
|
+
#require 'monitor'
|
2
|
+
require 'timeout'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'thread'
|
6
|
+
require 'fastthread'
|
7
|
+
|
8
|
+
# # This class provides an object value that can be passed between threads without
|
9
|
+
# # fear of collisions. As WaitOne is used, multiple consumers could wait on one producer,
|
10
|
+
# # and only one consumer would get each produced value.
|
11
|
+
# # Reading it will block until it is written to. Writing will block until the last
|
12
|
+
# # written value is read. Therefore, it acts like a blocking queue with a fixed
|
13
|
+
# # maximum length of one. It currently doesn't support timeouts.
|
14
|
+
# # Reading or writing may raise a UnblockException when the thread is aborted
|
15
|
+
# # externally.
|
16
|
+
# class MultiThreadVariable {
|
17
|
+
#
|
18
|
+
# AutoResetEvent areWrite = new AutoResetEvent(true);
|
19
|
+
# AutoResetEvent areRead = new AutoResetEvent(false);
|
20
|
+
#
|
21
|
+
# def initialize
|
22
|
+
# @unblock = false
|
23
|
+
# @val = nil
|
24
|
+
# @mutex = Mutex.new
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def value
|
28
|
+
# if (@unblock || !areRead.WaitOne())
|
29
|
+
# raise new UnblockException();
|
30
|
+
# @mutex.synchronize do
|
31
|
+
# object result = val;
|
32
|
+
# areWrite.Set();
|
33
|
+
# return result;
|
34
|
+
# end
|
35
|
+
# def value=
|
36
|
+
# if (@unblock || !areWrite.WaitOne())
|
37
|
+
# raise new UnblockException();
|
38
|
+
# @mutex.synchronize do
|
39
|
+
# val = value;
|
40
|
+
# areRead.Set();
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# #Call this when shutting down to break any existing block and prevent any future blocks
|
45
|
+
# def unblock()
|
46
|
+
# @unblock = true;
|
47
|
+
# areWrite.Set();
|
48
|
+
# areRead.Set();
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
|
52
|
+
|
53
|
+
# class MultiThreadVariableMonitor
|
54
|
+
#
|
55
|
+
# include MonitorMixin
|
56
|
+
#
|
57
|
+
# class UnblockError < StandardError; end
|
58
|
+
# class TimeoutError < StandardError; end
|
59
|
+
# class LockFailedError < StandardError; end
|
60
|
+
#
|
61
|
+
# def initialize(aTimeout=nil)
|
62
|
+
# mon_initialize()
|
63
|
+
# @value = nil
|
64
|
+
# @unblock = false
|
65
|
+
# @timeout = aTimeout
|
66
|
+
# @readable = new_cond
|
67
|
+
# @writable = new_cond
|
68
|
+
# @is_first_write = true
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# def value
|
72
|
+
# raise UnblockError.new if @unblock
|
73
|
+
# mon_synchronize do
|
74
|
+
# raise TimeoutError.new if not @readable.wait(@timeout)
|
75
|
+
# result = @value
|
76
|
+
# @writable.signal
|
77
|
+
# end
|
78
|
+
# return result
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# def value=(v)
|
82
|
+
# raise UnblockError.new if @unblock
|
83
|
+
# mon_synchronize do
|
84
|
+
# if @is_first_write
|
85
|
+
# @is_first_write = false
|
86
|
+
# else
|
87
|
+
# raise TimeoutError.new if not @writable.wait(@timeout)
|
88
|
+
# end
|
89
|
+
# @value = v
|
90
|
+
# @readable.signal
|
91
|
+
# end
|
92
|
+
# return v
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# def unblock()
|
96
|
+
# @unblock = true;
|
97
|
+
# @readable.broadcast
|
98
|
+
# @writable.broadcast
|
99
|
+
# while @readable.count_waiters()>0 or @writable.count_waiters()>0 do
|
100
|
+
# sleep(1)
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
|
105
|
+
class UnblockError < StandardError; end
|
106
|
+
|
107
|
+
SizedQueue.class_eval do
|
108
|
+
def unblock
|
109
|
+
@unblock = true
|
110
|
+
Thread.exclusive do
|
111
|
+
if @queue_wait
|
112
|
+
while t = @queue_wait.shift do
|
113
|
+
t.raise(UnblockError.new) unless t == Thread.current
|
114
|
+
end
|
115
|
+
end
|
116
|
+
if @waiting
|
117
|
+
while t = @waiting.shift do
|
118
|
+
t.raise(UnblockError.new) unless t == Thread.current
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
alias original_push push
|
125
|
+
def push(obj)
|
126
|
+
raise UnblockError.new if @unblock
|
127
|
+
original_push(obj)
|
128
|
+
end
|
129
|
+
|
130
|
+
alias original_pop pop
|
131
|
+
def pop(*args)
|
132
|
+
raise UnblockError.new if @unblock
|
133
|
+
original_pop(*args)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
# BEGIN
|
139
|
+
# $Id: semaphore.rb,v 1.2 2003/03/15 20:10:10 fukumoto Exp $
|
140
|
+
class CountingSemaphore
|
141
|
+
|
142
|
+
def initialize(initvalue = 0)
|
143
|
+
@counter = initvalue
|
144
|
+
@waiting_list = []
|
145
|
+
end
|
146
|
+
|
147
|
+
def wait
|
148
|
+
Thread.critical = true
|
149
|
+
if (@counter -= 1) < 0
|
150
|
+
@waiting_list.push(Thread.current)
|
151
|
+
Thread.stop
|
152
|
+
end
|
153
|
+
self
|
154
|
+
ensure
|
155
|
+
Thread.critical = false
|
156
|
+
end
|
157
|
+
|
158
|
+
def signal
|
159
|
+
Thread.critical = true
|
160
|
+
begin
|
161
|
+
if (@counter += 1) <= 0
|
162
|
+
t = @waiting_list.shift
|
163
|
+
t.wakeup if t
|
164
|
+
end
|
165
|
+
rescue ThreadError
|
166
|
+
retry
|
167
|
+
end
|
168
|
+
self
|
169
|
+
ensure
|
170
|
+
Thread.critical = false
|
171
|
+
end
|
172
|
+
|
173
|
+
alias down wait
|
174
|
+
alias up signal
|
175
|
+
alias P wait
|
176
|
+
alias V signal
|
177
|
+
|
178
|
+
def exclusive
|
179
|
+
wait
|
180
|
+
yield
|
181
|
+
ensure
|
182
|
+
signal
|
183
|
+
end
|
184
|
+
|
185
|
+
alias synchronize exclusive
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
Semaphore = CountingSemaphore
|
190
|
+
# END
|
191
|
+
|
192
|
+
|
193
|
+
class MultiThreadVariable
|
194
|
+
|
195
|
+
attr_accessor :read_timeout, :write_timeout
|
196
|
+
|
197
|
+
def initialize(aReadTimeout=nil,aWriteTimeout=nil)
|
198
|
+
@q = SizedQueue.new(1)
|
199
|
+
@read_timeout = aReadTimeout
|
200
|
+
@write_timeout = aWriteTimeout
|
201
|
+
end
|
202
|
+
|
203
|
+
def clear
|
204
|
+
@q.clear
|
205
|
+
end
|
206
|
+
|
207
|
+
def empty?
|
208
|
+
@q.empty?
|
209
|
+
end
|
210
|
+
|
211
|
+
def value
|
212
|
+
if @read_timeout
|
213
|
+
Timeout.timeout(@read_timeout) do
|
214
|
+
@q.pop
|
215
|
+
end
|
216
|
+
else
|
217
|
+
@q.pop
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def inner_value_set(aValue)
|
222
|
+
Thread.exclusive do
|
223
|
+
if @reject_next
|
224
|
+
clear
|
225
|
+
else
|
226
|
+
@q.push(aValue)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def value=(aValue)
|
232
|
+
if @write_timeout
|
233
|
+
Timeout.timeout(@write_timeout) { inner_value_set(aValue) }
|
234
|
+
else
|
235
|
+
inner_value_set(aValue)
|
236
|
+
end
|
237
|
+
aValue
|
238
|
+
end
|
239
|
+
|
240
|
+
def unblock()
|
241
|
+
@q.unblock()
|
242
|
+
end
|
243
|
+
|
244
|
+
def reject_value
|
245
|
+
Thread.exclusive do
|
246
|
+
if !empty?
|
247
|
+
clear
|
248
|
+
else
|
249
|
+
@reject_next = true
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
class MonitorVariable
|
258
|
+
|
259
|
+
def initialize(aMonitor)
|
260
|
+
@monitor = aMonitor
|
261
|
+
@cvRead = @monitor.new_cond
|
262
|
+
@cvWrite = @monitor.new_cond
|
263
|
+
@empty = true
|
264
|
+
end
|
265
|
+
|
266
|
+
def value
|
267
|
+
@monitor.synchronize do
|
268
|
+
while empty?
|
269
|
+
@cvRead.wait(@timeout)
|
270
|
+
end
|
271
|
+
result = @value
|
272
|
+
@value = nil
|
273
|
+
@empty = true
|
274
|
+
@cvWrite.signal
|
275
|
+
result
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def value=(aValue)
|
280
|
+
@monitor.synchronize do
|
281
|
+
until empty?
|
282
|
+
@cvWrite.wait(@timeout)
|
283
|
+
end
|
284
|
+
if @reject_next
|
285
|
+
clear
|
286
|
+
else
|
287
|
+
@value = aValue
|
288
|
+
@empty = false
|
289
|
+
@cvRead.signal
|
290
|
+
end
|
291
|
+
aValue
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def empty?
|
296
|
+
@empty
|
297
|
+
end
|
298
|
+
|
299
|
+
def clear
|
300
|
+
@monitor.synchronize do
|
301
|
+
@value = nil
|
302
|
+
@empty = true
|
303
|
+
@cvWrite.signal
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def reject_value
|
308
|
+
@monitor.synchronize do
|
309
|
+
if !empty?
|
310
|
+
clear
|
311
|
+
else
|
312
|
+
@reject_next = true
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
# This module decouples multiple master threads from multiple slave (worker) threads
|
320
|
+
# It provides two main methods :
|
321
|
+
# + master_attempt_command to be called by multiple clients with a command, returning a result or rasing an exception
|
322
|
+
# + slave_do_command to be called by worker threads with a block which processes the command.
|
323
|
+
#
|
324
|
+
# see ProviderWorker in pay_server/app/pay_server.rb
|
325
|
+
module MasterSlaveSynchroniserMixin
|
326
|
+
|
327
|
+
def logger
|
328
|
+
@logger || (@logger = Logger.new(STDERR))
|
329
|
+
end
|
330
|
+
|
331
|
+
def ms_synchronizer_initialize(aTimeout=nil,aLogger=nil)
|
332
|
+
@logger = aLogger
|
333
|
+
@semaphore = CountingSemaphore.new(1)
|
334
|
+
timeout = aTimeout && aTimeout/2.0
|
335
|
+
@mvCommand = MultiThreadVariable.new(nil,timeout)
|
336
|
+
@mvResponse = MultiThreadVariable.new(timeout,nil)
|
337
|
+
end
|
338
|
+
|
339
|
+
def master_attempt_command(aCommand)
|
340
|
+
@semaphore.exclusive do
|
341
|
+
command_sent = false
|
342
|
+
begin
|
343
|
+
before = Time.now
|
344
|
+
logger.debug { "master sending aCommand:"+aCommand.inspect }
|
345
|
+
@mvCommand.value = aCommand
|
346
|
+
command_sent = true
|
347
|
+
logger.debug { "master waiting for result" }
|
348
|
+
result = @mvResponse.value
|
349
|
+
logger.debug { "master received result:"+result.inspect }
|
350
|
+
rescue Exception => e
|
351
|
+
# exception causes thread critical status to be lost
|
352
|
+
logger.debug { "master exception:"+e.inspect }
|
353
|
+
if command_sent
|
354
|
+
logger.debug { "rejecting" }
|
355
|
+
@mvResponse.reject_value #!!! this doesn't seem to return
|
356
|
+
end
|
357
|
+
raise e
|
358
|
+
ensure
|
359
|
+
logger.debug { "master_attempt_command: command_sent=#{command_sent.to_s} elapsed:"+(Time.now-before).to_s }
|
360
|
+
end
|
361
|
+
result
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def slave_do_command(&block)
|
366
|
+
Thread.exclusive do
|
367
|
+
logger.debug { "slave waiting for command" }
|
368
|
+
command = @mvCommand.value
|
369
|
+
logger.debug { "slave received command:"+command.inspect }
|
370
|
+
result = yield(command)
|
371
|
+
logger.debug { "slave sending result:"+result.inspect }
|
372
|
+
@mvResponse.value = result
|
373
|
+
logger.debug { "slave finished" }
|
374
|
+
result
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def shutdown
|
379
|
+
@mvCommand.unblock()
|
380
|
+
@mvResponse.unblock()
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
class MasterSlaveSynchroniser
|
385
|
+
include MasterSlaveSynchroniserMixin
|
386
|
+
|
387
|
+
def initialize(aTimeout=nil,aLogger=nil)
|
388
|
+
ms_synchronizer_initialize(aTimeout,aLogger)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
|
393
|
+
|
394
|
+
module Worker
|
395
|
+
|
396
|
+
module PidFile
|
397
|
+
def self.store(aFilename, aPID)
|
398
|
+
File.open(aFilename, 'w') {|f| f << aPID}
|
399
|
+
end
|
400
|
+
|
401
|
+
def self.recall(aFilename)
|
402
|
+
IO.read(aFilename).to_i rescue nil
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
class Base
|
407
|
+
TempDirectory = Dir.tmpdir
|
408
|
+
|
409
|
+
def self.pid_filename
|
410
|
+
File.join(TempDirectory, "#{name}.pid")
|
411
|
+
end
|
412
|
+
|
413
|
+
def logger
|
414
|
+
if not @logger
|
415
|
+
@logger = Logger.new(STDERR)
|
416
|
+
@logger.level = Logger::DEBUG
|
417
|
+
end
|
418
|
+
@logger
|
419
|
+
end
|
420
|
+
|
421
|
+
def main_proc
|
422
|
+
begin
|
423
|
+
@is_stopped = false
|
424
|
+
@is_started = false
|
425
|
+
self.starting()
|
426
|
+
@is_started = true
|
427
|
+
@is_stopping = false
|
428
|
+
while !@is_stopping do
|
429
|
+
running();
|
430
|
+
logger.debug { "ServiceThread running loop: @is_stopping="+@is_stopping.to_s }
|
431
|
+
end
|
432
|
+
rescue SystemExit => e # smother and do nothing
|
433
|
+
rescue Exception => e
|
434
|
+
logger.warn { "Thread #{@name} #{e.inspect} exception in Starting() or Running()" }
|
435
|
+
logger.warn { e.backtrace }
|
436
|
+
ensure
|
437
|
+
@is_stopping = true
|
438
|
+
end
|
439
|
+
|
440
|
+
begin
|
441
|
+
stopping()
|
442
|
+
rescue Exception => e
|
443
|
+
logger.warn { "Thread #{@name} #{e.inspect} exception in stopping()" }
|
444
|
+
logger.warn { e.backtrace }
|
445
|
+
end
|
446
|
+
logger.info { "Thread #{@name} dropped out" }
|
447
|
+
@is_stopped = true
|
448
|
+
end
|
449
|
+
|
450
|
+
def wait_for_started(aTimeout)
|
451
|
+
before = Time.now
|
452
|
+
while !@is_started and (Time.now-before) < aTimeout
|
453
|
+
sleep(aTimeout / 10)
|
454
|
+
end
|
455
|
+
raise Timeout::Error.new("failed to start within timeout (#{aTimeout.to_s})") if !@is_started
|
456
|
+
end
|
457
|
+
|
458
|
+
def wait_for_stopped(aTimeout)
|
459
|
+
before = Time.now
|
460
|
+
while !@is_stopped and (Time.now-before) < aTimeout
|
461
|
+
sleep(aTimeout / 10)
|
462
|
+
end
|
463
|
+
raise Timeout::Error.new("failed to stop within timeout (#{aTimeout.to_s})") if !@is_stopped
|
464
|
+
end
|
465
|
+
|
466
|
+
def stop
|
467
|
+
@is_stopping = true
|
468
|
+
end
|
469
|
+
|
470
|
+
def starting
|
471
|
+
end
|
472
|
+
|
473
|
+
def running
|
474
|
+
end
|
475
|
+
|
476
|
+
def stopping
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
|
481
|
+
class Threader
|
482
|
+
|
483
|
+
attr_reader :worker,:thread
|
484
|
+
|
485
|
+
def self.start_new(aWorkerClass,aTimeout=0.1,&aCreateBlock)
|
486
|
+
threader = Threader.new(aWorkerClass,&aCreateBlock)
|
487
|
+
threader.start(aTimeout)
|
488
|
+
return threader
|
489
|
+
end
|
490
|
+
|
491
|
+
def initialize(aWorkerClass,&aCreateBlock)
|
492
|
+
@create_proc = aCreateBlock
|
493
|
+
@worker_class = aWorkerClass
|
494
|
+
if @create_proc
|
495
|
+
@worker = @create_proc.call(@worker_class)
|
496
|
+
else
|
497
|
+
@worker = @worker_class.new
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def start(aTimeout=0.1)
|
502
|
+
@thread = Thread.new(@worker) { |aWorker| aWorker.main_proc }
|
503
|
+
@worker.wait_for_started(aTimeout)
|
504
|
+
end
|
505
|
+
|
506
|
+
def stop(aTimeout=0.1)
|
507
|
+
@worker.stop
|
508
|
+
@worker.wait_for_stopped(aTimeout)
|
509
|
+
@thread.exit unless !@thread or (@thread.join(0) and not @thread.alive?)
|
510
|
+
#@thread.join()
|
511
|
+
@worker = nil
|
512
|
+
@thread = nil
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
module Daemonizer
|
517
|
+
|
518
|
+
# Assists in making a daemon script from a Worker. If a block is given, it is assumed to create the worker and return it.
|
519
|
+
# Otherwise the Worker is created with no arguments from aWorkerClass
|
520
|
+
# Either way, the worker is only created when starting
|
521
|
+
def self.daemonize(aWorkerClass,aConfig={})
|
522
|
+
case !ARGV.empty? && ARGV[0]
|
523
|
+
when 'start'
|
524
|
+
worker = block_given? ? yield(aWorkerClass) : aWorkerClass.new
|
525
|
+
if aConfig['no_fork']
|
526
|
+
start_no_fork(worker)
|
527
|
+
else
|
528
|
+
start(worker)
|
529
|
+
end
|
530
|
+
when 'stop'
|
531
|
+
stop(aWorkerClass)
|
532
|
+
when 'restart'
|
533
|
+
stop(aWorkerClass)
|
534
|
+
worker = block_given? ? yield(aWorkerClass) : aWorkerClass.new
|
535
|
+
start(worker)
|
536
|
+
else
|
537
|
+
puts "Invalid command. Please specify start, stop or restart."
|
538
|
+
exit
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
def self.start_no_fork(aWorker)
|
543
|
+
PidFile.store(aWorker.class.pid_filename, Process.pid)
|
544
|
+
Dir.chdir(aWorker.class::TempDirectory)
|
545
|
+
trap('TERM') do
|
546
|
+
begin
|
547
|
+
puts "Daemonizer::start_no_fork TERM"
|
548
|
+
#puts aWorker.inspect
|
549
|
+
aWorker.stop
|
550
|
+
puts "after worker stop"
|
551
|
+
aWorker.wait_for_stopped()
|
552
|
+
puts "after worker stop wait"
|
553
|
+
rescue Exception => e
|
554
|
+
puts "Exception: "+e.inspect
|
555
|
+
end
|
556
|
+
exit
|
557
|
+
end
|
558
|
+
trap('HUP','IGNORE') unless is_windows? # ignore SIGHUP - required for Capistrano
|
559
|
+
aWorker.main_proc
|
560
|
+
end
|
561
|
+
|
562
|
+
def self.start(aWorker)
|
563
|
+
fork do
|
564
|
+
Process.setsid
|
565
|
+
if child_pid = fork
|
566
|
+
Process.detach(child_pid)
|
567
|
+
else
|
568
|
+
PidFile.store(aWorker.class.pid_filename, Process.pid)
|
569
|
+
Dir.chdir(aWorker.class::TempDirectory)
|
570
|
+
File.umask 0000
|
571
|
+
STDIN.reopen "/dev/null"
|
572
|
+
STDOUT.reopen "/dev/null", "a" # problems here
|
573
|
+
STDERR.reopen STDOUT
|
574
|
+
trap('TERM') do
|
575
|
+
puts "Daemonizer::start TERM"
|
576
|
+
aWorker.stop
|
577
|
+
aWorker.wait_for_stopped()
|
578
|
+
exit
|
579
|
+
end
|
580
|
+
trap('HUP','IGNORE') # ignore SIGHUP - required for Capistrano
|
581
|
+
aWorker.main_proc
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def self.stop(aWorkerClass)
|
587
|
+
if !File.file?(aWorkerClass.pid_filename)
|
588
|
+
puts "Pid file not found. Is the aWorker started?"
|
589
|
+
exit
|
590
|
+
end
|
591
|
+
pid = PidFile.recall(aWorkerClass.pid_filename)
|
592
|
+
pid && Process.kill("TERM", pid)
|
593
|
+
FileUtils.rm(aWorkerClass.pid_filename)
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
|
599
|
+
# How to use :
|
600
|
+
#
|
601
|
+
# class Worker < ServiceThread
|
602
|
+
# def initialize
|
603
|
+
# super(:name => 'worker',:auto_start => true)
|
604
|
+
# end
|
605
|
+
#
|
606
|
+
# def starting
|
607
|
+
# # startup code
|
608
|
+
# end
|
609
|
+
#
|
610
|
+
# def running
|
611
|
+
# # repeated code, implicitly looped. set self.stopping = true to quit thread
|
612
|
+
# end
|
613
|
+
#
|
614
|
+
# def stopping
|
615
|
+
# # clean up code
|
616
|
+
# end
|
617
|
+
# end
|
618
|
+
#
|
619
|
+
# @worker = Worker.new
|
620
|
+
# logger.info "worker #{@worker.is_started ? 'has':'hasn''t'} started and {@worker.is_stopped ? 'has':'hasn''t'} stopped"
|
621
|
+
# @worker.stop
|
622
|
+
#
|
623
|
+
class ServiceThread
|
624
|
+
|
625
|
+
attr_accessor :name, :logger, :is_stopping
|
626
|
+
attr_reader :is_stopped, :is_started, :options
|
627
|
+
|
628
|
+
def random_word(min,max)
|
629
|
+
len = min + rand(max-min+1)
|
630
|
+
result = ' '*len
|
631
|
+
(len-1).downto(0) {|i| result[i] = (?a + rand(?z-?a+1)).chr}
|
632
|
+
return result
|
633
|
+
end
|
634
|
+
|
635
|
+
# if inheriting from ServiceThread and overriding initialize, remember to call super(aOptions) and that
|
636
|
+
# this method may start the thread, so any setup must be done before super, not after
|
637
|
+
def initialize(aOptions)
|
638
|
+
@options = aOptions
|
639
|
+
@thread = nil
|
640
|
+
@name = aOptions[:name] || random_word(8,8)
|
641
|
+
if not @logger = aOptions[:logger]
|
642
|
+
@logger = Logger.new(STDERR)
|
643
|
+
@logger.level = Logger::DEBUG
|
644
|
+
end
|
645
|
+
self.start() if aOptions[:auto_start]
|
646
|
+
end
|
647
|
+
|
648
|
+
def start
|
649
|
+
raise Exception.new("ServiceThread already started") if @thread
|
650
|
+
@thread = Thread.new() { main_proc() }
|
651
|
+
#Thread.pass unless @is_started or @is_stopped # no timeout !
|
652
|
+
end
|
653
|
+
|
654
|
+
def name
|
655
|
+
@name
|
656
|
+
end
|
657
|
+
|
658
|
+
def logger
|
659
|
+
@logger
|
660
|
+
end
|
661
|
+
|
662
|
+
|
663
|
+
def main_proc
|
664
|
+
begin
|
665
|
+
@is_stopped = false
|
666
|
+
@is_started = false
|
667
|
+
self.starting()
|
668
|
+
@is_started = true
|
669
|
+
@is_stopping = false
|
670
|
+
while !@is_stopping do
|
671
|
+
running();
|
672
|
+
logger.debug { "ServiceThread running loop: @is_stopping="+@is_stopping.to_s }
|
673
|
+
end
|
674
|
+
rescue Exception => e
|
675
|
+
@is_stopping = true
|
676
|
+
logger.warn { "Thread #{@name} #{e.inspect} exception in Starting() or Running()" }
|
677
|
+
logger.warn { e.backtrace }
|
678
|
+
end
|
679
|
+
|
680
|
+
begin
|
681
|
+
stopping()
|
682
|
+
rescue Exception => e
|
683
|
+
logger.warn { "Thread #{@name} #{e.inspect} exception in stopping()" }
|
684
|
+
logger.warn { e.backtrace }
|
685
|
+
end
|
686
|
+
logger.info { "Thread #{@name} dropped out" }
|
687
|
+
@is_stopped = true
|
688
|
+
end
|
689
|
+
|
690
|
+
def starting
|
691
|
+
end
|
692
|
+
|
693
|
+
def running
|
694
|
+
end
|
695
|
+
|
696
|
+
def stopping
|
697
|
+
end
|
698
|
+
|
699
|
+
def gentle_stop
|
700
|
+
@is_stopping = true
|
701
|
+
end
|
702
|
+
|
703
|
+
def stop
|
704
|
+
@is_stopping = true
|
705
|
+
@thread.exit unless !@thread or (@thread.join(0) and not @thread.alive?)
|
706
|
+
end
|
707
|
+
|
708
|
+
end
|
709
|
+
|