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.
- data/gemspec.rb +23 -0
- data/install.rb +206 -0
- data/instance_exec.rb +26 -0
- data/lib/fsm-0.0.0.rb +78 -0
- data/lib/fsm-0.0.0/dsl.rb +71 -0
- data/lib/fsm-0.0.0/event.rb +68 -0
- data/lib/fsm-0.0.0/fsm.rb +211 -0
- data/lib/fsm-0.0.0/fsm.rb.bak +366 -0
- data/lib/fsm-0.0.0/graph/base_extensions.rb +70 -0
- data/lib/fsm-0.0.0/graph/directed_graph.rb +481 -0
- data/lib/fsm-0.0.0/graph/graphviz_dot.rb +188 -0
- data/lib/fsm-0.0.0/observer.rb +194 -0
- data/lib/fsm-0.0.0/system.rb +65 -0
- data/lib/fsm-0.0.0/util.rb +172 -0
- data/lib/fsm.rb +78 -0
- data/sample/a.rb +81 -0
- metadata +58 -0
@@ -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
|