roby 0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (240) hide show
  1. data/.gitignore +29 -0
  2. data/History.txt +4 -0
  3. data/License-fr.txt +519 -0
  4. data/License.txt +515 -0
  5. data/Manifest.txt +245 -0
  6. data/NOTES +4 -0
  7. data/README.txt +163 -0
  8. data/Rakefile +161 -0
  9. data/TODO.txt +146 -0
  10. data/app/README.txt +24 -0
  11. data/app/Rakefile +8 -0
  12. data/app/config/ROBOT.rb +5 -0
  13. data/app/config/app.yml +91 -0
  14. data/app/config/init.rb +7 -0
  15. data/app/config/roby.yml +3 -0
  16. data/app/controllers/.gitattributes +0 -0
  17. data/app/controllers/ROBOT.rb +2 -0
  18. data/app/data/.gitattributes +0 -0
  19. data/app/planners/ROBOT/main.rb +6 -0
  20. data/app/planners/main.rb +5 -0
  21. data/app/scripts/distributed +3 -0
  22. data/app/scripts/generate/bookmarks +3 -0
  23. data/app/scripts/replay +3 -0
  24. data/app/scripts/results +3 -0
  25. data/app/scripts/run +3 -0
  26. data/app/scripts/server +3 -0
  27. data/app/scripts/shell +3 -0
  28. data/app/scripts/test +3 -0
  29. data/app/tasks/.gitattributes +0 -0
  30. data/app/tasks/ROBOT/.gitattributes +0 -0
  31. data/bin/roby +210 -0
  32. data/bin/roby-log +168 -0
  33. data/bin/roby-shell +25 -0
  34. data/doc/images/event_generalization.png +0 -0
  35. data/doc/images/exception_propagation_1.png +0 -0
  36. data/doc/images/exception_propagation_2.png +0 -0
  37. data/doc/images/exception_propagation_3.png +0 -0
  38. data/doc/images/exception_propagation_4.png +0 -0
  39. data/doc/images/exception_propagation_5.png +0 -0
  40. data/doc/images/replay_handler_error.png +0 -0
  41. data/doc/images/replay_handler_error_0.png +0 -0
  42. data/doc/images/replay_handler_error_1.png +0 -0
  43. data/doc/images/roby_cycle_overview.png +0 -0
  44. data/doc/images/roby_replay_02.png +0 -0
  45. data/doc/images/roby_replay_03.png +0 -0
  46. data/doc/images/roby_replay_04.png +0 -0
  47. data/doc/images/roby_replay_event_representation.png +0 -0
  48. data/doc/images/roby_replay_first_state.png +0 -0
  49. data/doc/images/roby_replay_relations.png +0 -0
  50. data/doc/images/roby_replay_startup.png +0 -0
  51. data/doc/images/task_event_generalization.png +0 -0
  52. data/doc/papers.rdoc +11 -0
  53. data/doc/styles/allison.css +314 -0
  54. data/doc/styles/allison.js +316 -0
  55. data/doc/styles/allison.rb +276 -0
  56. data/doc/styles/jamis.rb +593 -0
  57. data/doc/tutorials/01-GettingStarted.rdoc +86 -0
  58. data/doc/tutorials/02-GoForward.rdoc +220 -0
  59. data/doc/tutorials/03-PlannedPath.rdoc +268 -0
  60. data/doc/tutorials/04-EventPropagation.rdoc +236 -0
  61. data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
  62. data/doc/tutorials/06-Overview.rdoc +40 -0
  63. data/doc/videos.rdoc +69 -0
  64. data/ext/droby/dump.cc +175 -0
  65. data/ext/droby/extconf.rb +3 -0
  66. data/ext/graph/algorithm.cc +746 -0
  67. data/ext/graph/extconf.rb +7 -0
  68. data/ext/graph/graph.cc +529 -0
  69. data/ext/graph/graph.hh +183 -0
  70. data/ext/graph/iterator_sequence.hh +102 -0
  71. data/ext/graph/undirected_dfs.hh +226 -0
  72. data/ext/graph/undirected_graph.hh +421 -0
  73. data/lib/roby.rb +41 -0
  74. data/lib/roby/app.rb +870 -0
  75. data/lib/roby/app/rake.rb +56 -0
  76. data/lib/roby/app/run.rb +14 -0
  77. data/lib/roby/app/scripts/distributed.rb +13 -0
  78. data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
  79. data/lib/roby/app/scripts/replay.rb +31 -0
  80. data/lib/roby/app/scripts/results.rb +15 -0
  81. data/lib/roby/app/scripts/run.rb +26 -0
  82. data/lib/roby/app/scripts/server.rb +18 -0
  83. data/lib/roby/app/scripts/shell.rb +88 -0
  84. data/lib/roby/app/scripts/test.rb +40 -0
  85. data/lib/roby/basic_object.rb +151 -0
  86. data/lib/roby/config.rb +5 -0
  87. data/lib/roby/control.rb +747 -0
  88. data/lib/roby/decision_control.rb +17 -0
  89. data/lib/roby/distributed.rb +32 -0
  90. data/lib/roby/distributed/base.rb +440 -0
  91. data/lib/roby/distributed/communication.rb +871 -0
  92. data/lib/roby/distributed/connection_space.rb +592 -0
  93. data/lib/roby/distributed/distributed_object.rb +206 -0
  94. data/lib/roby/distributed/drb.rb +62 -0
  95. data/lib/roby/distributed/notifications.rb +539 -0
  96. data/lib/roby/distributed/peer.rb +550 -0
  97. data/lib/roby/distributed/protocol.rb +529 -0
  98. data/lib/roby/distributed/proxy.rb +343 -0
  99. data/lib/roby/distributed/subscription.rb +311 -0
  100. data/lib/roby/distributed/transaction.rb +498 -0
  101. data/lib/roby/event.rb +897 -0
  102. data/lib/roby/exceptions.rb +234 -0
  103. data/lib/roby/executives/simple.rb +30 -0
  104. data/lib/roby/graph.rb +166 -0
  105. data/lib/roby/interface.rb +390 -0
  106. data/lib/roby/log.rb +3 -0
  107. data/lib/roby/log/chronicle.rb +303 -0
  108. data/lib/roby/log/console.rb +72 -0
  109. data/lib/roby/log/data_stream.rb +197 -0
  110. data/lib/roby/log/dot.rb +279 -0
  111. data/lib/roby/log/event_stream.rb +151 -0
  112. data/lib/roby/log/file.rb +340 -0
  113. data/lib/roby/log/gui/basic_display.ui +83 -0
  114. data/lib/roby/log/gui/chronicle.rb +26 -0
  115. data/lib/roby/log/gui/chronicle_view.rb +40 -0
  116. data/lib/roby/log/gui/chronicle_view.ui +70 -0
  117. data/lib/roby/log/gui/data_displays.rb +172 -0
  118. data/lib/roby/log/gui/data_displays.ui +155 -0
  119. data/lib/roby/log/gui/notifications.rb +26 -0
  120. data/lib/roby/log/gui/relations.rb +248 -0
  121. data/lib/roby/log/gui/relations.ui +123 -0
  122. data/lib/roby/log/gui/relations_view.rb +185 -0
  123. data/lib/roby/log/gui/relations_view.ui +149 -0
  124. data/lib/roby/log/gui/replay.rb +327 -0
  125. data/lib/roby/log/gui/replay_controls.rb +200 -0
  126. data/lib/roby/log/gui/replay_controls.ui +259 -0
  127. data/lib/roby/log/gui/runtime.rb +130 -0
  128. data/lib/roby/log/hooks.rb +185 -0
  129. data/lib/roby/log/logger.rb +202 -0
  130. data/lib/roby/log/notifications.rb +244 -0
  131. data/lib/roby/log/plan_rebuilder.rb +470 -0
  132. data/lib/roby/log/relations.rb +1056 -0
  133. data/lib/roby/log/server.rb +550 -0
  134. data/lib/roby/log/sqlite.rb +47 -0
  135. data/lib/roby/log/timings.rb +164 -0
  136. data/lib/roby/plan-object.rb +247 -0
  137. data/lib/roby/plan.rb +762 -0
  138. data/lib/roby/planning.rb +13 -0
  139. data/lib/roby/planning/loops.rb +302 -0
  140. data/lib/roby/planning/model.rb +906 -0
  141. data/lib/roby/planning/task.rb +151 -0
  142. data/lib/roby/propagation.rb +562 -0
  143. data/lib/roby/query.rb +619 -0
  144. data/lib/roby/relations.rb +583 -0
  145. data/lib/roby/relations/conflicts.rb +70 -0
  146. data/lib/roby/relations/ensured.rb +20 -0
  147. data/lib/roby/relations/error_handling.rb +23 -0
  148. data/lib/roby/relations/events.rb +9 -0
  149. data/lib/roby/relations/executed_by.rb +193 -0
  150. data/lib/roby/relations/hierarchy.rb +239 -0
  151. data/lib/roby/relations/influence.rb +10 -0
  152. data/lib/roby/relations/planned_by.rb +63 -0
  153. data/lib/roby/robot.rb +7 -0
  154. data/lib/roby/standard_errors.rb +218 -0
  155. data/lib/roby/state.rb +5 -0
  156. data/lib/roby/state/events.rb +221 -0
  157. data/lib/roby/state/information.rb +55 -0
  158. data/lib/roby/state/pos.rb +110 -0
  159. data/lib/roby/state/shapes.rb +32 -0
  160. data/lib/roby/state/state.rb +353 -0
  161. data/lib/roby/support.rb +92 -0
  162. data/lib/roby/task-operations.rb +182 -0
  163. data/lib/roby/task.rb +1618 -0
  164. data/lib/roby/test/common.rb +399 -0
  165. data/lib/roby/test/distributed.rb +214 -0
  166. data/lib/roby/test/tasks/empty_task.rb +9 -0
  167. data/lib/roby/test/tasks/goto.rb +36 -0
  168. data/lib/roby/test/tasks/simple_task.rb +23 -0
  169. data/lib/roby/test/testcase.rb +519 -0
  170. data/lib/roby/test/tools.rb +160 -0
  171. data/lib/roby/thread_task.rb +87 -0
  172. data/lib/roby/transactions.rb +462 -0
  173. data/lib/roby/transactions/proxy.rb +292 -0
  174. data/lib/roby/transactions/updates.rb +139 -0
  175. data/plugins/fault_injection/History.txt +4 -0
  176. data/plugins/fault_injection/README.txt +37 -0
  177. data/plugins/fault_injection/Rakefile +18 -0
  178. data/plugins/fault_injection/TODO.txt +0 -0
  179. data/plugins/fault_injection/app.rb +52 -0
  180. data/plugins/fault_injection/fault_injection.rb +89 -0
  181. data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
  182. data/plugins/subsystems/README.txt +40 -0
  183. data/plugins/subsystems/Rakefile +18 -0
  184. data/plugins/subsystems/app.rb +171 -0
  185. data/plugins/subsystems/test/app/README +24 -0
  186. data/plugins/subsystems/test/app/Rakefile +8 -0
  187. data/plugins/subsystems/test/app/config/app.yml +71 -0
  188. data/plugins/subsystems/test/app/config/init.rb +9 -0
  189. data/plugins/subsystems/test/app/config/roby.yml +3 -0
  190. data/plugins/subsystems/test/app/planners/main.rb +20 -0
  191. data/plugins/subsystems/test/app/scripts/distributed +3 -0
  192. data/plugins/subsystems/test/app/scripts/replay +3 -0
  193. data/plugins/subsystems/test/app/scripts/results +3 -0
  194. data/plugins/subsystems/test/app/scripts/run +3 -0
  195. data/plugins/subsystems/test/app/scripts/server +3 -0
  196. data/plugins/subsystems/test/app/scripts/shell +3 -0
  197. data/plugins/subsystems/test/app/scripts/test +3 -0
  198. data/plugins/subsystems/test/app/tasks/services.rb +15 -0
  199. data/plugins/subsystems/test/test_subsystems.rb +71 -0
  200. data/test/distributed/test_communication.rb +178 -0
  201. data/test/distributed/test_connection.rb +282 -0
  202. data/test/distributed/test_execution.rb +373 -0
  203. data/test/distributed/test_mixed_plan.rb +341 -0
  204. data/test/distributed/test_plan_notifications.rb +238 -0
  205. data/test/distributed/test_protocol.rb +516 -0
  206. data/test/distributed/test_query.rb +102 -0
  207. data/test/distributed/test_remote_plan.rb +491 -0
  208. data/test/distributed/test_transaction.rb +463 -0
  209. data/test/mockups/tasks.rb +27 -0
  210. data/test/planning/test_loops.rb +380 -0
  211. data/test/planning/test_model.rb +427 -0
  212. data/test/planning/test_task.rb +106 -0
  213. data/test/relations/test_conflicts.rb +42 -0
  214. data/test/relations/test_ensured.rb +38 -0
  215. data/test/relations/test_executed_by.rb +149 -0
  216. data/test/relations/test_hierarchy.rb +158 -0
  217. data/test/relations/test_planned_by.rb +54 -0
  218. data/test/suite_core.rb +24 -0
  219. data/test/suite_distributed.rb +9 -0
  220. data/test/suite_planning.rb +3 -0
  221. data/test/suite_relations.rb +8 -0
  222. data/test/test_bgl.rb +508 -0
  223. data/test/test_control.rb +399 -0
  224. data/test/test_event.rb +894 -0
  225. data/test/test_exceptions.rb +592 -0
  226. data/test/test_interface.rb +37 -0
  227. data/test/test_log.rb +114 -0
  228. data/test/test_log_server.rb +132 -0
  229. data/test/test_plan.rb +584 -0
  230. data/test/test_propagation.rb +210 -0
  231. data/test/test_query.rb +266 -0
  232. data/test/test_relations.rb +180 -0
  233. data/test/test_state.rb +414 -0
  234. data/test/test_support.rb +16 -0
  235. data/test/test_task.rb +938 -0
  236. data/test/test_testcase.rb +122 -0
  237. data/test/test_thread_task.rb +73 -0
  238. data/test/test_transactions.rb +569 -0
  239. data/test/test_transactions_proxy.rb +198 -0
  240. metadata +570 -0
