enkidu 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/enkidu/dispatcher.rb +240 -0
- data/lib/enkidu/logging.rb +112 -0
- data/lib/enkidu/signals.rb +76 -0
- data/lib/enkidu/tools.rb +51 -0
- data/lib/enkidu.rb +7 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6cc70fb3863d61c5a67e5b6089de27b48a1cc1be
|
4
|
+
data.tar.gz: 7b35191f85577c0fa859ab15912b60eb9979f09f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1cfb5b0739aa65b78667a53f020cc4539ef78cf01f6eaae0f25c9898d164a3a37b6ef0d90cee24305057ff4aab6b3a8fa6b5615ecf23aa6ada11decb4b879b3b
|
7
|
+
data.tar.gz: 69b02f4e3fb66053fd80b4ec47612d65f79c0009399b1a764db096ac8e9256ebb0626eb01a7dbc472bdbc9f50f8da34b237d98f05c2a9d8810bd7a111b531e55
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
|
5
|
+
module Enkidu
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
# The Dispatcher maintains a queue of callable objects that are run in the order
|
11
|
+
# they get added. When there is nothing in the queue, it blocks the thread.
|
12
|
+
#
|
13
|
+
# Operations on the Dispatcher are thread-safe.
|
14
|
+
#
|
15
|
+
# If you do not want to block the current thread, check ThreadedDispatcher.
|
16
|
+
#
|
17
|
+
# d = Dispatcher.new
|
18
|
+
# d.schedule{ puts "hello from the scheduler" }
|
19
|
+
# d.schedule{ puts "i will run after the first one" }
|
20
|
+
# d.on('event'){|arg| puts arg } #Add a handler for 'event'
|
21
|
+
# d.on(/[ea]v[ea]nt/){|*a| puts "handling 'event' or 'avant'" }
|
22
|
+
#
|
23
|
+
# Thread.new{ sleep 5; d.stop } #Stop the dispatcher after 5 seconds
|
24
|
+
#
|
25
|
+
# d.signal 'event', 'argument here' #Signalling doesn't actually run handlers, just schedules them
|
26
|
+
# d.run #Blocks
|
27
|
+
class Dispatcher
|
28
|
+
|
29
|
+
STOP = Object.new
|
30
|
+
k=self;STOP.define_singleton_method(:inspect){ "<#{k.name}::STOP>" }
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@lock = Mutex.new
|
34
|
+
@queue = []
|
35
|
+
@handlers = []
|
36
|
+
@r, @w = IO.pipe
|
37
|
+
yield self if block_given?
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# Run the loop. This will block the current thread until the loop is stopped.
|
42
|
+
def run
|
43
|
+
loop do
|
44
|
+
IO.select [@r]
|
45
|
+
if vals = sync{ queue.shift }
|
46
|
+
sync{ @r.read(1) }
|
47
|
+
callable, args = *vals
|
48
|
+
if callable == STOP
|
49
|
+
break
|
50
|
+
else
|
51
|
+
callable.call(*args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Schedule a callable to be run. This will push the callable to the back of
|
59
|
+
# the queue, so anything scheduled before it will run first.
|
60
|
+
#
|
61
|
+
# schedule{ puts "I have been called" }
|
62
|
+
# callable = ->(arg){ p arg }
|
63
|
+
# schedule('an argument', callable: callable)
|
64
|
+
def schedule(*args, callable: nil, &b)
|
65
|
+
callable = callable(callable, b)
|
66
|
+
sync do
|
67
|
+
queue.push [callable, args]
|
68
|
+
@w.write '.'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
alias push schedule
|
72
|
+
|
73
|
+
# Schedule a callable to be run immediately by putting it at the front of the queue.
|
74
|
+
#
|
75
|
+
# schedule{ puts "Hey, that's not nice :(" }
|
76
|
+
# unshift{ puts "Cutting in line" }
|
77
|
+
def unshift(*args, callable: nil, &b)
|
78
|
+
callable = callable(callable, b)
|
79
|
+
sync do
|
80
|
+
queue.unshift [callable, args]
|
81
|
+
@w.write '.'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Stop the dispatcher. This schedules a special STOP signal that will stop the
|
87
|
+
# dispatcher when encountered. This means that all other items that were scheduled
|
88
|
+
# before it will run first.
|
89
|
+
def stop
|
90
|
+
schedule(callable: STOP)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Stop the dispatcher immediately by scheduling it at the front of the queue. This
|
94
|
+
# means that any other already scheduled items will be ignored.
|
95
|
+
def stop!
|
96
|
+
unshift(callable: STOP)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Signal an event
|
101
|
+
#
|
102
|
+
# If a handler is found that matches the given type, it is scheduled to be executed
|
103
|
+
#
|
104
|
+
# `type` is =~ against each handler's regex, each handler that matches is scheduled
|
105
|
+
#
|
106
|
+
# signal 'foo.bar.baz', arg1, arg2
|
107
|
+
# signal ['foo', 'bar', 'baz'], arg1, arg2 #Same as above
|
108
|
+
def signal(type, *args)
|
109
|
+
type = type.join('.') if Array === type
|
110
|
+
0.upto(handlers.size - 1).each do |index|
|
111
|
+
if vals = sync{ handlers[index] }
|
112
|
+
regex, handler = *vals
|
113
|
+
if regex =~ type
|
114
|
+
schedule(*args, callable: handler)
|
115
|
+
end
|
116
|
+
end#if vals
|
117
|
+
end#each
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
# Add an event handler. The given callable will be scheduled when an event
|
122
|
+
# matching `type` is `signal`ed.
|
123
|
+
#
|
124
|
+
# The `type` is either:
|
125
|
+
#
|
126
|
+
# * A Regexp: For each signalled event, `type` will be =~ against the
|
127
|
+
# event, and the callable scheduled on a match.
|
128
|
+
#
|
129
|
+
# * A String: The string will be converted to a Regexp that matches
|
130
|
+
# using the same rules as AMQP-style subscriptions:
|
131
|
+
#
|
132
|
+
# * foo.bar.baz.quux
|
133
|
+
# * foo.*.baz.quux
|
134
|
+
# * foo.#.quux
|
135
|
+
#
|
136
|
+
# * An Array: The elements of the array are joined with a '.', and the
|
137
|
+
# resulting string is used as above.
|
138
|
+
def add_handler(type, callable=nil, &b)
|
139
|
+
callable = callable(callable, b)
|
140
|
+
regex = regex_for(type)
|
141
|
+
sync do
|
142
|
+
handlers << [regex, callable]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
alias on add_handler
|
146
|
+
|
147
|
+
|
148
|
+
def add(source, name=nil)
|
149
|
+
source = source.new(self) if Class === source
|
150
|
+
sync do
|
151
|
+
define_singleton_method name do
|
152
|
+
source
|
153
|
+
end if name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def self.run(*a, &b)
|
159
|
+
d = new(*a, &b)
|
160
|
+
d.run
|
161
|
+
d
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# Convert an AMQP-style pattern into a Regexp matching the same strings.
|
168
|
+
#
|
169
|
+
# ['foo', 'bar', 'baz'] => /\Afoo\.bar\.baz\Z/ #foo.bar.baz
|
170
|
+
# ['foo', '*', 'baz'] => /\Afoo\.[^\.]+\.baz\Z/ #foo.*.baz
|
171
|
+
# ['foo', '#', 'baz'] => /\Afoo\..*?\.baz\Z/ #foo.#.baz
|
172
|
+
#
|
173
|
+
# A string will be split('.') first, and a RegExp returned as-is.
|
174
|
+
def regex_for(pattern)
|
175
|
+
return pattern if Regexp === pattern
|
176
|
+
pattern = pattern.split('.') if String === pattern
|
177
|
+
|
178
|
+
source = ''
|
179
|
+
pattern.each_with_index do |part, index|
|
180
|
+
if part == '*'
|
181
|
+
source << '\\.' unless index == 0
|
182
|
+
source << '[^\.]+'
|
183
|
+
elsif part == '#'
|
184
|
+
source << '.*?' # .*? ?
|
185
|
+
else
|
186
|
+
source << '\\.' unless index == 0
|
187
|
+
source << part
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
Regexp.new("\\A#{source}\\Z")
|
192
|
+
end
|
193
|
+
|
194
|
+
def synchronize
|
195
|
+
@lock.synchronize do
|
196
|
+
yield
|
197
|
+
end
|
198
|
+
end
|
199
|
+
alias sync synchronize
|
200
|
+
|
201
|
+
def handlers
|
202
|
+
@handlers
|
203
|
+
end
|
204
|
+
|
205
|
+
def queue
|
206
|
+
@queue
|
207
|
+
end
|
208
|
+
|
209
|
+
def callable(*cs)
|
210
|
+
cs.each do |c|
|
211
|
+
return c if c
|
212
|
+
end
|
213
|
+
raise ArgumentError, "No callable detected"
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
|
221
|
+
class ThreadedDispatcher < Dispatcher
|
222
|
+
|
223
|
+
def run
|
224
|
+
@thread = Thread.new do
|
225
|
+
super
|
226
|
+
end
|
227
|
+
@thread.abort_on_exception = true
|
228
|
+
@thread
|
229
|
+
end
|
230
|
+
|
231
|
+
def join
|
232
|
+
@thread.join
|
233
|
+
end
|
234
|
+
|
235
|
+
end#class ThreadedDispatcher
|
236
|
+
|
237
|
+
|
238
|
+
|
239
|
+
|
240
|
+
end#module Enkidu
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'enkidu/dispatcher'
|
2
|
+
require 'enkidu/tools'
|
3
|
+
|
4
|
+
module Enkidu
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
class LogSource
|
10
|
+
|
11
|
+
attr_reader :defaults
|
12
|
+
|
13
|
+
|
14
|
+
def initialize(d, defaults:{})
|
15
|
+
@defaults = defaults
|
16
|
+
@dispatcher = d
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def log(atts)
|
21
|
+
atts = Enkidu.deep_merge(defaults, atts)
|
22
|
+
path = atts[:type] || atts['type'] || 'log'
|
23
|
+
dispatcher.signal(path, atts)
|
24
|
+
end
|
25
|
+
|
26
|
+
def info(atts)
|
27
|
+
log(Enkidu.deep_merge({tags: ['INFO']}, atts))
|
28
|
+
end
|
29
|
+
|
30
|
+
def error(atts)
|
31
|
+
log(Enkidu.deep_merge({tags: ['ERROR']}, atts))
|
32
|
+
end
|
33
|
+
|
34
|
+
def exception(e)
|
35
|
+
atts = {tags: ['ERROR', 'EXCEPTION'], message: "#{e.class}: #{e.message}"}
|
36
|
+
atts[:exception] = {type: e.class.name, message: e.message, stacktrace: e.backtrace}
|
37
|
+
if e.respond_to?(:cause) && e.cause
|
38
|
+
atts[:exception][:cause] = {type: e.cause.class.name, message: e.cause.message, stacktrace: e.cause.backtrace}
|
39
|
+
end
|
40
|
+
|
41
|
+
log atts
|
42
|
+
end
|
43
|
+
|
44
|
+
def tail(pattern='#', &b)
|
45
|
+
dispatcher.on("log.#{pattern}", b)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def dispatcher
|
52
|
+
@dispatcher
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
end#class LogSource
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
class LogSink
|
62
|
+
|
63
|
+
attr_reader :filter
|
64
|
+
|
65
|
+
def initialize(d, io:, filter: 'log.#', formatter: nil)
|
66
|
+
@dispatcher = d
|
67
|
+
@io = io
|
68
|
+
@filter = filter
|
69
|
+
@formatter = formatter
|
70
|
+
run
|
71
|
+
end
|
72
|
+
|
73
|
+
def run
|
74
|
+
dispatcher.on filter do |msg|
|
75
|
+
log msg
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def log(msg)
|
80
|
+
io.puts format(msg)
|
81
|
+
end
|
82
|
+
|
83
|
+
def format(msg)
|
84
|
+
formatter.call(msg)
|
85
|
+
end
|
86
|
+
|
87
|
+
def formatter
|
88
|
+
@formatter ||= -> msg do
|
89
|
+
(msg[:tags] ? msg[:tags].map{|t| "[#{t}]" }.join+' ' : '') +
|
90
|
+
(msg[:atts] ? msg[:atts].map{|k,v| "[#{k}=#{"#{v}"[0,10]}]" }.join+' ' : '') +
|
91
|
+
"#{msg[:message]}" +
|
92
|
+
(msg[:exception] ? "\n#{msg[:exception][:type]}: #{msg[:exception][:message]}\n#{msg[:exception][:stacktrace].map{|l| " #{l}" }.join("\n")}" : '')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def dispatcher
|
100
|
+
@dispatcher
|
101
|
+
end
|
102
|
+
|
103
|
+
def io
|
104
|
+
@io
|
105
|
+
end
|
106
|
+
|
107
|
+
end#class LogSink
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
end#module Enkidu
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Enkidu
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
# A SignalSource will trap signals and put handlers on a Dispatcher's queue instead
|
9
|
+
# of handling the signal immediately.
|
10
|
+
#
|
11
|
+
# d = ThreadedDispatcher.new
|
12
|
+
#
|
13
|
+
# s = SignalSource.new(d)
|
14
|
+
# s.on 'INT', 'TERM' do |sig|
|
15
|
+
# puts "Received #{sig}, cleaning up and shutting down"
|
16
|
+
# d.stop
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# d.join
|
20
|
+
class SignalSource
|
21
|
+
|
22
|
+
SIGNALS = Signal.list
|
23
|
+
|
24
|
+
|
25
|
+
def initialize(dispatcher)
|
26
|
+
@dispatcher = dispatcher
|
27
|
+
@q = Queue.new
|
28
|
+
run
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
Thread.new do
|
33
|
+
loop do
|
34
|
+
sig = @q.pop
|
35
|
+
dispatcher.signal("signal.#{sig}", sig)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def on(*signals, callable:nil, &b)
|
41
|
+
signals = signals.map do |signal|
|
42
|
+
Integer === signal ? Signal.signame(signal) : signal
|
43
|
+
end
|
44
|
+
signals.each do |signal|
|
45
|
+
dispatcher.on("signal.#{signal}", callable || b)
|
46
|
+
end
|
47
|
+
register *signals
|
48
|
+
end
|
49
|
+
alias trap on
|
50
|
+
|
51
|
+
def register(*signals)
|
52
|
+
signals.each do |signal|
|
53
|
+
Signal.trap(signal){ @q.push signal } #TODO not reentrant
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
SIGNALS.each do |name, number|
|
59
|
+
define_method "on_#{name.downcase}" do |callable: nil, &b|
|
60
|
+
on(name, callable:callable, &b)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def dispatcher
|
67
|
+
@dispatcher
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
end#class SignalSource
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
end#module Enkidu
|
data/lib/enkidu/tools.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Enkidu
|
2
|
+
|
3
|
+
|
4
|
+
# Recursive (deep) merge of hashes and arrays
|
5
|
+
#
|
6
|
+
# For a hash, each key in o2 is merged onto a copy of o1. If the value is
|
7
|
+
# a hash or an array, it is itself deep_merged before it is added.
|
8
|
+
#
|
9
|
+
# For arrays, a copy of o1 is made, then each value in n2 not equal to the same
|
10
|
+
# position in n1 is pushed onto the copy. When both values are arrays or hashes,
|
11
|
+
# they are deep_merged onto the same position as before.
|
12
|
+
#
|
13
|
+
# deep_merge({foo: 'bar', bingo: 'dingo'}, {baz: 'quux', bingo: 'bongo'})
|
14
|
+
# => {foo: 'bar', bingo: 'bongo', baz: 'quux'}
|
15
|
+
#
|
16
|
+
# deep_merge([1, 2 {horse: 'donkey'}], [3, 4, {horse: 'rabbit', cat: 'dog'}])
|
17
|
+
# #=> [1, 2, {horse: 'rabbit', cat: 'dog'}, 3, 4]
|
18
|
+
def self.deep_merge(o1, o2)
|
19
|
+
if Hash === o1 && Hash === o2
|
20
|
+
res = o1.dup
|
21
|
+
o2.each do |k, nv|
|
22
|
+
ov = res[k]
|
23
|
+
res[k] = if Hash === ov && Hash === nv
|
24
|
+
deep_merge(ov, nv)
|
25
|
+
elsif Array === ov && Array === nv
|
26
|
+
deep_merge(ov, nv)
|
27
|
+
else
|
28
|
+
nv
|
29
|
+
end
|
30
|
+
end
|
31
|
+
elsif Array === o1 && Array === o2
|
32
|
+
res = o1.dup
|
33
|
+
nvals = []
|
34
|
+
o2.each_with_index do |nv, i|
|
35
|
+
ov = res[i]
|
36
|
+
if (Hash === ov && Hash === nv) || (Array === ov && Array === nv)
|
37
|
+
res[i] = deep_merge(ov, nv)
|
38
|
+
elsif ov != nv
|
39
|
+
nvals << nv
|
40
|
+
end
|
41
|
+
end
|
42
|
+
res.concat(nvals)
|
43
|
+
else
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
|
47
|
+
res
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end#module Enkidu
|
data/lib/enkidu.rb
ADDED
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: enkidu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tore Darell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-06 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Enkidu is a process sidekick
|
14
|
+
email: toredarell@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/enkidu.rb
|
20
|
+
- lib/enkidu/dispatcher.rb
|
21
|
+
- lib/enkidu/logging.rb
|
22
|
+
- lib/enkidu/signals.rb
|
23
|
+
- lib/enkidu/tools.rb
|
24
|
+
homepage:
|
25
|
+
licenses: []
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.4.5
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Enkidu process sidekick
|
47
|
+
test_files: []
|