buzzware-buzzcore 0.2.2
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/.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
|
+
|