enkidu 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6cc70fb3863d61c5a67e5b6089de27b48a1cc1be
4
- data.tar.gz: 7b35191f85577c0fa859ab15912b60eb9979f09f
3
+ metadata.gz: 1703c3004e749c132e16c7dca1eceec5fb30f875
4
+ data.tar.gz: ea4c9081b396051029e64e5429a1a4dbb57b9306
5
5
  SHA512:
6
- metadata.gz: 1cfb5b0739aa65b78667a53f020cc4539ef78cf01f6eaae0f25c9898d164a3a37b6ef0d90cee24305057ff4aab6b3a8fa6b5615ecf23aa6ada11decb4b879b3b
7
- data.tar.gz: 69b02f4e3fb66053fd80b4ec47612d65f79c0009399b1a764db096ac8e9256ebb0626eb01a7dbc472bdbc9f50f8da34b237d98f05c2a9d8810bd7a111b531e55
6
+ metadata.gz: 2829b255bdca0dfe35fc315992493929c29c7d6a8c3c6307d4dc011de8dfef8ba99dd96d1fbea4902ec2f326852582abafd761c0fa96fbdbbf57723b3db1a3f4
7
+ data.tar.gz: 738636e61003210c68cb3cd43eb317ce8938561ab1bdef0f5b9e768144766f453fe0e7eea9045d651300afb45b1b9f5a7301fdf6cda2f8e3a08c5217338305c5
@@ -26,46 +26,113 @@ module Enkidu
26
26
  # d.run #Blocks
27
27
  class Dispatcher
28
28
 
29
- STOP = Object.new
30
- k=self;STOP.define_singleton_method(:inspect){ "<#{k.name}::STOP>" }
29
+ class StateError < StandardError; end
31
30
 
32
- def initialize
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
- yield self if block_given?
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 == STOP
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
- def stop
90
- schedule(callable: STOP)
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 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)
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
- handlers << [regex, callable]
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 add(source, name=nil)
149
- source = source.new(self) if Class === source
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
- source
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 RegExp returned as-is.
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
- source << '\\.' unless index == 0
182
- source << '[^\.]+'
367
+ if index == 0
368
+ source << '[^\.]+'
369
+ else
370
+ source << '\\.[^\.]+'
371
+ end
183
372
  elsif part == '#'
184
- source << '.*?' # .*? ?
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
- @thread.abort_on_exception = true
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
- def join
232
- @thread.join
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
 
@@ -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(atts)
21
- atts = Enkidu.deep_merge(defaults, atts)
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(atts)
27
- log(Enkidu.deep_merge({tags: ['INFO']}, atts))
26
+ def info(*args)
27
+ atts = attify(args)
28
+ atts[:tags].unshift('INFO')
29
+ signal atts
28
30
  end
29
31
 
30
- def error(atts)
31
- log(Enkidu.deep_merge({tags: ['ERROR']}, atts))
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 = {tags: ['ERROR', 'EXCEPTION'], message: "#{e.class}: #{e.message}"}
36
- atts[:exception] = {type: e.class.name, message: e.message, stacktrace: e.backtrace}
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
- log atts
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
- run
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 ||= -> 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
247
+ @formatter ||= DefaultFormatter.new
248
+ end
249
+
250
+
251
+ def flush?
252
+ @flush
94
253
  end
95
254
 
96
255
 
@@ -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
- def initialize(dispatcher)
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
- @q = Queue.new
28
- run
212
+ @trapper = trapper
213
+ @subscriptions = []
29
214
  end
30
215
 
31
216
  def run
32
- Thread.new do
33
- loop do
34
- sig = @q.pop
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
- 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)
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
- Signal.trap(signal){ @q.push signal } #TODO not reentrant
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.1
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: 2015-05-06 00:00:00.000000000 Z
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.4.5
43
+ rubygems_version: 2.5.2
44
44
  signing_key:
45
45
  specification_version: 4
46
46
  summary: Enkidu process sidekick