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.
- data/History.txt +7 -5
- data/Manifest.txt +91 -16
- data/README.txt +24 -24
- data/Rakefile +92 -64
- data/app/config/app.yml +42 -43
- data/app/config/init.rb +26 -0
- data/benchmark/alloc_misc.rb +123 -0
- data/benchmark/discovery_latency.rb +67 -0
- data/benchmark/garbage_collection.rb +48 -0
- data/benchmark/genom.rb +31 -0
- data/benchmark/transactions.rb +62 -0
- data/bin/roby +1 -1
- data/bin/roby-log +16 -6
- data/doc/guide/.gitignore +2 -0
- data/doc/guide/config.yaml +34 -0
- data/doc/guide/ext/init.rb +14 -0
- data/doc/guide/ext/previous_next.rb +40 -0
- data/doc/guide/ext/rdoc_links.rb +33 -0
- data/doc/guide/index.rdoc +16 -0
- data/doc/guide/overview.rdoc +62 -0
- data/doc/guide/plan_modifications.rdoc +67 -0
- data/doc/guide/src/abstraction/achieve_with.page +8 -0
- data/doc/guide/src/abstraction/forwarding.page +8 -0
- data/doc/guide/src/abstraction/hierarchy.page +19 -0
- data/doc/guide/src/abstraction/index.page +28 -0
- data/doc/guide/src/abstraction/task_models.page +13 -0
- data/doc/guide/src/basics.template +6 -0
- data/doc/guide/src/basics/app.page +139 -0
- data/doc/guide/src/basics/code_examples.page +33 -0
- data/doc/guide/src/basics/dry.page +69 -0
- data/doc/guide/src/basics/errors.page +443 -0
- data/doc/guide/src/basics/events.page +179 -0
- data/doc/guide/src/basics/hierarchy.page +275 -0
- data/doc/guide/src/basics/index.page +11 -0
- data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_4.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_1.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_2.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_3.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_1.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_2.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_3.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_log_relation_window.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_replay_event_representation.png +0 -0
- data/doc/guide/src/basics/plan_objects.page +71 -0
- data/doc/guide/src/basics/relations_display.page +203 -0
- data/doc/guide/src/basics/roby_cycle_overview.png +0 -0
- data/doc/guide/src/basics/shell.page +102 -0
- data/doc/guide/src/basics/summary.page +32 -0
- data/doc/guide/src/basics/tasks.page +357 -0
- data/doc/guide/src/basics_shell_header.txt +16 -0
- data/doc/guide/src/cycle/cycle-overview.png +0 -0
- data/doc/guide/src/cycle/cycle-overview.svg +208 -0
- data/doc/guide/src/cycle/error_handling.page +168 -0
- data/doc/guide/src/cycle/error_instantaneous_repair.png +0 -0
- data/doc/guide/src/cycle/error_instantaneous_repair.svg +1224 -0
- data/doc/guide/src/cycle/garbage_collection.page +10 -0
- data/doc/guide/src/cycle/index.page +23 -0
- data/doc/guide/src/cycle/propagation.page +154 -0
- data/doc/guide/src/cycle/propagation_diamond.png +0 -0
- data/doc/guide/src/cycle/propagation_diamond.svg +1279 -0
- data/doc/guide/src/default.css +319 -0
- data/doc/guide/src/default.template +74 -0
- data/doc/guide/src/htmldoc.metainfo +20 -0
- data/doc/guide/src/htmldoc.virtual +18 -0
- data/doc/guide/src/images/bodybg.png +0 -0
- data/doc/guide/src/images/contbg.png +0 -0
- data/doc/guide/src/images/footerbg.png +0 -0
- data/doc/guide/src/images/gradient1.png +0 -0
- data/doc/guide/src/images/gradient2.png +0 -0
- data/doc/guide/src/index.page +7 -0
- data/doc/guide/src/introduction/index.page +29 -0
- data/doc/guide/src/introduction/install.page +133 -0
- data/doc/{papers.rdoc → guide/src/introduction/publications.page} +5 -2
- data/doc/{videos.rdoc → guide/src/introduction/videos.page} +4 -2
- data/doc/guide/src/plugins/fault_tolerance.page +44 -0
- data/doc/guide/src/plugins/index.page +11 -0
- data/doc/guide/src/plugins/subsystems.page +45 -0
- data/doc/guide/src/relations/dependency.page +89 -0
- data/doc/guide/src/relations/index.page +12 -0
- data/doc/misc/update_github +24 -0
- data/doc/tutorials/02-GoForward.rdoc +3 -3
- data/ext/graph/graph.cc +46 -0
- data/lib/roby.rb +57 -22
- data/lib/roby/app.rb +132 -112
- data/lib/roby/app/plugins/rake.rb +21 -0
- data/lib/roby/app/rake.rb +0 -7
- data/lib/roby/app/run.rb +1 -1
- data/lib/roby/app/scripts/distributed.rb +1 -2
- data/lib/roby/app/scripts/generate/bookmarks.rb +1 -1
- data/lib/roby/app/scripts/results.rb +2 -1
- data/lib/roby/app/scripts/run.rb +6 -2
- data/lib/roby/app/scripts/shell.rb +11 -11
- data/lib/roby/config.rb +1 -1
- data/lib/roby/decision_control.rb +62 -3
- data/lib/roby/distributed.rb +4 -0
- data/lib/roby/distributed/base.rb +8 -0
- data/lib/roby/distributed/communication.rb +12 -8
- data/lib/roby/distributed/connection_space.rb +61 -44
- data/lib/roby/distributed/distributed_object.rb +1 -1
- data/lib/roby/distributed/notifications.rb +22 -30
- data/lib/roby/distributed/peer.rb +13 -8
- data/lib/roby/distributed/proxy.rb +5 -5
- data/lib/roby/distributed/subscription.rb +4 -4
- data/lib/roby/distributed/transaction.rb +3 -3
- data/lib/roby/event.rb +176 -110
- data/lib/roby/exceptions.rb +12 -4
- data/lib/roby/execution_engine.rb +1604 -0
- data/lib/roby/external_process_task.rb +225 -0
- data/lib/roby/graph.rb +0 -6
- data/lib/roby/interface.rb +221 -137
- data/lib/roby/log/console.rb +5 -3
- data/lib/roby/log/data_stream.rb +94 -16
- data/lib/roby/log/dot.rb +8 -8
- data/lib/roby/log/event_stream.rb +13 -3
- data/lib/roby/log/file.rb +43 -18
- data/lib/roby/log/gui/basic_display_ui.rb +89 -0
- data/lib/roby/log/gui/chronicle_view_ui.rb +90 -0
- data/lib/roby/log/gui/data_displays.rb +4 -5
- data/lib/roby/log/gui/data_displays_ui.rb +146 -0
- data/lib/roby/log/gui/relations.rb +18 -18
- data/lib/roby/log/gui/relations_ui.rb +120 -0
- data/lib/roby/log/gui/relations_view_ui.rb +144 -0
- data/lib/roby/log/gui/replay.rb +41 -13
- data/lib/roby/log/gui/replay_controls.rb +3 -0
- data/lib/roby/log/gui/replay_controls.ui +133 -110
- data/lib/roby/log/gui/replay_controls_ui.rb +249 -0
- data/lib/roby/log/hooks.rb +19 -18
- data/lib/roby/log/logger.rb +7 -6
- data/lib/roby/log/notifications.rb +4 -4
- data/lib/roby/log/plan_rebuilder.rb +20 -22
- data/lib/roby/log/relations.rb +44 -16
- data/lib/roby/log/server.rb +1 -4
- data/lib/roby/log/timings.rb +88 -19
- data/lib/roby/plan-object.rb +135 -11
- data/lib/roby/plan.rb +408 -224
- data/lib/roby/planning/loops.rb +32 -25
- data/lib/roby/planning/model.rb +157 -51
- data/lib/roby/planning/task.rb +47 -20
- data/lib/roby/query.rb +128 -92
- data/lib/roby/relations.rb +254 -136
- data/lib/roby/relations/conflicts.rb +6 -9
- data/lib/roby/relations/dependency.rb +358 -0
- data/lib/roby/relations/ensured.rb +0 -1
- data/lib/roby/relations/error_handling.rb +0 -1
- data/lib/roby/relations/events.rb +0 -2
- data/lib/roby/relations/executed_by.rb +26 -11
- data/lib/roby/relations/planned_by.rb +14 -14
- data/lib/roby/robot.rb +46 -0
- data/lib/roby/schedulers/basic.rb +34 -0
- data/lib/roby/standalone.rb +4 -0
- data/lib/roby/standard_errors.rb +21 -15
- data/lib/roby/state/events.rb +5 -4
- data/lib/roby/support.rb +107 -6
- data/lib/roby/task-operations.rb +23 -19
- data/lib/roby/task.rb +522 -148
- data/lib/roby/task_index.rb +80 -0
- data/lib/roby/test/common.rb +283 -44
- data/lib/roby/test/distributed.rb +53 -37
- data/lib/roby/test/testcase.rb +9 -204
- data/lib/roby/test/tools.rb +3 -3
- data/lib/roby/transactions.rb +154 -111
- data/lib/roby/transactions/proxy.rb +40 -7
- data/manifest.xml +20 -0
- data/plugins/fault_injection/README.txt +0 -3
- data/plugins/fault_injection/Rakefile +2 -8
- data/plugins/fault_injection/app.rb +1 -1
- data/plugins/fault_injection/fault_injection.rb +3 -3
- data/plugins/fault_injection/test/test_fault_injection.rb +19 -25
- data/plugins/subsystems/README.txt +0 -3
- data/plugins/subsystems/Rakefile +2 -7
- data/plugins/subsystems/app.rb +27 -16
- data/plugins/subsystems/test/app/config/init.rb +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +1 -1
- data/plugins/subsystems/test/app/tasks/services.rb +1 -1
- data/plugins/subsystems/test/test_subsystems.rb +23 -16
- data/test/distributed/test_communication.rb +32 -15
- data/test/distributed/test_connection.rb +28 -26
- data/test/distributed/test_execution.rb +59 -54
- data/test/distributed/test_mixed_plan.rb +34 -34
- data/test/distributed/test_plan_notifications.rb +26 -26
- data/test/distributed/test_protocol.rb +57 -48
- data/test/distributed/test_query.rb +11 -7
- data/test/distributed/test_remote_plan.rb +71 -71
- data/test/distributed/test_transaction.rb +50 -47
- data/test/mockups/external_process +28 -0
- data/test/planning/test_loops.rb +163 -119
- data/test/planning/test_model.rb +3 -3
- data/test/planning/test_task.rb +27 -7
- data/test/relations/test_conflicts.rb +3 -3
- data/test/relations/test_dependency.rb +324 -0
- data/test/relations/test_ensured.rb +2 -2
- data/test/relations/test_executed_by.rb +94 -19
- data/test/relations/test_planned_by.rb +11 -9
- data/test/suite_core.rb +6 -3
- data/test/suite_distributed.rb +1 -0
- data/test/suite_planning.rb +1 -0
- data/test/suite_relations.rb +2 -2
- data/test/tasks/test_external_process.rb +126 -0
- data/test/{test_thread_task.rb → tasks/test_thread_task.rb} +17 -20
- data/test/test_bgl.rb +21 -1
- data/test/test_event.rb +229 -155
- data/test/test_exceptions.rb +79 -80
- data/test/test_execution_engine.rb +987 -0
- data/test/test_gui.rb +1 -1
- data/test/test_interface.rb +11 -5
- data/test/test_log.rb +18 -7
- data/test/test_log_server.rb +1 -0
- data/test/test_plan.rb +229 -395
- data/test/test_query.rb +193 -35
- data/test/test_relations.rb +88 -8
- data/test/test_state.rb +55 -37
- data/test/test_support.rb +1 -1
- data/test/test_task.rb +371 -218
- data/test/test_testcase.rb +32 -16
- data/test/test_transactions.rb +211 -170
- data/test/test_transactions_proxy.rb +37 -19
- metadata +169 -71
- data/.gitignore +0 -29
- data/doc/styles/allison.css +0 -314
- data/doc/styles/allison.js +0 -316
- data/doc/styles/allison.rb +0 -276
- data/doc/styles/jamis.rb +0 -593
- data/lib/roby/control.rb +0 -746
- data/lib/roby/executives/simple.rb +0 -30
- data/lib/roby/propagation.rb +0 -562
- data/lib/roby/relations/hierarchy.rb +0 -239
- data/lib/roby/transactions/updates.rb +0 -139
- data/test/relations/test_hierarchy.rb +0 -158
- data/test/test_control.rb +0 -399
- data/test/test_propagation.rb +0 -210
data/lib/roby/exceptions.rb
CHANGED
|
@@ -130,10 +130,10 @@ module Roby
|
|
|
130
130
|
handler.call(self, exception_object)
|
|
131
131
|
return true
|
|
132
132
|
rescue Exception => e
|
|
133
|
-
if
|
|
134
|
-
|
|
133
|
+
if !kind_of?(PlanObject)
|
|
134
|
+
engine.add_framework_error(e, 'global exception handling')
|
|
135
135
|
else
|
|
136
|
-
|
|
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
|
+
|