roby 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (236) hide show
  1. data/History.txt +7 -5
  2. data/Manifest.txt +91 -16
  3. data/README.txt +24 -24
  4. data/Rakefile +92 -64
  5. data/app/config/app.yml +42 -43
  6. data/app/config/init.rb +26 -0
  7. data/benchmark/alloc_misc.rb +123 -0
  8. data/benchmark/discovery_latency.rb +67 -0
  9. data/benchmark/garbage_collection.rb +48 -0
  10. data/benchmark/genom.rb +31 -0
  11. data/benchmark/transactions.rb +62 -0
  12. data/bin/roby +1 -1
  13. data/bin/roby-log +16 -6
  14. data/doc/guide/.gitignore +2 -0
  15. data/doc/guide/config.yaml +34 -0
  16. data/doc/guide/ext/init.rb +14 -0
  17. data/doc/guide/ext/previous_next.rb +40 -0
  18. data/doc/guide/ext/rdoc_links.rb +33 -0
  19. data/doc/guide/index.rdoc +16 -0
  20. data/doc/guide/overview.rdoc +62 -0
  21. data/doc/guide/plan_modifications.rdoc +67 -0
  22. data/doc/guide/src/abstraction/achieve_with.page +8 -0
  23. data/doc/guide/src/abstraction/forwarding.page +8 -0
  24. data/doc/guide/src/abstraction/hierarchy.page +19 -0
  25. data/doc/guide/src/abstraction/index.page +28 -0
  26. data/doc/guide/src/abstraction/task_models.page +13 -0
  27. data/doc/guide/src/basics.template +6 -0
  28. data/doc/guide/src/basics/app.page +139 -0
  29. data/doc/guide/src/basics/code_examples.page +33 -0
  30. data/doc/guide/src/basics/dry.page +69 -0
  31. data/doc/guide/src/basics/errors.page +443 -0
  32. data/doc/guide/src/basics/events.page +179 -0
  33. data/doc/guide/src/basics/hierarchy.page +275 -0
  34. data/doc/guide/src/basics/index.page +11 -0
  35. data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
  36. data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
  37. data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
  38. data/doc/guide/src/basics/log_replay/goForward_4.png +0 -0
  39. data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
  40. data/doc/guide/src/basics/log_replay/hierarchy_error_1.png +0 -0
  41. data/doc/guide/src/basics/log_replay/hierarchy_error_2.png +0 -0
  42. data/doc/guide/src/basics/log_replay/hierarchy_error_3.png +0 -0
  43. data/doc/guide/src/basics/log_replay/plan_repair_1.png +0 -0
  44. data/doc/guide/src/basics/log_replay/plan_repair_2.png +0 -0
  45. data/doc/guide/src/basics/log_replay/plan_repair_3.png +0 -0
  46. data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
  47. data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
  48. data/doc/guide/src/basics/log_replay/roby_log_relation_window.png +0 -0
  49. data/doc/guide/src/basics/log_replay/roby_replay_event_representation.png +0 -0
  50. data/doc/guide/src/basics/plan_objects.page +71 -0
  51. data/doc/guide/src/basics/relations_display.page +203 -0
  52. data/doc/guide/src/basics/roby_cycle_overview.png +0 -0
  53. data/doc/guide/src/basics/shell.page +102 -0
  54. data/doc/guide/src/basics/summary.page +32 -0
  55. data/doc/guide/src/basics/tasks.page +357 -0
  56. data/doc/guide/src/basics_shell_header.txt +16 -0
  57. data/doc/guide/src/cycle/cycle-overview.png +0 -0
  58. data/doc/guide/src/cycle/cycle-overview.svg +208 -0
  59. data/doc/guide/src/cycle/error_handling.page +168 -0
  60. data/doc/guide/src/cycle/error_instantaneous_repair.png +0 -0
  61. data/doc/guide/src/cycle/error_instantaneous_repair.svg +1224 -0
  62. data/doc/guide/src/cycle/garbage_collection.page +10 -0
  63. data/doc/guide/src/cycle/index.page +23 -0
  64. data/doc/guide/src/cycle/propagation.page +154 -0
  65. data/doc/guide/src/cycle/propagation_diamond.png +0 -0
  66. data/doc/guide/src/cycle/propagation_diamond.svg +1279 -0
  67. data/doc/guide/src/default.css +319 -0
  68. data/doc/guide/src/default.template +74 -0
  69. data/doc/guide/src/htmldoc.metainfo +20 -0
  70. data/doc/guide/src/htmldoc.virtual +18 -0
  71. data/doc/guide/src/images/bodybg.png +0 -0
  72. data/doc/guide/src/images/contbg.png +0 -0
  73. data/doc/guide/src/images/footerbg.png +0 -0
  74. data/doc/guide/src/images/gradient1.png +0 -0
  75. data/doc/guide/src/images/gradient2.png +0 -0
  76. data/doc/guide/src/index.page +7 -0
  77. data/doc/guide/src/introduction/index.page +29 -0
  78. data/doc/guide/src/introduction/install.page +133 -0
  79. data/doc/{papers.rdoc → guide/src/introduction/publications.page} +5 -2
  80. data/doc/{videos.rdoc → guide/src/introduction/videos.page} +4 -2
  81. data/doc/guide/src/plugins/fault_tolerance.page +44 -0
  82. data/doc/guide/src/plugins/index.page +11 -0
  83. data/doc/guide/src/plugins/subsystems.page +45 -0
  84. data/doc/guide/src/relations/dependency.page +89 -0
  85. data/doc/guide/src/relations/index.page +12 -0
  86. data/doc/misc/update_github +24 -0
  87. data/doc/tutorials/02-GoForward.rdoc +3 -3
  88. data/ext/graph/graph.cc +46 -0
  89. data/lib/roby.rb +57 -22
  90. data/lib/roby/app.rb +132 -112
  91. data/lib/roby/app/plugins/rake.rb +21 -0
  92. data/lib/roby/app/rake.rb +0 -7
  93. data/lib/roby/app/run.rb +1 -1
  94. data/lib/roby/app/scripts/distributed.rb +1 -2
  95. data/lib/roby/app/scripts/generate/bookmarks.rb +1 -1
  96. data/lib/roby/app/scripts/results.rb +2 -1
  97. data/lib/roby/app/scripts/run.rb +6 -2
  98. data/lib/roby/app/scripts/shell.rb +11 -11
  99. data/lib/roby/config.rb +1 -1
  100. data/lib/roby/decision_control.rb +62 -3
  101. data/lib/roby/distributed.rb +4 -0
  102. data/lib/roby/distributed/base.rb +8 -0
  103. data/lib/roby/distributed/communication.rb +12 -8
  104. data/lib/roby/distributed/connection_space.rb +61 -44
  105. data/lib/roby/distributed/distributed_object.rb +1 -1
  106. data/lib/roby/distributed/notifications.rb +22 -30
  107. data/lib/roby/distributed/peer.rb +13 -8
  108. data/lib/roby/distributed/proxy.rb +5 -5
  109. data/lib/roby/distributed/subscription.rb +4 -4
  110. data/lib/roby/distributed/transaction.rb +3 -3
  111. data/lib/roby/event.rb +176 -110
  112. data/lib/roby/exceptions.rb +12 -4
  113. data/lib/roby/execution_engine.rb +1604 -0
  114. data/lib/roby/external_process_task.rb +225 -0
  115. data/lib/roby/graph.rb +0 -6
  116. data/lib/roby/interface.rb +221 -137
  117. data/lib/roby/log/console.rb +5 -3
  118. data/lib/roby/log/data_stream.rb +94 -16
  119. data/lib/roby/log/dot.rb +8 -8
  120. data/lib/roby/log/event_stream.rb +13 -3
  121. data/lib/roby/log/file.rb +43 -18
  122. data/lib/roby/log/gui/basic_display_ui.rb +89 -0
  123. data/lib/roby/log/gui/chronicle_view_ui.rb +90 -0
  124. data/lib/roby/log/gui/data_displays.rb +4 -5
  125. data/lib/roby/log/gui/data_displays_ui.rb +146 -0
  126. data/lib/roby/log/gui/relations.rb +18 -18
  127. data/lib/roby/log/gui/relations_ui.rb +120 -0
  128. data/lib/roby/log/gui/relations_view_ui.rb +144 -0
  129. data/lib/roby/log/gui/replay.rb +41 -13
  130. data/lib/roby/log/gui/replay_controls.rb +3 -0
  131. data/lib/roby/log/gui/replay_controls.ui +133 -110
  132. data/lib/roby/log/gui/replay_controls_ui.rb +249 -0
  133. data/lib/roby/log/hooks.rb +19 -18
  134. data/lib/roby/log/logger.rb +7 -6
  135. data/lib/roby/log/notifications.rb +4 -4
  136. data/lib/roby/log/plan_rebuilder.rb +20 -22
  137. data/lib/roby/log/relations.rb +44 -16
  138. data/lib/roby/log/server.rb +1 -4
  139. data/lib/roby/log/timings.rb +88 -19
  140. data/lib/roby/plan-object.rb +135 -11
  141. data/lib/roby/plan.rb +408 -224
  142. data/lib/roby/planning/loops.rb +32 -25
  143. data/lib/roby/planning/model.rb +157 -51
  144. data/lib/roby/planning/task.rb +47 -20
  145. data/lib/roby/query.rb +128 -92
  146. data/lib/roby/relations.rb +254 -136
  147. data/lib/roby/relations/conflicts.rb +6 -9
  148. data/lib/roby/relations/dependency.rb +358 -0
  149. data/lib/roby/relations/ensured.rb +0 -1
  150. data/lib/roby/relations/error_handling.rb +0 -1
  151. data/lib/roby/relations/events.rb +0 -2
  152. data/lib/roby/relations/executed_by.rb +26 -11
  153. data/lib/roby/relations/planned_by.rb +14 -14
  154. data/lib/roby/robot.rb +46 -0
  155. data/lib/roby/schedulers/basic.rb +34 -0
  156. data/lib/roby/standalone.rb +4 -0
  157. data/lib/roby/standard_errors.rb +21 -15
  158. data/lib/roby/state/events.rb +5 -4
  159. data/lib/roby/support.rb +107 -6
  160. data/lib/roby/task-operations.rb +23 -19
  161. data/lib/roby/task.rb +522 -148
  162. data/lib/roby/task_index.rb +80 -0
  163. data/lib/roby/test/common.rb +283 -44
  164. data/lib/roby/test/distributed.rb +53 -37
  165. data/lib/roby/test/testcase.rb +9 -204
  166. data/lib/roby/test/tools.rb +3 -3
  167. data/lib/roby/transactions.rb +154 -111
  168. data/lib/roby/transactions/proxy.rb +40 -7
  169. data/manifest.xml +20 -0
  170. data/plugins/fault_injection/README.txt +0 -3
  171. data/plugins/fault_injection/Rakefile +2 -8
  172. data/plugins/fault_injection/app.rb +1 -1
  173. data/plugins/fault_injection/fault_injection.rb +3 -3
  174. data/plugins/fault_injection/test/test_fault_injection.rb +19 -25
  175. data/plugins/subsystems/README.txt +0 -3
  176. data/plugins/subsystems/Rakefile +2 -7
  177. data/plugins/subsystems/app.rb +27 -16
  178. data/plugins/subsystems/test/app/config/init.rb +3 -0
  179. data/plugins/subsystems/test/app/planners/main.rb +1 -1
  180. data/plugins/subsystems/test/app/tasks/services.rb +1 -1
  181. data/plugins/subsystems/test/test_subsystems.rb +23 -16
  182. data/test/distributed/test_communication.rb +32 -15
  183. data/test/distributed/test_connection.rb +28 -26
  184. data/test/distributed/test_execution.rb +59 -54
  185. data/test/distributed/test_mixed_plan.rb +34 -34
  186. data/test/distributed/test_plan_notifications.rb +26 -26
  187. data/test/distributed/test_protocol.rb +57 -48
  188. data/test/distributed/test_query.rb +11 -7
  189. data/test/distributed/test_remote_plan.rb +71 -71
  190. data/test/distributed/test_transaction.rb +50 -47
  191. data/test/mockups/external_process +28 -0
  192. data/test/planning/test_loops.rb +163 -119
  193. data/test/planning/test_model.rb +3 -3
  194. data/test/planning/test_task.rb +27 -7
  195. data/test/relations/test_conflicts.rb +3 -3
  196. data/test/relations/test_dependency.rb +324 -0
  197. data/test/relations/test_ensured.rb +2 -2
  198. data/test/relations/test_executed_by.rb +94 -19
  199. data/test/relations/test_planned_by.rb +11 -9
  200. data/test/suite_core.rb +6 -3
  201. data/test/suite_distributed.rb +1 -0
  202. data/test/suite_planning.rb +1 -0
  203. data/test/suite_relations.rb +2 -2
  204. data/test/tasks/test_external_process.rb +126 -0
  205. data/test/{test_thread_task.rb → tasks/test_thread_task.rb} +17 -20
  206. data/test/test_bgl.rb +21 -1
  207. data/test/test_event.rb +229 -155
  208. data/test/test_exceptions.rb +79 -80
  209. data/test/test_execution_engine.rb +987 -0
  210. data/test/test_gui.rb +1 -1
  211. data/test/test_interface.rb +11 -5
  212. data/test/test_log.rb +18 -7
  213. data/test/test_log_server.rb +1 -0
  214. data/test/test_plan.rb +229 -395
  215. data/test/test_query.rb +193 -35
  216. data/test/test_relations.rb +88 -8
  217. data/test/test_state.rb +55 -37
  218. data/test/test_support.rb +1 -1
  219. data/test/test_task.rb +371 -218
  220. data/test/test_testcase.rb +32 -16
  221. data/test/test_transactions.rb +211 -170
  222. data/test/test_transactions_proxy.rb +37 -19
  223. metadata +169 -71
  224. data/.gitignore +0 -29
  225. data/doc/styles/allison.css +0 -314
  226. data/doc/styles/allison.js +0 -316
  227. data/doc/styles/allison.rb +0 -276
  228. data/doc/styles/jamis.rb +0 -593
  229. data/lib/roby/control.rb +0 -746
  230. data/lib/roby/executives/simple.rb +0 -30
  231. data/lib/roby/propagation.rb +0 -562
  232. data/lib/roby/relations/hierarchy.rb +0 -239
  233. data/lib/roby/transactions/updates.rb +0 -139
  234. data/test/relations/test_hierarchy.rb +0 -158
  235. data/test/test_control.rb +0 -399
  236. data/test/test_propagation.rb +0 -210
