fsm 0.0.0

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