enkidu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []