roby 0.7
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/.gitignore +29 -0
- data/History.txt +4 -0
- data/License-fr.txt +519 -0
- data/License.txt +515 -0
- data/Manifest.txt +245 -0
- data/NOTES +4 -0
- data/README.txt +163 -0
- data/Rakefile +161 -0
- data/TODO.txt +146 -0
- data/app/README.txt +24 -0
- data/app/Rakefile +8 -0
- data/app/config/ROBOT.rb +5 -0
- data/app/config/app.yml +91 -0
- data/app/config/init.rb +7 -0
- data/app/config/roby.yml +3 -0
- data/app/controllers/.gitattributes +0 -0
- data/app/controllers/ROBOT.rb +2 -0
- data/app/data/.gitattributes +0 -0
- data/app/planners/ROBOT/main.rb +6 -0
- data/app/planners/main.rb +5 -0
- data/app/scripts/distributed +3 -0
- data/app/scripts/generate/bookmarks +3 -0
- data/app/scripts/replay +3 -0
- data/app/scripts/results +3 -0
- data/app/scripts/run +3 -0
- data/app/scripts/server +3 -0
- data/app/scripts/shell +3 -0
- data/app/scripts/test +3 -0
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby +210 -0
- data/bin/roby-log +168 -0
- data/bin/roby-shell +25 -0
- data/doc/images/event_generalization.png +0 -0
- data/doc/images/exception_propagation_1.png +0 -0
- data/doc/images/exception_propagation_2.png +0 -0
- data/doc/images/exception_propagation_3.png +0 -0
- data/doc/images/exception_propagation_4.png +0 -0
- data/doc/images/exception_propagation_5.png +0 -0
- data/doc/images/replay_handler_error.png +0 -0
- data/doc/images/replay_handler_error_0.png +0 -0
- data/doc/images/replay_handler_error_1.png +0 -0
- data/doc/images/roby_cycle_overview.png +0 -0
- data/doc/images/roby_replay_02.png +0 -0
- data/doc/images/roby_replay_03.png +0 -0
- data/doc/images/roby_replay_04.png +0 -0
- data/doc/images/roby_replay_event_representation.png +0 -0
- data/doc/images/roby_replay_first_state.png +0 -0
- data/doc/images/roby_replay_relations.png +0 -0
- data/doc/images/roby_replay_startup.png +0 -0
- data/doc/images/task_event_generalization.png +0 -0
- data/doc/papers.rdoc +11 -0
- data/doc/styles/allison.css +314 -0
- data/doc/styles/allison.js +316 -0
- data/doc/styles/allison.rb +276 -0
- data/doc/styles/jamis.rb +593 -0
- data/doc/tutorials/01-GettingStarted.rdoc +86 -0
- data/doc/tutorials/02-GoForward.rdoc +220 -0
- data/doc/tutorials/03-PlannedPath.rdoc +268 -0
- data/doc/tutorials/04-EventPropagation.rdoc +236 -0
- data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
- data/doc/tutorials/06-Overview.rdoc +40 -0
- data/doc/videos.rdoc +69 -0
- data/ext/droby/dump.cc +175 -0
- data/ext/droby/extconf.rb +3 -0
- data/ext/graph/algorithm.cc +746 -0
- data/ext/graph/extconf.rb +7 -0
- data/ext/graph/graph.cc +529 -0
- data/ext/graph/graph.hh +183 -0
- data/ext/graph/iterator_sequence.hh +102 -0
- data/ext/graph/undirected_dfs.hh +226 -0
- data/ext/graph/undirected_graph.hh +421 -0
- data/lib/roby.rb +41 -0
- data/lib/roby/app.rb +870 -0
- data/lib/roby/app/rake.rb +56 -0
- data/lib/roby/app/run.rb +14 -0
- data/lib/roby/app/scripts/distributed.rb +13 -0
- data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
- data/lib/roby/app/scripts/replay.rb +31 -0
- data/lib/roby/app/scripts/results.rb +15 -0
- data/lib/roby/app/scripts/run.rb +26 -0
- data/lib/roby/app/scripts/server.rb +18 -0
- data/lib/roby/app/scripts/shell.rb +88 -0
- data/lib/roby/app/scripts/test.rb +40 -0
- data/lib/roby/basic_object.rb +151 -0
- data/lib/roby/config.rb +5 -0
- data/lib/roby/control.rb +747 -0
- data/lib/roby/decision_control.rb +17 -0
- data/lib/roby/distributed.rb +32 -0
- data/lib/roby/distributed/base.rb +440 -0
- data/lib/roby/distributed/communication.rb +871 -0
- data/lib/roby/distributed/connection_space.rb +592 -0
- data/lib/roby/distributed/distributed_object.rb +206 -0
- data/lib/roby/distributed/drb.rb +62 -0
- data/lib/roby/distributed/notifications.rb +539 -0
- data/lib/roby/distributed/peer.rb +550 -0
- data/lib/roby/distributed/protocol.rb +529 -0
- data/lib/roby/distributed/proxy.rb +343 -0
- data/lib/roby/distributed/subscription.rb +311 -0
- data/lib/roby/distributed/transaction.rb +498 -0
- data/lib/roby/event.rb +897 -0
- data/lib/roby/exceptions.rb +234 -0
- data/lib/roby/executives/simple.rb +30 -0
- data/lib/roby/graph.rb +166 -0
- data/lib/roby/interface.rb +390 -0
- data/lib/roby/log.rb +3 -0
- data/lib/roby/log/chronicle.rb +303 -0
- data/lib/roby/log/console.rb +72 -0
- data/lib/roby/log/data_stream.rb +197 -0
- data/lib/roby/log/dot.rb +279 -0
- data/lib/roby/log/event_stream.rb +151 -0
- data/lib/roby/log/file.rb +340 -0
- data/lib/roby/log/gui/basic_display.ui +83 -0
- data/lib/roby/log/gui/chronicle.rb +26 -0
- data/lib/roby/log/gui/chronicle_view.rb +40 -0
- data/lib/roby/log/gui/chronicle_view.ui +70 -0
- data/lib/roby/log/gui/data_displays.rb +172 -0
- data/lib/roby/log/gui/data_displays.ui +155 -0
- data/lib/roby/log/gui/notifications.rb +26 -0
- data/lib/roby/log/gui/relations.rb +248 -0
- data/lib/roby/log/gui/relations.ui +123 -0
- data/lib/roby/log/gui/relations_view.rb +185 -0
- data/lib/roby/log/gui/relations_view.ui +149 -0
- data/lib/roby/log/gui/replay.rb +327 -0
- data/lib/roby/log/gui/replay_controls.rb +200 -0
- data/lib/roby/log/gui/replay_controls.ui +259 -0
- data/lib/roby/log/gui/runtime.rb +130 -0
- data/lib/roby/log/hooks.rb +185 -0
- data/lib/roby/log/logger.rb +202 -0
- data/lib/roby/log/notifications.rb +244 -0
- data/lib/roby/log/plan_rebuilder.rb +470 -0
- data/lib/roby/log/relations.rb +1056 -0
- data/lib/roby/log/server.rb +550 -0
- data/lib/roby/log/sqlite.rb +47 -0
- data/lib/roby/log/timings.rb +164 -0
- data/lib/roby/plan-object.rb +247 -0
- data/lib/roby/plan.rb +762 -0
- data/lib/roby/planning.rb +13 -0
- data/lib/roby/planning/loops.rb +302 -0
- data/lib/roby/planning/model.rb +906 -0
- data/lib/roby/planning/task.rb +151 -0
- data/lib/roby/propagation.rb +562 -0
- data/lib/roby/query.rb +619 -0
- data/lib/roby/relations.rb +583 -0
- data/lib/roby/relations/conflicts.rb +70 -0
- data/lib/roby/relations/ensured.rb +20 -0
- data/lib/roby/relations/error_handling.rb +23 -0
- data/lib/roby/relations/events.rb +9 -0
- data/lib/roby/relations/executed_by.rb +193 -0
- data/lib/roby/relations/hierarchy.rb +239 -0
- data/lib/roby/relations/influence.rb +10 -0
- data/lib/roby/relations/planned_by.rb +63 -0
- data/lib/roby/robot.rb +7 -0
- data/lib/roby/standard_errors.rb +218 -0
- data/lib/roby/state.rb +5 -0
- data/lib/roby/state/events.rb +221 -0
- data/lib/roby/state/information.rb +55 -0
- data/lib/roby/state/pos.rb +110 -0
- data/lib/roby/state/shapes.rb +32 -0
- data/lib/roby/state/state.rb +353 -0
- data/lib/roby/support.rb +92 -0
- data/lib/roby/task-operations.rb +182 -0
- data/lib/roby/task.rb +1618 -0
- data/lib/roby/test/common.rb +399 -0
- data/lib/roby/test/distributed.rb +214 -0
- data/lib/roby/test/tasks/empty_task.rb +9 -0
- data/lib/roby/test/tasks/goto.rb +36 -0
- data/lib/roby/test/tasks/simple_task.rb +23 -0
- data/lib/roby/test/testcase.rb +519 -0
- data/lib/roby/test/tools.rb +160 -0
- data/lib/roby/thread_task.rb +87 -0
- data/lib/roby/transactions.rb +462 -0
- data/lib/roby/transactions/proxy.rb +292 -0
- data/lib/roby/transactions/updates.rb +139 -0
- data/plugins/fault_injection/History.txt +4 -0
- data/plugins/fault_injection/README.txt +37 -0
- data/plugins/fault_injection/Rakefile +18 -0
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +52 -0
- data/plugins/fault_injection/fault_injection.rb +89 -0
- data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
- data/plugins/subsystems/README.txt +40 -0
- data/plugins/subsystems/Rakefile +18 -0
- data/plugins/subsystems/app.rb +171 -0
- data/plugins/subsystems/test/app/README +24 -0
- data/plugins/subsystems/test/app/Rakefile +8 -0
- data/plugins/subsystems/test/app/config/app.yml +71 -0
- data/plugins/subsystems/test/app/config/init.rb +9 -0
- data/plugins/subsystems/test/app/config/roby.yml +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +20 -0
- data/plugins/subsystems/test/app/scripts/distributed +3 -0
- data/plugins/subsystems/test/app/scripts/replay +3 -0
- data/plugins/subsystems/test/app/scripts/results +3 -0
- data/plugins/subsystems/test/app/scripts/run +3 -0
- data/plugins/subsystems/test/app/scripts/server +3 -0
- data/plugins/subsystems/test/app/scripts/shell +3 -0
- data/plugins/subsystems/test/app/scripts/test +3 -0
- data/plugins/subsystems/test/app/tasks/services.rb +15 -0
- data/plugins/subsystems/test/test_subsystems.rb +71 -0
- data/test/distributed/test_communication.rb +178 -0
- data/test/distributed/test_connection.rb +282 -0
- data/test/distributed/test_execution.rb +373 -0
- data/test/distributed/test_mixed_plan.rb +341 -0
- data/test/distributed/test_plan_notifications.rb +238 -0
- data/test/distributed/test_protocol.rb +516 -0
- data/test/distributed/test_query.rb +102 -0
- data/test/distributed/test_remote_plan.rb +491 -0
- data/test/distributed/test_transaction.rb +463 -0
- data/test/mockups/tasks.rb +27 -0
- data/test/planning/test_loops.rb +380 -0
- data/test/planning/test_model.rb +427 -0
- data/test/planning/test_task.rb +106 -0
- data/test/relations/test_conflicts.rb +42 -0
- data/test/relations/test_ensured.rb +38 -0
- data/test/relations/test_executed_by.rb +149 -0
- data/test/relations/test_hierarchy.rb +158 -0
- data/test/relations/test_planned_by.rb +54 -0
- data/test/suite_core.rb +24 -0
- data/test/suite_distributed.rb +9 -0
- data/test/suite_planning.rb +3 -0
- data/test/suite_relations.rb +8 -0
- data/test/test_bgl.rb +508 -0
- data/test/test_control.rb +399 -0
- data/test/test_event.rb +894 -0
- data/test/test_exceptions.rb +592 -0
- data/test/test_interface.rb +37 -0
- data/test/test_log.rb +114 -0
- data/test/test_log_server.rb +132 -0
- data/test/test_plan.rb +584 -0
- data/test/test_propagation.rb +210 -0
- data/test/test_query.rb +266 -0
- data/test/test_relations.rb +180 -0
- data/test/test_state.rb +414 -0
- data/test/test_support.rb +16 -0
- data/test/test_task.rb +938 -0
- data/test/test_testcase.rb +122 -0
- data/test/test_thread_task.rb +73 -0
- data/test/test_transactions.rb +569 -0
- data/test/test_transactions_proxy.rb +198 -0
- metadata +570 -0
data/lib/roby/plan.rb
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
require 'roby/event'
|
|
2
|
+
require 'roby/task'
|
|
3
|
+
require 'roby/relations'
|
|
4
|
+
require 'roby/basic_object'
|
|
5
|
+
|
|
6
|
+
module Roby
|
|
7
|
+
# A plan object is a collection of tasks and events. In plans, tasks can be
|
|
8
|
+
# +missions+ (#missions, #mission?), which means that they are the final
|
|
9
|
+
# goals of the system. A task is +useful+ if it helps into the realization
|
|
10
|
+
# of a mission (it is linked to a mission by #hierarchy_relation or one
|
|
11
|
+
# of the #service_relations), and is not useful otherwise. #garbage_collect
|
|
12
|
+
# removes the tasks that are not useful.
|
|
13
|
+
#
|
|
14
|
+
# The following event hooks are defined:
|
|
15
|
+
# * #inserted
|
|
16
|
+
# * #discarded
|
|
17
|
+
# * #discovered_tasks
|
|
18
|
+
# * #discovered_events
|
|
19
|
+
# * #replaced
|
|
20
|
+
# * #added_transaction
|
|
21
|
+
# * #removed_transaction
|
|
22
|
+
# * #garbage
|
|
23
|
+
# * #finalized_task
|
|
24
|
+
# * #finalized_event
|
|
25
|
+
#
|
|
26
|
+
class Plan < BasicObject
|
|
27
|
+
extend Logger::Hierarchy
|
|
28
|
+
extend Logger::Forward
|
|
29
|
+
|
|
30
|
+
# The task index for this plan
|
|
31
|
+
attr_reader :task_index
|
|
32
|
+
|
|
33
|
+
# The list of tasks that are included in this plan
|
|
34
|
+
attr_reader :known_tasks
|
|
35
|
+
# The set of events that are defined by #known_tasks
|
|
36
|
+
attr_reader :task_events
|
|
37
|
+
# The list of the robot's missions. Do not change that set directly, use
|
|
38
|
+
# #insert and #discard instead.
|
|
39
|
+
attr_reader :missions
|
|
40
|
+
# The list of events that are not included in a task
|
|
41
|
+
attr_reader :free_events
|
|
42
|
+
# The list of tasks that are kept outside GC. Do not change that set
|
|
43
|
+
# directly, use #permanent and #auto instead.
|
|
44
|
+
attr_reader :keepalive
|
|
45
|
+
|
|
46
|
+
# A map of event => task repairs. Whenever an exception is found,
|
|
47
|
+
# exception propagation checks that no repair is defined for that
|
|
48
|
+
# particular event or for events that are forwarded by it.
|
|
49
|
+
#
|
|
50
|
+
# See also #add_repair and #remove_repair
|
|
51
|
+
attr_reader :repairs
|
|
52
|
+
|
|
53
|
+
# A set of tasks which are useful (and as such would not been garbage
|
|
54
|
+
# collected), but we want to GC anyway
|
|
55
|
+
attr_reader :force_gc
|
|
56
|
+
|
|
57
|
+
# A set of task for which GC should not be attempted, either because
|
|
58
|
+
# they are not interruptible or because their start or stop command
|
|
59
|
+
# failed
|
|
60
|
+
attr_reader :gc_quarantine
|
|
61
|
+
|
|
62
|
+
# The set of transactions which are built on top of this plan
|
|
63
|
+
attr_reader :transactions
|
|
64
|
+
|
|
65
|
+
# If this object is the main plan, checks if we are subscribed to
|
|
66
|
+
# the whole remote plan
|
|
67
|
+
def sibling_on?(peer)
|
|
68
|
+
if Roby.plan == self then peer.remote_plan
|
|
69
|
+
else super
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def initialize
|
|
74
|
+
@missions = ValueSet.new
|
|
75
|
+
@keepalive = ValueSet.new
|
|
76
|
+
@known_tasks = ValueSet.new
|
|
77
|
+
@free_events = ValueSet.new
|
|
78
|
+
@task_events = ValueSet.new
|
|
79
|
+
@force_gc = ValueSet.new
|
|
80
|
+
@gc_quarantine = ValueSet.new
|
|
81
|
+
@transactions = ValueSet.new
|
|
82
|
+
@repairs = Hash.new
|
|
83
|
+
|
|
84
|
+
@task_index = Roby::TaskIndex.new
|
|
85
|
+
|
|
86
|
+
super() if defined? super
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def inspect
|
|
90
|
+
"#<#{to_s}: missions=#{missions.to_s} tasks=#{known_tasks.to_s} events=#{free_events.to_s} transactions=#{transactions.to_s}>"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# call-seq:
|
|
94
|
+
# plan.partition_event_task(objects) => events, tasks
|
|
95
|
+
#
|
|
96
|
+
def partition_event_task(objects)
|
|
97
|
+
if objects.respond_to?(:to_task) then return nil, [objects.to_task]
|
|
98
|
+
elsif objects.respond_to?(:to_event) then return [objects.to_event], nil
|
|
99
|
+
elsif !objects.respond_to?(:each)
|
|
100
|
+
raise TypeError, "expecting a task, event, or a collection of tasks and events, got #{objects}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
evts, tasks = objects.partition do |o|
|
|
104
|
+
if o.respond_to?(:to_event) then true
|
|
105
|
+
elsif o.respond_to?(:to_task) then false
|
|
106
|
+
else raise ArgumentError, "found #{o || 'nil'} which is neither a task nor an event"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
return evts, tasks
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# If this plan is a toplevel plan, returns self. If it is a
|
|
113
|
+
# transaction, returns the underlying plan
|
|
114
|
+
def real_plan
|
|
115
|
+
ret = self
|
|
116
|
+
while ret.respond_to?(:plan)
|
|
117
|
+
ret = ret.plan
|
|
118
|
+
end
|
|
119
|
+
ret
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Inserts a new mission in the plan. Its child tree is automatically
|
|
123
|
+
# inserted too. Returns the plan
|
|
124
|
+
def insert(task)
|
|
125
|
+
return if @missions.include?(task)
|
|
126
|
+
|
|
127
|
+
@missions << task
|
|
128
|
+
discover(task)
|
|
129
|
+
task.mission = true if task.self_owned?
|
|
130
|
+
inserted(task)
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
# Hook called when +tasks+ have been inserted in this plan
|
|
134
|
+
def inserted(tasks); super if defined? super end
|
|
135
|
+
|
|
136
|
+
# Forbid the GC to take out +task+
|
|
137
|
+
def permanent(task)
|
|
138
|
+
@keepalive << task
|
|
139
|
+
discover(task)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Make GC finalize +task+ if it is not useful anymore
|
|
143
|
+
def auto(task); @keepalive.delete(task) end
|
|
144
|
+
|
|
145
|
+
def edit
|
|
146
|
+
if block_given?
|
|
147
|
+
Roby::Control.synchronize do
|
|
148
|
+
yield
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def permanent?(task); @keepalive.include?(task) end
|
|
154
|
+
|
|
155
|
+
# Removes the task in +tasks+ from the list of missions
|
|
156
|
+
def discard(task)
|
|
157
|
+
@missions.delete(task)
|
|
158
|
+
discover(task)
|
|
159
|
+
task.mission = false if task.self_owned?
|
|
160
|
+
|
|
161
|
+
discarded(task)
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
# Hook called when +tasks+ have been discarded from this plan
|
|
165
|
+
def discarded(tasks); super if defined? super end
|
|
166
|
+
|
|
167
|
+
def owns?(object)
|
|
168
|
+
(object.owners - owners).empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Remove all tasks
|
|
172
|
+
def clear
|
|
173
|
+
@known_tasks.each { |t| t.clear_relations }
|
|
174
|
+
@known_tasks.clear
|
|
175
|
+
@free_events.each { |e| e.clear_relations }
|
|
176
|
+
@free_events.clear
|
|
177
|
+
@missions.clear
|
|
178
|
+
@keepalive.clear
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def handle_replace(from, to)
|
|
182
|
+
return if from == to
|
|
183
|
+
|
|
184
|
+
# Check that +to+ is valid in all hierarchy relations where +from+ is a child
|
|
185
|
+
if !to.fullfills?(*from.fullfilled_model)
|
|
186
|
+
raise InvalidReplace.new(from, to, "to does not fullfills #{from.fullfilled_model}")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check that +to+ is in the same execution state than +from+
|
|
190
|
+
if !to.compatible_state?(from)
|
|
191
|
+
raise InvalidReplace.new(from, to, "state. #{from.running?}, #{to.running?}")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Swap the subplans of +from+ and +to+
|
|
195
|
+
yield(from, to)
|
|
196
|
+
|
|
197
|
+
replaced(from, to)
|
|
198
|
+
if mission?(from)
|
|
199
|
+
discard(from)
|
|
200
|
+
insert(to)
|
|
201
|
+
elsif permanent?(from)
|
|
202
|
+
auto(from)
|
|
203
|
+
permanent(to)
|
|
204
|
+
else
|
|
205
|
+
discover(to)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def replace_task(from, to)
|
|
210
|
+
handle_replace(from, to) do
|
|
211
|
+
from.replace_by(to)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def replace(from, to)
|
|
216
|
+
handle_replace(from, to) do
|
|
217
|
+
from.replace_subplan_by(to)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Hook called when +to+ has replaced +from+ in this plan
|
|
222
|
+
def replaced(from, to); super if defined? super end
|
|
223
|
+
|
|
224
|
+
# Check that this is an executable plan. This is always true for
|
|
225
|
+
# plain Plan objects and false for transcations
|
|
226
|
+
def executable?; true end
|
|
227
|
+
|
|
228
|
+
# call-seq:
|
|
229
|
+
# plan.discover([t1, t2, ...]) => plan
|
|
230
|
+
#
|
|
231
|
+
# Updates Plan#known_tasks with either the child tree of the tasks in
|
|
232
|
+
# +objects+
|
|
233
|
+
def discover(objects)
|
|
234
|
+
event_seeds, tasks = partition_event_task(objects)
|
|
235
|
+
event_seeds = (event_seeds || ValueSet.new).to_value_set
|
|
236
|
+
|
|
237
|
+
if tasks
|
|
238
|
+
tasks = tasks.to_value_set
|
|
239
|
+
new_tasks = useful_task_component(nil, tasks, tasks)
|
|
240
|
+
unless new_tasks.empty?
|
|
241
|
+
old_task_events = task_events.dup
|
|
242
|
+
new_tasks = discover_task_set(new_tasks)
|
|
243
|
+
event_seeds.merge(task_events - old_task_events)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if !event_seeds.empty?
|
|
248
|
+
events = event_seeds.dup
|
|
249
|
+
|
|
250
|
+
# now, we include the set of free events that are linked to
|
|
251
|
+
# +new_tasks+ in +events+
|
|
252
|
+
EventStructure.each_root_relation do |rel|
|
|
253
|
+
components = rel.generated_subgraphs(event_seeds, false)
|
|
254
|
+
components.concat rel.reverse.generated_subgraphs(event_seeds, false)
|
|
255
|
+
for c in components
|
|
256
|
+
events.merge(c.to_value_set)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
discover_event_set(events - task_events - free_events)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
self
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Add +events+ to the set of known events and call discovered_events
|
|
267
|
+
# for the new events
|
|
268
|
+
#
|
|
269
|
+
# This is for internal use, use #discover instead
|
|
270
|
+
def discover_event_set(events)
|
|
271
|
+
events = events.difference(free_events)
|
|
272
|
+
events.delete_if do |e|
|
|
273
|
+
if !e.root_object?
|
|
274
|
+
true
|
|
275
|
+
else
|
|
276
|
+
e.plan = self
|
|
277
|
+
false
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
unless events.empty?
|
|
282
|
+
free_events.merge(events)
|
|
283
|
+
discovered_events(events)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
events
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Add +tasks+ to the set of known tasks and call discovered_tasks for
|
|
290
|
+
# the new tasks
|
|
291
|
+
#
|
|
292
|
+
# This is for internal use, use #discover instead
|
|
293
|
+
def discover_task_set(tasks)
|
|
294
|
+
tasks = tasks.difference(known_tasks)
|
|
295
|
+
for t in tasks
|
|
296
|
+
t.plan = self
|
|
297
|
+
task_events.merge t.bound_events.values.to_value_set
|
|
298
|
+
task_index.add t
|
|
299
|
+
end
|
|
300
|
+
known_tasks.merge tasks
|
|
301
|
+
discovered_tasks(tasks)
|
|
302
|
+
|
|
303
|
+
for t in tasks
|
|
304
|
+
t.instantiate_model_event_relations
|
|
305
|
+
end
|
|
306
|
+
tasks
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# DEPRECATED. Use #discovered_tasks instead
|
|
310
|
+
def discovered(tasks); super if defined? super end
|
|
311
|
+
# Hook called when new tasks have been discovered in this plan
|
|
312
|
+
def discovered_tasks(tasks)
|
|
313
|
+
discovered(tasks)
|
|
314
|
+
super if defined? super
|
|
315
|
+
end
|
|
316
|
+
# Hook called when new events have been discovered in this plan
|
|
317
|
+
def discovered_events(events)
|
|
318
|
+
super if defined? super
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Hook called when a new transaction has been built on top of this plan
|
|
322
|
+
def added_transaction(trsc); super if defined? super end
|
|
323
|
+
# Removes the transaction +trsc+ from the list of known transactions
|
|
324
|
+
# built on this plan
|
|
325
|
+
def remove_transaction(trsc)
|
|
326
|
+
transactions.delete(trsc)
|
|
327
|
+
removed_transaction(trsc)
|
|
328
|
+
end
|
|
329
|
+
# Hook called when a new transaction has been built on top of this plan
|
|
330
|
+
def removed_transaction(trsc); super if defined? super end
|
|
331
|
+
|
|
332
|
+
# Merges the set of tasks that are useful for +seeds+ into +useful_set+.
|
|
333
|
+
# Only the tasks that are in +complete_set+ are included.
|
|
334
|
+
def useful_task_component(complete_set, useful_set, seeds)
|
|
335
|
+
old_useful_set = useful_set.dup
|
|
336
|
+
for rel in TaskStructure.relations
|
|
337
|
+
next unless rel.root_relation?
|
|
338
|
+
for subgraph in rel.generated_subgraphs(seeds, false)
|
|
339
|
+
useful_set.merge(subgraph)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if complete_set
|
|
344
|
+
useful_set &= complete_set
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
if useful_set.size == old_useful_set.size || (complete_set && useful_set.size == complete_set.size)
|
|
348
|
+
useful_set
|
|
349
|
+
else
|
|
350
|
+
useful_task_component(complete_set, useful_set, (useful_set - old_useful_set))
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Returns the set of useful tasks in this plan
|
|
355
|
+
def locally_useful_tasks
|
|
356
|
+
# Remove all missions that are finished
|
|
357
|
+
for finished_mission in (@missions & task_index.by_state[:finished?])
|
|
358
|
+
if !task_index.repaired_tasks.include?(finished_mission)
|
|
359
|
+
discard(finished_mission)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
for finished_permanent in (@keepalive & task_index.by_state[:finished?])
|
|
363
|
+
if !task_index.repaired_tasks.include?(finished_permanent)
|
|
364
|
+
auto(finished_permanent)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Create the set of tasks which must be kept as-is
|
|
369
|
+
seeds = @missions | @keepalive
|
|
370
|
+
for trsc in transactions
|
|
371
|
+
seeds.merge trsc.proxy_objects.keys.to_value_set
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return ValueSet.new if seeds.empty?
|
|
375
|
+
|
|
376
|
+
# Compute the set of LOCAL tasks which serve the seeds. The set of
|
|
377
|
+
# locally_useful_tasks is the union of the seeds and of this one
|
|
378
|
+
useful_task_component(local_tasks, seeds & local_tasks, seeds) | seeds
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def local_tasks
|
|
382
|
+
task_index.by_owner[Roby::Distributed] || ValueSet.new
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def remote_tasks
|
|
386
|
+
if local_tasks = task_index.by_owner[Roby::Distributed]
|
|
387
|
+
known_tasks - local_tasks
|
|
388
|
+
else
|
|
389
|
+
known_tasks
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Returns the set of unused tasks
|
|
394
|
+
def unneeded_tasks
|
|
395
|
+
# Get the set of local tasks that are serving one of our own missions or
|
|
396
|
+
# permanent tasks
|
|
397
|
+
useful = self.locally_useful_tasks
|
|
398
|
+
|
|
399
|
+
# Append to that the set of tasks that are useful for our peers and
|
|
400
|
+
# include the set of local tasks that are serving tasks in
|
|
401
|
+
# +remotely_useful+
|
|
402
|
+
remotely_useful = Distributed.remotely_useful_objects(remote_tasks, true, nil)
|
|
403
|
+
serving_remote = useful_task_component(local_tasks, useful & local_tasks, remotely_useful)
|
|
404
|
+
|
|
405
|
+
useful.merge remotely_useful
|
|
406
|
+
useful.merge serving_remote
|
|
407
|
+
|
|
408
|
+
(known_tasks - useful)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Computes the set of useful tasks and checks that +task+ is in it.
|
|
412
|
+
# This is quite slow. It is here for debugging purposes. Do not use it
|
|
413
|
+
# in production code
|
|
414
|
+
def useful_task?(task)
|
|
415
|
+
known_tasks.include?(task) && !unneeded_tasks.include?(task)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def useful_event_component(useful_events)
|
|
419
|
+
current_size = useful_events.size
|
|
420
|
+
for rel in EventStructure.relations
|
|
421
|
+
next unless rel.root_relation?
|
|
422
|
+
|
|
423
|
+
for subgraph in rel.components(free_events, false)
|
|
424
|
+
subgraph = subgraph.to_value_set
|
|
425
|
+
if subgraph.intersects?(useful_events) || subgraph.intersects?(task_events)
|
|
426
|
+
useful_events.merge(subgraph)
|
|
427
|
+
if useful_events.include_all?(free_events)
|
|
428
|
+
return free_events
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
if useful_events.include_all?(free_events)
|
|
434
|
+
return free_events
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if current_size != useful_events.size
|
|
439
|
+
useful_event_component(useful_events)
|
|
440
|
+
else
|
|
441
|
+
useful_events
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Computes the set of events that are useful in the plan Events are
|
|
446
|
+
# 'useful' when they are chained to a task.
|
|
447
|
+
def useful_events
|
|
448
|
+
return ValueSet.new if free_events.empty?
|
|
449
|
+
(free_events & useful_event_component(ValueSet.new))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# The set of events that can be removed from the plan
|
|
453
|
+
def unneeded_events
|
|
454
|
+
useful_events = self.useful_events
|
|
455
|
+
|
|
456
|
+
result = (free_events - useful_events)
|
|
457
|
+
result.delete_if do |ev|
|
|
458
|
+
transactions.any? { |trsc| trsc.wrap(ev, false) }
|
|
459
|
+
end
|
|
460
|
+
result
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Checks if +task+ is included in this plan
|
|
464
|
+
def include?(object); @known_tasks.include?(object) || @free_events.include?(object) end
|
|
465
|
+
# Checks if +task+ is a mission of this plan
|
|
466
|
+
def mission?(task); @missions.include?(task) end
|
|
467
|
+
# Count of tasks in this plan
|
|
468
|
+
def size; @known_tasks.size end
|
|
469
|
+
# Returns true if there is no task in this plan
|
|
470
|
+
def empty?; @known_tasks.empty? end
|
|
471
|
+
# Iterates on all tasks
|
|
472
|
+
def each_task; @known_tasks.each { |t| yield(t) } end
|
|
473
|
+
|
|
474
|
+
# Install a plan repair for +failure_point+ with +task+. If +task+ is
|
|
475
|
+
# pending, it is started.
|
|
476
|
+
#
|
|
477
|
+
# See also #repairs and #remove_repair
|
|
478
|
+
def add_repair(failure_point, task)
|
|
479
|
+
if !failure_point.kind_of?(Event)
|
|
480
|
+
raise TypeError, "failure point #{failure_point} should be an event"
|
|
481
|
+
elsif task.plan && task.plan != self
|
|
482
|
+
raise ArgumentError, "wrong plan: #{task} is in #{task.plan}, not #{self}"
|
|
483
|
+
elsif repairs.has_key?(failure_point)
|
|
484
|
+
raise ArgumentError, "there is already a plan repair defined for #{failure_point}: #{repairs[failure_point]}"
|
|
485
|
+
elsif !task.plan
|
|
486
|
+
discover(task)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
repairs[failure_point] = task
|
|
490
|
+
if failure_point.generator.respond_to?(:task)
|
|
491
|
+
task_index.repaired_tasks << failure_point.generator.task
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
if task.pending?
|
|
495
|
+
Roby.once { task.start! }
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Removes +task+ from the set of active plan repairs.
|
|
500
|
+
#
|
|
501
|
+
# See also #repairs and #add_repair
|
|
502
|
+
def remove_repair(task)
|
|
503
|
+
repairs.delete_if do |ev, repair|
|
|
504
|
+
if repair == task
|
|
505
|
+
if ev.generator.respond_to?(:task)
|
|
506
|
+
task_index.repaired_tasks.delete(ev.generator.task)
|
|
507
|
+
end
|
|
508
|
+
true
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Return all repairs which apply on +event+
|
|
514
|
+
def repairs_for(event)
|
|
515
|
+
result = Hash.new
|
|
516
|
+
|
|
517
|
+
if event.generator.respond_to?(:task)
|
|
518
|
+
equivalent_generators = event.generator.generated_subgraph(EventStructure::Forwarding)
|
|
519
|
+
|
|
520
|
+
history = event.generator.task.history
|
|
521
|
+
id = event.propagation_id
|
|
522
|
+
index = history.index(event)
|
|
523
|
+
while index < history.size
|
|
524
|
+
ev = history[index]
|
|
525
|
+
break if ev.propagation_id != id
|
|
526
|
+
|
|
527
|
+
if equivalent_generators.include?(ev.generator) &&
|
|
528
|
+
(task = repairs[ev])
|
|
529
|
+
|
|
530
|
+
result[ev] = task
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
index += 1
|
|
534
|
+
end
|
|
535
|
+
elsif task = repairs[event]
|
|
536
|
+
result[event] = task
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
result
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Returns +object+ if object is a plan object from this plan, or if
|
|
543
|
+
# it has no plan yet (in which case it is added to the plan first).
|
|
544
|
+
# Otherwise, raises ArgumentError.
|
|
545
|
+
#
|
|
546
|
+
# This method is provided for consistency with Transaction#[]
|
|
547
|
+
def [](object)
|
|
548
|
+
if object.plan != self
|
|
549
|
+
raise ArgumentError, "#{object} is not from #{plan}"
|
|
550
|
+
elsif !object.plan
|
|
551
|
+
discover(object)
|
|
552
|
+
end
|
|
553
|
+
object
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def self.can_gc?(task)
|
|
557
|
+
if task.starting? then true # wait for the task to be started before deciding ...
|
|
558
|
+
elsif task.running? && !task.finishing?
|
|
559
|
+
task.event(:stop).controlable?
|
|
560
|
+
else true
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Kills and removes all unneeded tasks
|
|
565
|
+
def garbage_collect(force_on = nil)
|
|
566
|
+
if force_on && !force_on.empty?
|
|
567
|
+
force_gc.merge(force_on.to_value_set)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# The set of tasks for which we queued stop! at this cycle
|
|
571
|
+
# #finishing? is false until the next event propagation cycle
|
|
572
|
+
finishing = ValueSet.new
|
|
573
|
+
did_something = true
|
|
574
|
+
while did_something
|
|
575
|
+
did_something = false
|
|
576
|
+
|
|
577
|
+
tasks = unneeded_tasks | force_gc
|
|
578
|
+
local_tasks = self.local_tasks & tasks
|
|
579
|
+
remote_tasks = tasks - local_tasks
|
|
580
|
+
|
|
581
|
+
# Remote tasks are simply removed, regardless of other concerns
|
|
582
|
+
for t in remote_tasks
|
|
583
|
+
Plan.debug { "GC: removing the remote task #{t}" }
|
|
584
|
+
remove_object(t)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
break if local_tasks.empty?
|
|
588
|
+
|
|
589
|
+
if local_tasks.all? { |t| t.pending? || t.finished? }
|
|
590
|
+
local_tasks.each do |t|
|
|
591
|
+
Plan.debug { "GC: #{t} is not running, removed" }
|
|
592
|
+
garbage(t)
|
|
593
|
+
remove_object(t)
|
|
594
|
+
end
|
|
595
|
+
break
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Mark all root local_tasks as garbage
|
|
599
|
+
roots = nil
|
|
600
|
+
2.times do |i|
|
|
601
|
+
roots = local_tasks.find_all do |t|
|
|
602
|
+
if t.root?
|
|
603
|
+
garbage(t)
|
|
604
|
+
true
|
|
605
|
+
else
|
|
606
|
+
Plan.debug { "GC: ignoring #{t}, it is not root" }
|
|
607
|
+
false
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
break if i == 1 || !roots.empty?
|
|
612
|
+
|
|
613
|
+
# There is a cycle somewhere. Try to break it by removing
|
|
614
|
+
# weak relations within elements of local_tasks
|
|
615
|
+
Plan.debug "cycle found, removing weak relations"
|
|
616
|
+
|
|
617
|
+
local_tasks.each do |t|
|
|
618
|
+
next if t.root?
|
|
619
|
+
t.each_graph do |rel|
|
|
620
|
+
rel.remove(t) if rel.weak?
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
(roots.to_value_set - finishing - gc_quarantine).each do |local_task|
|
|
626
|
+
if local_task.pending?
|
|
627
|
+
Plan.info "GC: removing pending task #{local_task}"
|
|
628
|
+
remove_object(local_task)
|
|
629
|
+
did_something = true
|
|
630
|
+
elsif local_task.starting?
|
|
631
|
+
# wait for task to be started before killing it
|
|
632
|
+
Plan.debug { "GC: #{local_task} is starting" }
|
|
633
|
+
elsif local_task.finished?
|
|
634
|
+
Plan.debug { "GC: #{local_task} is not running, removed" }
|
|
635
|
+
remove_object(local_task)
|
|
636
|
+
did_something = true
|
|
637
|
+
elsif !local_task.finishing?
|
|
638
|
+
if local_task.event(:stop).controlable?
|
|
639
|
+
Plan.debug { "GC: queueing #{local_task}/stop" }
|
|
640
|
+
if !local_task.respond_to?(:stop!)
|
|
641
|
+
Plan.fatal "something fishy: #{local_task}/stop is controlable but there is no #stop! method"
|
|
642
|
+
gc_quarantine << local_task
|
|
643
|
+
else
|
|
644
|
+
finishing << local_task
|
|
645
|
+
Roby::Control.once do
|
|
646
|
+
Plan.debug { "GC: stopping #{local_task}" }
|
|
647
|
+
local_task.stop!(nil)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
else
|
|
651
|
+
Plan.warn "GC: ignored #{local_task}, it cannot be stopped"
|
|
652
|
+
gc_quarantine << local_task
|
|
653
|
+
end
|
|
654
|
+
elsif local_task.finishing?
|
|
655
|
+
Plan.debug { "GC: waiting for #{local_task} to finish" }
|
|
656
|
+
else
|
|
657
|
+
Plan.warn "GC: ignored #{local_task}"
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
unneeded_events.each do |event|
|
|
663
|
+
remove_object(event)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def remove_object(object)
|
|
668
|
+
if !object.root_object?
|
|
669
|
+
raise ArgumentError, "cannot remove #{object} which is a non-root object"
|
|
670
|
+
elsif object.plan != self
|
|
671
|
+
if known_tasks.include?(object) || free_events.include?(object)
|
|
672
|
+
raise ArgumentError, "#{object} is included in #{self} but #plan == #{object.plan}"
|
|
673
|
+
elsif !object.plan
|
|
674
|
+
if object.removed_at
|
|
675
|
+
raise ArgumentError, "#{object} has been removed at\n #{object.removed_at.join("\n ")}"
|
|
676
|
+
else
|
|
677
|
+
raise ArgumentError, "#{object} has not been included in this plan"
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
raise ArgumentError, "#{object} is not in #{self}: #plan == #{object.plan}"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Remove relations first. This is needed by transaction since
|
|
684
|
+
# removing relations may need wrapping some new objects, and in
|
|
685
|
+
# that case these new objects will be discovered as well
|
|
686
|
+
object.clear_relations
|
|
687
|
+
|
|
688
|
+
@free_events.delete(object)
|
|
689
|
+
@missions.delete(object)
|
|
690
|
+
@known_tasks.delete(object)
|
|
691
|
+
@keepalive.delete(object)
|
|
692
|
+
force_gc.delete(object)
|
|
693
|
+
|
|
694
|
+
object.plan = nil
|
|
695
|
+
object.removed_at = caller
|
|
696
|
+
|
|
697
|
+
case object
|
|
698
|
+
when EventGenerator
|
|
699
|
+
finalized_event(object)
|
|
700
|
+
|
|
701
|
+
when Task
|
|
702
|
+
task_index.remove(object)
|
|
703
|
+
|
|
704
|
+
for ev in object.bound_events.values
|
|
705
|
+
task_events.delete(ev)
|
|
706
|
+
finalized_event(ev)
|
|
707
|
+
end
|
|
708
|
+
finalized_task(object)
|
|
709
|
+
|
|
710
|
+
else
|
|
711
|
+
raise ArgumentError, "unknown object type #{object}"
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
self
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Backward compatibility
|
|
718
|
+
def remove_task(t) # :nodoc:
|
|
719
|
+
remove_object(t)
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# Hook called when +task+ is marked as garbage. It will be garbage
|
|
723
|
+
# collected as soon as possible
|
|
724
|
+
def garbage(task)
|
|
725
|
+
# Remove all signals that go *to* the task
|
|
726
|
+
#
|
|
727
|
+
# While we want events which come from the task to be properly
|
|
728
|
+
# forwarded, the signals that go to the task are to be ignored
|
|
729
|
+
if task.self_owned?
|
|
730
|
+
task.each_event do |ev|
|
|
731
|
+
ev.parent_objects(EventStructure::Signal).each do |signalling_event|
|
|
732
|
+
signalling_event.remove_signal ev
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
super if defined? super
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# backward compatibility
|
|
741
|
+
def finalized(task) # :nodoc:
|
|
742
|
+
super if defined? super
|
|
743
|
+
end
|
|
744
|
+
# Hook called when +task+ has been removed from this plan
|
|
745
|
+
def finalized_task(task)
|
|
746
|
+
super if defined? super
|
|
747
|
+
finalized(task)
|
|
748
|
+
end
|
|
749
|
+
# Hook called when +event+ has been removed from this plan
|
|
750
|
+
def finalized_event(event); super if defined? super end
|
|
751
|
+
|
|
752
|
+
# Replace +task+ with a fresh copy of itself
|
|
753
|
+
def respawn(task)
|
|
754
|
+
new_task = task.class.new(task.arguments.dup)
|
|
755
|
+
|
|
756
|
+
replace_task(task, new_task)
|
|
757
|
+
Control.once { new_task.start!(nil) }
|
|
758
|
+
new_task
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|