fsm 0.0.0

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.
@@ -0,0 +1,211 @@
1
+ unless defined? $__fsm_fsm__
2
+ $__fsm_fsm__ = __FILE__
3
+
4
+ module FSM
5
+ FSM::LIBDIR =
6
+ File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
7
+ defined? FSM::LIBDIR
8
+
9
+ FSM::INCDIR =
10
+ File::dirname(FSM::LIBDIR) + File::SEPARATOR unless
11
+ defined? FSM::INCDIR
12
+
13
+ require INCDIR + 'fsm'
14
+
15
+ class FSM
16
+ include Util
17
+
18
+ tattrs %w[
19
+ graph
20
+ state_attributes
21
+ state
22
+ subscribers
23
+ dsl
24
+ ]
25
+
26
+ def initialize *a, &b
27
+ super
28
+
29
+ @graph = DirectedGraph.new
30
+ @state_attributes = Hash.new{|h,k| h[k] = Hash.new}
31
+
32
+ @graph.add_node 'start'
33
+ @state_attributes['start'].update 'shape' => 'point'
34
+ @state = 'start'
35
+
36
+ @subscribers = []
37
+
38
+ states = a.flatten
39
+ states.each{|state| @graph.add_node state}
40
+
41
+ @dsl = DSL.new self
42
+ configure &b if b
43
+ end
44
+
45
+ def configure &b
46
+ ex{
47
+ @dsl.configure &b
48
+ }
49
+ end
50
+
51
+ def subscribe subscriber, event, *events
52
+ ex{
53
+ @subscriptions << Subscription.new(subscriber, event, *events)
54
+ }
55
+ end
56
+
57
+ def inspect
58
+ sh{
59
+ @graph.links.inspect
60
+ }
61
+ end
62
+
63
+ def start
64
+ transition 'start', 'start'
65
+ end
66
+ alias_method 'start!', 'start'
67
+
68
+ def add_observer o
69
+ ex{ @subscribers << o }
70
+ end
71
+
72
+ class TransitionError < ::StandardError; end
73
+
74
+ def transition state = nil, edge = nil, &b
75
+ validate_state = lambda do |state|
76
+ if state
77
+ raise TransitionError, "state == <#{ @state.inspect }> not <#{ state.inspect }>" unless
78
+ @state == state
79
+ end
80
+ end
81
+
82
+ validate_edge = lambda do |edge|
83
+ if edge
84
+ raise TransitionError, "no path <#{ state }> -->> <#{ edge }>" unless
85
+ @graph.links_from(state).map{|link| link.info}.include? edge
86
+ end
87
+ end
88
+
89
+ ex{
90
+ validate_state[state]
91
+ state ||= @state
92
+
93
+ validate_edge[edge]
94
+ edge ||= 'default'
95
+
96
+ notify Event::Exit
97
+
98
+ @state = [@state, edge]
99
+ notify Event::Transition
100
+
101
+ bcall b, *@state if b
102
+
103
+ begin
104
+ @state = @graph.transition *@state
105
+ rescue => e
106
+ m,c,b = e.message, e.class, e.backtrace.join("\n")
107
+ raise TransitionError, "#{ m } (#{ c })\n#{ b }"
108
+ end
109
+
110
+ notify Event::Entry
111
+
112
+ @state
113
+ }
114
+ end
115
+ alias_method 'transitioning', 'transition'
116
+
117
+ def notify type, *data
118
+ sh{
119
+ e = type::new @state, *data
120
+ @subscribers.each{|s| e.notify s}
121
+ }
122
+ end
123
+
124
+ def traverse *pairs
125
+ pairs.to_a.flatten!
126
+ raise ArgumentError, 'odd number of arguments' unless
127
+ pairs.size.modulo(2).zero?
128
+ ex{
129
+ string_list(*pairs).each_slice(2){|state, edge| transition state, edge}
130
+ }
131
+ end
132
+
133
+ def input *data
134
+ sh{
135
+ p data
136
+ notify Event::Input, *data
137
+ }
138
+ end
139
+
140
+ def add_state *states
141
+ ex{
142
+ string_list(states).each{|state|
143
+ @graph.add_node state
144
+ case @graph.num_nodes
145
+ when 2
146
+ add_transition 'start', 'start' => state
147
+ @state_attributes[state].update 'shape' => 'doublecircle'
148
+ else
149
+ @state_attributes[state].update 'shape' => 'circle'
150
+ end
151
+ }
152
+ self
153
+ }
154
+ end
155
+
156
+ def add_transition *a, &b
157
+ ex{
158
+ hashes, argv = a.partition{|arg| Hash === arg}
159
+ info = argv.shift || 'default'
160
+ raise ArgumentError, "too many argv in <#{ argv.inspect }>" unless argv.empty?
161
+ raise ArgumentError, "too few transitions in <#{ hashes.inspect }>" if hashes.empty?
162
+ transitions = hashes.inject({}){|t,h| t.update h}
163
+ transitions.each do |u,v|
164
+ @graph.add_link *string_list(u,v,info)
165
+ #add_transition_action u, info, &b if b
166
+ end
167
+ self
168
+ }
169
+ end
170
+
171
+ def plot dot_in = nil, dot_out = nil, fmt = 'jpg', &b
172
+ dot_in ||=
173
+ (tmp = Tempfile.new("#{ Process.pid }-#{ Time.now.to_i }-#{ rand }")).path
174
+ if tmp
175
+ tmp.puts to_dot
176
+ tmp.close
177
+ else
178
+ open(dot_in, 'w'){|f| f.puts to_dot}
179
+ end
180
+ dot_out = dot_in + '.' + fmt
181
+ cmd = "#{ DOT_CMD } -T#{ fmt } #{ dot_in } -o #{ dot_out }"
182
+ system cmd or raise "cmd <#{ cmd } failed with <#{ $?.exitstatus }>"
183
+ dot_out
184
+ end
185
+
186
+ def to_dot
187
+ sh{
188
+ dot = @graph.to_dot
189
+ @state_attributes.each do |state, attributes|
190
+ dot.set_node_attributes state, attributes
191
+ end
192
+ class << dot
193
+ def to_s(*a, &b) to_dot_specification(*a, &b) end
194
+ end
195
+ dot
196
+ }
197
+ end
198
+
199
+ def display
200
+ dot_out = plot
201
+ at_exit{ File.unlink dot_out rescue nil}
202
+ Thread.new{
203
+ Thread.current.abort_on_exception = true
204
+ cmd = "#{ DISPLAY_CMD } #{ dot_out } </dev/null >/dev/null 2>&1"
205
+ system cmd or raise "cmd <#{ cmd } failed with <#{ $?.exitstatus }>"
206
+ File.unlink dot_out rescue nil
207
+ }
208
+ end
209
+ end # class FSM
210
+ end # module FSM
211
+ end
@@ -0,0 +1,366 @@
1
+ unless defined? $__fsm_fsm__
2
+ module FSM
3
+ #--{{{
4
+ FSM::LIBDIR = File::dirname(File::expand_path(__FILE__)) + File::SEPARATOR unless
5
+ defined? FSM::LIBDIR
6
+
7
+ #require LIBDIR + 'util'
8
+ require 'fsm'
9
+
10
+ class FSM
11
+ #--{{{
12
+ include Util
13
+ include Sync_m
14
+ def sh(&b) synchronize(:SH, &b) end
15
+ def ex(&b) synchronize(:EX, &b) end
16
+
17
+ %w(
18
+ graph
19
+ state
20
+ state_attributes
21
+ entry_actions
22
+ exit_actions
23
+ transition_actions
24
+ input_actions
25
+ threadgroup
26
+ ).each{|a| attr_accessor a}
27
+
28
+ def initialize *a, &b
29
+ sync_initialize
30
+
31
+ @graph = DirectedGraph.new
32
+ @state_attributes = Hash.new{|h,k| h[k] = Hash.new}
33
+ @entry_actions = Hash.new{|h,k| h[k] = []}
34
+ @exit_actions = Hash.new{|h,k| h[k] = []}
35
+ @transition_actions = Hash.new{|h,k| h[k] = Hash.new{|h2,k| h2[k] = []}}
36
+ @input_actions = Hash.new{|h,k| h[k] = []}
37
+
38
+ @graph.add_node 'start'
39
+ @state_attributes['start'].update 'shape' => 'point'
40
+ @state = 'start'
41
+ @threadgroup = ThreadGroup.new
42
+
43
+ states = a.flatten
44
+ states.each{|state| @graph.add_node state}
45
+
46
+ configure &b if b
47
+ end
48
+
49
+ def configure &block
50
+ ex{
51
+ DSL.new(self).configure &block
52
+ }
53
+ end
54
+
55
+ def add_state *states
56
+ ex{
57
+ string_list(states).each{|state|
58
+ @graph.add_node state
59
+ case @graph.num_nodes
60
+ when 2
61
+ add_transition 'start', 'start' => state
62
+ @state_attributes[state].update 'shape' => 'doublecircle'
63
+ else
64
+ @state_attributes[state].update 'shape' => 'circle'
65
+ end
66
+ }
67
+ self
68
+ }
69
+ end
70
+
71
+ def add_transition *a, &b
72
+ ex{
73
+ hashes, argv = a.partition{|arg| Hash === arg}
74
+ info = argv.shift || 'default'
75
+ raise ArgumentError, "too many argv in <#{ argv.inspect }>" unless argv.empty?
76
+ raise ArgumentError, "too few transitions in <#{ hashes.inspect }>" if hashes.empty?
77
+ transitions = hashes.inject({}){|t,h| t.update h}
78
+ transitions.each do |u,v|
79
+ @graph.add_link *string_list(u,v,info)
80
+ add_transition_action u, info, &b if b
81
+ end
82
+ self
83
+ }
84
+ end
85
+
86
+ def start
87
+ ex{
88
+ transition 'start'
89
+ }
90
+ end
91
+ alias_method 'start!', 'start'
92
+
93
+ def add_entry_action state, &block
94
+ ex{
95
+ @entry_actions[string(state)] << block
96
+ @entry_actions[string(state)].size - 1
97
+ }
98
+ end
99
+ def delete_entry_action state, index
100
+ ex{
101
+ @entry_actions[string(state)].delete_at index
102
+ }
103
+ end
104
+
105
+ def add_exit_action state, &block
106
+ ex{
107
+ @exit_actions[string(state)] << block
108
+ @exit_actions[string(state)].size - 1
109
+ }
110
+ end
111
+ def delete_exit_action state, index
112
+ ex{
113
+ @exit_actions[string(state)].delete_at index
114
+ }
115
+ end
116
+
117
+ def add_transition_action state, along, &block
118
+ ex{
119
+ @transition_actions[string(state)][string(along)] << block
120
+ @transition_actions[string(state)][string(along)].size - 1
121
+ }
122
+ end
123
+ def delete_transition_action state, index
124
+ ex{
125
+ @transition_actions[string(state)][string(along)].delete_at index
126
+ }
127
+ end
128
+
129
+ def add_input_action state, &block
130
+ ex{
131
+ @input_actions[string(state)] << block
132
+ @input_actions[string(state)].size - 1
133
+ }
134
+ end
135
+ def delete_input_action state, index
136
+ ex{
137
+ @input_actions[string(state)].delete_at index
138
+ }
139
+ end
140
+
141
+ def transition along = 'default'
142
+ run = lambda{|table|
143
+ actions = Array === @state ? table[@state.first][@state.last] : table[@state]
144
+ actions.map{|action| bcall action, state}
145
+ }
146
+
147
+ ex{
148
+ state = @state
149
+ begin
150
+ run[@exit_actions]
151
+ @state = [@state, along]
152
+ run[@transition_actions]
153
+ @state = @graph.transition *@state
154
+ run[@entry_actions]
155
+ @state
156
+ rescue Exception => e
157
+ @state = state
158
+ raise
159
+ end
160
+ }
161
+ end
162
+ alias_method 'transitioning', 'transition'
163
+
164
+ def input *data
165
+ sh{
166
+ actions = @input_actions[@state]
167
+ actions.map do |action|
168
+ if bcall action, 'predicate', *data
169
+ bcall action, 'action', *data
170
+ end
171
+ end
172
+ }
173
+ end
174
+
175
+ def traverse *a
176
+ ex{
177
+ string_list(*a).each{|along| transition along}
178
+ }
179
+ end
180
+
181
+ def add_thread t
182
+ t.abort_on_exception = true
183
+ ex{
184
+ @threadgroup.add t
185
+ t
186
+ }
187
+ end
188
+
189
+
190
+ #
191
+ # entry hooks
192
+ #
193
+ =begin
194
+ def wait_for_entry state, *a, &b
195
+ state, q, this = string(state), Queue.new, self
196
+ ex{
197
+ index =
198
+ add_input_action(state) do
199
+ add_entry_action(state){ q.push state }
200
+ end
201
+ add_thread Thread.new(*a){|*a|
202
+ begin
203
+ bcall b, q.pop, *a
204
+ ensure
205
+ this.delete_entry_action state, index
206
+ end
207
+ }
208
+ }
209
+ end
210
+ alias_method 'wait_for', 'wait_for_entry'
211
+ def on_entry state, *a, &b
212
+ state, q = string(state), Queue.new
213
+ ex{
214
+ add_entry_action(state){ q.push state }
215
+ add_thread Thread.new(*a){|*a| loop{ bcall b, q.pop, *a } }
216
+ }
217
+ end
218
+ =end
219
+ def on_entry state, *a, &b
220
+ state = string(state)
221
+ ex{
222
+ add_entry_action(state){ bcall b, *a }
223
+ }
224
+ end
225
+ def once_on_entry state, *a, &b
226
+ index =
227
+ on_entry(state, *a) do
228
+ delete_entry_action state, index
229
+ end
230
+ end
231
+
232
+ #
233
+ # exit hooks
234
+ #
235
+ def wait_for_exit state, *a, &b
236
+ state, q, this = string(state), Queue.new, self
237
+ ex{
238
+ index =
239
+ add_input_action(state) do
240
+ add_exit_action(state){ q.push state }
241
+ end
242
+ add_thread Thread.new(*a){|*a|
243
+ begin
244
+ bcall b, q.pop, *a
245
+ ensure
246
+ this.delete_exit_action state, index
247
+ end
248
+ }
249
+ }
250
+ end
251
+ def on_exit state, *a, &b
252
+ state, q = string(state), Queue.new
253
+ ex{
254
+ add_exit_action(state){ q.push state }
255
+ add_thread Thread.new(*a){|*a| loop{ bcall b, q.pop, *a } }
256
+ }
257
+ end
258
+
259
+ #
260
+ # transition hooks
261
+ #
262
+ def wait_for_transition state, along, *a, &b
263
+ state, q, this = string(state), Queue.new, self
264
+ ex{
265
+ index =
266
+ add_input_action(state) do
267
+ add_transition_action(state){ q.push state and q.push along }
268
+ end
269
+ add_thread Thread.new(*a){|*a|
270
+ begin
271
+ bcall b, q.pop, q.pop, *a
272
+ ensure
273
+ this.delete_transition_action state, index
274
+ end
275
+ }
276
+ }
277
+ end
278
+ def on_transition state, along, *a, &b
279
+ state, q = string(state), Queue.new
280
+ ex{
281
+ add_transition_action(state){ q.push state and q.push along }
282
+ add_thread Thread.new(*a){|*a| loop{ bcall b, q.pop, q.pop, *a } }
283
+ }
284
+ end
285
+
286
+ #
287
+ # input hooks
288
+ #
289
+ def wait_for_input state, *a, &b
290
+ state, q, this = string(state), Queue.new, self
291
+ ex{
292
+ index =
293
+ add_input_action(state) do |mode, *data|
294
+ mode =~ %r/predicate/ ? true : (q.push(state); q.push(data))
295
+ end
296
+ add_thread Thread.new(*a){|*a|
297
+ begin
298
+ bcall b, q.pop, *(a + q.pop)
299
+ ensure
300
+ this.delete_input_action state, index
301
+ end
302
+ }
303
+ }
304
+ end
305
+ def on_input state, *a, &b
306
+ state, q = string(state), Queue.new
307
+ ex{
308
+ add_input_action(state) do |mode, *data|
309
+ mode =~ %r/predicate/ ? true : (q.push(state); q.push(data))
310
+ end
311
+ add_thread Thread.new(*a){|*a| loop{ bcall b, q.pop, *(a + q.pop) } }
312
+ }
313
+ end
314
+
315
+
316
+
317
+ def join
318
+ loop{
319
+ #p sh{ @threadgroup.list }
320
+ if sh{ @threadgroup.list }.empty?
321
+ break
322
+ else
323
+ sleep 0.42
324
+ end
325
+ }
326
+ self
327
+ end
328
+
329
+
330
+ # rotate text!
331
+ def plot fmt = 'png', dot_in = nil, dot_out = nil
332
+ sh{
333
+ dot_in ||= Tempfile.new("#{ Process.pid }-#{ Time.now.to_i }-#{ rand }").path
334
+ open(dot_in, 'w') do |f|
335
+ dot = @graph.to_dot
336
+ @state_attributes.each do |state, attributes|
337
+ dot.set_node_attributes state, attributes
338
+ end
339
+ dot.orientation = 'landscape'
340
+ f.puts dot.to_dot_specification
341
+ end
342
+ dot_out = dot_in + '.' + fmt
343
+ cmd = "#{ DOT_CMD } -T#{ fmt } #{ dot_in } -o #{ dot_out }"
344
+ system cmd or raise "cmd <#{ cmd } failed with <#{ $?.exitstatus }>"
345
+ dot_out
346
+ }
347
+ end
348
+
349
+ def display
350
+ sh{
351
+ dot_out = plot
352
+ Thread.new{
353
+ #Thread.current.abort_on_exception = true
354
+ cmd = "#{ DISPLAY_CMD } #{ dot_out } </dev/null >/dev/null 2>&1"
355
+ system cmd or raise "cmd <#{ cmd } failed with <#{ $?.exitstatus }>"
356
+ at_exit{ File.unlink dot_out }
357
+ dot_out
358
+ }
359
+ }
360
+ end
361
+ #--}}}
362
+ end # class FSM
363
+ #--}}}
364
+ end # module FSM
365
+ $__fsm_fsm__ = __FILE__
366
+ end