roby 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+