enkidu 0.0.1
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.
- 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: []
|