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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
module Roby
|
|
2
|
+
trap 'SIGCHLD' do
|
|
3
|
+
begin
|
|
4
|
+
while pid = ::Process.wait(-1, ::Process::WNOHANG)
|
|
5
|
+
ExternalProcessTask.dead! pid, $?.dup
|
|
6
|
+
end
|
|
7
|
+
rescue Errno::ECHILD
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# This task class can be used to monitor the execution of an external
|
|
12
|
+
# process. Among the useful features, it can redirect standard output and
|
|
13
|
+
# error output to files.
|
|
14
|
+
#
|
|
15
|
+
# The events will act as follows:
|
|
16
|
+
# * the start command starts the process per se. The event is emitted once
|
|
17
|
+
# exec() has been called with success
|
|
18
|
+
# * the signaled event is emitted when the process dies because of a signal
|
|
19
|
+
# * the failed event is emitted whenever the process exits with a nonzero
|
|
20
|
+
# status
|
|
21
|
+
# * the success event is emitted when the process exits with a zero status
|
|
22
|
+
# * the stop event is emitted when the process exits
|
|
23
|
+
class ExternalProcessTask < Roby::Task
|
|
24
|
+
##
|
|
25
|
+
# :attr_reader:
|
|
26
|
+
# This task argument is an array whose first element is the executable
|
|
27
|
+
# to start and the rest the arguments that need to be passed to it.
|
|
28
|
+
#
|
|
29
|
+
# It can also be set to a simple string, which is interpreted as the
|
|
30
|
+
# executable name with no arguments.
|
|
31
|
+
argument :command_line
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# :attr_reader:
|
|
35
|
+
# The working directory. If not set, the current directory is used.
|
|
36
|
+
argument :working_directory
|
|
37
|
+
|
|
38
|
+
# Redirection specification. See #redirect_output
|
|
39
|
+
attr_reader :redirection
|
|
40
|
+
|
|
41
|
+
def initialize(arguments)
|
|
42
|
+
arguments[:working_directory] ||= nil
|
|
43
|
+
arguments[:command_line] = [arguments[:command_line]] unless arguments[:command_line].kind_of?(Array)
|
|
44
|
+
super(arguments)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# The set of running ExternalProcessTask instances. It is a mapping
|
|
49
|
+
# from the PID value to the instances.
|
|
50
|
+
attr_reader :processes
|
|
51
|
+
end
|
|
52
|
+
@processes = Hash.new
|
|
53
|
+
|
|
54
|
+
# Called by the SIGCHLD handler to announce that a particular process
|
|
55
|
+
# has finished. It calls #dead!(result) in the context of the execution
|
|
56
|
+
# thread, on the corresponding task
|
|
57
|
+
def self.dead!(pid, result) # :nodoc:
|
|
58
|
+
task = processes[pid]
|
|
59
|
+
return if !task
|
|
60
|
+
if engine = task.plan.engine
|
|
61
|
+
engine.once { task.dead!(result) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Called to announce that this task has been killed. +result+ is the
|
|
66
|
+
# corresponding Process::Status object.
|
|
67
|
+
def dead!(result)
|
|
68
|
+
if result.success?
|
|
69
|
+
emit :success
|
|
70
|
+
elsif result.signaled?
|
|
71
|
+
emit :signaled, result
|
|
72
|
+
else
|
|
73
|
+
emit :failed, result
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# This event gets emitted if the process died because of a signal
|
|
78
|
+
event :signaled
|
|
79
|
+
|
|
80
|
+
forward :signaled => :failed
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# If set to a string, the process' standard output will be redirected to
|
|
84
|
+
# the given file. The following replacement is done:
|
|
85
|
+
# * '%p' is replaced by the process PID
|
|
86
|
+
#
|
|
87
|
+
# The last form (with nil argument) removes any redirection. A specific
|
|
88
|
+
# redirection can also be disabled using the hash form:
|
|
89
|
+
# redirect_output :stdout => nil
|
|
90
|
+
#
|
|
91
|
+
# :call-seq:
|
|
92
|
+
# redirect_output "file"
|
|
93
|
+
# redirect_output :stdout => "file-out", :stderr => "another-file"
|
|
94
|
+
# redirect_output nil
|
|
95
|
+
#
|
|
96
|
+
def redirect_output(args)
|
|
97
|
+
if !args
|
|
98
|
+
@redirection = nil
|
|
99
|
+
elsif args.respond_to? :to_str
|
|
100
|
+
@redirection = args.to_str
|
|
101
|
+
else
|
|
102
|
+
args = validate_options args, :stdout => nil, :stderr => nil
|
|
103
|
+
if args[:stdout] == args[:stderr]
|
|
104
|
+
@redirection = args[:stdout].to_str
|
|
105
|
+
else
|
|
106
|
+
@redirection = Hash.new
|
|
107
|
+
@redirection[:stdout] = args[:stdout].to_str if args[:stdout]
|
|
108
|
+
@redirection[:stderr] = args[:stderr].to_str if args[:stderr]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The PID of the child process, or nil if the child process is not
|
|
114
|
+
# running
|
|
115
|
+
attr_reader :pid
|
|
116
|
+
|
|
117
|
+
# Error codes between the child and the parent. Note that the error
|
|
118
|
+
# codes must not be greater than 9
|
|
119
|
+
# :stopdoc:
|
|
120
|
+
KO_REDIRECTION = 1
|
|
121
|
+
KO_NO_SUCH_FILE = 2
|
|
122
|
+
KO_EXEC = 3
|
|
123
|
+
# :startdoc:
|
|
124
|
+
|
|
125
|
+
# Returns the file name based on the redirection pattern and the current
|
|
126
|
+
# PID values. This is called in the child process before exec().
|
|
127
|
+
def redirection_path(pattern) # :nodoc:
|
|
128
|
+
pattern.gsub '%p', Process.pid.to_s
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Starts the child process
|
|
132
|
+
def start_process # :nodoc:
|
|
133
|
+
# Open a pipe to monitor the child startup
|
|
134
|
+
r, w = IO.pipe
|
|
135
|
+
|
|
136
|
+
@pid = fork do
|
|
137
|
+
# Open the redirection outputs
|
|
138
|
+
stdout, stderr = nil
|
|
139
|
+
begin
|
|
140
|
+
if redirection.respond_to?(:to_str)
|
|
141
|
+
stdout = stderr = File.open(redirection_path(redirection), "w")
|
|
142
|
+
elsif redirection
|
|
143
|
+
if stdout_file = redirection[:stdout]
|
|
144
|
+
stdout = File.open(redirection_path(stdout_file), "w")
|
|
145
|
+
end
|
|
146
|
+
if stderr_file = redirection[:stderr]
|
|
147
|
+
stderr = File.open(redirection_path(stderr_file), "w")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
rescue Exception => e
|
|
151
|
+
Roby.fatal e.message
|
|
152
|
+
w.write("#{KO_REDIRECTION}")
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
STDOUT.reopen(stdout) if stdout
|
|
157
|
+
STDERR.reopen(stderr) if stderr
|
|
158
|
+
|
|
159
|
+
r.close
|
|
160
|
+
w.fcntl(Fcntl::F_SETFD, 1) # set close on exit
|
|
161
|
+
::Process.setpgrp
|
|
162
|
+
begin
|
|
163
|
+
exec(*command_line)
|
|
164
|
+
rescue Errno::ENOENT
|
|
165
|
+
w.write("#{KO_NO_SUCH_FILE}")
|
|
166
|
+
rescue Exception => e
|
|
167
|
+
Roby.fatal e.message
|
|
168
|
+
w.write("#{KO_EXEC}")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
ExternalProcessTask.processes[pid] = self
|
|
173
|
+
|
|
174
|
+
is_running = begin
|
|
175
|
+
!Process.waitpid(pid, Process::WNOHANG)
|
|
176
|
+
rescue Errno::ECHILD
|
|
177
|
+
false
|
|
178
|
+
end
|
|
179
|
+
if !is_running
|
|
180
|
+
raise "child #{command_line.first} died unexpectedly during its startup"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
w.close
|
|
184
|
+
control = Integer(r.read(1))
|
|
185
|
+
if control == KO_REDIRECTION
|
|
186
|
+
raise "could not start #{command_line.first}: cannot establish output redirections"
|
|
187
|
+
elsif control == KO_NO_SUCH_FILE
|
|
188
|
+
raise "could not start #{command_line.first}: provided command does not exist"
|
|
189
|
+
elsif control == KO_EXEC
|
|
190
|
+
raise "could not start #{command_line.first}: exec() call failed"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
rescue Exception => e
|
|
195
|
+
ExternalProcessTask.processes.delete(pid)
|
|
196
|
+
raise e
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# :method: start!
|
|
201
|
+
#
|
|
202
|
+
# Starts the child process. Emits +start+ when the process is actually
|
|
203
|
+
# started.
|
|
204
|
+
event :start do |_|
|
|
205
|
+
if working_directory
|
|
206
|
+
Dir.chdir(working_directory) do
|
|
207
|
+
start_process
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
start_process
|
|
211
|
+
end
|
|
212
|
+
emit :start
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Kills the child process
|
|
216
|
+
def kill(signo)
|
|
217
|
+
Process.kill(signo, pid)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
on :stop do |_|
|
|
221
|
+
ExternalProcessTask.processes.delete(pid)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
data/lib/roby/graph.rb
CHANGED
data/lib/roby/interface.rb
CHANGED
|
@@ -1,60 +1,4 @@
|
|
|
1
|
-
require 'thread'
|
|
2
|
-
require 'roby'
|
|
3
|
-
require 'roby/planning'
|
|
4
|
-
require 'facets/basicobject'
|
|
5
1
|
require 'utilrb/column_formatter'
|
|
6
|
-
require 'stringio'
|
|
7
|
-
require 'roby/robot'
|
|
8
|
-
|
|
9
|
-
module Robot
|
|
10
|
-
def self.prepare_action(name, arguments)
|
|
11
|
-
control = Roby.control
|
|
12
|
-
|
|
13
|
-
# Check if +name+ is a planner method, and in that case
|
|
14
|
-
# add a planning method for it and plan it
|
|
15
|
-
planner_model = control.planners.find do |planner_model|
|
|
16
|
-
planner_model.has_method?(name)
|
|
17
|
-
end
|
|
18
|
-
if !planner_model
|
|
19
|
-
raise ArgumentError, "no such planning method #{name}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
m = planner_model.model_of(name, arguments)
|
|
23
|
-
|
|
24
|
-
# HACK: m.returns should not be nil, but it sometimes happen
|
|
25
|
-
returns_model = (m.returns if m && m.returns) || Task
|
|
26
|
-
|
|
27
|
-
if returns_model.kind_of?(Roby::TaskModelTag)
|
|
28
|
-
task = Roby::Task.new
|
|
29
|
-
task.extend returns_model
|
|
30
|
-
else
|
|
31
|
-
# Create an abstract task which will be planned
|
|
32
|
-
task = returns_model.new
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
planner = Roby::PlanningTask.new(:planner_model => planner_model, :method_name => name, :method_options => arguments)
|
|
36
|
-
task.planned_by planner
|
|
37
|
-
return task, planner
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def self.method_missing(name, *args)
|
|
41
|
-
if name.to_s =~ /!$/
|
|
42
|
-
name = $`.to_sym
|
|
43
|
-
else
|
|
44
|
-
super
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if args.size > 1
|
|
48
|
-
raise ArgumentError, "wrong number of arguments (#{args.size} for 1) in #{name}!"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
options = args.first || {}
|
|
52
|
-
task, planner = Robot.prepare_action(name, options)
|
|
53
|
-
Roby.control.plan.insert(task)
|
|
54
|
-
|
|
55
|
-
return task, planner
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
2
|
|
|
59
3
|
module Roby
|
|
60
4
|
# An augmented DRbObject which allow to properly interface with remotely
|
|
@@ -88,7 +32,32 @@ module Roby
|
|
|
88
32
|
# object.
|
|
89
33
|
def initialize(interface)
|
|
90
34
|
@interface = interface
|
|
91
|
-
|
|
35
|
+
reconnect
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reconnect
|
|
39
|
+
remote_models = @interface.task_models
|
|
40
|
+
remote_models.map do |klass|
|
|
41
|
+
klass = klass.proxy(nil)
|
|
42
|
+
|
|
43
|
+
if klass.respond_to?(:remote_name)
|
|
44
|
+
# This is a local proxy for a remote model. Add it in our
|
|
45
|
+
# namespace as well.
|
|
46
|
+
path = klass.remote_name.split '::'
|
|
47
|
+
klass_name = path.pop
|
|
48
|
+
mod = Object
|
|
49
|
+
while !path.empty?
|
|
50
|
+
name = path.shift
|
|
51
|
+
mod = begin
|
|
52
|
+
mod.const_get(name)
|
|
53
|
+
rescue NameError
|
|
54
|
+
mod.const_set(name, Module.new)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
mod.const_set(klass_name, klass)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
92
61
|
|
|
93
62
|
# Returns a Query object which can be used to interactively query the
|
|
94
63
|
# running plan
|
|
@@ -128,6 +97,167 @@ module Roby
|
|
|
128
97
|
Interface.instance_methods(false).
|
|
129
98
|
actions.map { |name| "#{name}!" }
|
|
130
99
|
end
|
|
100
|
+
|
|
101
|
+
def actions_summary(with_advanced = false)
|
|
102
|
+
methods = @interface.actions
|
|
103
|
+
if !with_advanced
|
|
104
|
+
methods = methods.delete_if { |m| m.description.advanced? }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if !methods.empty?
|
|
108
|
+
puts
|
|
109
|
+
desc = methods.map do |p|
|
|
110
|
+
doc = p.description.doc || ["(no description set)"]
|
|
111
|
+
Hash['Name' => "#{p.name}!", 'Description' => doc.join("\n")]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
ColumnFormatter.from_hashes(desc, STDOUT,
|
|
115
|
+
:header_delimiter => true,
|
|
116
|
+
:column_delimiter => "|",
|
|
117
|
+
:order => %w{Name Description})
|
|
118
|
+
puts
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def actions(with_advanced = false)
|
|
125
|
+
@interface.actions.each do |m|
|
|
126
|
+
next if m.description.advanced? if !with_advanced
|
|
127
|
+
display_action_description(m)
|
|
128
|
+
puts
|
|
129
|
+
end
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Standard way to display a set of tasks
|
|
134
|
+
def task_set_to_s(task_set) # :nodoc:
|
|
135
|
+
if task_set.empty?
|
|
136
|
+
return "no tasks"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
task = task_set.map do |task|
|
|
140
|
+
state_name = %w{pending starting running finishing finished}.find do |state_name|
|
|
141
|
+
task.send("#{state_name}?")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
since = task.start_time
|
|
145
|
+
lifetime = task.lifetime
|
|
146
|
+
Hash['Task' => task.to_s,
|
|
147
|
+
'State' => state_name,
|
|
148
|
+
'Since' => (since.asctime if since),
|
|
149
|
+
'Lifetime' => (Time.at(lifetime).to_hms if lifetime)
|
|
150
|
+
]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
io = StringIO.new
|
|
154
|
+
ColumnFormatter.from_hashes(task, STDOUT,
|
|
155
|
+
:header_delimiter => true,
|
|
156
|
+
:column_delimiter => "|",
|
|
157
|
+
:order => %w{Task State Lifetime Since})
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Displays information about the plan's missions
|
|
161
|
+
def missions
|
|
162
|
+
missions = find_tasks.mission.to_a
|
|
163
|
+
task_set_to_s(missions)
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Displays information about the running tasks
|
|
168
|
+
def running_tasks
|
|
169
|
+
tasks = find_tasks.running.to_a
|
|
170
|
+
task_set_to_s(tasks)
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Displays details about the actions matching 'regex'
|
|
175
|
+
def describe(name, with_advanced = false)
|
|
176
|
+
name = Regexp.new(name)
|
|
177
|
+
m = @interface.actions.find_all { |p| name === p.name }
|
|
178
|
+
|
|
179
|
+
if !with_advanced
|
|
180
|
+
filtered = m.find_all { |m| !m.description.advanced? }
|
|
181
|
+
m = filtered if !filtered.empty?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if m.empty?
|
|
185
|
+
puts "no such method"
|
|
186
|
+
else
|
|
187
|
+
m.each do |desc|
|
|
188
|
+
puts
|
|
189
|
+
display_action_description(desc)
|
|
190
|
+
puts
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Displays a help message
|
|
197
|
+
def help
|
|
198
|
+
puts
|
|
199
|
+
puts "Available Actions"
|
|
200
|
+
puts "================="
|
|
201
|
+
actions_summary
|
|
202
|
+
puts ""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
puts <<-EOHELP
|
|
206
|
+
each action is started with action_name!(:arg1 => value1, :arg2 => value2, ...)
|
|
207
|
+
and returns the corresponding task object. A message is displayed in the shell
|
|
208
|
+
when the task finishes."
|
|
209
|
+
|
|
210
|
+
Shell Commands
|
|
211
|
+
==============
|
|
212
|
+
Command | Help
|
|
213
|
+
---------------------------------------------------------------------------------------------
|
|
214
|
+
actions_summary(advanced = false) | displays the list of actions with a short documentation |
|
|
215
|
+
actions(advanced = false) | displays details for each available actions |
|
|
216
|
+
describe(regex) | displays details about the actions matching 'regex' |
|
|
217
|
+
missions | displays the set of running missions with their status |
|
|
218
|
+
running_tasks | displays the set of running tasks with their status |
|
|
219
|
+
| |
|
|
220
|
+
help | this help message |
|
|
221
|
+
|
|
222
|
+
EOHELP
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Standard display of an action description. +m+ is a PlanningMethod
|
|
226
|
+
# object.
|
|
227
|
+
def display_action_description(m) # :nodoc:
|
|
228
|
+
args = m.description.arguments.
|
|
229
|
+
sort_by { |arg_desc| arg_desc.name }
|
|
230
|
+
|
|
231
|
+
first = true
|
|
232
|
+
args_summary = args.map do |arg_desc|
|
|
233
|
+
name = arg_desc.name
|
|
234
|
+
is_required = arg_desc.required
|
|
235
|
+
format = if is_required then "%s"
|
|
236
|
+
else "[%s]"
|
|
237
|
+
end
|
|
238
|
+
text = format % ["#{", " if !first}:#{name} => #{name}"]
|
|
239
|
+
first = false
|
|
240
|
+
text
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
args_table = args.
|
|
244
|
+
map do |arg_desc|
|
|
245
|
+
Hash['Argument' => arg_desc.name,
|
|
246
|
+
'Description' => (arg_desc.doc || "(no description set)")]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
method_doc = m.description.doc || [""]
|
|
250
|
+
puts "#{m.name}! #{args_summary.join("")}\n#{method_doc.join("\n")}"
|
|
251
|
+
if m.description.arguments.empty?
|
|
252
|
+
puts "No arguments"
|
|
253
|
+
else
|
|
254
|
+
ColumnFormatter.from_hashes(args_table, STDOUT,
|
|
255
|
+
:left_padding => " ",
|
|
256
|
+
:header_delimiter => true,
|
|
257
|
+
:column_delimiter => "|",
|
|
258
|
+
:order => %w{Argument Description})
|
|
259
|
+
end
|
|
260
|
+
end
|
|
131
261
|
|
|
132
262
|
|
|
133
263
|
def method_missing(m, *args) # :nodoc:
|
|
@@ -145,6 +275,8 @@ module Roby
|
|
|
145
275
|
# This class is used to interface with the Roby event loop and plan. It is the
|
|
146
276
|
# main front object when accessing a Roby core remotely
|
|
147
277
|
class Interface
|
|
278
|
+
# This module defines the hooks needed to plug Interface objects onto
|
|
279
|
+
# ExecutionEngine
|
|
148
280
|
module GatherExceptions
|
|
149
281
|
# The set of Interface objects that have been registered to us
|
|
150
282
|
attribute(:interfaces) { Array.new }
|
|
@@ -152,14 +284,14 @@ module Roby
|
|
|
152
284
|
# Register a new Interface object so that it gets feedback information
|
|
153
285
|
# from the running controller.
|
|
154
286
|
def register_interface(iface)
|
|
155
|
-
Roby
|
|
287
|
+
Roby.synchronize do
|
|
156
288
|
interfaces << iface
|
|
157
289
|
end
|
|
158
290
|
end
|
|
159
291
|
|
|
160
292
|
# Pushes a exception message to all the already registered remote interfaces.
|
|
161
293
|
def push_exception_message(name, error, tasks)
|
|
162
|
-
Roby
|
|
294
|
+
Roby.synchronize do
|
|
163
295
|
msg = Roby.format_exception(error.exception).join("\n")
|
|
164
296
|
msg << "\nThe following tasks have been killed:\n"
|
|
165
297
|
tasks.each do |t|
|
|
@@ -190,38 +322,39 @@ module Roby
|
|
|
190
322
|
end
|
|
191
323
|
end
|
|
192
324
|
|
|
193
|
-
# The
|
|
194
|
-
|
|
325
|
+
# The engine this interface is tied to
|
|
326
|
+
attr_reader :engine
|
|
195
327
|
# The set of pending messages that are to be displayed on the remote interface
|
|
196
328
|
attr_reader :pending_messages
|
|
197
329
|
# Creates a local server for a remote interface, acting on +control+
|
|
198
|
-
def initialize(
|
|
199
|
-
@control = control
|
|
330
|
+
def initialize(engine)
|
|
200
331
|
@pending_messages = Queue.new
|
|
332
|
+
@engine = engine
|
|
201
333
|
|
|
202
|
-
|
|
203
|
-
|
|
334
|
+
engine.extend GatherExceptions
|
|
335
|
+
engine.register_interface self
|
|
204
336
|
end
|
|
205
337
|
|
|
206
338
|
# Clear the current plan: remove all running and permanent tasks.
|
|
207
339
|
def clear
|
|
208
|
-
|
|
340
|
+
engine.execute do
|
|
209
341
|
plan.missions.dup.each { |t| plan.discard(t) }
|
|
210
|
-
plan.
|
|
342
|
+
plan.permanent_tasks.dup.each { |t| plan.auto(t) }
|
|
343
|
+
plan.permanent_events.dup.each { |t| plan.auto(t) }
|
|
211
344
|
end
|
|
212
345
|
end
|
|
213
346
|
|
|
214
347
|
# Make the Roby event loop quit
|
|
215
|
-
def stop;
|
|
348
|
+
def stop; engine.quit; nil end
|
|
216
349
|
# The Roby plan
|
|
217
|
-
def plan;
|
|
350
|
+
def plan; engine.plan end
|
|
218
351
|
|
|
219
352
|
# Synchronously call +m+ on +tasks+ with the given arguments. This,
|
|
220
353
|
# along with the implementation of RemoteInterface#method_missing,
|
|
221
354
|
# ensures that no interactive operations are performed outside the
|
|
222
355
|
# control thread.
|
|
223
356
|
def call(task, m, *args)
|
|
224
|
-
|
|
357
|
+
engine.execute do
|
|
225
358
|
if m.to_s =~ /!$/
|
|
226
359
|
event_name = $`
|
|
227
360
|
# Check if the called event is terminal. If it is the case,
|
|
@@ -229,7 +362,7 @@ module Roby
|
|
|
229
362
|
# will get a message
|
|
230
363
|
#
|
|
231
364
|
if task.event(event_name).terminal?
|
|
232
|
-
plan.
|
|
365
|
+
plan.unmark_mission(task)
|
|
233
366
|
task.on(:stop) { |ev| pending_messages << "task #{ev.task} stopped by user request" }
|
|
234
367
|
else
|
|
235
368
|
task.on(event_name) { |ev| pending_messages << "done emitting #{ev.generator}" }
|
|
@@ -270,76 +403,27 @@ module Roby
|
|
|
270
403
|
nil
|
|
271
404
|
end
|
|
272
405
|
|
|
273
|
-
#
|
|
274
|
-
|
|
406
|
+
# Returns the set of task models as DRobyTaskModel objects. The standard
|
|
407
|
+
# Roby task models are excluded.
|
|
408
|
+
def task_models
|
|
275
409
|
task_models = []
|
|
276
|
-
|
|
410
|
+
engine.execute do
|
|
277
411
|
ObjectSpace.each_object(Class) do |obj|
|
|
278
|
-
|
|
412
|
+
if obj <= Roby::Task && obj.name !~ /^Roby::/
|
|
413
|
+
task_models << obj
|
|
414
|
+
end
|
|
279
415
|
end
|
|
280
416
|
end
|
|
281
|
-
|
|
282
|
-
task_models.map do |model|
|
|
283
|
-
"#{model} #{model.superclass}"
|
|
284
|
-
end
|
|
417
|
+
task_models.map { |t| t.droby_dump(nil) }
|
|
285
418
|
end
|
|
286
419
|
|
|
287
|
-
#
|
|
288
|
-
#
|
|
420
|
+
# Returns the set of PlanningMethod objects that describe the methods
|
|
421
|
+
# exported in the application's planners.
|
|
289
422
|
def actions
|
|
290
|
-
|
|
291
|
-
map
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Pretty-prints a set of tasks
|
|
297
|
-
def task_set_to_s(task_set) # :nodoc:
|
|
298
|
-
if task_set.empty?
|
|
299
|
-
return "no tasks"
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
task = task_set.map do |task|
|
|
303
|
-
state_name = %w{pending starting running finishing finished}.find do |state_name|
|
|
304
|
-
task.send("#{state_name}?")
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
start_event = task.history.find { |ev| ev.symbol == :start }
|
|
308
|
-
since = if start_event then start_event.time
|
|
309
|
-
else 'N/A'
|
|
310
|
-
end
|
|
311
|
-
{ 'Task' => task.to_s, 'Since' => since, 'State' => state_name }
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
io = StringIO.new
|
|
315
|
-
ColumnFormatter.from_hashes(task, io) { %w{Task Since State} }
|
|
316
|
-
"\n#{io.string}"
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# Returns a string representing the set of running tasks
|
|
320
|
-
def running_tasks
|
|
321
|
-
Roby.execute do
|
|
322
|
-
task_set_to_s(Roby.plan.find_tasks.running.to_a)
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# Returns a string representing the set of missions
|
|
327
|
-
def missions
|
|
328
|
-
Roby.execute do
|
|
329
|
-
task_set_to_s(control.plan.missions)
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# Returns a string representing the set of tasks present in the plan
|
|
334
|
-
def tasks
|
|
335
|
-
Roby.execute do
|
|
336
|
-
task_set_to_s(Roby.plan.known_tasks)
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def methods
|
|
341
|
-
result = super
|
|
342
|
-
result + actions.map { |n| "#{n}!" }
|
|
423
|
+
Roby.app.planners.
|
|
424
|
+
map do |p|
|
|
425
|
+
p.planning_methods
|
|
426
|
+
end.flatten.sort_by { |p| p.name }
|
|
343
427
|
end
|
|
344
428
|
|
|
345
429
|
# Called every once in a while by RemoteInterface to read and clear the
|
|
@@ -369,15 +453,15 @@ module Roby
|
|
|
369
453
|
options = args.first || {}
|
|
370
454
|
task, planner = Robot.prepare_action(name, options)
|
|
371
455
|
begin
|
|
372
|
-
|
|
373
|
-
|
|
456
|
+
engine.wait_until(planner.event(:success)) do
|
|
457
|
+
plan.add_mission(task)
|
|
374
458
|
yield(task, planner) if block_given?
|
|
375
459
|
end
|
|
376
460
|
rescue Roby::UnreachableEvent
|
|
377
461
|
raise RuntimeError, "cannot start #{name}: #{planner.terminal_event.context.first}"
|
|
378
462
|
end
|
|
379
463
|
|
|
380
|
-
|
|
464
|
+
engine.execute do
|
|
381
465
|
result = planner.result
|
|
382
466
|
result.on(:failed) { |ev| pending_messages << "task #{ev.task} failed" }
|
|
383
467
|
result.on(:success) { |ev| pending_messages << "task #{ev.task} finished successfully" }
|