enkidu 0.0.1 → 0.0.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.
- checksums.yaml +4 -4
- data/lib/enkidu/dispatcher.rb +259 -30
- data/lib/enkidu/logging.rb +182 -23
- data/lib/enkidu/signals.rb +220 -18
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1703c3004e749c132e16c7dca1eceec5fb30f875
|
4
|
+
data.tar.gz: ea4c9081b396051029e64e5429a1a4dbb57b9306
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2829b255bdca0dfe35fc315992493929c29c7d6a8c3c6307d4dc011de8dfef8ba99dd96d1fbea4902ec2f326852582abafd761c0fa96fbdbbf57723b3db1a3f4
|
7
|
+
data.tar.gz: 738636e61003210c68cb3cd43eb317ce8938561ab1bdef0f5b9e768144766f453fe0e7eea9045d651300afb45b1b9f5a7301fdf6cda2f8e3a08c5217338305c5
|
data/lib/enkidu/dispatcher.rb
CHANGED
@@ -26,46 +26,113 @@ module Enkidu
|
|
26
26
|
# d.run #Blocks
|
27
27
|
class Dispatcher
|
28
28
|
|
29
|
-
|
30
|
-
k=self;STOP.define_singleton_method(:inspect){ "<#{k.name}::STOP>" }
|
29
|
+
class StateError < StandardError; end
|
31
30
|
|
32
|
-
|
31
|
+
RUNNING = :running
|
32
|
+
STOPPED = :stopped
|
33
|
+
|
34
|
+
class STOP
|
35
|
+
attr_reader :callback, :cleanup
|
36
|
+
alias cleanup? cleanup
|
37
|
+
def initialize(callback:nil, cleanup:true)
|
38
|
+
@callback = callback
|
39
|
+
@cleanup = cleanup
|
40
|
+
end
|
41
|
+
def callable?
|
42
|
+
!!callback
|
43
|
+
end
|
44
|
+
def call(*a)
|
45
|
+
@callback && @callback.call(*a)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :state
|
50
|
+
|
51
|
+
|
52
|
+
def initialize(&b)
|
33
53
|
@lock = Mutex.new
|
34
54
|
@queue = []
|
35
55
|
@handlers = []
|
56
|
+
@plugins = []
|
57
|
+
@handler_serial = -1
|
36
58
|
@r, @w = IO.pipe
|
37
|
-
|
59
|
+
@state = STOPPED
|
60
|
+
if b
|
61
|
+
if b.arity.nonzero?
|
62
|
+
yield self
|
63
|
+
else
|
64
|
+
instance_eval(&b)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def running?
|
71
|
+
state == RUNNING
|
72
|
+
end
|
73
|
+
|
74
|
+
def stopped?
|
75
|
+
state == STOPPED
|
38
76
|
end
|
39
77
|
|
40
78
|
|
41
79
|
# Run the loop. This will block the current thread until the loop is stopped.
|
42
80
|
def run
|
81
|
+
raise StateError, "Dispatcher is already running" if running?
|
82
|
+
@state = RUNNING
|
83
|
+
value = nil
|
84
|
+
plugins.each do |plugin|
|
85
|
+
plugin.run if plugin.respond_to?(:run)
|
86
|
+
end
|
43
87
|
loop do
|
44
88
|
IO.select [@r]
|
45
|
-
if vals = sync{ queue.shift }
|
46
|
-
sync{ @r.read(1) }
|
89
|
+
if vals = sync{ @r.read(1); queue.shift }
|
47
90
|
callable, args = *vals
|
48
|
-
if callable
|
91
|
+
if callable.is_a?(STOP)
|
92
|
+
if callable.cleanup?
|
93
|
+
plugins.each do |plugin|
|
94
|
+
plugin.stop if plugin.respond_to?(:stop)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
if callable.callable?
|
98
|
+
value = callable.call(*args)
|
99
|
+
end
|
100
|
+
@state = STOPPED
|
49
101
|
break
|
50
102
|
else
|
51
|
-
callable.call(*args)
|
103
|
+
value = callable.call(*args)
|
52
104
|
end
|
53
105
|
end
|
54
106
|
end
|
107
|
+
|
108
|
+
value
|
109
|
+
end
|
110
|
+
|
111
|
+
# Runs the loop in the same manner as `run`, but will only execute any already
|
112
|
+
# scheduled (as in scheduled before the call to `run_once`) callables and then stop.
|
113
|
+
#
|
114
|
+
# d = Dispatcher.new
|
115
|
+
# d.schedule{ puts "hello" }
|
116
|
+
# d.run_once #Runs the 1 scheduled callable above and returns
|
117
|
+
def run_once(*args, **kwargs, &b)
|
118
|
+
schedule_stop(*args, callable: b, **kwargs)
|
119
|
+
run
|
55
120
|
end
|
56
121
|
|
57
122
|
|
58
123
|
# Schedule a callable to be run. This will push the callable to the back of
|
59
124
|
# the queue, so anything scheduled before it will run first.
|
125
|
+
# TODO: Allow kwargs to be passed to callable
|
60
126
|
#
|
61
127
|
# schedule{ puts "I have been called" }
|
62
128
|
# callable = ->(arg){ p arg }
|
63
129
|
# schedule('an argument', callable: callable)
|
64
|
-
def schedule(*args, callable: nil, &b)
|
130
|
+
def schedule(*args, callable: nil, **kwargs, &b)
|
131
|
+
args << kwargs
|
65
132
|
callable = callable(callable, b)
|
66
133
|
sync do
|
67
134
|
queue.push [callable, args]
|
68
|
-
@w.write '.'
|
135
|
+
@w.write '.' #TODO Figure out what to do when this blocks (pipe is full); lock will not be released
|
69
136
|
end
|
70
137
|
end
|
71
138
|
alias push schedule
|
@@ -74,7 +141,8 @@ module Enkidu
|
|
74
141
|
#
|
75
142
|
# schedule{ puts "Hey, that's not nice :(" }
|
76
143
|
# unshift{ puts "Cutting in line" }
|
77
|
-
def unshift(*args, callable: nil, &b)
|
144
|
+
def unshift(*args, callable: nil, **kwargs, &b)
|
145
|
+
args << kwargs
|
78
146
|
callable = callable(callable, b)
|
79
147
|
sync do
|
80
148
|
queue.unshift [callable, args]
|
@@ -82,18 +150,83 @@ module Enkidu
|
|
82
150
|
end
|
83
151
|
end
|
84
152
|
|
153
|
+
# Schedule multiple callables at once
|
154
|
+
#
|
155
|
+
# Takes an array of [callable, args, position] arrays. args defaults to [], position to :back
|
156
|
+
# All callables are guaranteed to be added to the scheduler atomically; that is, neither of
|
157
|
+
# the passed callables nor any already scheduled callables will run until they have all been
|
158
|
+
# scheduled.
|
159
|
+
#
|
160
|
+
# The array bundles are processed in the order they appear in the array, so each callable
|
161
|
+
# will be added either at the back (default) or the front in that order.
|
162
|
+
#
|
163
|
+
# # Note: the :back here is unnecessary:
|
164
|
+
# schedule_multiple([[->(msg){ puts msg }, ['hello'], :back], [->{ puts "I will run first" }, [], :front], [->{ puts "I will run last" }, []]])
|
165
|
+
def schedule_multiple(bundles)
|
166
|
+
sync do
|
167
|
+
bundles.each do |callable, args=[], position=:back|
|
168
|
+
if position == :front
|
169
|
+
queue.unshift [callable, args]
|
170
|
+
else
|
171
|
+
queue.push [callable, args]
|
172
|
+
end
|
173
|
+
@w.write '.'
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
def schedule_stop(*args, callable: nil, **kwargs, &b)
|
180
|
+
schedule(*args, callable: STOP.new(callback: callable || b), **kwargs)
|
181
|
+
end
|
182
|
+
|
183
|
+
def unshift_stop(*args, callable: nil, cleanup: true, **kwargs, &b)
|
184
|
+
unshift(*args, callable: STOP.new(callback: callable || b, cleanup: cleanup), **kwargs)
|
185
|
+
end
|
186
|
+
|
85
187
|
|
86
188
|
# Stop the dispatcher. This schedules a special STOP signal that will stop the
|
87
189
|
# dispatcher when encountered. This means that all other items that were scheduled
|
88
190
|
# before it will run first.
|
89
|
-
|
90
|
-
|
191
|
+
#
|
192
|
+
# This action is idempotent; it returns true if the dispatcher is currently running
|
193
|
+
# and will be stopped, false if it's already stopped.
|
194
|
+
#
|
195
|
+
# A callable can be provided either in the form of the callable: kwarg or a block,
|
196
|
+
# which will be called after the dispatcher has stopped (also if dispatcher is already stopped).
|
197
|
+
#
|
198
|
+
# All attached plugins will have their `stop` method called before shutdown.
|
199
|
+
def stop(*args, callable: nil, **kwargs, &b)
|
200
|
+
callable ||= b
|
201
|
+
if stopped?
|
202
|
+
callable && callable.call(*args, **kwargs)
|
203
|
+
false
|
204
|
+
else
|
205
|
+
schedule_stop(*args, callable: callable, **kwargs)
|
206
|
+
true
|
207
|
+
end
|
91
208
|
end
|
92
209
|
|
93
|
-
# Stop the dispatcher immediately by scheduling
|
94
|
-
# means that any other already scheduled items will be ignored.
|
95
|
-
|
96
|
-
|
210
|
+
# Stop the dispatcher immediately by scheduling the stop action at the front of the
|
211
|
+
# queue. This means that any other already scheduled items will be ignored.
|
212
|
+
#
|
213
|
+
# This action is idempotent; it returns true if the dispatcher is currently running
|
214
|
+
# and will be stopped, false if it's already stopped.
|
215
|
+
#
|
216
|
+
# A callable can be provided either in the form of the `callable` option or a block,
|
217
|
+
# which will be called after the dispatcher has stopped (also if dispatcher is already stopped).
|
218
|
+
#
|
219
|
+
# If the `cleanup` option is true, all attached plugins will have their `stop` method
|
220
|
+
# called before shutdown.
|
221
|
+
def stop!(*args, callable: nil, cleanup: false, **kwargs, &b)
|
222
|
+
callable ||= b
|
223
|
+
if stopped?
|
224
|
+
callable && callable.call(*args, **kwargs)
|
225
|
+
false
|
226
|
+
else
|
227
|
+
unshift_stop(*args, callable: callable, cleanup: cleanup, **kwargs)
|
228
|
+
true
|
229
|
+
end
|
97
230
|
end
|
98
231
|
|
99
232
|
|
@@ -107,14 +240,19 @@ module Enkidu
|
|
107
240
|
# signal ['foo', 'bar', 'baz'], arg1, arg2 #Same as above
|
108
241
|
def signal(type, *args)
|
109
242
|
type = type.join('.') if Array === type
|
243
|
+
scheduled_handlers = []
|
244
|
+
#puts "SIGNAL: #{type}, #{handlers.size} handlers"
|
110
245
|
0.upto(handlers.size - 1).each do |index|
|
111
246
|
if vals = sync{ handlers[index] }
|
112
|
-
regex, handler = *vals
|
247
|
+
id, regex, handler = *vals
|
248
|
+
#puts "HANDLER: #{regex.inspect} =~ #{type} => #{!!(regex =~ type)} -> #{handler.inspect}"
|
113
249
|
if regex =~ type
|
114
|
-
schedule(*args, callable: handler)
|
250
|
+
schedule(type, *args, callable: handler)
|
251
|
+
scheduled_handlers << id
|
115
252
|
end
|
116
253
|
end#if vals
|
117
254
|
end#each
|
255
|
+
scheduled_handlers
|
118
256
|
end
|
119
257
|
|
120
258
|
|
@@ -135,24 +273,72 @@ module Enkidu
|
|
135
273
|
#
|
136
274
|
# * An Array: The elements of the array are joined with a '.', and the
|
137
275
|
# resulting string is used as above.
|
276
|
+
#
|
277
|
+
# Returns a unique ID which can be used to deregister the handler from the
|
278
|
+
# dispatcher with `remove_handler`.
|
138
279
|
def add_handler(type, callable=nil, &b)
|
139
280
|
callable = callable(callable, b)
|
140
281
|
regex = regex_for(type)
|
141
282
|
sync do
|
142
|
-
|
283
|
+
id = @handler_serial+=1
|
284
|
+
handlers << [id, regex, callable]
|
285
|
+
id
|
143
286
|
end
|
144
287
|
end
|
288
|
+
alias subscribe add_handler
|
145
289
|
alias on add_handler
|
146
290
|
|
147
291
|
|
148
|
-
def
|
149
|
-
|
292
|
+
def remove_handler(id)
|
293
|
+
index = handlers.index{|i,*| i == id }
|
294
|
+
sync{ handlers.delete_at(index) }
|
295
|
+
end
|
296
|
+
alias unsubscribe remove_handler
|
297
|
+
|
298
|
+
|
299
|
+
# Add a plugin to this dispatcher. Plugins are objects that attach themselves
|
300
|
+
# to the dispatcher during its lifecycle and listen for or send events. Examples
|
301
|
+
# of plugins are SignalSource, LogSource and LogSink, that use the dispatcher to
|
302
|
+
# listen for and dispatch interrupt signals and log messages.
|
303
|
+
#
|
304
|
+
# `plugin` can be any object. It can also be a class, in which case its `new` method
|
305
|
+
# will be called with the dispatcher as the argument. It is not required to add objects
|
306
|
+
# that interact with the scheduler using this method, but doing so has some advantages:
|
307
|
+
#
|
308
|
+
# * If a second argument is provided, an accessor to the object will be available
|
309
|
+
# on the dispatcher with the name provided:
|
310
|
+
#
|
311
|
+
# dispatcher.use(SignalSource, :signals)
|
312
|
+
# dispatcher.signals.on_int{ puts "Got INT"; dispatcher.stop }
|
313
|
+
#
|
314
|
+
# * Any object registered in this way will have its `run` and `stop` methods called
|
315
|
+
# when the dispatcher starts and stops to do initialization/cleanup. The `run`
|
316
|
+
# method will also be called when a plugin is added if the dispatcher is already
|
317
|
+
# running.
|
318
|
+
#
|
319
|
+
# class PingPong
|
320
|
+
# def initialize(d)
|
321
|
+
# @dispatcher = d
|
322
|
+
# end
|
323
|
+
# def run
|
324
|
+
# @handler_id = @dispatcher.on('ping'){ @dispatcher.signal('pong') }
|
325
|
+
# end
|
326
|
+
# def stop
|
327
|
+
# @dispatcher.remove_handler(@handler_id)
|
328
|
+
# end
|
329
|
+
# end
|
330
|
+
#TODO rename -> `use`
|
331
|
+
def add(plugin, name=nil)
|
332
|
+
plugin = plugin.new(self) if plugin.is_a?(Class)
|
150
333
|
sync do
|
334
|
+
plugins << plugin
|
151
335
|
define_singleton_method name do
|
152
|
-
|
336
|
+
plugin
|
153
337
|
end if name
|
154
338
|
end
|
339
|
+
plugin.run if running? && plugin.respond_to?(:run)
|
155
340
|
end
|
341
|
+
alias use add
|
156
342
|
|
157
343
|
|
158
344
|
def self.run(*a, &b)
|
@@ -170,7 +356,7 @@ module Enkidu
|
|
170
356
|
# ['foo', '*', 'baz'] => /\Afoo\.[^\.]+\.baz\Z/ #foo.*.baz
|
171
357
|
# ['foo', '#', 'baz'] => /\Afoo\..*?\.baz\Z/ #foo.#.baz
|
172
358
|
#
|
173
|
-
# A string will be split('.') first, and a
|
359
|
+
# A string will be split('.') first, and a Regexp returned as-is.
|
174
360
|
def regex_for(pattern)
|
175
361
|
return pattern if Regexp === pattern
|
176
362
|
pattern = pattern.split('.') if String === pattern
|
@@ -178,10 +364,19 @@ module Enkidu
|
|
178
364
|
source = ''
|
179
365
|
pattern.each_with_index do |part, index|
|
180
366
|
if part == '*'
|
181
|
-
|
182
|
-
|
367
|
+
if index == 0
|
368
|
+
source << '[^\.]+'
|
369
|
+
else
|
370
|
+
source << '\\.[^\.]+'
|
371
|
+
end
|
183
372
|
elsif part == '#'
|
184
|
-
|
373
|
+
if index == 0
|
374
|
+
source << '.*?'
|
375
|
+
elsif index == pattern.size-1
|
376
|
+
source << "\\Z|\\A#{source}\\..*"
|
377
|
+
else
|
378
|
+
source << '\\..*?'
|
379
|
+
end
|
185
380
|
else
|
186
381
|
source << '\\.' unless index == 0
|
187
382
|
source << part
|
@@ -206,6 +401,10 @@ module Enkidu
|
|
206
401
|
@queue
|
207
402
|
end
|
208
403
|
|
404
|
+
def plugins
|
405
|
+
@plugins
|
406
|
+
end
|
407
|
+
|
209
408
|
def callable(*cs)
|
210
409
|
cs.each do |c|
|
211
410
|
return c if c
|
@@ -213,6 +412,19 @@ module Enkidu
|
|
213
412
|
raise ArgumentError, "No callable detected"
|
214
413
|
end
|
215
414
|
|
415
|
+
# Called from inside the loop to execute a callable. This method is extracted only to
|
416
|
+
# avoid repetition; it's not to be used for other purposes.
|
417
|
+
def run_callable(callable, args)
|
418
|
+
if callable.is_a?(STOP)
|
419
|
+
@state = STOPPED
|
420
|
+
callable.call(*args)
|
421
|
+
true #Loop should be stopped
|
422
|
+
else
|
423
|
+
callable.call(*args)
|
424
|
+
false #Loop should continue
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
216
428
|
end
|
217
429
|
|
218
430
|
|
@@ -220,18 +432,35 @@ module Enkidu
|
|
220
432
|
|
221
433
|
class ThreadedDispatcher < Dispatcher
|
222
434
|
|
435
|
+
attr_reader :thread
|
436
|
+
|
437
|
+
|
223
438
|
def run
|
439
|
+
running = false
|
440
|
+
unshift{ running = true }
|
224
441
|
@thread = Thread.new do
|
442
|
+
Thread.current.abort_on_exception = true
|
225
443
|
super
|
226
444
|
end
|
227
|
-
|
445
|
+
sleep 0.01 until running #Block current thread until scheduler thread is up and running
|
228
446
|
@thread
|
229
447
|
end
|
230
448
|
|
231
|
-
|
232
|
-
|
449
|
+
|
450
|
+
def join(*a)
|
451
|
+
@thread.join(*a)
|
233
452
|
end
|
234
453
|
|
454
|
+
def value
|
455
|
+
@thread.value
|
456
|
+
end
|
457
|
+
|
458
|
+
def wait(*a)
|
459
|
+
stop
|
460
|
+
join(*a)
|
461
|
+
end
|
462
|
+
|
463
|
+
|
235
464
|
end#class ThreadedDispatcher
|
236
465
|
|
237
466
|
|
data/lib/enkidu/logging.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'enkidu/dispatcher'
|
2
2
|
require 'enkidu/tools'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'time'
|
3
5
|
|
4
6
|
module Enkidu
|
5
7
|
|
@@ -17,28 +19,34 @@ module Enkidu
|
|
17
19
|
end
|
18
20
|
|
19
21
|
|
20
|
-
def log(
|
21
|
-
|
22
|
-
path = atts[:type] || atts['type'] || 'log'
|
23
|
-
dispatcher.signal(path, atts)
|
22
|
+
def log(*args)
|
23
|
+
signal attify(args)
|
24
24
|
end
|
25
25
|
|
26
|
-
def info(
|
27
|
-
|
26
|
+
def info(*args)
|
27
|
+
atts = attify(args)
|
28
|
+
atts[:tags].unshift('INFO')
|
29
|
+
signal atts
|
28
30
|
end
|
29
31
|
|
30
|
-
def error(
|
31
|
-
|
32
|
+
def error(*args)
|
33
|
+
atts = attify(args)
|
34
|
+
atts[:tags].unshift('ERROR')
|
35
|
+
signal atts
|
32
36
|
end
|
33
37
|
|
34
|
-
def exception(e)
|
35
|
-
atts =
|
36
|
-
atts[:
|
38
|
+
def exception(e, *args)
|
39
|
+
atts = attify(args)
|
40
|
+
atts[:tags].unshift('EXCEPTION')
|
41
|
+
atts[:tags].unshift('ERROR')
|
42
|
+
|
43
|
+
atts[:message] ||= "#{e.class}: #{e.message}"
|
44
|
+
atts[:data][:exception] = {type: e.class.name, message: e.message, stacktrace: e.backtrace}
|
37
45
|
if e.respond_to?(:cause) && e.cause
|
38
|
-
atts[:exception][:cause] = {type: e.cause.class.name, message: e.cause.message, stacktrace: e.cause.backtrace}
|
46
|
+
atts[:data][:exception][:cause] = {type: e.cause.class.name, message: e.cause.message, stacktrace: e.cause.backtrace}
|
39
47
|
end
|
40
48
|
|
41
|
-
|
49
|
+
signal atts
|
42
50
|
end
|
43
51
|
|
44
52
|
def tail(pattern='#', &b)
|
@@ -48,6 +56,29 @@ module Enkidu
|
|
48
56
|
|
49
57
|
private
|
50
58
|
|
59
|
+
def signal(atts)
|
60
|
+
dispatcher.signal(atts[:type], atts)
|
61
|
+
end
|
62
|
+
|
63
|
+
def attify(args)
|
64
|
+
aa = if args[0].is_a?(String)
|
65
|
+
args[1] ? {message: args[0]}.merge(args[1]) : {message: args[0]}
|
66
|
+
else
|
67
|
+
args[0] ? args[0].dup : {}
|
68
|
+
end
|
69
|
+
if aa[:atts]
|
70
|
+
if aa[:attributes]
|
71
|
+
aa[:attributes].update(aa.delete(:atts))
|
72
|
+
else
|
73
|
+
aa[:attributes] = aa.delete(:atts)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
Enkidu.deep_merge(
|
77
|
+
Enkidu.deep_merge(defaults, {id: SecureRandom.uuid, time: Time.now.utc, type: 'log', tags: [], attributes: {}, data: {}}),
|
78
|
+
aa
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
51
82
|
def dispatcher
|
52
83
|
@dispatcher
|
53
84
|
end
|
@@ -60,24 +91,152 @@ module Enkidu
|
|
60
91
|
|
61
92
|
class LogSink
|
62
93
|
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
class DefaultFormatter
|
99
|
+
|
100
|
+
|
101
|
+
def initialize
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def tags(msg)
|
106
|
+
return '[] ' unless msg[:tags]
|
107
|
+
"[#{msg[:tags].map{|t| escape_tag(t) }.join(', ')}] "
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def attributes(msg)
|
112
|
+
return '{} ' unless msg[:attributes]
|
113
|
+
"{#{msg[:attributes].map{|k,v| "#{escape_attr k}=#{escape_attr v}" }.join(', ')}} "
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def message(msg)
|
118
|
+
"#{msg[:message]}" +
|
119
|
+
if e = msg[:data][:exception]
|
120
|
+
"\n #{e[:type]}: #{e[:message]}\n#{e[:stacktrace].map{|l| " #{l}" }.join("\n")}"
|
121
|
+
else
|
122
|
+
''
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def timestamp(msg)
|
128
|
+
msg[:time].getutc.iso8601(3)
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
def call(msg)
|
133
|
+
id = msg[:id].split('-')[0]
|
134
|
+
"<#{id} #{timestamp msg}> " + tags(msg) + attributes(msg) + message(msg) + " </#{id}>"
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def escape_tag(str)
|
139
|
+
"#{str}".gsub(/([\[\],])/, '\\\\\\1')
|
140
|
+
end
|
141
|
+
|
142
|
+
def escape_attr(str)
|
143
|
+
"#{str}".gsub(/([\{\}=,])/, '\\\\\\1')
|
144
|
+
end
|
145
|
+
|
146
|
+
end#class DefaultFormatter
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
class HumanFormatter
|
152
|
+
|
153
|
+
def tags(m)
|
154
|
+
return '' unless m[:tags] && m[:tags].any?
|
155
|
+
tags_separator(m) + m[:tags].map{|t| tag(t) }.join(', ')
|
156
|
+
end
|
157
|
+
|
158
|
+
def tag(t)
|
159
|
+
if ['ERROR', 'EXCEPTION'].include?(t)
|
160
|
+
c [1, 91], t
|
161
|
+
else
|
162
|
+
c 1, t
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def tags_separator(m)
|
167
|
+
" #{c 96, '❯❯'} "
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
def atts(m)
|
172
|
+
return '' unless m[:attributes] && m[:attributes].any?
|
173
|
+
atts_separator(m) + c(37, m[:attributes].map{|k,v| "#{k}: #{v}" }.join(', '))
|
174
|
+
end
|
175
|
+
|
176
|
+
def atts_separator(m)
|
177
|
+
" #{c 96, '❯❯'} "
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
def timestamp(m)
|
182
|
+
m[:time].getlocal.strftime('%H:%M:%S')
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
def message(m)
|
187
|
+
" ❯❯ #{m[:message]}" +
|
188
|
+
if e = m[:data][:exception]
|
189
|
+
"\n #{c 1, e[:type]}: #{e[:message]}\n#{e[:stacktrace].map{|l| " #{l}" }.join("\n")}"
|
190
|
+
else
|
191
|
+
''
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
def call(m)
|
197
|
+
timestamp(m) + tags(m) + atts(m) + message(m)
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
def color(n, s=nil)
|
202
|
+
c = Array(n).map{|nn| "\e[0;#{nn}m" }.join
|
203
|
+
c << "#{s}\e[0m" if s
|
204
|
+
c
|
205
|
+
end
|
206
|
+
alias c color
|
207
|
+
|
208
|
+
|
209
|
+
def stop
|
210
|
+
"\e[0m"
|
211
|
+
end
|
212
|
+
alias s stop
|
213
|
+
|
214
|
+
|
215
|
+
end#class HumanFormatter
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
|
63
221
|
attr_reader :filter
|
64
222
|
|
65
|
-
def initialize(d, io:, filter: 'log.#', formatter: nil)
|
223
|
+
def initialize(d, io:, filter: 'log.#', formatter: nil, flush: false)
|
66
224
|
@dispatcher = d
|
67
225
|
@io = io
|
68
226
|
@filter = filter
|
69
|
-
@formatter = formatter
|
70
|
-
|
227
|
+
@formatter = formatter.is_a?(Class) ? formatter.new : formatter
|
228
|
+
@flush = io.respond_to?(:flush) ? flush : false
|
71
229
|
end
|
72
230
|
|
73
231
|
def run
|
74
|
-
dispatcher.on filter do |msg|
|
232
|
+
dispatcher.on filter do |_,msg|
|
75
233
|
log msg
|
76
234
|
end
|
77
235
|
end
|
78
236
|
|
79
237
|
def log(msg)
|
80
238
|
io.puts format(msg)
|
239
|
+
io.flush if flush?
|
81
240
|
end
|
82
241
|
|
83
242
|
def format(msg)
|
@@ -85,12 +244,12 @@ module Enkidu
|
|
85
244
|
end
|
86
245
|
|
87
246
|
def formatter
|
88
|
-
@formatter ||=
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
247
|
+
@formatter ||= DefaultFormatter.new
|
248
|
+
end
|
249
|
+
|
250
|
+
|
251
|
+
def flush?
|
252
|
+
@flush
|
94
253
|
end
|
95
254
|
|
96
255
|
|
data/lib/enkidu/signals.rb
CHANGED
@@ -1,8 +1,138 @@
|
|
1
|
-
require 'thread'
|
2
|
-
|
3
1
|
module Enkidu
|
4
2
|
|
5
3
|
|
4
|
+
# SignalTrapper is a generic callback-based signal handling utility. It queues
|
5
|
+
# all signals onto a background thread and all callbacks are run, synchronously
|
6
|
+
# and serially, on this thread. This means signal handlers will be reentrant;
|
7
|
+
# any callbacks registered are guaranteed to finish before any other callback
|
8
|
+
# triggered by a signal starts running.
|
9
|
+
#
|
10
|
+
# Only one SignalTrapper should exist per process, as having multiple risks
|
11
|
+
# one overriding the other's signal handlers.
|
12
|
+
#
|
13
|
+
# st = SignalTrapper.new
|
14
|
+
# handler = -> s do
|
15
|
+
# puts "Received #{s}, shutting down..."
|
16
|
+
# app.stop #Do cleanup and shutdown
|
17
|
+
# st.stop #Tell ST to stop
|
18
|
+
# end
|
19
|
+
# st.register('INT', handler)
|
20
|
+
# st.register('TERM', handler)
|
21
|
+
# usr1 = st.register('USR1', ->(s){ puts app.stats })
|
22
|
+
# st.register('USR2', ->(s){ st.deregister(usr1) })
|
23
|
+
#
|
24
|
+
# st.join
|
25
|
+
class SignalTrapper
|
26
|
+
|
27
|
+
|
28
|
+
class Subscription
|
29
|
+
attr_reader :signal, :id
|
30
|
+
def initialize(s, i)
|
31
|
+
@signal, @id = s, i
|
32
|
+
end
|
33
|
+
def ==(other)
|
34
|
+
if other.is_a?(self.class)
|
35
|
+
id == other.id
|
36
|
+
else
|
37
|
+
id == other
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
SIGNALS = Signal.list
|
43
|
+
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@q = []
|
47
|
+
@r, @w = IO.pipe
|
48
|
+
@trapped_signals = []
|
49
|
+
@callbacks = Hash.new{|h,k| h[k] = [] }
|
50
|
+
@callback_serial = -1
|
51
|
+
@thread = Thread.new do
|
52
|
+
loop do
|
53
|
+
IO.select [@r]
|
54
|
+
sig = @q.shift
|
55
|
+
break if sig == :stop
|
56
|
+
@r.read 1
|
57
|
+
@callbacks[sig].each do |id, callable|
|
58
|
+
callable.call(sig)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Register a callable for a specific signal. The callback will be scheduled
|
66
|
+
# on the handler thread when the signal is received.
|
67
|
+
#
|
68
|
+
# Returns an ID which can be used to `deregister` the callback
|
69
|
+
def register(sig, callable)
|
70
|
+
sig = self.class.normalize_signal(sig)
|
71
|
+
trap sig
|
72
|
+
id = Subscription.new(sig, @callback_serial += 1)
|
73
|
+
@callbacks[sig] << [id, callable]
|
74
|
+
id
|
75
|
+
end
|
76
|
+
|
77
|
+
# Deregister a callback using the ID returned by `register`
|
78
|
+
def deregister(id)
|
79
|
+
@callbacks[id.signal].delete_if{|i, _c| i == id }
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Tell signal handling thread to stop. It will execute any already scheduled
|
84
|
+
# handlers and then exit. No more handlers will be executed after that.
|
85
|
+
def stop
|
86
|
+
@q << :stop
|
87
|
+
@w.write '.'
|
88
|
+
end
|
89
|
+
|
90
|
+
# Join the callback execution thread. This will block until the SignalTrapper
|
91
|
+
# is told to `stop`.
|
92
|
+
def join(*a, &b)
|
93
|
+
@thread.join(*a, &b)
|
94
|
+
end
|
95
|
+
|
96
|
+
# `stop` and `join`
|
97
|
+
def wait(*a, &b)
|
98
|
+
stop
|
99
|
+
join(*a, &b)
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Normalize signal name
|
104
|
+
#
|
105
|
+
# 2 -> INT
|
106
|
+
# SIGINT -> INT
|
107
|
+
# INT -> INT
|
108
|
+
# 123 -> ArgumentError
|
109
|
+
def self.normalize_signal(sig)
|
110
|
+
if sig.is_a?(Integer) || sig =~ /^\d+$/
|
111
|
+
SIGNALS.key(sig.to_i) || raise(ArgumentError, "Unrecognized signal #{sig}")
|
112
|
+
elsif sig =~ /^SIG(.+)$/
|
113
|
+
$1
|
114
|
+
else
|
115
|
+
sig
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def trap(sig)
|
123
|
+
unless @trapped_signals.include?(sig)
|
124
|
+
Signal.trap sig do
|
125
|
+
@q << sig
|
126
|
+
@w.write '.'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
end#class SignalTrapper
|
133
|
+
|
134
|
+
|
135
|
+
|
6
136
|
|
7
137
|
|
8
138
|
# A SignalSource will trap signals and put handlers on a Dispatcher's queue instead
|
@@ -19,38 +149,106 @@ module Enkidu
|
|
19
149
|
# d.join
|
20
150
|
class SignalSource
|
21
151
|
|
22
|
-
SIGNALS = Signal.list
|
23
152
|
|
24
153
|
|
25
|
-
|
154
|
+
class ShutdownProcedure
|
155
|
+
|
156
|
+
attr_reader :signals, :count
|
157
|
+
|
158
|
+
def initialize(signals, callable)
|
159
|
+
@signals = signals
|
160
|
+
@handlers = []
|
161
|
+
@count = 0
|
162
|
+
callable.call(self)
|
163
|
+
end
|
164
|
+
|
165
|
+
def handle(&b)
|
166
|
+
@handlers << b
|
167
|
+
end
|
168
|
+
|
169
|
+
def force(status=false, &b)
|
170
|
+
@force = {status: status, callback: b}
|
171
|
+
end
|
172
|
+
|
173
|
+
def call(signal)
|
174
|
+
@count += 1
|
175
|
+
|
176
|
+
if handler = @handlers.shift
|
177
|
+
handler.call(signal, count)
|
178
|
+
end#if handler
|
179
|
+
|
180
|
+
if @handlers.empty? && @force
|
181
|
+
signals.each do |signal|
|
182
|
+
Signal.trap signal do
|
183
|
+
begin
|
184
|
+
@force[:callback].call(signal) if @force[:callback]
|
185
|
+
rescue => e
|
186
|
+
warn "Exception while running force handler: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
187
|
+
ensure
|
188
|
+
exit @force[:status]
|
189
|
+
end
|
190
|
+
end#Signal.trap
|
191
|
+
end#signals.each
|
192
|
+
end#if @handlers.empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
|
199
|
+
SIGNALS = SignalTrapper::SIGNALS
|
200
|
+
|
201
|
+
attr_reader :trapper
|
202
|
+
|
203
|
+
|
204
|
+
def self.trapper
|
205
|
+
@trappers ||= {}
|
206
|
+
@trappers[$$] ||= SignalTrapper.new
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
def initialize(dispatcher, trapper: self.class.trapper)
|
26
211
|
@dispatcher = dispatcher
|
27
|
-
@
|
28
|
-
|
212
|
+
@trapper = trapper
|
213
|
+
@subscriptions = []
|
29
214
|
end
|
30
215
|
|
31
216
|
def run
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
dispatcher.signal("signal.#{sig}", sig)
|
36
|
-
end
|
37
|
-
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def stop
|
38
220
|
end
|
39
221
|
|
40
222
|
def on(*signals, callable:nil, &b)
|
41
|
-
|
42
|
-
|
43
|
-
end
|
44
|
-
signals.each do |signal|
|
45
|
-
dispatcher.on("signal.#{signal}", callable || b)
|
223
|
+
subscriptions = signals.map do |signal|
|
224
|
+
dispatcher.on("signal.#{normalize signal}", callable || b)
|
46
225
|
end
|
47
226
|
register *signals
|
227
|
+
subscriptions
|
48
228
|
end
|
49
229
|
alias trap on
|
50
230
|
|
231
|
+
def off(*ids)
|
232
|
+
ids.each do |id|
|
233
|
+
dispatcher.unsubscribe(id)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def shutdown(*signals, callable:nil, &b)
|
238
|
+
sp = ShutdownProcedure.new(signals, callable || b)
|
239
|
+
on(*signals){|_,s| sp.call(s) }
|
240
|
+
sp
|
241
|
+
end
|
242
|
+
|
51
243
|
def register(*signals)
|
52
244
|
signals.each do |signal|
|
53
|
-
|
245
|
+
signal = normalize(signal)
|
246
|
+
unless @subscriptions.any?{|s| s.signal == signal }
|
247
|
+
@subscriptions << trapper.register(
|
248
|
+
signal,
|
249
|
+
->(sig){ dispatcher.signal("signal.#{signal}", sig) }
|
250
|
+
)
|
251
|
+
end
|
54
252
|
end
|
55
253
|
end
|
56
254
|
|
@@ -67,6 +265,10 @@ module Enkidu
|
|
67
265
|
@dispatcher
|
68
266
|
end
|
69
267
|
|
268
|
+
def normalize(signal)
|
269
|
+
SignalTrapper.normalize_signal(signal)
|
270
|
+
end
|
271
|
+
|
70
272
|
|
71
273
|
end#class SignalSource
|
72
274
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: enkidu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tore Darell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Enkidu is a process sidekick
|
14
14
|
email: toredarell@gmail.com
|
@@ -40,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
40
|
version: '0'
|
41
41
|
requirements: []
|
42
42
|
rubyforge_project:
|
43
|
-
rubygems_version: 2.
|
43
|
+
rubygems_version: 2.5.2
|
44
44
|
signing_key:
|
45
45
|
specification_version: 4
|
46
46
|
summary: Enkidu process sidekick
|