@@ -130,10 +130,10 @@ module Roby
130
130
  handler.call(self, exception_object)
131
131
  return true
132
132
  rescue Exception => e
133
- if self == Roby
134
- Propagation.add_framework_error(e, 'global exception handling')
133
+ if !kind_of?(PlanObject)
134
+ engine.add_framework_error(e, 'global exception handling')
135
135
  else
136
- Propagation.add_error(FailedExceptionHandler.new(e, self, exception_object))
136
+ engine.add_error(FailedExceptionHandler.new(e, self, exception_object))
137
137
  end
138
138
  end
139
139
  end
@@ -144,7 +144,15 @@ module Roby
144
144
  end
145
145
 
146
146
  RX_IN_FRAMEWORK = /^((?:\s*\(druby:\/\/.+\)\s*)?#{Regexp.quote(ROBY_LIB_DIR)}\/)/
147
- def self.filter_backtrace(original_backtrace)
147
+ def self.filter_backtrace(original_backtrace = nil)
148
+ if !original_backtrace && block_given?
149
+ begin
150
+ return yield
151
+ rescue Exception => e
152
+ raise e, e.message, filter_backtrace(e.backtrace)
153
+ end
154
+ end
155
+
148
156
  if Roby.app.filter_backtraces? && original_backtrace
149
157
  app_dir = if defined? APP_DIR then Regexp.quote(APP_DIR) end
150
158
 
@@ -0,0 +1,1604 @@
1
+ module Roby
2
+ # This class contains all code necessary for the propagation steps during
3
+ # execution. This includes event and exception propagation. This
4
+ # documentation will first present some useful tools provided by execution
5
+ # engines, and will continue by an overview of the implementation of the
6
+ # execution engine itself.
7
+ #
8
+ # == Misc tools
9
+ #
10
+ # === Block execution queueing
11
+ # <em>periodic handlers</em> are code blocks called at the beginning of the
12
+ # execution cycle, at the given periodicity (of course rounded to a cycle
13
+ # length). They are added by #every and removed by
14
+ # #remove_periodic_handler.
15
+ #
16
+ # === Thread synchronization primitives
17
+ # Most direct plan modifications and propagation operations are forbidden
18
+ # outside the execution engine's thread, to avoid the need for handling
19
+ # asynchronicity. Nonetheless, it is possible that a separate thread has to
20
+ # execute some of those operations. To simplify that, the following methods
21
+ # are available:
22
+ # * #execute blocks the calling thread until the given code
23
+ # block is executed by the execution engine. Any exception that is raised
24
+ # by the code block is raised back into the original thread and will not
25
+ # affect the engine thread.
26
+ # * #once queues a block to be executed at the beginning of
27
+ # the next execution cycle. Exceptions raised in it _will_ affect the
28
+ # execution thread and most likely cause its shutdown.
29
+ # * #wait_until(ev) blocks the calling thread until +ev+ is emitted. If +ev+
30
+ # becomes unreachable, an UnreachableEvent exception is raised in the
31
+ # calling thread.
32
+ #
33
+ # To simplify the controller development, those tools are available directly
34
+ # as singleton methods of the Roby module, which forwards them to the
35
+ # main execution engine (Roby.engine). One can for instance do
36
+ # Roby.once { puts "start of the execution thread" }
37
+ #
38
+ # Instead of
39
+ # Roby.engine.once { ... }
40
+ #
41
+ # Or
42
+ # engine.once { ... }
43
+ #
44
+ # Nonetheless, note that it breaks the object-orientation of the system and
45
+ # therefore won't work in cases where you want multiple execution engine to
46
+ # run in parallel.
47
+ #
48
+ # == Execution cycle
49
+ #
50
+ # link:../../images/roby_cycle_overview.png
51
+ #
52
+ # === Event propagation
53
+ # Event propagation is based on three main event relations:
54
+ #
55
+ # * Signal describes the commands that must be called when an event occurs. The
56
+ # signalled event command is called when the signalling events are emitted. If
57
+ # more than one event are signalling the same event in the same execution
58
+ # cycle, the command will be called only once
59
+ # * Forwarding describes the events that must be emitted whenever a source
60
+ # event is. It is to be used as a way to define event aliases (for instance
61
+ # 'stop' is an alias for 'success'), because a task is stopped when it has
62
+ # finished with success. Unlike with signals, if more than one event is
63
+ # forwarded to the same event in the same cycle, the target event will be
64
+ # emitted as many times as the incoming events.
65
+ # * the Precedence relation is a subset of the two preceding relations. It
66
+ # represents a partial ordering of the events that must be maintained during
67
+ # the propagation stage (i.e. a notion of causality).
68
+ #
69
+ # In the code, the followin procedure is followed: when a code fragment calls
70
+ # EventGenerator#emit or EventGenerator#call, the event is not emitted right
71
+ # away. Instead, it is queued in the set of "pending" events through the use of
72
+ # #add_event_propagation. The execution engine will then consider
73
+ # the pending set of events, choose the appropriate one by following the
74
+ # information contained in the Precedence relation and emit or call it. The
75
+ # actual call/emission is done through EventGenerator#call_without_propagation
76
+ # and EventGenerator#emit_without_propagation. The error checking (i.e. wether
77
+ # or not the emission/call is allowed) is done at both steps of propagation,
78
+ # because doing it late in the *_without_propagation versions would make the
79
+ # system more difficult to debug/test.
80
+ #
81
+ # === Error handling
82
+ # Each user-provided code fragment (i.e. event handlers, event commands,
83
+ # polling blocks, ...) are called into a specific error-gathering context.
84
+ # Once an exception is caught, it is added to the set of detected errors
85
+ # through #add_error. Those errors are handled after the
86
+ # event propagation cycle by the #propagate_exceptions
87
+ # method. It follows the following steps:
88
+ #
89
+ # * it removes all exceptions for which a running repair exists
90
+ # (#remove_inhibited_exceptions)
91
+ #
92
+ # * it checks for repairs declared through the
93
+ # Roby::TaskStructure::ErrorHandling relation. If one exists, the
94
+ # corresponding task is started, adds it to the set of running repairs
95
+ # (Plan#add_repair)
96
+ #
97
+ # For example, the following code fragment declares that +repair_task+
98
+ # is a plan repair for all errors involving the +low_battery+ event of the
99
+ # +moving+ task
100
+ #
101
+ # task.event(:moving).handle_with repair_task
102
+ #
103
+ # * it executes the exception handlers that have been declared for this
104
+ # exception by a call to Roby::Task.on_exception. The following code
105
+ # fragment defines an exception handler for LowBattery exceptions:
106
+ #
107
+ # class Moving
108
+ # on_exception(LowBattery) { |error| do_something_to_handle_that }
109
+ # end
110
+ #
111
+ # Exception handling is finished whenever an exception handler did not
112
+ # call #pass_exception to notify that it cannot handle the given
113
+ # exception.
114
+ #
115
+ # * if no exception handler is found, or if all of them called
116
+ # #pass_exception, then plan-level exception handlers are searched in the
117
+ # corresponding Roby::Plan instance. Plan-level exception handlers are
118
+ # defined by Plan#on_exception. Alternatively, for the main plan,
119
+ # Roby.on_exception can be also used.
120
+ #
121
+ # * finally, tasks that are still involved in an error are injected into the
122
+ # garbage collection process through the +force+ argument of
123
+ # #garbage_collect, so that they get killed and removed from the plan.
124
+ #
125
+ class ExecutionEngine
126
+ extend Logger::Hierarchy
127
+ extend Logger::Forward
128
+
129
+ # Create an execution engine acting on +plan+, using +control+ as the
130
+ # decision control object
131
+ #
132
+ # See Roby::Plan and Roby::DecisionControl
133
+ def initialize(plan, control)
134
+ @plan = plan
135
+ plan.engine = self
136
+ @control = control
137
+
138
+ @propagation_id = 0
139
+ @delayed_events = []
140
+ @process_once = Queue.new
141
+ @event_ordering = Array.new
142
+ @event_priorities = Hash.new
143
+ @propagation_handlers = []
144
+ @at_cycle_end_handlers = Array.new
145
+ @process_every = Array.new
146
+ @waiting_threads = Array.new
147
+
148
+ each_cycle(&ExecutionEngine.method(:call_every))
149
+
150
+ @quit = 0
151
+ @allow_propagation = true
152
+ @thread = nil
153
+ @cycle_index = 0
154
+ @cycle_start = Time.now
155
+ @cycle_length = 0
156
+ @last_stop_count = 0
157
+ @finalizers = []
158
+ @gc_warning = true
159
+ end
160
+
161
+ # The Plan this engine is acting on
162
+ attr_accessor :plan
163
+ # The DecisionControl object associated with this engine
164
+ attr_accessor :control
165
+ # A numeric ID giving the count of the current propagation cycle
166
+ attr_reader :propagation_id
167
+
168
+ @propagation_handlers = []
169
+ class << self
170
+ # Code blocks that get called at the beginning of each cycle. See
171
+ # #add_propagation_handler
172
+ attr_reader :propagation_handlers
173
+
174
+ # call-seq:
175
+ # ExecutionEngine.add_propagation_handler { |plan| ... }
176
+ #
177
+ # The propagation handlers are a set of block objects that have to be
178
+ # called at the beginning of every propagation phase for all plans.
179
+ # These objects are called in propagation context, which means that the
180
+ # events they would call or emit are injected in the propagation
181
+ # process itself.
182
+ #
183
+ # This method adds a new propagation handler. In its first form, the
184
+ # argument is the proc object to be added. In the second form, the
185
+ # block is taken the handler. In both cases, the method returns a value
186
+ # which can be used to remove the propagation handler later. In both
187
+ # cases, the block or proc is called with the plan to propagate on
188
+ # as argument.
189
+ #
190
+ # This method sets up global propagation handlers (i.e. to be used for
191
+ # all propagation on all plans). For per-plan propagation handlers, see
192
+ # ExecutionEngine#add_propagation_handler.
193
+ #
194
+ # See also ExecutionEngine.remove_propagation_handler
195
+ def add_propagation_handler(proc_obj = nil, &block)
196
+ proc_obj ||= block
197
+ check_arity proc_obj, 1
198
+ propagation_handlers << proc_obj
199
+ proc_obj.object_id
200
+ end
201
+
202
+ # This method removes a propagation handler which has been added by
203
+ # ExecutionEngine.add_propagation_handler. THe +id+ value is the
204
+ # value returned by ExecutionEngine.add_propagation_handler.
205
+ def remove_propagation_handler(id)
206
+ propagation_handlers.delete_if { |p| p.object_id == id }
207
+ nil
208
+ end
209
+ end
210
+
211
+ # A set of block objects that have to be called at the beginning of every
212
+ # propagation phase. These objects are called in propagation context, which
213
+ # means that the events they would call or emit are injected in the
214
+ # propagation process itself.
215
+ attr_reader :propagation_handlers
216
+
217
+ # call-seq:
218
+ # engine.add_propagation_handler { |plan| ... }
219
+ #
220
+ # The propagation handlers are a set of block objects that have to be
221
+ # called at the beginning of every propagation phase for all plans.
222
+ # These objects are called in propagation context, which means that the
223
+ # events they would call or emit are injected in the propagation
224
+ # process itself.
225
+ #
226
+ # This method adds a new propagation handler. In its first form, the
227
+ # argument is the proc object to be added. In the second form, the
228
+ # block is taken the handler. In both cases, the method returns a value
229
+ # which can be used to remove the propagation handler later.
230
+ #
231
+ # See also #remove_propagation_handler
232
+ def add_propagation_handler(proc_obj = nil, &block)
233
+ proc_obj ||= block
234
+ check_arity proc_obj, 1
235
+ propagation_handlers << proc_obj
236
+ proc_obj.object_id
237
+ end
238
+
239
+ # This method removes a propagation handler which has been added by
240
+ # #add_propagation_handler. THe +id+ value is the value returned by
241
+ # #add_propagation_handler. In its first form, the argument is the proc
242
+ # object to be added. In the second form, the block is taken the
243
+ # handler. In both cases, the method returns a value which can be used
244
+ # to remove the propagation handler later.
245
+ #
246
+ # See also #add_propagation_handler
247
+ def remove_propagation_handler(id)
248
+ propagation_handlers.delete_if { |p| p.object_id == id }
249
+ nil
250
+ end
251
+
252
+ # call-seq:
253
+ # Roby.each_cycle { |plan| ... }
254
+ #
255
+ # Execute the given block at the beginning of each cycle, in propagation
256
+ # context.
257
+ #
258
+ # The returned value is an ID that can be used to remove the handler using
259
+ # #remove_propagation_handler
260
+ def each_cycle(&block)
261
+ add_propagation_handler(block)
262
+ end
263
+
264
+ # The scheduler is the object which handles non-generic parts of the
265
+ # propagation cycle. For now, its #initial_events method is called at
266
+ # the beginning of each propagation cycle and can call or emit a set of
267
+ # events.
268
+ #
269
+ # See Schedulers::Basic
270
+ attr_accessor :scheduler
271
+
272
+ # True if we are currently in the propagation stage
273
+ def gathering?; !!@propagation end
274
+
275
+ attr_predicate :allow_propagation
276
+
277
+ # The set of source events for the current propagation action. This is a
278
+ # mix of EventGenerator and Event objects.
279
+ attr_reader :propagation_sources
280
+ # The set of events extracted from #sources
281
+ def propagation_source_events
282
+ result = ValueSet.new
283
+ for ev in @propagation_sources
284
+ if ev.respond_to?(:generator)
285
+ result << ev
286
+ end
287
+ end
288
+ result
289
+ end
290
+
291
+ # The set of generators extracted from #sources
292
+ def propagation_source_generators
293
+ result = ValueSet.new
294
+ for ev in @propagation_sources
295
+ result << if ev.respond_to?(:generator)
296
+ ev.generator
297
+ else
298
+ ev
299
+ end
300
+ end
301
+ result
302
+ end
303
+
304
+ # The set of pending delayed events. This is an array of the form
305
+ #
306
+ # [[time, is_forward, source, target, context], ...]
307
+ #
308
+ # See #add_event_delay for more information
309
+ attr_reader :delayed_events
310
+
311
+ # Adds a propagation step to be performed when the current time is
312
+ # greater than +time+. The propagation step is a signal if +is_forward+
313
+ # is false and a forward otherwise.
314
+ #
315
+ # This method should not be called directly. Use #add_event_propagation
316
+ # with the appropriate +timespec+ argument.
317
+ #
318
+ # See also #delayed_events and #execute_delayed_events
319
+ def add_event_delay(time, is_forward, source, target, context)
320
+ delayed_events << [time, is_forward, source, target, context]
321
+ end
322
+
323
+ # Adds the events in +delayed_events+ whose time has passed into the
324
+ # propagation. This must be called in propagation context.
325
+ #
326
+ # See #add_event_delay and #delayed_events
327
+ def execute_delayed_events
328
+ reftime = Time.now
329
+ delayed_events.delete_if do |time, forward, source, signalled, context|
330
+ if time <= reftime
331
+ add_event_propagation(forward, [source], signalled, context, nil)
332
+ true
333
+ end
334
+ end
335
+ end
336
+
337
+ # Called by #plan when an event has been finalized
338
+ def finalized_event(event)
339
+ event.unreachable!(nil, plan)
340
+ delayed_events.delete_if { |_, _, _, signalled, _| signalled == event }
341
+ end
342
+
343
+ # Sets up a propagation context, yielding the block in it. During this
344
+ # propagation stage, all calls to #emit and #call are stored in an
345
+ # internal hash of the form:
346
+ # target => [forward_sources, signal_sources]
347
+ #
348
+ # where the two +_sources+ are arrays of the form
349
+ # [[source, context], ...]
350
+ #
351
+ # The method returns the resulting hash. Use #gathering? to know if the
352
+ # current engine is in a propagation context, and #add_event_propagation
353
+ # to add a new entry to this set.
354
+ def gather_propagation(initial_set = Hash.new)
355
+ raise InternalError, "nested call to #gather_propagation" if gathering?
356
+ @propagation = initial_set
357
+
358
+ propagation_context(nil) { yield }
359
+
360
+ return @propagation
361
+ ensure
362
+ @propagation = nil
363
+ end
364
+
365
+ # Converts the Exception object +error+ into a Roby::ExecutionException
366
+ def self.to_execution_exception(error)
367
+ if error.kind_of?(Roby::ExecutionException)
368
+ error
369
+ else
370
+ Roby::ExecutionException.new(error)
371
+ end
372
+ end
373
+
374
+ # If called in execution context, adds the plan-based error +e+ to be
375
+ # handled later in the execution cycle. Otherwise, calls
376
+ # #add_framework_error
377
+ def add_error(e)
378
+ if @propagation_exceptions
379
+ plan_exception = ExecutionEngine.to_execution_exception(e)
380
+ @propagation_exceptions << plan_exception
381
+ else
382
+ if e.respond_to?(:error) && e.error
383
+ add_framework_error(e.error, "error outside error handling")
384
+ else
385
+ add_framework_error(e, "error outside error handling")
386
+ end
387
+ end
388
+ end
389
+
390
+ # Yields to the block, calling #add_framework_error if an exception is
391
+ # raised
392
+ def gather_framework_errors(source)
393
+ yield
394
+ rescue Exception => e
395
+ add_framework_error(e, source)
396
+ end
397
+
398
+ # If called in execution context, adds the framework error +error+ to be
399
+ # handled later in the execution cycle. Otherwise, either raises the
400
+ # error again if Application#abort_on_application_exception is true. IF
401
+ # abort_on_application_exception is false, simply displays a warning
402
+ def add_framework_error(error, source)
403
+ if @application_exceptions
404
+ @application_exceptions << [error, source]
405
+ elsif Roby.app.abort_on_application_exception? || error.kind_of?(SignalException)
406
+ raise error, "in #{source}: #{error.message}", error.backtrace
407
+ else
408
+ ExecutionEngine.error "Application error in #{source}"
409
+ Roby.format_exception(error).each do |line|
410
+ Roby.warn line
411
+ end
412
+ end
413
+ end
414
+
415
+ # Sets the source_event and source_generator variables according
416
+ # to +source+. +source+ is the +from+ argument of #add_event_propagation
417
+ def propagation_context(sources)
418
+ raise InternalError, "not in a gathering context in #fire" unless gathering?
419
+
420
+ if sources
421
+ current_sources = sources
422
+ @propagation_sources = sources
423
+ else
424
+ @propagation_sources = []
425
+ end
426
+
427
+ yield @propagation
428
+
429
+ ensure
430
+ @propagation_sources = sources
431
+ end
432
+
433
+ # Adds a propagation to the next propagation step: it registers a
434
+ # propagation step to be performed between +source+ and +target+ with
435
+ # the given +context+. If +is_forward+ is true, the propagation will be
436
+ # a forwarding, otherwise it is a signal.
437
+ #
438
+ # If +timespec+ is not nil, it defines a delay to be applied before
439
+ # calling the target event.
440
+ #
441
+ # See #gather_propagation
442
+ def add_event_propagation(is_forward, from, target, context, timespec)
443
+ if target.plan != plan
444
+ raise Roby::EventNotExecutable.new(target), "#{target} not in executed plan"
445
+ end
446
+
447
+ step = (@propagation[target] ||= [nil, nil])
448
+ from = [nil] unless from && !from.empty?
449
+
450
+ step = if is_forward then (step[0] ||= [])
451
+ else (step[1] ||= [])
452
+ end
453
+
454
+ from.each do |ev|
455
+ step << ev << context << timespec
456
+ end
457
+ end
458
+
459
+ # Calls its block in a #gather_propagation context and propagate events
460
+ # that have been called and/or emitted by the block
461
+ #
462
+ # If a block is given, it is called with the initial set of events: the
463
+ # events we should consider as already emitted in the following propagation.
464
+ # +seeds+ si a list of procs which should be called to initiate the propagation
465
+ # (i.e. build an initial set of events)
466
+ def propagate_events(seeds = nil)
467
+ if @propagation_exceptions
468
+ raise InternalError, "recursive call to propagate_events"
469
+ end
470
+
471
+ @propagation_id = (@propagation_id += 1)
472
+ @propagation_exceptions = []
473
+
474
+ initial_set = []
475
+ next_step = gather_propagation do
476
+ gather_framework_errors('initial set setup') { yield(initial_set) } if block_given?
477
+ gather_framework_errors('distributed events') { Roby::Distributed.process_pending }
478
+ gather_framework_errors('delayed events') { execute_delayed_events }
479
+ while !process_once.empty?
480
+ p = process_once.pop
481
+ gather_framework_errors("'once' block #{p}") { p.call }
482
+ end
483
+ if seeds
484
+ for s in seeds
485
+ gather_framework_errors("seed #{s}") { s.call }
486
+ end
487
+ end
488
+ if scheduler
489
+ gather_framework_errors('scheduler') { scheduler.initial_events }
490
+ end
491
+ for h in self.class.propagation_handlers
492
+ gather_framework_errors("propagation handler #{h}") { h.call(plan) }
493
+ end
494
+ for h in propagation_handlers
495
+ gather_framework_errors("propagation handler #{h}") { h.call(plan) }
496
+ end
497
+ end
498
+
499
+ while !next_step.empty?
500
+ next_step = event_propagation_step(next_step)
501
+ end
502
+ @propagation_exceptions
503
+
504
+ ensure
505
+ @propagation_exceptions = nil
506
+ end
507
+
508
+ # Validates +timespec+ as a delay specification. A valid delay
509
+ # specification is either +nil+ or a hash, in which case two forms are
510
+ # possible:
511
+ #
512
+ # :at => absolute_time
513
+ # :delay => number
514
+ #
515
+ def self.validate_timespec(timespec)
516
+ if timespec
517
+ timespec = validate_options timespec, [:delay, :at]
518
+ end
519
+ end
520
+
521
+ # Returns a Time object which represents the absolute point in time
522
+ # referenced by +timespec+ in the context of delaying a propagation
523
+ # between +source+ and +target+.
524
+ #
525
+ # See validate_timespec for more information
526
+ def self.make_delay(timeref, source, target, timespec)
527
+ if delay = timespec[:delay] then timeref + delay
528
+ elsif at = timespec[:at] then at
529
+ else
530
+ raise ArgumentError, "invalid timespec #{timespec}"
531
+ end
532
+ end
533
+
534
+ # The topological ordering of events w.r.t. the Precedence relation.
535
+ # This gets updated on-demand when the event relations change.
536
+ attr_reader :event_ordering
537
+ # The event => index hash which give the propagation priority for each
538
+ # event
539
+ attr_reader :event_priorities
540
+
541
+ # call-seq:
542
+ # next_event(pending) => event, propagation_info
543
+ #
544
+ # Determines the event in +current_step+ which should be signalled now.
545
+ # Removes it from the set and returns the event and the associated
546
+ # propagation information.
547
+ #
548
+ # See #gather_propagation for the format of the returned # +propagation_info+
549
+ def next_event(pending)
550
+ # this variable is 2 if selected_event is being forwarded, 1 if it
551
+ # is both forwarded and signalled and 0 if it is only signalled
552
+ priority, selected_event = nil
553
+ for propagation_step in pending
554
+ target_event = propagation_step[0]
555
+ forwards, signals = *propagation_step[1]
556
+ target_priority = if forwards && signals then 1
557
+ elsif signals then 0
558
+ else 2
559
+ end
560
+
561
+ do_select = if selected_event
562
+ if EventStructure::Precedence.reachable?(selected_event, target_event)
563
+ false
564
+ elsif EventStructure::Precedence.reachable?(target_event, selected_event)
565
+ true
566
+ else
567
+ priority < target_priority
568
+ end
569
+ else
570
+ true
571
+ end
572
+
573
+ if do_select
574
+ selected_event = target_event
575
+ priority = target_priority
576
+ end
577
+ end
578
+ [selected_event, *pending.delete(selected_event)]
579
+ end
580
+
581
+ # call-seq:
582
+ # prepare_propagation(target, is_forward, info) => source_events, source_generators, context
583
+ # prepare_propagation(target, is_forward, info) => nil
584
+ #
585
+ # Parses the propagation information +info+ in the context of a
586
+ # signalling if +is_forward+ is true and a forwarding otherwise.
587
+ # +target+ is the target event.
588
+ #
589
+ # The method adds the appropriate delayed events using #add_event_delay,
590
+ # and returns either nil if no propagation is to be performed, or the
591
+ # propagation source events, generators and context.
592
+ #
593
+ # The format of +info+ is the same as the hash values described in
594
+ # #gather_propagation.
595
+ def prepare_propagation(target, is_forward, info)
596
+ timeref = Time.now
597
+
598
+ source_events, source_generators, context = ValueSet.new, ValueSet.new, []
599
+
600
+ delayed = true
601
+ info.each_slice(3) do |src, ctxt, time|
602
+ if time && (delay = ExecutionEngine.make_delay(timeref, src, target, time))
603
+ add_event_delay(delay, is_forward, src, target, ctxt)
604
+ next
605
+ end
606
+
607
+ delayed = false
608
+
609
+ # Merge identical signals. Needed because two different event handlers
610
+ # can both call #emit, and two signals are set up
611
+ if src
612
+ if src.respond_to?(:generator)
613
+ source_events << src
614
+ source_generators << src.generator
615
+ else
616
+ source_generators << src
617
+ end
618
+ end
619
+ if ctxt
620
+ context.concat ctxt
621
+ end
622
+ end
623
+
624
+ unless delayed
625
+ [source_events, source_generators, (context unless context.empty?)]
626
+ end
627
+ end
628
+
629
+
630
+ # Propagate one step
631
+ #
632
+ # +current_step+ describes all pending emissions and calls.
633
+ #
634
+ # This method calls ExecutionEngine.next_event to get the description of the
635
+ # next event to call. If there are signals going to this event, they are
636
+ # processed and the forwardings will be treated in the next step.
637
+ #
638
+ # The method returns the next set of pending emissions and calls, adding
639
+ # the forwardings and signals that the propagation of the considered event
640
+ # have added.
641
+ def event_propagation_step(current_step)
642
+ signalled, forward_info, call_info = next_event(current_step)
643
+
644
+ next_step = nil
645
+ if call_info
646
+ source_events, source_generators, context = prepare_propagation(signalled, false, call_info)
647
+ if source_events
648
+ for source_ev in source_events
649
+ source_ev.generator.signalling(source_ev, signalled)
650
+ end
651
+
652
+ if signalled.self_owned?
653
+ next_step = gather_propagation(current_step) do
654
+ propagation_context(source_events | source_generators) do |result|
655
+ begin
656
+ signalled.call_without_propagation(context)
657
+ rescue Roby::LocalizedError => e
658
+ signalled.emit_failed(e)
659
+ rescue Exception => e
660
+ signalled.emit_failed(Roby::CommandFailed.new(e, signalled))
661
+ end
662
+ end
663
+ end
664
+ end
665
+ end
666
+
667
+ if forward_info
668
+ next_step ||= Hash.new
669
+ next_step[signalled] ||= []
670
+ next_step[signalled][0] ||= []
671
+ next_step[signalled][0].concat forward_info
672
+ end
673
+
674
+ elsif forward_info
675
+ source_events, source_generators, context = prepare_propagation(signalled, true, forward_info)
676
+ if source_events
677
+ for source_ev in source_events
678
+ source_ev.generator.forwarding(source_ev, signalled)
679
+ end
680
+
681
+ # If the destination event is not owned, but if the peer is not
682
+ # connected, the event is our responsibility now.
683
+ if signalled.self_owned? || !signalled.owners.any? { |peer| peer != Roby::Distributed && peer.connected? }
684
+ next_step = gather_propagation(current_step) do
685
+ propagation_context(source_events | source_generators) do |result|
686
+ begin
687
+ signalled.emit_without_propagation(context)
688
+ rescue Roby::LocalizedError => e
689
+ add_error(e)
690
+ rescue Exception => e
691
+ add_error(Roby::EmissionFailed.new(e, signalled))
692
+ end
693
+ end
694
+ end
695
+ end
696
+ end
697
+ end
698
+
699
+ current_step.merge!(next_step) if next_step
700
+ current_step
701
+ end
702
+
703
+ # Checks if +error+ is being repaired in the corresponding plan. Note that
704
+ # +error+ is supposed to be the original exception, not the corresponding
705
+ # ExecutionException object
706
+ def remove_inhibited_exceptions(exceptions)
707
+ exceptions.find_all do |e, _|
708
+ error = e.exception
709
+ if !error.respond_to?(:failed_event) ||
710
+ !(failure_point = error.failed_event)
711
+ true
712
+ else
713
+ plan.repairs_for(failure_point).empty?
714
+ end
715
+ end
716
+ end
717
+
718
+ # Removes the set of repairs defined on #plan that are not useful
719
+ # anymore, and returns it.
720
+ def remove_useless_repairs
721
+ finished_repairs = plan.repairs.dup.delete_if { |_, task| task.starting? || task.running? }
722
+ for repair in finished_repairs
723
+ plan.remove_repair(repair[1])
724
+ end
725
+
726
+ finished_repairs
727
+ end
728
+
729
+ # Performs exception propagation for the given ExecutionException objects
730
+ # Returns all exceptions which have found no handlers in the task hierarchy
731
+ def propagate_exceptions(exceptions)
732
+ fatal = [] # the list of exceptions for which no handler has been found
733
+
734
+ # Remove finished repairs. Those are still considered during this cycle,
735
+ # as it is possible that some actions have been scheduled for the
736
+ # beginning of the next cycle through #once
737
+ finished_repairs = remove_useless_repairs
738
+ # Remove remove exceptions for which a repair exists
739
+ exceptions = remove_inhibited_exceptions(exceptions)
740
+
741
+ # Install new repairs based on the HandledBy task relation. If a repair
742
+ # is installed, remove the exception from the set of errors to handle
743
+ exceptions.delete_if do |e, _|
744
+ # Check for handled_by relations which would be able to handle +e+
745
+ error = e.exception
746
+ next unless (failed_event = error.failed_event)
747
+ next unless (failed_task = error.failed_task)
748
+ next if finished_repairs.has_key?(failed_event)
749
+
750
+ failed_generator = error.failed_generator
751
+
752
+ repair = failed_task.find_error_handler do |repairing_task, event_set|
753
+ event_set.find do |repaired_generator|
754
+ repaired_generator = failed_task.event(repaired_generator)
755
+
756
+ !repairing_task.finished? &&
757
+ (repaired_generator == failed_generator ||
758
+ Roby::EventStructure::Forwarding.reachable?(failed_generator, repaired_generator))
759
+ end
760
+ end
761
+
762
+ if repair
763
+ plan.add_repair(failed_event, repair)
764
+ if repair.pending?
765
+ once { repair.start! }
766
+ end
767
+ true
768
+ else
769
+ false
770
+ end
771
+ end
772
+
773
+ while !exceptions.empty?
774
+ by_task = Hash.new { |h, k| h[k] = Array.new }
775
+ by_task = exceptions.inject(by_task) do |by_task, (e, parents)|
776
+ unless e.task
777
+ Roby.log_exception(e.exception, Roby, :fatal)
778
+ raise NotImplementedError, "we do not yet handle exceptions from external event generators. Got #{e.exception.full_message}"
779
+ end
780
+ parents ||= e.task.parent_objects(Roby::TaskStructure::Hierarchy)
781
+
782
+ has_parent = false
783
+ [*parents].each do |parent|
784
+ next if parent.finished?
785
+
786
+ if has_parent # we have more than one parent
787
+ e = e.fork
788
+ end
789
+
790
+ parent_exceptions = by_task[parent]
791
+ if s = parent_exceptions.find { |s| s.siblings.include?(e) }
792
+ s.merge(e)
793
+ else parent_exceptions << e
794
+ end
795
+
796
+ has_parent = true
797
+ end
798
+
799
+ # Add unhandled exceptions to the fatal set. Merge siblings
800
+ # exceptions if possible
801
+ unless has_parent
802
+ if s = fatal.find { |s| s.siblings.include?(e) }
803
+ s.merge(e)
804
+ else fatal << e
805
+ end
806
+ end
807
+
808
+ by_task
809
+ end
810
+
811
+ parent_trees = by_task.map do |task, _|
812
+ [task, task.reverse_generated_subgraph(Roby::TaskStructure::Hierarchy)]
813
+ end
814
+
815
+ # Handle the exception in all tasks that are in no other parent trees
816
+ new_exceptions = ValueSet.new
817
+ by_task.each do |task, task_exceptions|
818
+ if parent_trees.find { |t, tree| t != task && tree.include?(task) }
819
+ task_exceptions.each { |e| new_exceptions << [e, [task]] }
820
+ next
821
+ end
822
+
823
+ task_exceptions.each do |e|
824
+ next if e.handled?
825
+ handled = task.handle_exception(e)
826
+
827
+ if handled
828
+ handled_exception(e, task)
829
+ e.handled = true
830
+ else
831
+ # We do not have the framework to handle concurrent repairs
832
+ # For now, the first handler is the one ...
833
+ new_exceptions << e
834
+ e.trace << task
835
+ end
836
+ end
837
+ end
838
+
839
+ exceptions = new_exceptions
840
+ end
841
+
842
+ if !fatal.empty?
843
+ Roby::ExecutionEngine.debug do
844
+ "remaining fatal exceptions: #{fatal.map(&:exception).map(&:to_s).join(", ")}"
845
+ end
846
+ end
847
+ # Call global exception handlers for exceptions in +fatal+. Return the
848
+ # set of still unhandled exceptions
849
+ fatal.
850
+ find_all { |e| !e.handled? }.
851
+ reject { |e| plan.handle_exception(e) }
852
+ end
853
+
854
+ # A set of proc objects which should be executed at the beginning of the
855
+ # next execution cycle.
856
+ attr_reader :process_once
857
+
858
+ # Schedules +block+ to be called at the beginning of the next execution
859
+ # cycle, in propagation context.
860
+ def once(&block)
861
+ process_once.push block
862
+ end
863
+
864
+ # The set of errors which have been generated outside of the plan's
865
+ # control. For now, those errors cause the whole controller to shut
866
+ # down.
867
+ attr_reader :application_exceptions
868
+ def clear_application_exceptions
869
+ result, @application_exceptions = @application_exceptions, nil
870
+ result
871
+ end
872
+
873
+ # Abort the control loop because of +exceptions+
874
+ def reraise(exceptions)
875
+ if exceptions.size == 1
876
+ e = exceptions.first
877
+ if e.kind_of?(Roby::ExecutionException)
878
+ e = e.exception
879
+ end
880
+ raise e, e.message, e.backtrace
881
+ else
882
+ raise Aborting.new(exceptions)
883
+ end
884
+ end
885
+
886
+ # Process the pending events. The time at each event loop step
887
+ # is saved into +stats+.
888
+ def process_events(stats = {:start => Time.now})
889
+ @application_exceptions = []
890
+
891
+ add_timepoint(stats, :real_start)
892
+
893
+ # Gather new events and propagate them
894
+ events_errors = begin
895
+ old_allow_propagation, @allow_propagation = @allow_propagation, true
896
+ propagate_events
897
+ ensure @allow_propagation = old_allow_propagation
898
+ end
899
+ add_timepoint(stats, :events)
900
+
901
+ # HACK: events_errors is sometime nil here. It shouldn't
902
+ events_errors ||= []
903
+
904
+ # Generate exceptions from task structure
905
+ structure_errors = plan.check_structure
906
+ add_timepoint(stats, :structure_check)
907
+
908
+ # Propagate the errors. Note that the plan repairs are taken into
909
+ # account in ExecutionEngine.propagate_exceptions drectly. We keep
910
+ # event and structure errors separate since in the first case there
911
+ # is not two-stage handling (all errors that have not been handled
912
+ # are fatal), and in the second case we call #check_structure
913
+ # again to get the remaining errors
914
+ events_errors = propagate_exceptions(events_errors)
915
+ propagate_exceptions(structure_errors)
916
+ add_timepoint(stats, :exception_propagation)
917
+
918
+ # Get the remaining problems in the plan structure, and act on it
919
+ fatal_structure_errors = remove_inhibited_exceptions(plan.check_structure)
920
+ fatal_errors = fatal_structure_errors.to_a + events_errors
921
+ if !fatal_errors.empty?
922
+ Roby::ExecutionEngine.info "EE: #{fatal_errors.size} fatal exceptions remaining"
923
+ kill_tasks = fatal_errors.inject(ValueSet.new) do |kill_tasks, (error, tasks)|
924
+ tasks ||= [*error.origin]
925
+ for parent in [*tasks]
926
+ new_tasks = parent.reverse_generated_subgraph(Roby::TaskStructure::Hierarchy) - plan.force_gc
927
+ if !new_tasks.empty?
928
+ fatal_exception(error, new_tasks)
929
+ end
930
+ kill_tasks.merge(new_tasks)
931
+ end
932
+ kill_tasks
933
+ end
934
+ if !kill_tasks.empty?
935
+ Roby::ExecutionEngine.info do
936
+ Roby::ExecutionEngine.info "EE: will kill the following tasks because of unhandled exceptions:"
937
+ kill_tasks.each do |task|
938
+ Roby::ExecutionEngine.info " " + task.to_s
939
+ end
940
+ ""
941
+ end
942
+ end
943
+ end
944
+ add_timepoint(stats, :exceptions_fatal)
945
+
946
+ garbage_collect(kill_tasks)
947
+ add_timepoint(stats, :garbage_collect)
948
+
949
+ application_errors, @application_exceptions =
950
+ @application_exceptions, nil
951
+ for error, origin in application_errors
952
+ add_framework_error(error, origin)
953
+ end
954
+
955
+ if Roby.app.abort_on_exception? && !fatal_errors.empty?
956
+ reraise(fatal_errors.map { |e, _| e })
957
+ end
958
+
959
+ ensure
960
+ @application_exceptions = nil
961
+ end
962
+
963
+ # Hook called when a set of tasks is being killed because of an exception
964
+ def fatal_exception(error, tasks)
965
+ super if defined? super
966
+ Roby.format_exception(error.exception).each do |line|
967
+ ExecutionEngine.warn line
968
+ end
969
+ end
970
+
971
+ # Hook called when an exception +e+ has been handled by +task+
972
+ def handled_exception(e, task); super if defined? super end
973
+
974
+ # Kills and removes all unneeded tasks. +force_on+ is a set of task
975
+ # whose garbage-collection must be performed, even though those tasks
976
+ # are actually useful for the system. This is used to properly kill
977
+ # tasks for which errors have been detected.
978
+ def garbage_collect(force_on = nil)
979
+ if force_on && !force_on.empty?
980
+ ExecutionEngine.info "GC: adding #{force_on.size} tasks in the force_gc set"
981
+ plan.force_gc.merge(force_on.to_value_set)
982
+ end
983
+
984
+ # The set of tasks for which we queued stop! at this cycle
985
+ # #finishing? is false until the next event propagation cycle
986
+ finishing = ValueSet.new
987
+ did_something = true
988
+ while did_something
989
+ did_something = false
990
+
991
+ tasks = plan.unneeded_tasks | plan.force_gc
992
+ local_tasks = plan.local_tasks & tasks
993
+ remote_tasks = tasks - local_tasks
994
+
995
+ # Remote tasks are simply removed, regardless of other concerns
996
+ for t in remote_tasks
997
+ ExecutionEngine.debug { "GC: removing the remote task #{t}" }
998
+ plan.remove_object(t)
999
+ end
1000
+
1001
+ break if local_tasks.empty?
1002
+
1003
+ if local_tasks.all? { |t| t.pending? || t.finished? }
1004
+ local_tasks.each do |t|
1005
+ ExecutionEngine.debug { "GC: #{t} is not running, removed" }
1006
+ plan.garbage(t)
1007
+ plan.remove_object(t)
1008
+ end
1009
+ break
1010
+ end
1011
+
1012
+ # Mark all root local_tasks as garbage
1013
+ roots = nil
1014
+ 2.times do |i|
1015
+ roots = local_tasks.find_all do |t|
1016
+ if t.root?
1017
+ plan.garbage(t)
1018
+ true
1019
+ else
1020
+ ExecutionEngine.debug { "GC: ignoring #{t}, it is not root" }
1021
+ false
1022
+ end
1023
+ end
1024
+
1025
+ break if i == 1 || !roots.empty?
1026
+
1027
+ # There is a cycle somewhere. Try to break it by removing
1028
+ # weak relations within elements of local_tasks
1029
+ ExecutionEngine.debug "cycle found, removing weak relations"
1030
+
1031
+ local_tasks.each do |t|
1032
+ t.each_graph do |rel|
1033
+ rel.remove(t) if rel.weak?
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ (roots.to_value_set - finishing - plan.gc_quarantine).each do |local_task|
1039
+ if local_task.pending?
1040
+ ExecutionEngine.info "GC: removing pending task #{local_task}"
1041
+ plan.remove_object(local_task)
1042
+ did_something = true
1043
+ elsif local_task.starting?
1044
+ # wait for task to be started before killing it
1045
+ ExecutionEngine.debug { "GC: #{local_task} is starting" }
1046
+ elsif !local_task.running?
1047
+ ExecutionEngine.debug { "GC: #{local_task} is not running, removed" }
1048
+ plan.remove_object(local_task)
1049
+ did_something = true
1050
+ elsif !local_task.finishing?
1051
+ if local_task.event(:stop).controlable?
1052
+ ExecutionEngine.debug { "GC: queueing #{local_task}/stop" }
1053
+ if !local_task.respond_to?(:stop!)
1054
+ ExecutionEngine.fatal "something fishy: #{local_task}/stop is controlable but there is no #stop! method"
1055
+ plan.gc_quarantine << local_task
1056
+ else
1057
+ finishing << local_task
1058
+ once do
1059
+ ExecutionEngine.info { "GC: stopping #{local_task}" }
1060
+ local_task.stop!(nil)
1061
+ end
1062
+ end
1063
+ else
1064
+ ExecutionEngine.warn "GC: ignored #{local_task}, it cannot be stopped"
1065
+ plan.gc_quarantine << local_task
1066
+ end
1067
+ elsif local_task.finishing?
1068
+ ExecutionEngine.debug { "GC: waiting for #{local_task} to finish" }
1069
+ else
1070
+ ExecutionEngine.warn "GC: ignored #{local_task}"
1071
+ end
1072
+ end
1073
+ end
1074
+
1075
+ plan.unneeded_events.each do |event|
1076
+ plan.remove_object(event)
1077
+ end
1078
+ end
1079
+
1080
+ # Do not sleep or call Thread#pass if there is less that
1081
+ # this much time left in the cycle
1082
+ SLEEP_MIN_TIME = 0.01
1083
+
1084
+ # The priority of the control thread
1085
+ THREAD_PRIORITY = 10
1086
+
1087
+ # Blocks until at least once execution cycle has been done
1088
+ def wait_one_cycle
1089
+ current_cycle = execute { cycle_index }
1090
+ while current_cycle == execute { cycle_index }
1091
+ raise ExecutionQuitError if !running?
1092
+ sleep(cycle_length)
1093
+ end
1094
+ end
1095
+
1096
+ # Calls the periodic blocks which should be called
1097
+ def self.call_every(plan) # :nodoc:
1098
+ engine = plan.engine
1099
+ now = engine.cycle_start
1100
+ length = engine.cycle_length
1101
+ engine.process_every.map! do |block, last_call, duration|
1102
+ begin
1103
+ # Check if the nearest timepoint is the beginning of
1104
+ # this cycle or of the next cycle
1105
+ if !last_call || (duration - (now - last_call)) < length / 2
1106
+ block.call
1107
+ last_call = now
1108
+ end
1109
+ rescue Exception => e
1110
+ engine.add_framework_error(e, "#call_every, in #{block}")
1111
+ end
1112
+ [block, last_call, duration]
1113
+ end
1114
+ end
1115
+
1116
+ # A list of threads which are currently waitiing for the control thread
1117
+ # (see for instance Roby.execute)
1118
+ #
1119
+ # #run will raise ExecutionQuitError on this threads if they
1120
+ # are still waiting while the control is quitting
1121
+ attr_reader :waiting_threads
1122
+
1123
+ # A set of blocks that are called at each cycle end
1124
+ attr_reader :at_cycle_end_handlers
1125
+
1126
+ # Call +block+ at the end of the execution cycle
1127
+ def at_cycle_end(&block)
1128
+ at_cycle_end_handlers << block
1129
+ end
1130
+
1131
+ # A set of blocks which are called every cycle
1132
+ attr_reader :process_every
1133
+
1134
+ # Call +block+ every +duration+ seconds. Note that +duration+ is round
1135
+ # up to the cycle size (time between calls is *at least* duration)
1136
+ #
1137
+ # The returned value is the periodic handler ID. It can be passed to
1138
+ # #remove_periodic_handler to undefine it.
1139
+ def every(duration, &block)
1140
+ once do
1141
+ block.call
1142
+ process_every << [block, cycle_start, duration]
1143
+ end
1144
+ block.object_id
1145
+ end
1146
+
1147
+ # Removes a periodic handler defined by #every. +id+ is the value
1148
+ # returned by #every.
1149
+ def remove_periodic_handler(id)
1150
+ execute do
1151
+ process_every.delete_if { |spec| spec[0].object_id == id }
1152
+ end
1153
+ end
1154
+
1155
+ # The execution thread if there is one running
1156
+ attr_accessor :thread
1157
+ # True if an execution thread is running
1158
+ def running?; !!@thread end
1159
+
1160
+ # The cycle length in seconds
1161
+ attr_reader :cycle_length
1162
+
1163
+ # The starting Time of this cycle
1164
+ attr_reader :cycle_start
1165
+
1166
+ # The number of this cycle since the beginning
1167
+ attr_reader :cycle_index
1168
+
1169
+ # True if the current thread is the execution thread of this engine
1170
+ #
1171
+ # See #outside_control? for a discussion of the use of #inside_control?
1172
+ # and #outside_control? when testing the threading context
1173
+ def inside_control?
1174
+ t = thread
1175
+ !t || t == Thread.current
1176
+ end
1177
+
1178
+ # True if the current thread is not the execution thread of this
1179
+ # engine, or if there is not control thread. When you check the current
1180
+ # thread context, always use a negated form. Do not do
1181
+ #
1182
+ # if Roby.inside_control?
1183
+ # ERROR
1184
+ # end
1185
+ #
1186
+ # Do instead
1187
+ #
1188
+ # if !Roby.outside_control?
1189
+ # ERROR
1190
+ # end
1191
+ #
1192
+ # Since the first form will fail if there is no control thread, while
1193
+ # the second form will work. Use the first form only if you require
1194
+ # that there actually IS a control thread.
1195
+ def outside_control?
1196
+ t = thread
1197
+ !t || t != Thread.current
1198
+ end
1199
+
1200
+ # Main event loop. Valid options are
1201
+ # cycle:: the cycle duration in seconds (default: 0.1)
1202
+ def run(options = {})
1203
+ if running?
1204
+ raise "there is already a control running in thread #{@thread}"
1205
+ end
1206
+
1207
+ options = validate_options options, :cycle => 0.1
1208
+
1209
+ @quit = 0
1210
+ @allow_propagation = false
1211
+
1212
+ # Start the control thread and wait for @thread to be set
1213
+ Roby.condition_variable(true) do |cv, mt|
1214
+ mt.synchronize do
1215
+ @thread = Thread.new do
1216
+ @thread = Thread.current
1217
+ @thread.priority = THREAD_PRIORITY
1218
+
1219
+ begin
1220
+ mt.synchronize { cv.signal }
1221
+ @cycle_length = options[:cycle]
1222
+ event_loop
1223
+
1224
+ ensure
1225
+ Roby.synchronize do
1226
+ # reset the options only if we are in the control thread
1227
+ @thread = nil
1228
+ waiting_threads.each do |th|
1229
+ th.raise ExecutionQuitError
1230
+ end
1231
+ finalizers.each { |blk| blk.call rescue nil }
1232
+ @quit = 0
1233
+ @allow_propagation = true
1234
+ end
1235
+ end
1236
+ end
1237
+ cv.wait(mt)
1238
+ end
1239
+ end
1240
+ end
1241
+
1242
+ attr_reader :last_stop_count # :nodoc:
1243
+
1244
+ # Sets up the plan for clearing: it discards all missions and undefines
1245
+ # all permanent tasks and events.
1246
+ #
1247
+ # Returns nil if the plan is cleared, and the set of remaining tasks
1248
+ # otherwise. Note that quaranteened tasks are not counted as remaining,
1249
+ # as it is not possible for the execution engine to stop them.
1250
+ def clear
1251
+ Roby.synchronize do
1252
+ plan.missions.dup.each { |t| plan.unmark_mission(t) }
1253
+ plan.permanent_tasks.dup.each { |t| plan.unmark_permanent(t) }
1254
+ plan.permanent_events.dup.each { |t| plan.unmark_permanent(t) }
1255
+ plan.force_gc.merge( plan.known_tasks )
1256
+
1257
+ quaranteened_subplan = plan.useful_task_component(nil, ValueSet.new, plan.gc_quarantine.dup)
1258
+ remaining = plan.known_tasks - quaranteened_subplan
1259
+
1260
+ if remaining.empty?
1261
+ # Have to call #garbage_collect one more to make
1262
+ # sure that unneeded events are removed as well
1263
+ garbage_collect
1264
+ # Done cleaning the tasks, clear the remains
1265
+ plan.transactions.each do |trsc|
1266
+ trsc.discard_transaction if trsc.self_owned?
1267
+ end
1268
+ plan.clear
1269
+ return
1270
+ end
1271
+
1272
+ if last_stop_count != remaining.size
1273
+ if last_stop_count == 0
1274
+ ExecutionEngine.info "control quitting. Waiting for #{remaining.size} tasks to finish (#{plan.size} tasks still in plan)"
1275
+ ExecutionEngine.info " " + remaining.to_a.join("\n ")
1276
+ else
1277
+ ExecutionEngine.info "waiting for #{remaining.size} tasks to finish (#{plan.size} tasks still in plan)"
1278
+ ExecutionEngine.info " #{remaining.to_a.join("\n ")}"
1279
+ end
1280
+ if plan.gc_quarantine.size != 0
1281
+ ExecutionEngine.info "#{plan.gc_quarantine.size} tasks in quarantine"
1282
+ end
1283
+ @last_stop_count = remaining.size
1284
+ end
1285
+ remaining
1286
+ end
1287
+ end
1288
+
1289
+ # How much time remains before the end of the cycle. Updated by
1290
+ # #add_timepoint
1291
+ attr_reader :remaining_cycle_time
1292
+
1293
+ # Adds to the stats the given duration as the expected duration of the
1294
+ # +name+ step. The field in +stats+ is named "expected_#{name}".
1295
+ def add_expected_duration(stats, name, duration)
1296
+ stats[:"expected_#{name}"] = Time.now + duration - stats[:start]
1297
+ end
1298
+
1299
+ # Adds in +stats+ the current time as a timepoint named +time+, and
1300
+ # update #remaining_cycle_time
1301
+ def add_timepoint(stats, name)
1302
+ stats[:end] = stats[name] = Time.now - stats[:start]
1303
+ @remaining_cycle_time = cycle_length - stats[:end]
1304
+ end
1305
+
1306
+ # If set to true, Roby will warn if the GC cannot be controlled by Roby
1307
+ attr_predicate :gc_warning?, true
1308
+
1309
+ # The main event loop. It returns when the execution engine is asked to
1310
+ # quit. In general, this does not need to be called direclty: use #run
1311
+ # to start the event loop in a separate thread.
1312
+ def event_loop
1313
+ @last_stop_count = 0
1314
+ @cycle_start = Time.now
1315
+ @cycle_index = 0
1316
+
1317
+ gc_enable_has_argument = begin
1318
+ GC.enable(true)
1319
+ true
1320
+ rescue
1321
+ if gc_warning?
1322
+ ExecutionEngine.warn "GC.enable does not accept an argument. GC will not be controlled by Roby"
1323
+ end
1324
+ false
1325
+ end
1326
+ stats = Hash.new
1327
+ if ObjectSpace.respond_to?(:live_objects)
1328
+ last_allocated_objects = ObjectSpace.allocated_objects
1329
+ end
1330
+ last_cpu_time = Process.times
1331
+ last_cpu_time = (last_cpu_time.utime + last_cpu_time.stime) * 1000
1332
+
1333
+ GC.start
1334
+ if gc_enable_has_argument
1335
+ already_disabled_gc = GC.disable
1336
+ end
1337
+ loop do
1338
+ begin
1339
+ if quitting?
1340
+ thread.priority = 0
1341
+ begin
1342
+ return if forced_exit? || !clear
1343
+ rescue Exception => e
1344
+ ExecutionEngine.warn "Execution thread failed to clean up"
1345
+ Roby.format_exception(e).each do |line|
1346
+ ExecutionEngine.warn line
1347
+ end
1348
+ return
1349
+ end
1350
+ end
1351
+
1352
+ while Time.now > cycle_start + cycle_length
1353
+ @cycle_start += cycle_length
1354
+ @cycle_index += 1
1355
+ end
1356
+ stats[:start] = cycle_start
1357
+ stats[:cycle_index] = cycle_index
1358
+
1359
+ Roby.synchronize do
1360
+ process_events(stats)
1361
+ end
1362
+
1363
+ @remaining_cycle_time = cycle_length - stats[:end]
1364
+
1365
+ # If the ruby interpreter we run on offers a true/false argument to
1366
+ # GC.enable, we disabled the GC and just run GC.enable(true) to make
1367
+ # it run immediately if needed. Then, we re-disable it just after.
1368
+ if gc_enable_has_argument && remaining_cycle_time > SLEEP_MIN_TIME
1369
+ GC.enable(true)
1370
+ GC.disable
1371
+ end
1372
+ add_timepoint(stats, :ruby_gc)
1373
+
1374
+ # Sleep if there is enough time for it
1375
+ if remaining_cycle_time > SLEEP_MIN_TIME
1376
+ add_expected_duration(stats, :sleep, remaining_cycle_time)
1377
+ sleep(remaining_cycle_time)
1378
+ end
1379
+ add_timepoint(stats, :sleep)
1380
+
1381
+ # Add some statistics and call cycle_end
1382
+ if defined? Roby::Log
1383
+ stats[:log_queue_size] = Roby::Log.logged_events.size
1384
+ end
1385
+ stats[:plan_task_count] = plan.known_tasks.size
1386
+ stats[:plan_event_count] = plan.free_events.size
1387
+ cpu_time = Process.times
1388
+ cpu_time = (cpu_time.utime + cpu_time.stime) * 1000
1389
+ stats[:cpu_time] = cpu_time - last_cpu_time
1390
+ last_cpu_time = cpu_time
1391
+
1392
+ if ObjectSpace.respond_to?(:live_objects)
1393
+ stats[:object_allocation] = ObjectSpace.allocated_objects - last_allocated_objects
1394
+ stats[:live_objects] = ObjectSpace.live_objects
1395
+ last_allocated_objects = ObjectSpace.allocated_objects
1396
+ end
1397
+ if ObjectSpace.respond_to?(:heap_slots)
1398
+ stats[:heap_slots] = ObjectSpace.heap_slots
1399
+ end
1400
+
1401
+ stats[:start] = [cycle_start.tv_sec, cycle_start.tv_usec]
1402
+ stats[:state] = Roby::State
1403
+ cycle_end(stats)
1404
+ stats = Hash.new
1405
+
1406
+ @cycle_start += cycle_length
1407
+ @cycle_index += 1
1408
+
1409
+ rescue Exception => e
1410
+ ExecutionEngine.warn "Execution thread quitting because of unhandled exception"
1411
+ Roby.format_exception(e).each do |line|
1412
+ ExecutionEngine.warn line
1413
+ end
1414
+ quit
1415
+ end
1416
+ end
1417
+
1418
+ ensure
1419
+ GC.enable if !already_disabled_gc
1420
+
1421
+ if !plan.known_tasks.empty?
1422
+ ExecutionEngine.warn "the following tasks are still present in the plan:"
1423
+ plan.known_tasks.each do |t|
1424
+ ExecutionEngine.warn " #{t}"
1425
+ end
1426
+ end
1427
+ end
1428
+
1429
+ # A set of proc objects which are to be called when the execution engine
1430
+ # quits.
1431
+ attr_reader :finalizers
1432
+
1433
+ # True if the control thread is currently quitting
1434
+ def quitting?; @quit > 0 end
1435
+ # True if the control thread is currently quitting
1436
+ def forced_exit?; @quit > 1 end
1437
+ # Make control quit
1438
+ def quit; @quit += 1 end
1439
+
1440
+ # Called at each cycle end
1441
+ def cycle_end(stats)
1442
+ super if defined? super
1443
+
1444
+ at_cycle_end_handlers.each do |handler|
1445
+ begin
1446
+ handler.call
1447
+ rescue Exception => e
1448
+ add_framework_error(e, "during cycle end handler #{handler}")
1449
+ end
1450
+ end
1451
+ end
1452
+
1453
+ # If the event thread has been started in its own thread,
1454
+ # wait for it to terminate
1455
+ def join
1456
+ thread.join if thread
1457
+
1458
+ rescue Interrupt
1459
+ Roby.synchronize do
1460
+ return unless thread
1461
+
1462
+ ExecutionEngine.logger.level = Logger::INFO
1463
+ ExecutionEngine.warn "received interruption request"
1464
+ quit
1465
+ if @quit > 2
1466
+ thread.raise Interrupt, "interrupting control thread at user request"
1467
+ end
1468
+ end
1469
+
1470
+ retry
1471
+ end
1472
+
1473
+ # Block until the given block is executed by the execution thread, at
1474
+ # the beginning of the event loop, in propagation context. If the block
1475
+ # raises, the exception is raised back in the calling thread.
1476
+ #
1477
+ # This cannot be used in the execution thread itself.
1478
+ #
1479
+ # If no execution thread is present, yields after having taken
1480
+ # Roby.global_lock
1481
+ def execute
1482
+ if inside_control?
1483
+ return Roby.synchronize { yield }
1484
+ end
1485
+
1486
+ cv = Roby.condition_variable
1487
+
1488
+ return_value = nil
1489
+ Roby.synchronize do
1490
+ if !running?
1491
+ raise "control thread not running"
1492
+ end
1493
+
1494
+ caller_thread = Thread.current
1495
+ waiting_threads << caller_thread
1496
+
1497
+ once do
1498
+ begin
1499
+ return_value = yield
1500
+ cv.broadcast
1501
+ rescue Exception => e
1502
+ caller_thread.raise e, e.message, e.backtrace
1503
+ end
1504
+ waiting_threads.delete(caller_thread)
1505
+ end
1506
+ cv.wait(Roby.global_lock)
1507
+ end
1508
+ return_value
1509
+
1510
+ ensure
1511
+ Roby.return_condition_variable(cv)
1512
+ end
1513
+
1514
+ # Stops the current thread until the given even is emitted. If the event
1515
+ # becomes unreachable, an UnreachableEvent exception is raised.
1516
+ def wait_until(ev)
1517
+ if inside_control?
1518
+ raise ThreadMismatch, "cannot use #wait_until in execution threads"
1519
+ end
1520
+
1521
+ Roby.condition_variable(true) do |cv, mt|
1522
+ caller_thread = Thread.current
1523
+ # Note: no need to add the caller thread in waiting_threads,
1524
+ # since the event will become unreachable if the execution
1525
+ # thread quits
1526
+
1527
+ mt.synchronize do
1528
+ once do
1529
+ ev.if_unreachable(true) do |reason|
1530
+ caller_thread.raise UnreachableEvent.new(ev, reason)
1531
+ end
1532
+ ev.on do
1533
+ mt.synchronize { cv.broadcast }
1534
+ end
1535
+ yield if block_given?
1536
+ end
1537
+ cv.wait(mt)
1538
+ end
1539
+ end
1540
+ end
1541
+ end
1542
+
1543
+ class << self
1544
+ # The ExecutionEngine object which executes Roby.plan
1545
+ attr_reader :engine
1546
+
1547
+ # Sets the engine. This can be done only once
1548
+ def engine=(new_engine)
1549
+ if engine
1550
+ raise ArgumentError, "cannot change the execution engine"
1551
+ elsif plan && plan.engine && plan.engine != new_engine
1552
+ raise ArgumentError, "must have Roby.engine == Roby.plan.engine"
1553
+ elsif control && new_engine.control != control
1554
+ raise ArgumentError, "must have Roby.control == Roby.engine.control"
1555
+ end
1556
+
1557
+ @engine = new_engine
1558
+ @control = new_engine.control
1559
+ end
1560
+ end
1561
+
1562
+ # Execute the given block in the main plan's propagation context, but don't
1563
+ # wait for its completion like Roby.execute does
1564
+ #
1565
+ # See ExecutionEngine#once
1566
+ def self.once; engine.once { yield } end
1567
+
1568
+ # Make the main engine call +block+ during each propagation step.
1569
+ # See ExecutionEngine#each_cycle
1570
+ def self.each_cycle(&block); engine.each_cycle(&block) end
1571
+
1572
+ # Install a periodic handler on the main engine
1573
+ def self.every(duration, &block); engine.every(duration, &block) end
1574
+
1575
+ # True if the current thread is the execution thread of the main engine
1576
+ #
1577
+ # See ExecutionEngine#inside_control?
1578
+ def self.inside_control?; engine.inside_control? end
1579
+
1580
+ # True if the current thread is not the execution thread of the main engine
1581
+ #
1582
+ # See ExecutionEngine#outside_control?
1583
+ def self.outside_control?; engine.outside_control? end
1584
+
1585
+ # Execute the given block during the event propagation step of the main
1586
+ # engine. See ExecutionEngine#execute
1587
+ def self.execute
1588
+ engine.execute do
1589
+ yield
1590
+ end
1591
+ end
1592
+
1593
+ # Blocks until the main engine has executed at least one cycle.
1594
+ # See ExecutionEngine#wait_one_cycle
1595
+ def self.wait_one_cycle; engine.wait_one_cycle end
1596
+
1597
+ # Stops the current thread until the given even is emitted. If the event
1598
+ # becomes unreachable, an UnreachableEvent exception is raised.
1599
+ #
1600
+ # See ExecutionEngine#wait_until
1601
+ def self.wait_until(ev, &block); engine.wait_until(ev, &block) end
1602
+ end
1603
+
1604
+