data/lib/roby/app.rb ADDED
@@ -0,0 +1,870 @@
1
+ require 'roby'
2
+ require 'roby/distributed'
3
+ require 'roby/planning'
4
+ require 'roby/log'
5
+ require 'roby/log/event_stream'
6
+
7
+ require 'roby/robot'
8
+ require 'yaml'
9
+
10
+ module Roby
11
+ # Returns the only one Application object
12
+ def self.app; Application.instance end
13
+
14
+ # = Roby Applications
15
+ #
16
+ # There is one and only one Application object, which holds mainly the
17
+ # system-wide configuration and takes care of file loading and system-wide
18
+ # setup (#setup). A Roby application can be started in multiple modes. The
19
+ # first and most important mode is the runtime mode
20
+ # (<tt>scripts/run</tt>). Other modes are the testing mode (#testing?
21
+ # returns true, entered through <tt>scripts/test</tt>) and the shell mode
22
+ # (#shell? returns true, entered through <tt>scripts/shell</tt>). Usually,
23
+ # user code does not have to take the modes into account, but it is
24
+ # sometime useful.
25
+ #
26
+ # Finally, in both testing and runtime mode, the code can be started in
27
+ # simulation or live setups (see #simulation?). Specific plugins can for
28
+ # instance start and set up a simulation system in simulation mode, and as
29
+ # well set up some simulation-specific configuration for the functional
30
+ # layer of the architecture.
31
+ #
32
+ # == Configuration files
33
+ #
34
+ # In all modes, a specific set of configuration files are loaded. The
35
+ # files that are actually loaded are defined by the robot name and type, as
36
+ # specified to #robot. The loaded files are, in order, the following:
37
+ # [config/app.yml]
38
+ # the application configuration as a YAML file. See the comments in that
39
+ # file for more details.
40
+ # [config/init.rb]
41
+ # Ruby code for the common configuration of all robots
42
+ # [config/ROBOT_NAME.rb or config/ROBOT_TYPE.rb]
43
+ # Ruby code for the configuration of either all robots of the same type,
44
+ # or a specific robot. It is one or the other. If a given robot needs to
45
+ # inherit the configuration of its type, explicitely require the
46
+ # ROBOT_TYPE.rb file in config/ROBOT_NAME.rb.
47
+ #
48
+ # == Runtime mode (<tt>scripts/run</tt>)
49
+ # Then, in runtime mode the robot controller
50
+ # <tt>controller/ROBOT_NAME.rb</tt> or <tt>controller/ROBOT_TYPE.rb</tt> is
51
+ # loaded. The same rules than for the configuration file
52
+ # <tt>config/ROBOT_NAME.rb</tt> apply.
53
+ #
54
+ # == Testing mode (<tt>scripts/test</tt>)
55
+ # This mode is used to run test suites in the +test+ directory. See
56
+ # Roby::Test::TestCase for a description of Roby-specific tests.
57
+ class Application
58
+ include Singleton
59
+
60
+ # The plain option hash saved in config/app.yml
61
+ attr_reader :options
62
+
63
+ # Logging options.
64
+ # events:: save a log of all events in the system. This log can be read using scripts/replay
65
+ # If this value is 'stats', only the data necessary for timing statistics is saved.
66
+ # levels:: a component => level hash of the minimum level of the messages that
67
+ # should be displayed on the console. The levels are DEBUG, INFO, WARN and FATAL.
68
+ # Roby: FATAL
69
+ # Roby::Distributed: INFO
70
+ # dir:: the log directory. Uses APP_DIR/log if not set
71
+ # filter_backtraces:: true if the framework code should be removed from the error backtraces
72
+ attr_reader :log
73
+
74
+ # A [name, dir, file, module] array of available plugins, where 'name'
75
+ # is the plugin name, 'dir' the directory in which it is installed,
76
+ # 'file' the file which should be required to load the plugin and
77
+ # 'module' the Application-compatible module for configuration of the
78
+ # plug-in
79
+ attr_reader :available_plugins
80
+ # An [name, module] array of the loaded plugins
81
+ attr_reader :plugins
82
+
83
+ # The discovery options in multi-robot mode
84
+ attr_reader :discovery
85
+ # The robot's dRoby options
86
+ # period:: the period of neighbour discovery
87
+ # max_errors:: disconnect from a peer if there is more than +max_errors+ consecutive errors
88
+ # detected
89
+ attr_reader :droby
90
+
91
+ # Configuration of the control loop
92
+ # abort_on_exception:: if the control loop should abort if an uncaught task or event exception is received. Defaults
93
+ # to false
94
+ # abort_on_application_exception:: if the control should abort if an uncaught application exception (not originating
95
+ # from a task or event) is caught. Defaults to true.
96
+ attr_reader :control
97
+
98
+ # An array of directories in which to search for plugins
99
+ attr_reader :plugin_dirs
100
+
101
+ # True if user interaction is disabled during tests
102
+ attr_predicate :automatic_testing?, true
103
+
104
+ # True if all logs should be kept after testing
105
+ attr_predicate :testing_keep_logs?, true
106
+
107
+ # True if all logs should be kept after testing
108
+ attr_predicate :testing_overwrites_logs?, true
109
+
110
+ # True if we should remove the framework code from the error backtraces
111
+ def filter_backtraces?; log['filter_backtraces'] end
112
+ def filter_backtraces=(value); log['filter_backtraces'] = value end
113
+
114
+ def initialize
115
+ @plugins = Array.new
116
+ @available_plugins = Array.new
117
+ @log = Hash['events' => 'stats', 'levels' => Hash.new, 'filter_backtraces' => true]
118
+ @discovery = Hash.new
119
+ @droby = Hash['period' => 0.5, 'max_errors' => 1]
120
+ @control = Hash[ 'abort_on_exception' => false,
121
+ 'abort_on_application_exception' => true ]
122
+
123
+ @automatic_testing = true
124
+ @testing_keep_logs = false
125
+
126
+ @plugin_dirs = []
127
+ end
128
+
129
+ # Adds +dir+ in the list of directories searched for plugins
130
+ def plugin_dir(dir)
131
+ dir = File.expand_path(dir)
132
+ @plugin_dirs << dir
133
+ $LOAD_PATH.unshift File.expand_path(dir)
134
+
135
+ Dir.new(dir).each do |subdir|
136
+ subdir = File.join(dir, subdir)
137
+ next unless File.directory?(subdir)
138
+ appfile = File.join(subdir, "app.rb")
139
+ next unless File.file?(appfile)
140
+
141
+ begin
142
+ require appfile
143
+ rescue
144
+ Roby.warn "cannot load plugin in #{subdir}: #{$!.full_message}\n"
145
+ end
146
+ Roby.info "loaded plugin in #{subdir}"
147
+ end
148
+ end
149
+
150
+ # Returns true if +name+ is a loaded plugin
151
+ def loaded_plugin?(name)
152
+ plugins.any? { |plugname, _| plugname == name }
153
+ end
154
+
155
+ # Returns the [name, dir, file, module] array definition of the plugin
156
+ # +name+, or nil if +name+ is not a known plugin
157
+ def plugin_definition(name)
158
+ available_plugins.find { |plugname, *_| plugname == name }
159
+ end
160
+
161
+ # True if +name+ is a plugin known to us
162
+ def defined_plugin?(name)
163
+ available_plugins.any? { |plugname, *_| plugname == name }
164
+ end
165
+
166
+ def each_plugin(on_available = false)
167
+ plugins = self.plugins
168
+ if on_available
169
+ plugins = available_plugins.map { |name, _, mod, _| [name, mod] }
170
+ end
171
+ plugins.each do |_, mod|
172
+ yield(mod)
173
+ end
174
+ end
175
+
176
+ # Yields each extension modules that respond to +method+
177
+ def each_responding_plugin(method, on_available = false)
178
+ each_plugin do |mod|
179
+ yield(mod) if mod.respond_to?(method)
180
+ end
181
+ end
182
+
183
+ # Call +method+ on each loaded extension module which define it, with
184
+ # arguments +args+
185
+ def call_plugins(method, *args)
186
+ each_responding_plugin(method) do |config_extension|
187
+ config_extension.send(method, *args)
188
+ end
189
+ end
190
+
191
+ # Load configuration from the given option hash
192
+ def load_yaml(options)
193
+ options = options.dup
194
+
195
+ if robot_name && (robot_config = options['robots'])
196
+ if robot_config = robot_config[robot_name]
197
+ robot_config.each do |section, values|
198
+ if options[section]
199
+ options[section].merge! values
200
+ else
201
+ options[section] = values
202
+ end
203
+ end
204
+ options.delete('robots')
205
+ end
206
+ end
207
+
208
+ @options = options
209
+
210
+ load_option_hashes(options, %w{log control discovery droby})
211
+ call_plugins(:load, self, options)
212
+ end
213
+
214
+ def load_option_hashes(options, names)
215
+ names.each do |optname|
216
+ if options[optname]
217
+ send(optname).merge! options[optname]
218
+ end
219
+ end
220
+ end
221
+
222
+ # Loads the plugins whose name are listed in +names+
223
+ def using(*names)
224
+ names.each do |name|
225
+ name = name.to_s
226
+ unless plugin = plugin_definition(name)
227
+ raise ArgumentError, "#{name} is not a known plugin (#{available_plugins.map { |n, *_| n }.join(", ")})"
228
+ end
229
+ name, dir, mod, init = *plugin
230
+ if plugins.find { |n, m| n == name && m == mod }
231
+ next
232
+ end
233
+
234
+ if init
235
+ begin
236
+ $LOAD_PATH.unshift dir
237
+ init.call
238
+ mod.reset(self) if mod.respond_to?(:reset)
239
+ rescue Exception => e
240
+ Roby.fatal "cannot load plugin #{name}: #{e.full_message}"
241
+ exit(1)
242
+ ensure
243
+ $LOAD_PATH.shift
244
+ end
245
+ end
246
+
247
+ plugins << [name, mod]
248
+ extend mod
249
+ # If +load+ has already been called, call it on the module
250
+ if mod.respond_to?(:load) && options
251
+ mod.load(self, options)
252
+ end
253
+ end
254
+ end
255
+
256
+ def reset
257
+ if defined? State
258
+ State.clear
259
+ else
260
+ Roby.const_set(:State, StateSpace.new)
261
+ end
262
+ call_plugins(:reset, self)
263
+ end
264
+
265
+ # The robot name
266
+ attr_reader :robot_name
267
+ # The robot type
268
+ attr_reader :robot_type
269
+ # Sets up the name and type of the robot. This can be called only once
270
+ # in a given Roby controller.
271
+ def robot(name, type = name)
272
+ if @robot_name
273
+ if name != @robot_name && type != @robot_type
274
+ raise ArgumentError, "the robot is already set to #{name}, of type #{type}"
275
+ end
276
+ return
277
+ end
278
+ @robot_name = name
279
+ @robot_type = type
280
+ end
281
+
282
+ # The directory in which logs are to be saved
283
+ # Defaults to APP_DIR/log
284
+ def log_dir
285
+ File.expand_path(log['dir'] || 'log', APP_DIR)
286
+ end
287
+
288
+ # A path => File hash, to re-use the same file object for different
289
+ # logs
290
+ attribute(:log_files) { Hash.new }
291
+
292
+ # The directory in which results should be saved
293
+ # Defaults to APP_DIR/results
294
+ def results_dir
295
+ File.expand_path(log['results'] || 'results', APP_DIR)
296
+ end
297
+
298
+ # Returns a unique directory name as a subdirectory of
299
+ # +base_dir+, based on +path_spec+. The generated name
300
+ # is of the form
301
+ # <base_dir>/a/b/c/YYYYMMDD-basename
302
+ # if <tt>path_spec = "a/b/c/basename"</tt>. A .<number> suffix
303
+ # is appended if the path already exists.
304
+ def self.unique_dirname(base_dir, path_spec)
305
+ if path_spec =~ /\/$/
306
+ basename = ""
307
+ dirname = path_spec
308
+ else
309
+ basename = File.basename(path_spec)
310
+ dirname = File.dirname(path_spec)
311
+ end
312
+
313
+ date = Date.today
314
+ date = "%i%02i%02i" % [date.year, date.month, date.mday]
315
+ if basename && !basename.empty?
316
+ basename = date + "-" + basename
317
+ else
318
+ basename = date
319
+ end
320
+
321
+ # Check if +basename+ already exists, and if it is the case add a
322
+ # .x suffix to it
323
+ full_path = File.expand_path(File.join(dirname, basename), base_dir)
324
+ base_dir = File.dirname(full_path)
325
+
326
+ unless File.exists?(base_dir)
327
+ FileUtils.mkdir_p(base_dir)
328
+ end
329
+
330
+ final_path, i = full_path, 0
331
+ while File.exists?(final_path)
332
+ i += 1
333
+ final_path = full_path + ".#{i}"
334
+ end
335
+
336
+ final_path
337
+ end
338
+
339
+ # Sets up all the default loggers. It creates the logger for the Robot
340
+ # module (accessible through Robot.logger), and sets up log levels as
341
+ # specified in the <tt>config/app.yml</tt> file.
342
+ def setup_loggers
343
+ # Create the robot namespace
344
+ STDOUT.sync = true
345
+ Robot.logger = Logger.new(STDOUT)
346
+ Robot.logger.level = Logger::INFO
347
+ Robot.logger.formatter = Roby.logger.formatter
348
+ Robot.logger.progname = robot_name
349
+
350
+ # Set up log levels
351
+ log['levels'].each do |name, value|
352
+ name = name.camelize
353
+ if value =~ /^(\w+):(.+)$/
354
+ level, file = $1, $2
355
+ level = Logger.const_get(level)
356
+ file = file.gsub('ROBOT', robot_name) if robot_name
357
+ else
358
+ level = Logger.const_get(value)
359
+ end
360
+
361
+ new_logger = if file
362
+ path = File.expand_path(file, log_dir)
363
+ io = (log_files[path] ||= File.open(path, 'w'))
364
+ Logger.new(io)
365
+ else Logger.new(STDOUT)
366
+ end
367
+ new_logger.level = level
368
+ new_logger.formatter = Roby.logger.formatter
369
+
370
+ if (mod = name.constantize rescue nil)
371
+ if robot_name
372
+ new_logger.progname = "#{name} #{robot_name}"
373
+ else
374
+ new_logger.progname = name
375
+ end
376
+ mod.logger = new_logger
377
+ end
378
+ end
379
+ end
380
+
381
+ def setup_dirs
382
+ Dir.mkdir(log_dir) unless File.exists?(log_dir)
383
+ if File.directory?(libdir = File.join(APP_DIR, 'lib'))
384
+ if !$LOAD_PATH.include?(libdir)
385
+ $LOAD_PATH.unshift File.join(APP_DIR, 'lib')
386
+ end
387
+ end
388
+
389
+ Roby::State.datadirs = []
390
+ datadir = File.join(APP_DIR, "data")
391
+ if File.directory?(datadir)
392
+ Roby::State.datadirs << datadir
393
+ end
394
+ end
395
+
396
+ # Loads the models, based on the given robot name and robot type
397
+ def require_models
398
+ # Require all common task models and the task models specific to
399
+ # this robot
400
+ require_dir(File.join(APP_DIR, 'tasks'))
401
+ require_robotdir(File.join(APP_DIR, 'tasks', 'ROBOT'))
402
+
403
+ # Load robot-specific configuration
404
+ planner_dir = File.join(APP_DIR, 'planners')
405
+ models_search = [planner_dir]
406
+ if robot_name
407
+ load_robotfile(File.join(APP_DIR, 'config', "ROBOT.rb"))
408
+
409
+ models_search << File.join(planner_dir, robot_name) << File.join(planner_dir, robot_type)
410
+ if !require_robotfile(File.join(APP_DIR, 'planners', 'ROBOT', 'main.rb'))
411
+ require File.join(APP_DIR, "planners", "main")
412
+ end
413
+ else
414
+ require File.join(APP_DIR, "planners", "main")
415
+ end
416
+
417
+ # Load the other planners
418
+ models_search.each do |base_dir|
419
+ next unless File.directory?(base_dir)
420
+ Dir.new(base_dir).each do |file|
421
+ if File.file?(file) && file =~ /\.rb$/ && file !~ 'main\.rb$'
422
+ require file
423
+ end
424
+ end
425
+ end
426
+ end
427
+
428
+ def setup
429
+ reset
430
+
431
+ $LOAD_PATH.unshift(APP_DIR) unless $LOAD_PATH.include?(APP_DIR)
432
+
433
+ # Get the application-wide configuration
434
+ file = File.join(APP_DIR, 'config', 'app.yml')
435
+ file = YAML.load(File.open(file))
436
+ load_yaml(file)
437
+ if File.exists?(initfile = File.join(APP_DIR, 'config', 'init.rb'))
438
+ load initfile
439
+ end
440
+
441
+ setup_dirs
442
+ setup_loggers
443
+
444
+ # Import some constants directly at toplevel before loading the
445
+ # user-defined models
446
+ unless Object.const_defined?(:Application)
447
+ Object.const_set(:Application, Roby::Application)
448
+ Object.const_set(:State, Roby::State)
449
+ end
450
+
451
+ require_models
452
+
453
+ # MainPlanner is always included in the planner list
454
+ Roby.control.planners << MainPlanner
455
+
456
+ # Set up the loaded plugins
457
+ call_plugins(:setup, self)
458
+
459
+ # If we are in test mode, import the test extensions from plugins
460
+ if testing?
461
+ require 'roby/test/testcase'
462
+ each_plugin do |mod|
463
+ if mod.const_defined?(:Test)
464
+ Roby::Test::TestCase.include mod.const_get(:Test)
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ def run(&block)
471
+ # Set up dRoby, setting an Interface object as front server, for shell access
472
+ host = droby['host'] || ""
473
+ if host !~ /:\d+$/
474
+ host << ":#{Distributed::DEFAULT_DROBY_PORT}"
475
+ end
476
+
477
+ if single? || !robot_name
478
+ host =~ /:(\d+)$/
479
+ DRb.start_service "druby://:#{$1 || '0'}", Interface.new(Roby.control)
480
+ else
481
+ DRb.start_service "druby://#{host}", Interface.new(Roby.control)
482
+ droby_config = { :ring_discovery => !!discovery['ring'],
483
+ :name => robot_name,
484
+ :plan => Roby.plan,
485
+ :period => discovery['period'] || 0.5 }
486
+
487
+ if discovery['tuplespace']
488
+ droby_config[:discovery_tuplespace] = DRbObject.new_with_uri("druby://#{discovery['tuplespace']}")
489
+ end
490
+ Roby::Distributed.state = Roby::Distributed::ConnectionSpace.new(droby_config)
491
+
492
+ if discovery['ring']
493
+ Roby::Distributed.publish discovery['ring']
494
+ end
495
+ Roby::Control.every(discovery['period'] || 0.5) do
496
+ Roby::Distributed.state.start_neighbour_discovery
497
+ end
498
+ end
499
+
500
+ @robot_name ||= 'common'
501
+ @robot_type ||= 'common'
502
+
503
+ control_config = self.control
504
+ control = Roby.control
505
+ options = { :detach => true, :cycle => control_config['cycle'] || 0.1 }
506
+
507
+ # Add an executive if one is defined
508
+ if control_config['executive']
509
+ self.executive = control_config['executive']
510
+ end
511
+
512
+ if log['events']
513
+ require 'roby/log/file'
514
+ logfile = File.join(log_dir, robot_name)
515
+ logger = Roby::Log::FileLogger.new(logfile)
516
+ logger.stats_mode = log['events'] == 'stats'
517
+ Roby::Log.add_logger logger
518
+ end
519
+ control.abort_on_exception =
520
+ control_config['abort_on_exception']
521
+ control.abort_on_application_exception =
522
+ control_config['abort_on_application_exception']
523
+ control.run options
524
+
525
+ plugins = self.plugins.map { |_, mod| mod if mod.respond_to?(:run) }.compact
526
+ run_plugins(plugins, &block)
527
+
528
+ rescue Exception => e
529
+ if e.respond_to?(:pretty_print)
530
+ pp e
531
+ else
532
+ pp e.full_message
533
+ end
534
+ end
535
+ def run_plugins(mods, &block)
536
+ control = Roby.control
537
+
538
+ if mods.empty?
539
+ yield
540
+ control.join
541
+ else
542
+ mod = mods.shift
543
+ mod.run(self) do
544
+ run_plugins(mods, &block)
545
+ end
546
+ end
547
+
548
+ rescue Exception => e
549
+ if Roby.control.running?
550
+ control.quit
551
+ control.join
552
+ raise e, e.message, e.backtrace
553
+ else
554
+ raise
555
+ end
556
+ end
557
+
558
+ attr_reader :executive
559
+
560
+ def executive=(name)
561
+ if executive
562
+ Control.event_processing.delete(executive.method(:initial_events))
563
+ @executive = nil
564
+ end
565
+ return unless name
566
+
567
+ full_name = "roby/executives/#{name}"
568
+ require full_name
569
+ @executive = full_name.camelize.constantize.new
570
+ Control.event_processing << executive.method(:initial_events)
571
+ end
572
+
573
+ def stop; call_plugins(:stop, self) end
574
+
575
+ DISCOVERY_TEMPLATE = [:droby, nil, nil]
576
+
577
+ # Starts services needed for distributed operations. These services are
578
+ # supposed to be started only once for a whole system
579
+ #
580
+ # If you have external servers to start for every robot, plug it into
581
+ # #start_server
582
+ def start_distributed
583
+ Thread.abort_on_exception = true
584
+
585
+ if !File.exists?(log_dir)
586
+ Dir.mkdir(log_dir)
587
+ end
588
+
589
+ unless single? || !discovery['tuplespace']
590
+ ts = Rinda::TupleSpace.new
591
+
592
+
593
+ discovery['tuplespace'] =~ /(:\d+)$/
594
+ DRb.start_service "druby://#{$1}", ts
595
+
596
+ new_db = ts.notify('write', DISCOVERY_TEMPLATE)
597
+ take_db = ts.notify('take', DISCOVERY_TEMPLATE)
598
+
599
+ Thread.start do
600
+ new_db.each { |_, t| STDERR.puts "new host #{t[1]}" }
601
+ end
602
+ Thread.start do
603
+ take_db.each { |_, t| STDERR.puts "host #{t[1]} has disconnected" }
604
+ end
605
+ Roby.warn "Started service discovery on #{discovery['tuplespace']}"
606
+ end
607
+
608
+ call_plugins(:start_distributed, self)
609
+ end
610
+
611
+ # Stop services needed for distributed operations. See #start_distributed
612
+ def stop_distributed
613
+ DRb.stop_service
614
+
615
+ call_plugins(:stop_distributed, self)
616
+ rescue Interrupt
617
+ end
618
+
619
+ attr_reader :log_server
620
+ attr_reader :log_sources
621
+
622
+ # Start services that should exist for every robot in the system. Services that
623
+ # are needed only once for all robots should be started in #start_distributed
624
+ def start_server
625
+ Thread.abort_on_exception = true
626
+
627
+ # Start a log server if needed, and poll the log directory for new
628
+ # data sources
629
+ if log_server = (log.has_key?('server') ? log['server'] : true)
630
+ require 'roby/log/server'
631
+ port = if log_server.kind_of?(Hash) && log_server['port']
632
+ Integer(log_server['port'])
633
+ end
634
+
635
+ @log_server = Log::Server.new(port ||= Log::Server::RING_PORT)
636
+ Roby::Log::Server.info "log server published on port #{port}"
637
+ @log_streams = []
638
+ @log_streams_poll = Thread.new do
639
+ begin
640
+ loop do
641
+ Thread.exclusive do
642
+ known_streams = @log_server.streams
643
+ streams = data_streams
644
+
645
+ (streams - known_streams).each do |s|
646
+ Roby::Log::Server.info "new stream found #{s.name} [#{s.type}]"
647
+ s.open
648
+ @log_server.added_stream(s)
649
+ end
650
+ (known_streams - streams).each do |s|
651
+ Roby::Log::Server.info "end of stream #{s.name} [#{s.type}]"
652
+ s.close
653
+ @log_server.removed_stream(s)
654
+ end
655
+ end
656
+ sleep(5)
657
+ end
658
+ rescue Interrupt
659
+ rescue
660
+ Roby::Log::Server.fatal $!.full_message
661
+ end
662
+ end
663
+ end
664
+
665
+ call_plugins(:start_server, self)
666
+ end
667
+
668
+ # Stop server. See #start_server
669
+ def stop_server
670
+ if @log_server
671
+ @log_streams_poll.raise Interrupt, "quitting"
672
+ @log_streams_poll.join
673
+
674
+ @log_server.quit
675
+ @log_streams.clear
676
+ end
677
+
678
+ call_plugins(:stop_server, self)
679
+ end
680
+
681
+ # Require all files in +dirname+
682
+ def require_dir(dirname)
683
+ Dir.new(dirname).each do |file|
684
+ file = File.join(dirname, file)
685
+ file = file.gsub(/^#{Regexp.quote(APP_DIR)}\//, '')
686
+ require file if file =~ /\.rb$/ && File.file?(file)
687
+ end
688
+ end
689
+
690
+ # Require all files in the directories matching +pattern+. If +pattern+
691
+ # contains the word ROBOT, it is replaced by -- in order -- the robot
692
+ # name and then the robot type
693
+ def require_robotdir(pattern)
694
+ return unless robot_name && robot_type
695
+
696
+ [robot_name, robot_type].each do |name|
697
+ dirname = pattern.gsub(/ROBOT/, name)
698
+ require_dir(dirname) if File.directory?(dirname)
699
+ end
700
+ end
701
+
702
+ # Loads the first file found matching +pattern+
703
+ #
704
+ # See #require_robotfile
705
+ def load_robotfile(pattern)
706
+ require_robotfile(pattern, :load)
707
+ end
708
+
709
+ # Requires or loads (according to the value of +method+) the first file
710
+ # found matching +pattern+. +pattern+ can contain the word ROBOT, in
711
+ # which case the file is first checked against the robot name and then
712
+ # against the robot type
713
+ def require_robotfile(pattern, method = :require)
714
+ return unless robot_name && robot_type
715
+
716
+ robot_config = pattern.gsub(/ROBOT/, robot_name)
717
+ if File.file?(robot_config)
718
+ Kernel.send(method, robot_config)
719
+ true
720
+ else
721
+ robot_config = pattern.gsub(/ROBOT/, robot_type)
722
+ if File.file?(robot_config)
723
+ Kernel.send(method, robot_config)
724
+ true
725
+ else
726
+ false
727
+ end
728
+ end
729
+ end
730
+
731
+ attr_predicate :simulation?, true
732
+ def simulation; self.simulation = true end
733
+
734
+ attr_predicate :testing?, true
735
+ def testing; self.testing = true end
736
+ attr_predicate :shell?, true
737
+ def shell; self.shell = true end
738
+ def single?; @single || discovery.empty? end
739
+ def single; @single = true end
740
+
741
+ # Guesses the type of +filename+ if it is a source suitable for
742
+ # data display in this application
743
+ def data_streams_of(filenames)
744
+ if filenames.size == 1
745
+ path = filenames.first
746
+ path = if path =~ /-(events|timings)\.log$/
747
+ $`
748
+ elsif File.exists?("#{path}-events.log")
749
+ path
750
+ end
751
+ if path
752
+ return [Roby::Log::EventStream.new(path)]
753
+ end
754
+ end
755
+
756
+ each_responding_plugin(:data_streams_of, true) do |config|
757
+ if streams = config.data_streams_of(filenames)
758
+ return streams
759
+ end
760
+ end
761
+ nil
762
+ end
763
+
764
+ # Returns the list of data streams suitable for data display known
765
+ # to the application
766
+ def data_streams(log_dir = nil)
767
+ log_dir ||= self.log_dir
768
+ streams = []
769
+ Dir.glob(File.join(log_dir, '*-events.log*')).each do |file|
770
+ next unless file =~ /-events\.log$/
771
+ streams << Roby::Log::EventStream.new($`)
772
+ end
773
+ each_responding_plugin(:data_streams, true) do |config|
774
+ if s = config.data_streams(log_dir)
775
+ streams += s
776
+ end
777
+ end
778
+ streams
779
+ end
780
+
781
+ def self.find_data(name)
782
+ Roby::State.datadirs.each do |dir|
783
+ path = File.join(dir, name)
784
+ return path if File.exists?(path)
785
+ end
786
+ raise Errno::ENOENT, "no file #{name} found in #{Roby::State.datadirs.join(":")}"
787
+ end
788
+
789
+ def self.register_plugin(name, mod, &init)
790
+ caller(1)[0] =~ /^([^:]+):\d/
791
+ dir = File.expand_path(File.dirname($1))
792
+ Roby.app.available_plugins << [name, dir, mod, init]
793
+ end
794
+
795
+ @@reload_model_filter = []
796
+ # Add a filter to model reloading. A task or planner model is
797
+ # reinitialized only if all filter blocks return true for it
798
+ def self.filter_reloaded_models(&block)
799
+ @@reload_model_filter << block
800
+ end
801
+
802
+ def model?(model)
803
+ (model <= Roby::Task) || (model.kind_of?(Roby::TaskModelTag)) ||
804
+ (model <= Planning::Planner) || (model <= Planning::Library)
805
+ end
806
+
807
+ def reload_model?(model)
808
+ @@reload_model_filter.all? { |filter| filter[model] }
809
+ end
810
+
811
+ def app_file?(path)
812
+ (path =~ %r{(^|/)#{APP_DIR}(/|$)}) ||
813
+ ((path[0] != ?/) && File.file?(File.join(APP_DIR, path)))
814
+ end
815
+ def framework_file?(path)
816
+ if path =~ /roby\/.*\.rb$/
817
+ true
818
+ else
819
+ Roby.app.plugins.any? do |name, _|
820
+ _, dir, _, _ = Roby.app.plugin_definition(name)
821
+ path =~ %r{(^|/)#{dir}(/|$)}
822
+ end
823
+ end
824
+ end
825
+
826
+ def reload
827
+ # Always reload this file first. This ensure that one can use #reload
828
+ # to fix the reload code itself
829
+ load __FILE__
830
+
831
+ # Clear all event definitions in task models that are filtered out by
832
+ # Application.filter_reloaded_models
833
+ ObjectSpace.each_object(Class) do |model|
834
+ next unless model?(model)
835
+ next unless reload_model?(model)
836
+
837
+ model.clear_model
838
+ end
839
+
840
+ # Remove what we want to reload from LOADED_FEATURES and use
841
+ # require. Do not use 'load' as the reload order should be the
842
+ # require order.
843
+ needs_reload = []
844
+ $LOADED_FEATURES.delete_if do |feature|
845
+ if framework_file?(feature) || app_file?(feature)
846
+ needs_reload << feature
847
+ end
848
+ end
849
+
850
+ needs_reload.each do |feature|
851
+ begin
852
+ require feature.gsub(/\.rb$/, '')
853
+ rescue Exception => e
854
+ STDERR.puts e.full_message
855
+ end
856
+ end
857
+ end
858
+ end
859
+
860
+ # Load the plugins 'main' files
861
+ Roby.app.plugin_dir File.join(ROBY_ROOT_DIR, 'plugins')
862
+ if plugin_path = ENV['ROBY_PLUGIN_PATH']
863
+ plugin_path.split(':').each do |dir|
864
+ if File.directory?(dir)
865
+ Roby.app.plugin_dir File.expand_path(dir)
866
+ end
867
+ end
868
+ end
869
+ end
870
+