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 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
@@ -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
@@ -0,0 +1,7 @@
1
+ # Enkidu - a process sidekick
2
+ module Enkidu
3
+ end#module Enkidu
4
+
5
+ require 'enkidu/dispatcher'
6
+ require 'enkidu/signals'
7
+ require 'enkidu/logging'
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: []