roby 0.8.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (644) hide show
  1. checksums.yaml +7 -0
  2. data/.deep-cover.rb +3 -0
  3. data/.gitattributes +1 -0
  4. data/.gitignore +24 -0
  5. data/.simplecov +10 -0
  6. data/.travis.yml +17 -0
  7. data/.yardopts +4 -0
  8. data/Gemfile +15 -0
  9. data/README.md +11 -0
  10. data/Rakefile +47 -177
  11. data/benchmark/{alloc_misc.rb → attic/alloc_misc.rb} +2 -2
  12. data/benchmark/{discovery_latency.rb → attic/discovery_latency.rb} +19 -19
  13. data/benchmark/{garbage_collection.rb → attic/garbage_collection.rb} +9 -9
  14. data/benchmark/{genom.rb → attic/genom.rb} +0 -0
  15. data/benchmark/attic/transactions.rb +62 -0
  16. data/benchmark/plan_basic_operations.rb +28 -0
  17. data/benchmark/relations/graph.rb +63 -0
  18. data/benchmark/ruby/identity.rb +18 -0
  19. data/benchmark/ruby/set_intersect_vs_hash_merge.rb +39 -0
  20. data/benchmark/ruby/yield_vs_block.rb +35 -0
  21. data/benchmark/run +5 -0
  22. data/benchmark/synthetic_plan_modifications_with_transactions.rb +79 -0
  23. data/benchmark/transactions.rb +99 -51
  24. data/bin/roby +38 -197
  25. data/bin/roby-display +14 -0
  26. data/bin/roby-log +3 -176
  27. data/doc/guide/{src → attic}/abstraction/achieve_with.page +1 -1
  28. data/doc/guide/{src → attic}/abstraction/forwarding.page +1 -1
  29. data/doc/guide/{src → attic}/abstraction/hierarchy.page +1 -1
  30. data/doc/guide/{src → attic}/abstraction/index.page +1 -1
  31. data/doc/guide/{src → attic}/abstraction/task_models.page +1 -1
  32. data/doc/guide/{overview.rdoc → attic/cycle/api_overview.rdoc} +6 -1
  33. data/doc/guide/{src → attic}/cycle/cycle-overview.png +0 -0
  34. data/doc/guide/{src → attic}/cycle/cycle-overview.svg +0 -0
  35. data/doc/guide/attic/cycle/error_handling.page +98 -0
  36. data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.png +0 -0
  37. data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.svg +0 -0
  38. data/doc/guide/{src/cycle/error_handling.page → attic/cycle/error_sources.page} +46 -89
  39. data/doc/guide/{src → attic}/cycle/garbage_collection.page +1 -1
  40. data/doc/guide/{src → attic}/cycle/index.page +1 -1
  41. data/doc/guide/{src → attic}/cycle/propagation.page +11 -1
  42. data/doc/guide/{src → attic}/cycle/propagation_diamond.png +0 -0
  43. data/doc/guide/{src → attic}/cycle/propagation_diamond.svg +0 -0
  44. data/doc/guide/attic/plans/building_plans.page +89 -0
  45. data/doc/guide/attic/plans/code.page +192 -0
  46. data/doc/guide/{src/basics → attic/plans}/events.page +3 -4
  47. data/doc/guide/attic/plans/index.page +7 -0
  48. data/doc/guide/{plan_modifications.rdoc → attic/plans/plan_modifications.rdoc} +5 -3
  49. data/doc/guide/{src/basics → attic/plans}/plan_objects.page +2 -1
  50. data/doc/guide/attic/plans/querying_plans.page +5 -0
  51. data/doc/guide/{src/basics → attic/plans}/tasks.page +20 -20
  52. data/doc/guide/config.yaml +7 -4
  53. data/doc/guide/ext/extended_menu.rb +29 -0
  54. data/doc/guide/ext/init.rb +6 -0
  55. data/doc/guide/ext/rdoc_links.rb +7 -6
  56. data/doc/guide/src/advanced_concepts/history.page +5 -0
  57. data/doc/guide/src/advanced_concepts/index.page +11 -0
  58. data/doc/guide/src/advanced_concepts/recognizing_patterns.page +83 -0
  59. data/doc/guide/src/advanced_concepts/scheduling.page +87 -0
  60. data/doc/guide/src/advanced_concepts/transactions.page +5 -0
  61. data/doc/guide/src/advanced_concepts/unreachability.page +42 -0
  62. data/doc/guide/src/base.template +96 -0
  63. data/doc/guide/src/basics_shell_header.txt +5 -7
  64. data/doc/guide/src/building/action_coordination.page +96 -0
  65. data/doc/guide/src/building/actions.page +124 -0
  66. data/doc/guide/src/building/file_layout.page +71 -0
  67. data/doc/guide/src/building/index.page +50 -0
  68. data/doc/guide/src/building/patterns.page +86 -0
  69. data/doc/guide/src/building/patterns_forwarding.png +0 -0
  70. data/doc/guide/src/building/patterns_forwarding.svg +277 -0
  71. data/doc/guide/src/building/runtime.page +95 -0
  72. data/doc/guide/src/building/task_models.page +94 -0
  73. data/doc/guide/src/building/tasks.page +284 -0
  74. data/doc/guide/src/concepts/error_handling.page +100 -0
  75. data/doc/guide/src/concepts/exception_propagation.png +0 -0
  76. data/doc/guide/src/concepts/exception_propagation.svg +445 -0
  77. data/doc/guide/src/concepts/execution.page +85 -0
  78. data/doc/guide/src/concepts/execution.png +0 -0
  79. data/doc/guide/src/concepts/execution.svg +573 -0
  80. data/doc/guide/src/concepts/execution_cycle.png +0 -0
  81. data/doc/guide/src/concepts/garbage_collection.page +57 -0
  82. data/doc/guide/src/concepts/index.page +27 -0
  83. data/doc/guide/src/concepts/plans.page +101 -0
  84. data/doc/guide/src/concepts/policy.page +31 -0
  85. data/doc/guide/src/concepts/reactor.page +61 -0
  86. data/doc/guide/src/concepts/simple_plan_example.png +0 -0
  87. data/doc/guide/src/concepts/simple_plan_example.svg +376 -0
  88. data/doc/guide/src/default.template +9 -74
  89. data/doc/guide/src/event_relations/forward.page +71 -0
  90. data/doc/guide/src/event_relations/index.page +12 -0
  91. data/doc/guide/src/event_relations/scheduling_constraints.page +43 -0
  92. data/doc/guide/src/event_relations/signal.page +55 -0
  93. data/doc/guide/src/event_relations/temporal_constraints.page +77 -0
  94. data/doc/guide/src/htmldoc.metainfo +21 -8
  95. data/doc/guide/src/index.page +8 -3
  96. data/doc/guide/src/{introduction/install.page → installation/index.page} +37 -25
  97. data/doc/guide/src/installation/publications.page +14 -0
  98. data/doc/guide/src/{introduction → installation}/videos.page +14 -7
  99. data/doc/guide/src/interacting/index.page +16 -0
  100. data/doc/guide/src/interacting/run.page +33 -0
  101. data/doc/guide/src/interacting/shell.page +95 -0
  102. data/doc/guide/src/plugins/creating_plugins.page +72 -0
  103. data/doc/guide/src/plugins/index.page +27 -5
  104. data/doc/guide/src/plugins/{fault_tolerance.page → standard_plugins/fault_tolerance.page} +2 -2
  105. data/doc/guide/src/plugins/standard_plugins/index.page +11 -0
  106. data/doc/guide/src/plugins/{subsystems.page → standard_plugins/subsystems.page} +2 -2
  107. data/doc/guide/src/style_screen.css +687 -0
  108. data/doc/guide/src/task_relations/dependency.page +107 -0
  109. data/doc/guide/src/task_relations/executed_by.page +77 -0
  110. data/doc/guide/src/task_relations/index.page +12 -0
  111. data/doc/guide/src/task_relations/new_relations.page +119 -0
  112. data/doc/guide/src/task_relations/planned_by.page +46 -0
  113. data/doc/guide/src/tutorial/app.page +117 -0
  114. data/doc/guide/src/{basics → tutorial}/code_examples.page +6 -5
  115. data/doc/guide/src/{basics → tutorial}/dry.page +15 -15
  116. data/doc/guide/src/{basics → tutorial}/errors.page +43 -68
  117. data/doc/guide/src/tutorial/events.page +195 -0
  118. data/doc/guide/src/{basics → tutorial}/hierarchy.page +53 -52
  119. data/doc/guide/src/tutorial/index.page +13 -0
  120. data/doc/guide/src/tutorial/log_replay/goForward_1.png +0 -0
  121. data/doc/guide/src/tutorial/log_replay/goForward_2.png +0 -0
  122. data/doc/guide/src/tutorial/log_replay/goForward_3.png +0 -0
  123. data/doc/guide/src/{basics → tutorial}/log_replay/goForward_4.png +0 -0
  124. data/doc/guide/src/tutorial/log_replay/goForward_5.png +0 -0
  125. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_1.png +0 -0
  126. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_2.png +0 -0
  127. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_3.png +0 -0
  128. data/doc/guide/src/tutorial/log_replay/moveto_code_error.png +0 -0
  129. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_1.png +0 -0
  130. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_2.png +0 -0
  131. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_3.png +0 -0
  132. data/doc/guide/src/tutorial/log_replay/plan_repair_4.png +0 -0
  133. data/doc/guide/src/tutorial/log_replay/roby_log_main_window.png +0 -0
  134. data/doc/guide/src/{basics → tutorial}/log_replay/roby_log_relation_window.png +0 -0
  135. data/doc/guide/src/{basics → tutorial}/log_replay/roby_replay_event_representation.png +0 -0
  136. data/doc/guide/src/tutorial/relations_display.page +153 -0
  137. data/doc/guide/src/{basics → tutorial}/roby_cycle_overview.png +0 -0
  138. data/doc/guide/src/tutorial/shell.page +121 -0
  139. data/doc/guide/src/{basics → tutorial}/summary.page +1 -1
  140. data/doc/guide/src/tutorial/tasks.page +374 -0
  141. data/lib/roby.rb +102 -47
  142. data/lib/roby/actions.rb +17 -0
  143. data/lib/roby/actions/action.rb +80 -0
  144. data/lib/roby/actions/interface.rb +45 -0
  145. data/lib/roby/actions/library.rb +23 -0
  146. data/lib/roby/actions/models/action.rb +224 -0
  147. data/lib/roby/actions/models/coordination_action.rb +58 -0
  148. data/lib/roby/actions/models/interface.rb +22 -0
  149. data/lib/roby/actions/models/interface_base.rb +294 -0
  150. data/lib/roby/actions/models/library.rb +12 -0
  151. data/lib/roby/actions/models/method_action.rb +90 -0
  152. data/lib/roby/actions/task.rb +114 -0
  153. data/lib/roby/and_generator.rb +125 -0
  154. data/lib/roby/app.rb +2795 -829
  155. data/lib/roby/app/autotest_console_reporter.rb +138 -0
  156. data/lib/roby/app/base.rb +21 -0
  157. data/lib/roby/app/cucumber.rb +2 -0
  158. data/lib/roby/app/cucumber/controller.rb +439 -0
  159. data/lib/roby/app/cucumber/helpers.rb +280 -0
  160. data/lib/roby/app/cucumber/world.rb +32 -0
  161. data/lib/roby/app/debug.rb +136 -0
  162. data/lib/roby/app/gen.rb +2 -0
  163. data/lib/roby/app/rake.rb +178 -38
  164. data/lib/roby/app/robot_config.rb +9 -0
  165. data/lib/roby/app/robot_names.rb +115 -0
  166. data/lib/roby/app/run.rb +3 -2
  167. data/lib/roby/app/scripts.rb +72 -0
  168. data/lib/roby/app/scripts/autotest.rb +173 -0
  169. data/lib/roby/app/scripts/display.rb +2 -0
  170. data/lib/roby/app/scripts/restart.rb +52 -0
  171. data/lib/roby/app/scripts/results.rb +17 -8
  172. data/lib/roby/app/scripts/run.rb +155 -24
  173. data/lib/roby/app/scripts/shell.rb +147 -62
  174. data/lib/roby/app/scripts/test.rb +107 -22
  175. data/lib/roby/app/test_reporter.rb +74 -0
  176. data/lib/roby/app/test_server.rb +159 -0
  177. data/lib/roby/app/vagrant.rb +47 -0
  178. data/lib/roby/backports.rb +16 -0
  179. data/lib/roby/cli/display.rb +190 -0
  180. data/lib/roby/cli/exceptions.rb +17 -0
  181. data/lib/roby/cli/gen/actions/class.rb +5 -0
  182. data/lib/roby/cli/gen/actions/test.rb +6 -0
  183. data/lib/roby/cli/gen/app/.yardopts +6 -0
  184. data/lib/roby/cli/gen/app/README.md +28 -0
  185. data/lib/roby/cli/gen/app/Rakefile +15 -0
  186. data/{app → lib/roby/cli/gen/app}/config/app.yml +29 -39
  187. data/lib/roby/cli/gen/app/models/.gitattributes +1 -0
  188. data/{app → lib/roby/cli/gen/app/scripts}/controllers/.gitattributes +0 -0
  189. data/{app/data/.gitattributes → lib/roby/cli/gen/app/test/.gitignore} +0 -0
  190. data/lib/roby/cli/gen/class/class.rb +6 -0
  191. data/lib/roby/cli/gen/class/test.rb +7 -0
  192. data/lib/roby/cli/gen/helpers.rb +203 -0
  193. data/lib/roby/cli/gen/module/module.rb +5 -0
  194. data/lib/roby/cli/gen/module/test.rb +6 -0
  195. data/lib/roby/cli/gen/roby_app/config/init.rb +17 -0
  196. data/lib/roby/cli/gen/roby_app/config/robots/robot.rb +40 -0
  197. data/lib/roby/cli/gen/task/class.rb +44 -0
  198. data/lib/roby/cli/gen/task/test.rb +6 -0
  199. data/lib/roby/cli/gen_main.rb +120 -0
  200. data/lib/roby/cli/log.rb +276 -0
  201. data/lib/roby/cli/log/flamegraph.html +499 -0
  202. data/lib/roby/cli/log/flamegraph_renderer.rb +88 -0
  203. data/lib/roby/cli/main.rb +153 -0
  204. data/lib/roby/coordination.rb +60 -0
  205. data/lib/roby/coordination/action_script.rb +25 -0
  206. data/lib/roby/coordination/action_state_machine.rb +125 -0
  207. data/lib/roby/coordination/actions.rb +106 -0
  208. data/lib/roby/coordination/base.rb +145 -0
  209. data/lib/roby/coordination/calculus.rb +40 -0
  210. data/lib/roby/coordination/child.rb +28 -0
  211. data/lib/roby/coordination/event.rb +29 -0
  212. data/lib/roby/coordination/fault_handler.rb +25 -0
  213. data/lib/roby/coordination/fault_handling_task.rb +13 -0
  214. data/lib/roby/coordination/fault_response_table.rb +110 -0
  215. data/lib/roby/coordination/models/action_script.rb +64 -0
  216. data/lib/roby/coordination/models/action_state_machine.rb +224 -0
  217. data/lib/roby/coordination/models/actions.rb +191 -0
  218. data/lib/roby/coordination/models/arguments.rb +55 -0
  219. data/lib/roby/coordination/models/base.rb +176 -0
  220. data/lib/roby/coordination/models/capture.rb +86 -0
  221. data/lib/roby/coordination/models/child.rb +35 -0
  222. data/lib/roby/coordination/models/event.rb +41 -0
  223. data/lib/roby/coordination/models/exceptions.rb +42 -0
  224. data/lib/roby/coordination/models/fault_handler.rb +219 -0
  225. data/lib/roby/coordination/models/fault_response_table.rb +77 -0
  226. data/lib/roby/coordination/models/root.rb +22 -0
  227. data/lib/roby/coordination/models/script.rb +283 -0
  228. data/lib/roby/coordination/models/task.rb +184 -0
  229. data/lib/roby/coordination/models/task_from_action.rb +50 -0
  230. data/lib/roby/coordination/models/task_from_as_plan.rb +33 -0
  231. data/lib/roby/coordination/models/task_from_instanciation_object.rb +31 -0
  232. data/lib/roby/coordination/models/task_from_variable.rb +27 -0
  233. data/lib/roby/coordination/models/task_with_dependencies.rb +48 -0
  234. data/lib/roby/coordination/models/variable.rb +32 -0
  235. data/lib/roby/coordination/script.rb +200 -0
  236. data/lib/roby/coordination/script_instruction.rb +12 -0
  237. data/lib/roby/coordination/task.rb +45 -0
  238. data/lib/roby/coordination/task_base.rb +69 -0
  239. data/lib/roby/coordination/task_script.rb +293 -0
  240. data/lib/roby/coordination/task_state_machine.rb +308 -0
  241. data/lib/roby/decision_control.rb +33 -21
  242. data/lib/roby/distributed_object.rb +76 -0
  243. data/lib/roby/droby.rb +17 -0
  244. data/lib/roby/droby/droby_id.rb +6 -0
  245. data/lib/roby/droby/enable.rb +153 -0
  246. data/lib/roby/droby/event_logger.rb +189 -0
  247. data/lib/roby/droby/event_logging.rb +57 -0
  248. data/lib/roby/droby/exceptions.rb +14 -0
  249. data/lib/roby/droby/identifiable.rb +22 -0
  250. data/lib/roby/droby/logfile.rb +141 -0
  251. data/lib/roby/droby/logfile/client.rb +176 -0
  252. data/lib/roby/droby/logfile/file_format.md +97 -0
  253. data/lib/roby/droby/logfile/index.rb +117 -0
  254. data/lib/roby/droby/logfile/reader.rb +139 -0
  255. data/lib/roby/droby/logfile/server.rb +199 -0
  256. data/lib/roby/droby/logfile/writer.rb +114 -0
  257. data/lib/roby/droby/marshal.rb +264 -0
  258. data/lib/roby/droby/marshallable.rb +12 -0
  259. data/lib/roby/droby/null_event_logger.rb +25 -0
  260. data/lib/roby/droby/object_manager.rb +205 -0
  261. data/lib/roby/droby/peer_id.rb +6 -0
  262. data/lib/roby/droby/plan_rebuilder.rb +373 -0
  263. data/lib/roby/droby/rebuilt_plan.rb +160 -0
  264. data/lib/roby/droby/remote_droby_id.rb +6 -0
  265. data/lib/roby/droby/timepoints.rb +205 -0
  266. data/lib/roby/droby/timepoints_ctf.metadata.erb +101 -0
  267. data/lib/roby/droby/timepoints_ctf.rb +125 -0
  268. data/lib/roby/droby/v5.rb +14 -0
  269. data/lib/roby/droby/v5/builtin.rb +120 -0
  270. data/lib/roby/droby/v5/droby_class.rb +45 -0
  271. data/lib/roby/droby/v5/droby_constant.rb +81 -0
  272. data/lib/roby/droby/v5/droby_dump.rb +1026 -0
  273. data/lib/roby/droby/v5/droby_id.rb +44 -0
  274. data/lib/roby/droby/v5/droby_model.rb +82 -0
  275. data/lib/roby/droby/v5/peer_id.rb +10 -0
  276. data/lib/roby/droby/v5/remote_droby_id.rb +42 -0
  277. data/lib/roby/event.rb +79 -957
  278. data/lib/roby/event_constraints.rb +835 -0
  279. data/lib/roby/event_generator.rb +1047 -0
  280. data/lib/roby/event_structure/causal_link.rb +6 -0
  281. data/lib/roby/event_structure/forwarding.rb +6 -0
  282. data/lib/roby/event_structure/precedence.rb +7 -0
  283. data/lib/roby/event_structure/signal.rb +8 -0
  284. data/lib/roby/event_structure/temporal_constraints.rb +640 -0
  285. data/lib/roby/exceptions.rb +446 -152
  286. data/lib/roby/executable_plan.rb +549 -0
  287. data/lib/roby/execution_engine.rb +1997 -950
  288. data/lib/roby/filter_generator.rb +26 -0
  289. data/lib/roby/gui/chronicle_view.rb +225 -0
  290. data/lib/roby/gui/chronicle_widget.rb +925 -0
  291. data/lib/roby/gui/dot_id.rb +11 -0
  292. data/lib/roby/gui/exception_view.rb +44 -0
  293. data/lib/roby/gui/log_display.rb +273 -0
  294. data/lib/roby/gui/model_views.rb +2 -0
  295. data/lib/roby/gui/model_views/action_interface.rb +53 -0
  296. data/lib/roby/gui/model_views/task.rb +47 -0
  297. data/lib/roby/gui/model_views/task.rhtml +41 -0
  298. data/lib/roby/gui/object_info_view.rb +89 -0
  299. data/lib/roby/gui/plan_dot_layout.rb +427 -0
  300. data/lib/roby/gui/plan_rebuilder_widget.rb +357 -0
  301. data/lib/roby/gui/qt4_toMSecsSinceEpoch.rb +8 -0
  302. data/lib/roby/gui/relations_view.rb +278 -0
  303. data/lib/roby/gui/relations_view/relations.ui +139 -0
  304. data/lib/roby/gui/relations_view/relations_canvas.rb +1088 -0
  305. data/lib/roby/gui/relations_view/relations_config.rb +292 -0
  306. data/lib/roby/gui/relations_view/relations_view.ui +53 -0
  307. data/lib/roby/gui/scheduler_view.css +24 -0
  308. data/lib/roby/gui/scheduler_view.rb +46 -0
  309. data/lib/roby/gui/scheduler_view.rhtml +53 -0
  310. data/lib/roby/gui/stepping.rb +93 -0
  311. data/lib/roby/gui/stepping.ui +181 -0
  312. data/lib/roby/gui/styles.rb +81 -0
  313. data/lib/roby/gui/task_display_configuration.rb +42 -0
  314. data/lib/roby/gui/task_state_at.rb +38 -0
  315. data/lib/roby/hooks.rb +26 -0
  316. data/lib/roby/interface.rb +136 -469
  317. data/lib/roby/interface/async.rb +20 -0
  318. data/lib/roby/interface/async/action_monitor.rb +188 -0
  319. data/lib/roby/interface/async/interface.rb +498 -0
  320. data/lib/roby/interface/async/job_monitor.rb +213 -0
  321. data/lib/roby/interface/async/log.rb +238 -0
  322. data/lib/roby/interface/async/new_job_listener.rb +79 -0
  323. data/lib/roby/interface/async/ui_connector.rb +183 -0
  324. data/lib/roby/interface/client.rb +553 -0
  325. data/lib/roby/interface/command.rb +24 -0
  326. data/lib/roby/interface/command_argument.rb +16 -0
  327. data/lib/roby/interface/command_library.rb +92 -0
  328. data/lib/roby/interface/droby_channel.rb +174 -0
  329. data/lib/roby/interface/exceptions.rb +22 -0
  330. data/lib/roby/interface/interface.rb +655 -0
  331. data/lib/roby/interface/job.rb +47 -0
  332. data/lib/roby/interface/rest.rb +10 -0
  333. data/lib/roby/interface/rest/api.rb +29 -0
  334. data/lib/roby/interface/rest/helpers.rb +24 -0
  335. data/lib/roby/interface/rest/server.rb +212 -0
  336. data/lib/roby/interface/server.rb +154 -0
  337. data/lib/roby/interface/shell_client.rb +468 -0
  338. data/lib/roby/interface/shell_subcommand.rb +24 -0
  339. data/lib/roby/interface/subcommand_client.rb +35 -0
  340. data/lib/roby/interface/tcp.rb +168 -0
  341. data/lib/roby/models/arguments.rb +112 -0
  342. data/lib/roby/models/plan_object.rb +83 -0
  343. data/lib/roby/models/task.rb +835 -0
  344. data/lib/roby/models/task_event.rb +62 -0
  345. data/lib/roby/models/task_service.rb +78 -0
  346. data/lib/roby/or_generator.rb +88 -0
  347. data/lib/roby/plan.rb +1751 -864
  348. data/lib/roby/plan_object.rb +611 -0
  349. data/lib/roby/plan_service.rb +200 -0
  350. data/lib/roby/promise.rb +332 -0
  351. data/lib/roby/queries.rb +23 -0
  352. data/lib/roby/queries/and_matcher.rb +32 -0
  353. data/lib/roby/queries/any.rb +27 -0
  354. data/lib/roby/queries/code_error_matcher.rb +58 -0
  355. data/lib/roby/queries/event_generator_matcher.rb +9 -0
  356. data/lib/roby/queries/execution_exception_matcher.rb +165 -0
  357. data/lib/roby/queries/index.rb +165 -0
  358. data/lib/roby/queries/localized_error_matcher.rb +149 -0
  359. data/lib/roby/queries/matcher_base.rb +107 -0
  360. data/lib/roby/queries/none.rb +27 -0
  361. data/lib/roby/queries/not_matcher.rb +30 -0
  362. data/lib/roby/queries/op_matcher.rb +8 -0
  363. data/lib/roby/queries/or_matcher.rb +30 -0
  364. data/lib/roby/queries/plan_object_matcher.rb +363 -0
  365. data/lib/roby/queries/query.rb +188 -0
  366. data/lib/roby/queries/task_event_generator_matcher.rb +86 -0
  367. data/lib/roby/queries/task_matcher.rb +344 -0
  368. data/lib/roby/relations.rb +42 -678
  369. data/lib/roby/relations/bidirectional_directed_adjacency_graph.rb +492 -0
  370. data/lib/roby/relations/directed_relation_support.rb +268 -0
  371. data/lib/roby/relations/event_relation_graph.rb +19 -0
  372. data/lib/roby/relations/fork_merge_visitor.rb +154 -0
  373. data/lib/roby/relations/graph.rb +533 -0
  374. data/lib/roby/relations/models/directed_relation_support.rb +11 -0
  375. data/lib/roby/relations/models/graph.rb +75 -0
  376. data/lib/roby/relations/models/task_relation_graph.rb +18 -0
  377. data/lib/roby/relations/space.rb +380 -0
  378. data/lib/roby/relations/task_relation_graph.rb +20 -0
  379. data/lib/roby/robot.rb +85 -38
  380. data/lib/roby/schedulers/basic.rb +155 -25
  381. data/lib/roby/schedulers/null.rb +20 -0
  382. data/lib/roby/schedulers/reporting.rb +31 -0
  383. data/lib/roby/schedulers/state.rb +129 -0
  384. data/lib/roby/schedulers/temporal.rb +91 -0
  385. data/lib/roby/singletons.rb +87 -0
  386. data/lib/roby/standalone.rb +4 -2
  387. data/lib/roby/standard_errors.rb +405 -82
  388. data/lib/roby/state.rb +6 -3
  389. data/lib/roby/state/conf_model.rb +5 -0
  390. data/lib/roby/state/events.rb +181 -95
  391. data/lib/roby/state/goal_model.rb +77 -0
  392. data/lib/roby/state/open_struct.rb +591 -0
  393. data/lib/roby/state/open_struct_model.rb +68 -0
  394. data/lib/roby/state/pos.rb +45 -45
  395. data/lib/roby/state/shapes.rb +11 -11
  396. data/lib/roby/state/state_model.rb +303 -0
  397. data/lib/roby/state/task.rb +43 -0
  398. data/lib/roby/support.rb +88 -148
  399. data/lib/roby/task.rb +1361 -1750
  400. data/lib/roby/task_arguments.rb +428 -0
  401. data/lib/roby/task_event.rb +127 -0
  402. data/lib/roby/task_event_generator.rb +337 -0
  403. data/lib/roby/task_service.rb +6 -0
  404. data/lib/roby/task_structure/conflicts.rb +104 -0
  405. data/lib/roby/task_structure/dependency.rb +932 -0
  406. data/lib/roby/task_structure/error_handling.rb +118 -0
  407. data/lib/roby/task_structure/executed_by.rb +234 -0
  408. data/lib/roby/task_structure/planned_by.rb +90 -0
  409. data/lib/roby/tasks/aggregator.rb +37 -0
  410. data/lib/roby/tasks/external_process.rb +275 -0
  411. data/lib/roby/tasks/group.rb +27 -0
  412. data/lib/roby/tasks/null.rb +19 -0
  413. data/lib/roby/tasks/parallel.rb +43 -0
  414. data/lib/roby/tasks/sequence.rb +88 -0
  415. data/lib/roby/tasks/simple.rb +21 -0
  416. data/lib/roby/{thread_task.rb → tasks/thread.rb} +50 -24
  417. data/lib/roby/tasks/timeout.rb +17 -0
  418. data/lib/roby/tasks/virtual.rb +55 -0
  419. data/lib/roby/template_plan.rb +7 -0
  420. data/lib/roby/test/aruba_minitest.rb +74 -0
  421. data/lib/roby/test/assertion.rb +16 -0
  422. data/lib/roby/test/assertions.rb +490 -0
  423. data/lib/roby/test/common.rb +368 -591
  424. data/lib/roby/test/dsl.rb +149 -0
  425. data/lib/roby/test/error.rb +18 -0
  426. data/lib/roby/test/event_reporter.rb +83 -0
  427. data/lib/roby/test/execution_expectations.rb +1134 -0
  428. data/lib/roby/test/expect_execution.rb +151 -0
  429. data/lib/roby/test/minitest_helpers.rb +166 -0
  430. data/lib/roby/test/roby_app_helpers.rb +200 -0
  431. data/lib/roby/test/run_planners.rb +155 -0
  432. data/lib/roby/test/self.rb +112 -0
  433. data/lib/roby/test/spec.rb +198 -0
  434. data/lib/roby/test/tasks/empty_task.rb +4 -4
  435. data/lib/roby/test/tasks/goto.rb +28 -27
  436. data/lib/roby/test/teardown_plans.rb +100 -0
  437. data/lib/roby/test/testcase.rb +239 -307
  438. data/lib/roby/test/tools.rb +159 -155
  439. data/lib/roby/test/validate_state_machine.rb +75 -0
  440. data/lib/roby/transaction.rb +1125 -0
  441. data/lib/roby/transaction/event_generator_proxy.rb +63 -0
  442. data/lib/roby/transaction/plan_object_proxy.rb +99 -0
  443. data/lib/roby/transaction/plan_service_proxy.rb +43 -0
  444. data/lib/roby/transaction/proxying.rb +120 -0
  445. data/lib/roby/transaction/task_event_generator_proxy.rb +19 -0
  446. data/lib/roby/transaction/task_proxy.rb +135 -0
  447. data/lib/roby/until_generator.rb +30 -0
  448. data/lib/roby/version.rb +5 -0
  449. data/lib/roby/yard.rb +169 -0
  450. data/lib/yard-roby.rb +1 -0
  451. data/manifest.xml +32 -6
  452. data/roby.gemspec +59 -0
  453. metadata +788 -587
  454. data/Manifest.txt +0 -321
  455. data/NOTES +0 -4
  456. data/README.txt +0 -166
  457. data/TODO.txt +0 -146
  458. data/app/README.txt +0 -24
  459. data/app/Rakefile +0 -8
  460. data/app/config/ROBOT.rb +0 -5
  461. data/app/config/init.rb +0 -33
  462. data/app/config/roby.yml +0 -3
  463. data/app/controllers/ROBOT.rb +0 -2
  464. data/app/planners/ROBOT/main.rb +0 -6
  465. data/app/planners/main.rb +0 -5
  466. data/app/scripts/distributed +0 -3
  467. data/app/scripts/generate/bookmarks +0 -3
  468. data/app/scripts/replay +0 -3
  469. data/app/scripts/results +0 -3
  470. data/app/scripts/run +0 -3
  471. data/app/scripts/server +0 -3
  472. data/app/scripts/shell +0 -3
  473. data/app/scripts/test +0 -3
  474. data/app/tasks/.gitattributes +0 -0
  475. data/app/tasks/ROBOT/.gitattributes +0 -0
  476. data/bin/roby-shell +0 -25
  477. data/doc/guide/src/basics/app.page +0 -139
  478. data/doc/guide/src/basics/index.page +0 -11
  479. data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
  480. data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
  481. data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
  482. data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
  483. data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
  484. data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
  485. data/doc/guide/src/basics/relations_display.page +0 -203
  486. data/doc/guide/src/basics/shell.page +0 -102
  487. data/doc/guide/src/default.css +0 -319
  488. data/doc/guide/src/introduction/index.page +0 -29
  489. data/doc/guide/src/introduction/publications.page +0 -14
  490. data/doc/guide/src/relations/dependency.page +0 -89
  491. data/doc/guide/src/relations/index.page +0 -12
  492. data/ext/droby/dump.cc +0 -175
  493. data/ext/droby/extconf.rb +0 -3
  494. data/ext/graph/algorithm.cc +0 -746
  495. data/ext/graph/extconf.rb +0 -7
  496. data/ext/graph/graph.cc +0 -575
  497. data/ext/graph/graph.hh +0 -183
  498. data/ext/graph/iterator_sequence.hh +0 -102
  499. data/ext/graph/undirected_dfs.hh +0 -226
  500. data/ext/graph/undirected_graph.hh +0 -421
  501. data/lib/roby/app/scripts/generate/bookmarks.rb +0 -162
  502. data/lib/roby/app/scripts/replay.rb +0 -31
  503. data/lib/roby/app/scripts/server.rb +0 -18
  504. data/lib/roby/basic_object.rb +0 -151
  505. data/lib/roby/config.rb +0 -14
  506. data/lib/roby/distributed.rb +0 -36
  507. data/lib/roby/distributed/base.rb +0 -448
  508. data/lib/roby/distributed/communication.rb +0 -875
  509. data/lib/roby/distributed/connection_space.rb +0 -616
  510. data/lib/roby/distributed/distributed_object.rb +0 -206
  511. data/lib/roby/distributed/drb.rb +0 -62
  512. data/lib/roby/distributed/notifications.rb +0 -531
  513. data/lib/roby/distributed/peer.rb +0 -555
  514. data/lib/roby/distributed/protocol.rb +0 -529
  515. data/lib/roby/distributed/proxy.rb +0 -343
  516. data/lib/roby/distributed/subscription.rb +0 -311
  517. data/lib/roby/distributed/transaction.rb +0 -498
  518. data/lib/roby/external_process_task.rb +0 -225
  519. data/lib/roby/graph.rb +0 -160
  520. data/lib/roby/log.rb +0 -3
  521. data/lib/roby/log/chronicle.rb +0 -303
  522. data/lib/roby/log/console.rb +0 -74
  523. data/lib/roby/log/data_stream.rb +0 -275
  524. data/lib/roby/log/dot.rb +0 -279
  525. data/lib/roby/log/event_stream.rb +0 -161
  526. data/lib/roby/log/file.rb +0 -396
  527. data/lib/roby/log/gui/basic_display.ui +0 -83
  528. data/lib/roby/log/gui/basic_display_ui.rb +0 -89
  529. data/lib/roby/log/gui/chronicle.rb +0 -26
  530. data/lib/roby/log/gui/chronicle_view.rb +0 -40
  531. data/lib/roby/log/gui/chronicle_view.ui +0 -70
  532. data/lib/roby/log/gui/chronicle_view_ui.rb +0 -90
  533. data/lib/roby/log/gui/data_displays.rb +0 -171
  534. data/lib/roby/log/gui/data_displays.ui +0 -155
  535. data/lib/roby/log/gui/data_displays_ui.rb +0 -146
  536. data/lib/roby/log/gui/notifications.rb +0 -26
  537. data/lib/roby/log/gui/relations.rb +0 -269
  538. data/lib/roby/log/gui/relations.ui +0 -123
  539. data/lib/roby/log/gui/relations_ui.rb +0 -120
  540. data/lib/roby/log/gui/relations_view.rb +0 -185
  541. data/lib/roby/log/gui/relations_view.ui +0 -149
  542. data/lib/roby/log/gui/relations_view_ui.rb +0 -144
  543. data/lib/roby/log/gui/replay.rb +0 -366
  544. data/lib/roby/log/gui/replay_controls.rb +0 -206
  545. data/lib/roby/log/gui/replay_controls.ui +0 -282
  546. data/lib/roby/log/gui/replay_controls_ui.rb +0 -249
  547. data/lib/roby/log/gui/runtime.rb +0 -130
  548. data/lib/roby/log/hooks.rb +0 -186
  549. data/lib/roby/log/logger.rb +0 -203
  550. data/lib/roby/log/notifications.rb +0 -244
  551. data/lib/roby/log/plan_rebuilder.rb +0 -468
  552. data/lib/roby/log/relations.rb +0 -1084
  553. data/lib/roby/log/server.rb +0 -547
  554. data/lib/roby/log/sqlite.rb +0 -47
  555. data/lib/roby/log/timings.rb +0 -233
  556. data/lib/roby/plan-object.rb +0 -371
  557. data/lib/roby/planning.rb +0 -13
  558. data/lib/roby/planning/loops.rb +0 -309
  559. data/lib/roby/planning/model.rb +0 -1012
  560. data/lib/roby/planning/task.rb +0 -180
  561. data/lib/roby/query.rb +0 -655
  562. data/lib/roby/relations/conflicts.rb +0 -67
  563. data/lib/roby/relations/dependency.rb +0 -358
  564. data/lib/roby/relations/ensured.rb +0 -19
  565. data/lib/roby/relations/error_handling.rb +0 -22
  566. data/lib/roby/relations/events.rb +0 -7
  567. data/lib/roby/relations/executed_by.rb +0 -208
  568. data/lib/roby/relations/influence.rb +0 -10
  569. data/lib/roby/relations/planned_by.rb +0 -63
  570. data/lib/roby/state/information.rb +0 -55
  571. data/lib/roby/state/state.rb +0 -367
  572. data/lib/roby/task-operations.rb +0 -186
  573. data/lib/roby/task_index.rb +0 -80
  574. data/lib/roby/test/distributed.rb +0 -230
  575. data/lib/roby/test/tasks/simple_task.rb +0 -23
  576. data/lib/roby/transactions.rb +0 -507
  577. data/lib/roby/transactions/proxy.rb +0 -325
  578. data/plugins/fault_injection/History.txt +0 -4
  579. data/plugins/fault_injection/README.txt +0 -34
  580. data/plugins/fault_injection/Rakefile +0 -12
  581. data/plugins/fault_injection/TODO.txt +0 -0
  582. data/plugins/fault_injection/app.rb +0 -52
  583. data/plugins/fault_injection/fault_injection.rb +0 -89
  584. data/plugins/fault_injection/test/test_fault_injection.rb +0 -78
  585. data/plugins/subsystems/README.txt +0 -37
  586. data/plugins/subsystems/Rakefile +0 -13
  587. data/plugins/subsystems/app.rb +0 -182
  588. data/plugins/subsystems/test/app/README +0 -24
  589. data/plugins/subsystems/test/app/Rakefile +0 -8
  590. data/plugins/subsystems/test/app/config/app.yml +0 -71
  591. data/plugins/subsystems/test/app/config/init.rb +0 -12
  592. data/plugins/subsystems/test/app/config/roby.yml +0 -3
  593. data/plugins/subsystems/test/app/planners/main.rb +0 -20
  594. data/plugins/subsystems/test/app/scripts/distributed +0 -3
  595. data/plugins/subsystems/test/app/scripts/replay +0 -3
  596. data/plugins/subsystems/test/app/scripts/results +0 -3
  597. data/plugins/subsystems/test/app/scripts/run +0 -3
  598. data/plugins/subsystems/test/app/scripts/server +0 -3
  599. data/plugins/subsystems/test/app/scripts/shell +0 -3
  600. data/plugins/subsystems/test/app/scripts/test +0 -3
  601. data/plugins/subsystems/test/app/tasks/services.rb +0 -15
  602. data/plugins/subsystems/test/test_subsystems.rb +0 -78
  603. data/test/distributed/test_communication.rb +0 -195
  604. data/test/distributed/test_connection.rb +0 -284
  605. data/test/distributed/test_execution.rb +0 -378
  606. data/test/distributed/test_mixed_plan.rb +0 -341
  607. data/test/distributed/test_plan_notifications.rb +0 -238
  608. data/test/distributed/test_protocol.rb +0 -525
  609. data/test/distributed/test_query.rb +0 -106
  610. data/test/distributed/test_remote_plan.rb +0 -491
  611. data/test/distributed/test_transaction.rb +0 -466
  612. data/test/mockups/external_process +0 -28
  613. data/test/mockups/tasks.rb +0 -27
  614. data/test/planning/test_loops.rb +0 -432
  615. data/test/planning/test_model.rb +0 -427
  616. data/test/planning/test_task.rb +0 -126
  617. data/test/relations/test_conflicts.rb +0 -42
  618. data/test/relations/test_dependency.rb +0 -324
  619. data/test/relations/test_ensured.rb +0 -38
  620. data/test/relations/test_executed_by.rb +0 -224
  621. data/test/relations/test_planned_by.rb +0 -56
  622. data/test/suite_core.rb +0 -29
  623. data/test/suite_distributed.rb +0 -10
  624. data/test/suite_planning.rb +0 -4
  625. data/test/suite_relations.rb +0 -8
  626. data/test/tasks/test_external_process.rb +0 -126
  627. data/test/tasks/test_thread_task.rb +0 -70
  628. data/test/test_bgl.rb +0 -528
  629. data/test/test_event.rb +0 -969
  630. data/test/test_exceptions.rb +0 -591
  631. data/test/test_execution_engine.rb +0 -987
  632. data/test/test_gui.rb +0 -20
  633. data/test/test_interface.rb +0 -43
  634. data/test/test_log.rb +0 -125
  635. data/test/test_log_server.rb +0 -133
  636. data/test/test_plan.rb +0 -418
  637. data/test/test_query.rb +0 -424
  638. data/test/test_relations.rb +0 -260
  639. data/test/test_state.rb +0 -432
  640. data/test/test_support.rb +0 -16
  641. data/test/test_task.rb +0 -1181
  642. data/test/test_testcase.rb +0 -138
  643. data/test/test_transactions.rb +0 -610
  644. data/test/test_transactions_proxy.rb +0 -216
@@ -0,0 +1,125 @@
1
+ module Roby
2
+ # Combine event generators using an AND. The generator will emit once all
3
+ # its source events have emitted, and become unreachable if any of its
4
+ # source events have become unreachable.
5
+ #
6
+ # For instance,
7
+ #
8
+ # a = task1.start_event
9
+ # b = task2.start_event
10
+ # (a & b) # will emit when both tasks have started
11
+ #
12
+ # And events will emit only once, unless #reset is called:
13
+ #
14
+ # a = task1.intermediate_event
15
+ # b = task2.intermediate_event
16
+ # and_ev = (a & b)
17
+ #
18
+ # a.intermediate_event!
19
+ # b.intermediate_event! # and_ev emits here
20
+ # a.intermediate_event!
21
+ # b.intermediate_event! # and_ev does *not* emit
22
+ #
23
+ # and_ev.reset
24
+ # a.intermediate_event!
25
+ # b.intermediate_event! # and_ev emits here
26
+ #
27
+ # The AndGenerator tracks its sources via the signalling relations, so
28
+ #
29
+ # and_ev << c.intermediate_event
30
+ #
31
+ # is equivalent to
32
+ #
33
+ # c.intermediate_event.add_signal and_ev
34
+ #
35
+ class AndGenerator < EventGenerator
36
+ def initialize
37
+ super do |context|
38
+ emit_if_achieved(context)
39
+ end
40
+
41
+ # This hash is a event_generator => event mapping of the last
42
+ # events of each event generator. We compare the event stored in
43
+ # this hash with the last events of each source to know if the
44
+ # source fired since it has been added to this AndGenerator
45
+ @events = Hash.new
46
+
47
+ # This flag is true unless we are not waiting for the emission
48
+ # anymore.
49
+ @active = true
50
+ end
51
+
52
+ # After this call, the AndGenerator will emit as soon as all its source
53
+ # events have been emitted again.
54
+ #
55
+ # Example:
56
+ # a = task1.intermediate_event
57
+ # b = task2.intermediate_event
58
+ # and_ev = (a & b)
59
+ #
60
+ # a.intermediate_event!
61
+ # b.intermediate_event! # and_ev emits here
62
+ # a.intermediate_event!
63
+ # b.intermediate_event! # and_ev does *not* emit
64
+ #
65
+ # and_ev.reset
66
+ # a.intermediate_event!
67
+ # b.intermediate_event! # and_ev emits here
68
+ def reset
69
+ @active = true
70
+ each_parent_object(EventStructure::Signal) do |source|
71
+ @events[source] = source.last
72
+ if source.respond_to?(:reset)
73
+ source.reset
74
+ end
75
+ end
76
+ end
77
+
78
+ # Helper method that will emit the event if all the sources are emitted.
79
+ def emit_if_achieved(context) # :nodoc:
80
+ return if @events.empty? || !@active
81
+ each_parent_object(EventStructure::Signal) do |source|
82
+ return if @events[source] == source.last
83
+ end
84
+ @active = false
85
+ emit(nil)
86
+ end
87
+
88
+ # True if the generator has no sources
89
+ def empty?; relation_graph_for(EventStructure::Signal).root?(self) end
90
+
91
+ # Adds a new source to +events+ when a source event is added
92
+ def added_signal_parent(parent, info) # :nodoc:
93
+ super
94
+ @events[parent] = parent.last
95
+
96
+ # If the parent is unreachable, check that it has neither been
97
+ # removed, nor it has been emitted
98
+ parent.if_unreachable(cancel_at_emission: true) do |reason, event|
99
+ if @events.has_key?(parent) && @events[parent] == parent.last
100
+ @active = false
101
+ unreachable!(reason || parent)
102
+ end
103
+ end
104
+ end
105
+
106
+ # Removes a source from +events+ when the source is removed
107
+ def removed_signal_parent(parent) # :nodoc:
108
+ super
109
+ @events.delete(parent)
110
+ emit_if_achieved(nil)
111
+ end
112
+
113
+ # The set of source events
114
+ def events; each_parent_object(EventStructure::Signal).to_set end
115
+ # The set of generators that have not been emitted yet.
116
+ def waiting; each_parent_object(EventStructure::Signal).find_all { |ev| @events[ev] == ev.last } end
117
+
118
+ # Add a new source to this generator
119
+ def << (generator)
120
+ generator.add_signal self
121
+ self
122
+ end
123
+ end
124
+ end
125
+
@@ -1,7 +1,24 @@
1
+ require 'facets/string/camelcase'
2
+ require 'roby/support'
3
+ require 'roby/robot'
4
+ require 'roby/app/robot_names'
5
+ require 'roby/interface'
1
6
  require 'singleton'
7
+ require 'utilrb/hash'
8
+ require 'utilrb/module/attr_predicate'
9
+ require 'yaml'
10
+ require 'utilrb/pathname/find_matching_parent'
11
+ require 'roby/app/base'
12
+
2
13
  module Roby
3
- # = Roby Applications
4
- #
14
+ # Regular expression that matches backtrace paths that are within the
15
+ # Roby framework
16
+ RX_IN_FRAMEWORK = /^((?:\s*\(druby:\/\/.+\)\s*)?#{Regexp.quote(ROBY_LIB_DIR)}\/)|^\(eval\)|^\/usr\/lib\/ruby/
17
+ RX_IN_METARUBY = /^(?:\s*\(druby:\/\/.+\)\s*)?#{Regexp.quote(MetaRuby::LIB_DIR)}\//
18
+ RX_IN_UTILRB = /^(?:\s*\(druby:\/\/.+\)\s*)?#{Regexp.quote(Utilrb::LIB_DIR)}\//
19
+ # Regular expression that matches backtrace paths that are require lines
20
+ RX_REQUIRE = /in `(gem_original_)?require'$/
21
+
5
22
  # There is one and only one Application object, which holds mainly the
6
23
  # system-wide configuration and takes care of file loading and system-wide
7
24
  # setup (#setup). A Roby application can be started in multiple modes. The
@@ -43,848 +60,2797 @@ module Roby
43
60
  # == Testing mode (<tt>scripts/test</tt>)
44
61
  # This mode is used to run test suites in the +test+ directory. See
45
62
  # Roby::Test::TestCase for a description of Roby-specific tests.
63
+ #
64
+ # == Plugin Integration
65
+ # Plugins are integrated by providing methods that get called during setup
66
+ # and teardown of the application. It is therefore important to understand
67
+ # the order in which methods get called, and where the plugins can
68
+ # 'plug-in' this process.
69
+ #
70
+ # On setup, the following methods are called:
71
+ # - load base configuration files. app.yml and init.rb
72
+ # - load_base_config hook
73
+ # - set up directories (log dir, ...) and loggers
74
+ # - set up singletons
75
+ # - base_setup hook
76
+ # - setup hook. The difference is that the setup hook is called only if
77
+ # #setup is called. base_setup is always called.
78
+ # - load models in models/tasks
79
+ # - require_models hook
80
+ # - load models in models/planners and models/actions
81
+ # - require_planners hook
82
+ # - load additional model files
83
+ # - finalize_model_loading hook
84
+ # - load config file config/ROBOT.rb
85
+ # - require_config hook
86
+ # - setup main planner
87
+ # - setup testing if in testing mode
88
+ # - setup shell interface
46
89
  class Application
47
- include Singleton
90
+ extend Logger::Hierarchy
91
+ extend Logger::Forward
92
+
93
+ class NoSuchRobot < ArgumentError; end
94
+ class NotInCurrentApp < RuntimeError; end
95
+ class LogDirNotInitialized < RuntimeError; end
96
+ class PluginsDisabled < RuntimeError; end
97
+
98
+ # The main plan on which this application acts
99
+ #
100
+ # @return [ExecutablePlan]
101
+ attr_reader :plan
102
+
103
+ # The engine associated with {#plan}
104
+ #
105
+ # @return [ExecutionEngine,nil]
106
+ def execution_engine; plan.execution_engine if plan end
48
107
 
49
- # A set of planners declared in this application
50
- attr_reader :planners
51
-
52
- # The plain option hash saved in config/app.yml
53
- attr_reader :options
54
-
55
- # Logging options.
56
- # events:: save a log of all events in the system. This log can be read using scripts/replay
57
- # If this value is 'stats', only the data necessary for timing statistics is saved.
58
- # levels:: a component => level hash of the minimum level of the messages that
59
- # should be displayed on the console. The levels are DEBUG, INFO, WARN and FATAL.
60
- # Roby: FATAL
61
- # Roby::Distributed: INFO
62
- # dir:: the log directory. Uses APP_DIR/log if not set
63
- # filter_backtraces:: true if the framework code should be removed from the error backtraces
64
- attr_reader :log
108
+ # A set of planners declared in this application
109
+ #
110
+ # @return [Array]
111
+ attr_reader :planners
65
112
 
66
- # ExecutionEngine setup
67
- attr_reader :engine
68
-
69
- # A [name, dir, file, module] array of available plugins, where 'name'
70
- # is the plugin name, 'dir' the directory in which it is installed,
71
- # 'file' the file which should be required to load the plugin and
72
- # 'module' the Application-compatible module for configuration of the
73
- # plug-in
74
- attr_reader :available_plugins
75
- # An [name, module] array of the loaded plugins
76
- attr_reader :plugins
77
-
78
- # The discovery options in multi-robot mode
79
- attr_reader :discovery
80
- # The robot's dRoby options
81
- # period:: the period of neighbour discovery
82
- # max_errors:: disconnect from a peer if there is more than +max_errors+ consecutive errors
83
- # detected
84
- attr_reader :droby
85
-
86
- # If true, abort if an unhandled exception is found
87
- attr_predicate :abort_on_exception, true
88
- # If true, abort if an application exception is found
89
- attr_predicate :abort_on_application_exception, true
90
-
91
- # An array of directories in which to search for plugins
92
- attr_reader :plugin_dirs
93
-
94
- # True if user interaction is disabled during tests
95
- attr_predicate :automatic_testing?, true
96
-
97
- # True if all logs should be kept after testing
98
- attr_predicate :testing_keep_logs?, true
99
-
100
- # True if all logs should be kept after testing
101
- attr_predicate :testing_overwrites_logs?, true
102
-
103
- # True if we should remove the framework code from the error backtraces
104
- def filter_backtraces?; log['filter_backtraces'] end
105
- def filter_backtraces=(value); log['filter_backtraces'] = value end
106
-
107
- def initialize
108
- @plugins = Array.new
109
- @available_plugins = Array.new
110
- @log = Hash['events' => 'stats', 'levels' => Hash.new, 'filter_backtraces' => true]
111
- @discovery = Hash.new
112
- @droby = Hash['period' => 0.5, 'max_errors' => 1]
113
- @engine = Hash.new
114
-
115
- @automatic_testing = true
116
- @testing_keep_logs = false
117
-
118
- @plugin_dirs = []
119
- @planners = []
120
- end
121
-
122
- # Adds +dir+ in the list of directories searched for plugins
123
- def plugin_dir(dir)
124
- dir = File.expand_path(dir)
125
- @plugin_dirs << dir
126
- $LOAD_PATH.unshift File.expand_path(dir)
127
-
128
- Dir.new(dir).each do |subdir|
129
- subdir = File.join(dir, subdir)
130
- next unless File.directory?(subdir)
131
- appfile = File.join(subdir, "app.rb")
132
- next unless File.file?(appfile)
133
-
134
- begin
135
- require appfile
136
- rescue
137
- Roby.warn "cannot load plugin in #{subdir}: #{$!.full_message}\n"
138
- end
139
- Roby.info "loaded plugin in #{subdir}"
140
- end
141
- end
142
-
143
- # Returns true if +name+ is a loaded plugin
144
- def loaded_plugin?(name)
145
- plugins.any? { |plugname, _| plugname == name }
146
- end
147
-
148
- # Returns the [name, dir, file, module] array definition of the plugin
149
- # +name+, or nil if +name+ is not a known plugin
150
- def plugin_definition(name)
151
- available_plugins.find { |plugname, *_| plugname == name }
152
- end
153
-
154
- # True if +name+ is a plugin known to us
155
- def defined_plugin?(name)
156
- available_plugins.any? { |plugname, *_| plugname == name }
157
- end
158
-
159
- def each_plugin(on_available = false)
160
- plugins = self.plugins
161
- if on_available
162
- plugins = available_plugins.map { |name, _, mod, _| [name, mod] }
163
- end
164
- plugins.each do |_, mod|
165
- yield(mod)
166
- end
167
- end
168
-
169
- # Yields each extension modules that respond to +method+
170
- def each_responding_plugin(method, on_available = false)
171
- each_plugin do |mod|
172
- yield(mod) if mod.respond_to?(method)
173
- end
174
- end
175
-
176
- # Call +method+ on each loaded extension module which define it, with
177
- # arguments +args+
178
- def call_plugins(method, *args)
179
- each_responding_plugin(method) do |config_extension|
180
- config_extension.send(method, *args)
181
- end
182
- end
183
-
184
- # Load configuration from the given option hash
185
- def load_yaml(options)
186
- options = options.dup
187
-
188
- if robot_name && (robot_config = options['robots'])
189
- if robot_config = robot_config[robot_name]
190
- robot_config.each do |section, values|
191
- if options[section]
192
- options[section].merge! values
193
- else
194
- options[section] = values
195
- end
196
- end
197
- options.delete('robots')
198
- end
199
- end
200
-
201
- @options = options
202
-
203
- load_option_hashes(options, %w{log engine discovery droby})
204
- call_plugins(:load, self, options)
205
- end
206
-
207
- def load_option_hashes(options, names)
208
- names.each do |optname|
209
- if options[optname]
210
- send(optname).merge! options[optname]
211
- end
212
- end
213
- end
214
-
215
- # Loads the plugins whose name are listed in +names+
216
- def using(*names)
217
- names.each do |name|
218
- name = name.to_s
219
- unless plugin = plugin_definition(name)
220
- raise ArgumentError, "#{name} is not a known plugin (#{available_plugins.map { |n, *_| n }.join(", ")})"
221
- end
222
- name, dir, mod, init = *plugin
223
- if plugins.find { |n, m| n == name && m == mod }
224
- next
225
- end
226
-
227
- if init
228
- begin
229
- $LOAD_PATH.unshift dir
230
- init.call
231
- mod.reset(self) if mod.respond_to?(:reset)
232
- rescue Exception => e
233
- Roby.fatal "cannot load plugin #{name}: #{e.full_message}"
234
- exit(1)
235
- ensure
236
- $LOAD_PATH.shift
237
- end
238
- end
239
-
240
- plugins << [name, mod]
241
- extend mod
242
- # If +load+ has already been called, call it on the module
243
- if mod.respond_to?(:load) && options
244
- mod.load(self, options)
245
- end
246
- end
247
- end
248
-
249
- def reset
250
- if defined? State
251
- State.clear
252
- else
253
- Roby.const_set(:State, StateSpace.new)
254
- end
255
- call_plugins(:reset, self)
256
- end
113
+ # Applicatio configuration information is stored in a YAML file
114
+ # config/app.yml. The options are saved in a hash.
115
+ #
116
+ # This attribute contains the raw hash as read from the file. It is
117
+ # overlaid
118
+ attr_reader :options
257
119
 
258
- # The robot name
259
- attr_reader :robot_name
260
- # The robot type
261
- attr_reader :robot_type
262
- # Sets up the name and type of the robot. This can be called only once
263
- # in a given Roby controller.
264
- def robot(name, type = name)
265
- if @robot_name
266
- if name != @robot_name && type != @robot_type
267
- raise ArgumentError, "the robot is already set to #{name}, of type #{type}"
268
- end
269
- return
270
- end
271
- @robot_name = name
272
- @robot_type = type
273
- end
274
-
275
- # The directory in which logs are to be saved
276
- # Defaults to APP_DIR/log
277
- def log_dir
278
- File.expand_path(log['dir'] || 'log', APP_DIR)
279
- end
280
-
281
- # A path => File hash, to re-use the same file object for different
282
- # logs
283
- attribute(:log_files) { Hash.new }
284
-
285
- # The directory in which results should be saved
286
- # Defaults to APP_DIR/results
287
- def results_dir
288
- File.expand_path(log['results'] || 'results', APP_DIR)
289
- end
120
+ # A set of exceptions that have been encountered by the application
121
+ # The associated string, if given, is a hint about in which context
122
+ # this exception got raised
123
+ #
124
+ # @return [Array<(Exception,String)>]
125
+ # @see #register_exception #clear_exceptions
126
+ attr_reader :registered_exceptions
290
127
 
291
- # Returns a unique directory name as a subdirectory of
292
- # +base_dir+, based on +path_spec+. The generated name
293
- # is of the form
294
- # <base_dir>/a/b/c/YYYYMMDD-basename
295
- # if <tt>path_spec = "a/b/c/basename"</tt>. A .<number> suffix
296
- # is appended if the path already exists.
297
- def self.unique_dirname(base_dir, path_spec)
298
- if path_spec =~ /\/$/
299
- basename = ""
300
- dirname = path_spec
301
- else
302
- basename = File.basename(path_spec)
303
- dirname = File.dirname(path_spec)
304
- end
305
-
306
- date = Date.today
307
- date = "%i%02i%02i" % [date.year, date.month, date.mday]
308
- if basename && !basename.empty?
309
- basename = date + "-" + basename
310
- else
311
- basename = date
312
- end
313
-
314
- # Check if +basename+ already exists, and if it is the case add a
315
- # .x suffix to it
316
- full_path = File.expand_path(File.join(dirname, basename), base_dir)
317
- base_dir = File.dirname(full_path)
318
-
319
- unless File.exists?(base_dir)
320
- FileUtils.mkdir_p(base_dir)
321
- end
322
-
323
- final_path, i = full_path, 0
324
- while File.exists?(final_path)
325
- i += 1
326
- final_path = full_path + ".#{i}"
327
- end
328
-
329
- final_path
330
- end
128
+ # @!method development_mode?
129
+ #
130
+ # Whether the app should run in development mode
131
+ #
132
+ # Some expensive tests are disabled when not in development mode. This
133
+ # is the default
134
+ attr_predicate :development_mode?, true
331
135
 
332
- # Sets up all the default loggers. It creates the logger for the Robot
333
- # module (accessible through Robot.logger), and sets up log levels as
334
- # specified in the <tt>config/app.yml</tt> file.
335
- def setup_loggers
336
- # Create the robot namespace
337
- STDOUT.sync = true
338
- Robot.logger = Logger.new(STDOUT)
339
- Robot.logger.level = Logger::INFO
340
- Robot.logger.formatter = Roby.logger.formatter
341
- Robot.logger.progname = robot_name
342
-
343
- # Set up log levels
344
- log['levels'].each do |name, value|
345
- name = name.camelcase(true)
346
- if value =~ /^(\w+):(.+)$/
347
- level, file = $1, $2
348
- level = Logger.const_get(level)
349
- file = file.gsub('ROBOT', robot_name) if robot_name
350
- else
351
- level = Logger.const_get(value)
352
- end
353
-
354
- new_logger = if file
355
- path = File.expand_path(file, log_dir)
356
- io = (log_files[path] ||= File.open(path, 'w'))
357
- Logger.new(io)
358
- else Logger.new(STDOUT)
359
- end
360
- new_logger.level = level
361
- new_logger.formatter = Roby.logger.formatter
362
-
363
- mod = Kernel.constant(name)
364
- if robot_name
365
- new_logger.progname = "#{name} #{robot_name}"
136
+ # The --set options passed on the command line
137
+ attr_reader :argv_set
138
+
139
+ # Allows to attribute configuration keys to override configuration
140
+ # parameters stored in config/app.yml
141
+ #
142
+ # For instance,
143
+ #
144
+ # attr_config 'log'
145
+ #
146
+ # creates a log_overrides attribute, which contains a hash. Any value
147
+ # stored in this hash will override those stored in the config file. The
148
+ # final value can be accessed by accessing the generated #config_key
149
+ # method:
150
+ #
151
+ # E.g.,
152
+ #
153
+ # in config/app.yml:
154
+ #
155
+ # log:
156
+ # dir: test
157
+ #
158
+ # in config/init.rb:
159
+ #
160
+ # Roby.app.log_overrides['dir'] = 'bla'
161
+ #
162
+ # Then, Roby.app.log will return { 'dir' => 'bla }
163
+ #
164
+ # This override mechanism is not meant to be used directly by the user,
165
+ # but as a tool for the rest of the framework
166
+ def self.attr_config(config_key)
167
+ config_key = config_key.to_s
168
+
169
+ # Ignore if already done
170
+ return if method_defined?("#{config_key}_overrides")
171
+
172
+ attribute("#{config_key}_overrides") { Hash.new }
173
+ define_method(config_key) do
174
+ plain = self.options[config_key] || Hash.new
175
+ overrides = instance_variable_get "@#{config_key}_overrides"
176
+ if overrides
177
+ plain.recursive_merge(overrides)
366
178
  else
367
- new_logger.progname = name
179
+ plain
368
180
  end
369
- mod.logger = new_logger
370
- end
371
- end
372
-
373
- def setup_dirs
374
- FileUtils.mkdir_p(log_dir) unless File.exists?(log_dir)
375
- if File.directory?(libdir = File.join(APP_DIR, 'lib'))
376
- if !$LOAD_PATH.include?(libdir)
377
- $LOAD_PATH.unshift File.join(APP_DIR, 'lib')
378
- end
379
- end
380
-
381
- Roby::State.datadirs = []
382
- datadir = File.join(APP_DIR, "data")
383
- if File.directory?(datadir)
384
- Roby::State.datadirs << datadir
385
- end
386
- end
387
-
388
- # Loads the models, based on the given robot name and robot type
389
- def require_models
390
- # Require all common task models and the task models specific to
391
- # this robot
392
- list_dir('tasks') { |p| require(p) }
393
- list_robotdir('tasks', 'ROBOT') { |p| require(p) }
394
-
395
- # Load robot-specific configuration
396
- models_search = ['planners']
397
- if robot_name
398
- models_search << File.join('planners', robot_name) << File.join('planners', robot_type)
399
- file = robotfile('planners', 'ROBOT', 'main.rb')
400
- end
401
- file ||= File.join("planners", "main")
402
- require file if File.file?(file)
403
-
404
- # Load the other planners
405
- models_search.each do |base_dir|
406
- next unless File.directory?(base_dir)
407
- Dir.new(base_dir).each do |file|
408
- if File.file?(file) && file =~ /\.rb$/ && file !~ 'main\.rb$'
409
- require file
410
- end
411
- end
412
- end
413
-
414
- # Set up the loaded plugins
415
- call_plugins(:require_models, self)
416
- end
417
-
418
- def setup
419
- if !Roby.plan
420
- Roby.instance_variable_set :@plan, Plan.new
421
- end
422
-
423
- reset
424
- require 'roby/planning'
425
- require 'roby/interface'
181
+ end
182
+ end
183
+
184
+ # Defines accessors for a configuration parameter stored in #options
185
+ #
186
+ # This method allows to define a getter and a setter for a parameter
187
+ # stored in #options that should be user-overridable. It builds upon
188
+ # #attr_config.
189
+ #
190
+ # For instance:
191
+ #
192
+ # overridable_configuration 'log', 'filter_backtraces'
193
+ #
194
+ # will create a #filter_backtraces getter and a #filter_backtraces=
195
+ # setter which allow to respectively access the log/filter_backtraces
196
+ # configuration value, and override it from its value in config/app.yml
197
+ #
198
+ # The :predicate option allows to make the setter look like a predicate:
199
+ #
200
+ # overridable_configuration 'log', 'filter_backtraces', predicate: true
201
+ #
202
+ # will define #filter_backtraces? instead of #filter_backtraces
203
+ def self.overridable_configuration(config_set, config_key, options = Hash.new)
204
+ options = Kernel.validate_options options, predicate: false, attr_name: config_key
205
+ attr_config(config_set)
206
+ define_method("#{options[:attr_name]}#{"?" if options[:predicate]}") do
207
+ send(config_set)[config_key]
208
+ end
209
+ define_method("#{options[:attr_name]}=") do |new_value|
210
+ send("#{config_set}_overrides")[config_key] = new_value
211
+ end
212
+ end
426
213
 
427
- $LOAD_PATH.unshift(APP_DIR) unless $LOAD_PATH.include?(APP_DIR)
428
-
429
- # Get the application-wide configuration
430
- file = File.join(APP_DIR, 'config', 'app.yml')
431
- file = YAML.load(File.open(file))
432
- load_yaml(file)
433
- if File.exists?(initfile = File.join(APP_DIR, 'config', 'init.rb'))
434
- load initfile
435
- end
436
-
437
- setup_dirs
438
- setup_loggers
439
-
440
- # Import some constants directly at toplevel before loading the
441
- # user-defined models
442
- unless Object.const_defined?(:Application)
443
- Object.const_set(:Application, Roby::Application)
444
- Object.const_set(:State, Roby::State)
445
- end
446
-
447
- # Set up the loaded plugins
448
- call_plugins(:setup, self)
449
-
450
- require_models
451
-
452
- if file = robotfile(APP_DIR, 'config', "ROBOT.rb")
453
- load file
454
- end
455
-
456
-
457
- # MainPlanner is always included in the planner list
458
- if defined? MainPlanner
459
- self.planners << MainPlanner
460
- end
461
-
462
- # If we are in test mode, import the test extensions from plugins
463
- if testing?
464
- require 'roby/test/testcase'
465
- each_plugin do |mod|
466
- if mod.const_defined?(:Test)
467
- Roby::Test::TestCase.include mod.const_get(:Test)
468
- end
469
- end
470
- end
471
- end
472
-
473
- def run(&block)
474
- setup_global_singletons
475
-
476
- # Set up dRoby, setting an Interface object as front server, for shell access
477
- host = droby['host'] || ""
478
- if host !~ /:\d+$/
479
- host << ":#{Distributed::DEFAULT_DROBY_PORT}"
480
- end
481
-
482
- if single? || !robot_name
483
- host =~ /:(\d+)$/
484
- DRb.start_service "druby://:#{$1 || '0'}", Interface.new(Roby.engine)
485
- else
486
- DRb.start_service "druby://#{host}", Interface.new(Roby.engine)
487
- droby_config = { :ring_discovery => !!discovery['ring'],
488
- :name => robot_name,
489
- :plan => Roby.plan,
490
- :period => discovery['period'] || 0.5 }
491
-
492
- if discovery['tuplespace']
493
- droby_config[:discovery_tuplespace] = DRbObject.new_with_uri("druby://#{discovery['tuplespace']}")
494
- end
495
- Roby::Distributed.state = Roby::Distributed::ConnectionSpace.new(droby_config)
496
-
497
- if discovery['ring']
498
- Roby::Distributed.publish discovery['ring']
499
- end
500
- Roby.every(discovery['period'] || 0.5) do
501
- Roby::Distributed.state.start_neighbour_discovery
502
- end
503
- end
504
-
505
- @robot_name ||= 'common'
506
- @robot_type ||= 'common'
507
-
508
- engine_config = self.engine
509
- engine = Roby.engine
510
- options = { :cycle => engine_config['cycle'] || 0.1 }
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, :plugins => plugins.map { |n, _| n })
516
- logger.stats_mode = log['events'] == 'stats'
517
- Roby::Log.add_logger logger
518
- end
519
- engine.run options
520
-
521
- plugins = self.plugins.map { |_, mod| mod if mod.respond_to?(:run) }.compact
522
- run_plugins(plugins, &block)
214
+ # Allows to override the application base directory. See #app_dir
215
+ attr_writer :app_dir
523
216
 
524
- rescue Exception => e
525
- if e.respond_to?(:pretty_print)
526
- pp e
217
+ # @!method ignore_all_load_errors?
218
+ # @!method ignore_all_load_errors=(flag)
219
+ #
220
+ # If set to true, files that generate errors while loading will be
221
+ # ignored. This is used for model browsing GUIs to be usable even if
222
+ # there are errors
223
+ #
224
+ # It is false by default
225
+ attr_predicate :ignore_all_load_errors?, true
226
+
227
+ # @!method backward_compatible_naming?
228
+ # @!method backward_compatible_naming=(flag)
229
+ #
230
+ # If set to true, the app will enable backward-compatible behaviour
231
+ # related to naming schemes, file placements and so on
232
+ #
233
+ # The default is true
234
+ attr_predicate :backward_compatible_naming?, true
235
+
236
+ # Returns the name of the application
237
+ def app_name
238
+ if @app_name
239
+ @app_name
240
+ elsif app_dir
241
+ @app_name = File.basename(app_dir).gsub(/[^\w]/, '_')
242
+ else 'default'
243
+ end
244
+ end
245
+
246
+ # Allows to override the app name
247
+ attr_writer :app_name
248
+
249
+ # Allows to override the app's module name
250
+ #
251
+ # The default is to convert the app dir's basename to camelcase, but
252
+ # that fails in some cases (mostly, when there are acronyms in the name)
253
+ attr_writer :module_name
254
+
255
+ # Returns the name of this app's toplevel module
256
+ def module_name
257
+ @module_name || app_name.camelcase(:upper)
258
+ end
259
+
260
+ # Returns this app's toplevel module
261
+ def app_module
262
+ constant("::#{module_name}")
263
+ end
264
+
265
+ # Returns this app's main action interface
266
+ #
267
+ # This is usually set up in the robot configuration file by calling
268
+ # Robot.actions
269
+ def main_action_interface
270
+ app_module::Actions::Main
271
+ end
272
+
273
+ # Returns the application base directory
274
+ #
275
+ # @return [String,nil]
276
+ def app_dir
277
+ if defined?(APP_DIR)
278
+ APP_DIR
279
+ elsif @app_dir
280
+ @app_dir
281
+ end
282
+ end
283
+
284
+ def app_path
285
+ @app_path ||= Pathname.new(app_dir)
286
+ end
287
+
288
+ # The PID of the server that gives access to the log file
289
+ #
290
+ # Its port is allocated automatically, and must be discovered through
291
+ # the Roby interface
292
+ #
293
+ # @return [Integer,nil]
294
+ attr_reader :log_server_pid
295
+
296
+ # The port on which the log server is started
297
+ #
298
+ # It is by default started on an ephemeral port, that needs to be
299
+ # discovered by clients through the Roby interface's
300
+ # {Interface#log_server_port}
301
+ #
302
+ # @return [Integer,nil]
303
+ attr_reader :log_server_port
304
+
305
+ # The TCP server that gives access to the {Interface}
306
+ attr_reader :shell_interface
307
+
308
+ # Tests if the given directory looks like the root of a Roby app
309
+ #
310
+ # @param [String] test_dir the path to test
311
+ def self.is_app_dir?(test_dir)
312
+ File.file?(File.join(test_dir, 'config', 'app.yml')) ||
313
+ File.directory?(File.join(test_dir, 'models')) ||
314
+ File.directory?(File.join(test_dir, 'scripts', 'controllers')) ||
315
+ File.directory?(File.join(test_dir, 'config', 'robots'))
316
+ end
317
+
318
+ class InvalidRobyAppDirEnv < ArgumentError; end
319
+
320
+ # Guess the app directory based on the current directory
321
+ #
322
+ # @return [String,nil] the base of the app, or nil if the current
323
+ # directory is not within an app
324
+ def self.guess_app_dir
325
+ if test_dir = ENV['ROBY_APP_DIR']
326
+ if !Application.is_app_dir?(test_dir)
327
+ raise InvalidRobyAppDirEnv, "the ROBY_APP_DIR envvar is set to #{test_dir}, but this is not a valid Roby application path"
328
+ end
329
+ return test_dir
330
+ end
331
+
332
+ path = Pathname.new(Dir.pwd).find_matching_parent do |test_dir|
333
+ Application.is_app_dir?(test_dir.to_s)
334
+ end
335
+ if path
336
+ path.to_s
337
+ end
338
+ end
339
+
340
+ # Whether there is a supporting app directory
341
+ def has_app?
342
+ !!@app_dir
343
+ end
344
+
345
+ # Guess the app directory based on the current directory, and sets
346
+ # {#app_dir}. It will not do anything if the current directory is not in
347
+ # a Roby app. Moreover, it does nothing if #app_dir is already set
348
+ #
349
+ # @return [String] the selected app directory
350
+ def guess_app_dir
351
+ return if @app_dir
352
+ if app_dir = self.class.guess_app_dir
353
+ @app_dir = app_dir
354
+ end
355
+ end
356
+
357
+ # Call to require this roby application to be in a Roby application
358
+ #
359
+ # It tries to guess the app directory. If none is found, it raises.
360
+ def require_app_dir(needs_current: false, allowed_outside: true)
361
+ guess_app_dir
362
+ if !@app_dir
363
+ raise ArgumentError, "your current directory does not seem to be a Roby application directory; did you forget to run 'roby init'?"
364
+ end
365
+ if needs_current
366
+ needs_to_be_in_current_app(allowed_outside: allowed_outside)
367
+ end
368
+ end
369
+
370
+ # Call to check whether the current directory is within {#app_dir}. If
371
+ # not, raises
372
+ #
373
+ # This is called by tools for which being in another app than the
374
+ # currently selected would be really too confusing
375
+ def needs_to_be_in_current_app(allowed_outside: true)
376
+ guessed_dir = self.class.guess_app_dir
377
+ if guessed_dir && (@app_dir != guessed_dir)
378
+ raise NotInCurrentApp, "#{@app_dir} is currently selected, but the current directory is within #{guessed_dir}"
379
+ elsif !guessed_dir && !allowed_outside
380
+ raise NotInCurrentApp, "not currently within an app dir"
381
+ end
382
+ end
383
+
384
+ # A list of paths in which files should be looked for in {#find_dirs},
385
+ # {#find_files} and {#find_files_in_dirs}
386
+ #
387
+ # If uninitialized, [app_dir] is used
388
+ attr_writer :search_path
389
+
390
+ # The list of paths in which the application should be looking for files
391
+ #
392
+ # @return [Array<String>]
393
+ def search_path
394
+ if !@search_path
395
+ if app_dir
396
+ [app_dir]
397
+ else []
398
+ end
527
399
  else
528
- pp e.full_message
529
- end
530
- end
531
- def run_plugins(mods, &block)
532
- engine = Roby.engine
533
-
534
- if mods.empty?
535
- yield
536
- engine.join
537
- else
538
- mod = mods.shift
539
- mod.run(self) do
540
- run_plugins(mods, &block)
541
- end
542
- end
543
-
544
- rescue Exception => e
545
- if Roby.engine.running?
546
- engine.quit
547
- engine.join
548
- raise e, e.message, e.backtrace
549
- else
550
- raise
551
- end
552
- end
553
-
554
- def stop; call_plugins(:stop, self) end
555
-
556
- DISCOVERY_TEMPLATE = [:droby, nil, nil]
557
-
558
- # Starts services needed for distributed operations. These services are
559
- # supposed to be started only once for a whole system
560
- #
561
- # If you have external servers to start for every robot, plug it into
562
- # #start_server
563
- def start_distributed
564
- Thread.abort_on_exception = true
565
-
566
- if !File.exists?(log_dir)
567
- FileUtils.mkdir_p(log_dir)
568
- end
569
-
570
- unless single? || !discovery['tuplespace']
571
- ts = Rinda::TupleSpace.new
572
-
573
-
574
- discovery['tuplespace'] =~ /(:\d+)$/
575
- DRb.start_service "druby://#{$1}", ts
576
-
577
- new_db = ts.notify('write', DISCOVERY_TEMPLATE)
578
- take_db = ts.notify('take', DISCOVERY_TEMPLATE)
579
-
580
- Thread.start do
581
- new_db.each { |_, t| STDERR.puts "new host #{t[1]}" }
582
- end
583
- Thread.start do
584
- take_db.each { |_, t| STDERR.puts "host #{t[1]} has disconnected" }
585
- end
586
- Roby.warn "Started service discovery on #{discovery['tuplespace']}"
587
- end
588
-
589
- call_plugins(:start_distributed, self)
590
- end
591
-
592
- # Stop services needed for distributed operations. See #start_distributed
593
- def stop_distributed
594
- DRb.stop_service
595
-
596
- call_plugins(:stop_distributed, self)
597
- rescue Interrupt
598
- end
599
-
600
- attr_reader :log_server
601
- attr_reader :log_sources
602
-
603
- # Start services that should exist for every robot in the system. Services that
604
- # are needed only once for all robots should be started in #start_distributed
605
- def start_server
606
- Thread.abort_on_exception = true
607
-
608
- # Start a log server if needed, and poll the log directory for new
609
- # data sources
610
- if log_server = (log.has_key?('server') ? log['server'] : true)
611
- require 'roby/log/server'
612
- port = if log_server.kind_of?(Hash) && log_server['port']
613
- Integer(log_server['port'])
614
- end
615
-
616
- @log_server = Log::Server.new(port ||= Log::Server::RING_PORT)
617
- Roby::Log::Server.info "log server published on port #{port}"
618
- @log_streams = []
619
- @log_streams_poll = Thread.new do
620
- begin
621
- loop do
622
- Thread.exclusive do
623
- known_streams = @log_server.streams
624
- streams = data_streams
625
-
626
- (streams - known_streams).each do |s|
627
- Roby::Log::Server.info "new stream found #{s.name} [#{s.type}]"
628
- s.open
629
- @log_server.added_stream(s)
630
- end
631
- (known_streams - streams).each do |s|
632
- Roby::Log::Server.info "end of stream #{s.name} [#{s.type}]"
633
- s.close
634
- @log_server.removed_stream(s)
635
- end
636
- end
637
- sleep(5)
638
- end
639
- rescue Interrupt
640
- rescue
641
- Roby::Log::Server.fatal $!.full_message
642
- end
643
- end
644
- end
645
-
646
- call_plugins(:start_server, self)
647
- end
648
-
649
- # Stop server. See #start_server
650
- def stop_server
651
- if @log_server
652
- @log_streams_poll.raise Interrupt, "quitting"
653
- @log_streams_poll.join
654
-
655
- @log_server.quit
656
- @log_streams.clear
657
- end
658
-
659
- call_plugins(:stop_server, self)
660
- end
661
-
662
- def list_dir(*path)
663
- if !block_given?
664
- return enum_for(:list_dir, *path)
665
- end
666
-
667
- dirname = File.join(*path)
668
- Dir.new(dirname).each do |file|
669
- file = File.join(dirname, file)
670
- if file =~ /\.rb$/ && File.file?(file)
671
- file = file.gsub(/^#{Regexp.quote(APP_DIR)}\//, '')
672
- yield(file)
673
- end
674
- end
675
- end
676
-
677
- # Require all files in the directories matching +pattern+. If +pattern+
678
- # contains the word ROBOT, it is replaced by -- in order -- the robot
679
- # name and then the robot type
680
- def list_robotdir(*path, &block)
681
- if !block_given?
682
- return enum_for(:list_robotdir, *path)
683
- end
684
-
685
- return unless robot_name && robot_type
686
-
687
- pattern = File.expand_path(File.join(*path), APP_DIR)
688
- [robot_name, robot_type].uniq.each do |name|
689
- dirname = pattern.gsub(/ROBOT/, name)
690
- list_dir(dirname, &block) if File.directory?(dirname)
691
- end
692
- end
693
-
694
- def robotfile(*path) # :nodoc
695
- return unless robot_name && robot_type
696
-
697
- pattern = File.join(*path)
698
- robot_config = pattern.gsub(/ROBOT/, robot_name)
699
- if File.file?(robot_config)
700
- robot_config
701
- else
702
- robot_config = pattern.gsub(/ROBOT/, robot_type)
703
- if File.file?(robot_config)
704
- robot_config
705
- end
706
- end
707
- end
708
-
709
- attr_predicate :simulation?, true
710
- def simulation; self.simulation = true end
711
-
712
- attr_predicate :testing?, true
713
- def testing; self.testing = true end
714
- attr_predicate :shell?, true
715
- def shell; self.shell = true end
716
- def single?; @single || discovery.empty? end
717
- def single; @single = true end
718
-
719
- def setup_global_singletons
720
- if !Roby.plan
721
- Roby.instance_variable_set :@plan, Plan.new
722
- end
723
-
724
- if !Roby.engine && Roby.plan.engine
725
- # This checks coherence with Roby.control, and sets it
726
- # accordingly
727
- Roby.engine = Roby.plan.engine
728
- elsif !Roby.control
729
- Roby.control = DecisionControl.new
730
- end
731
-
732
- if !Roby.engine
733
- Roby.engine = ExecutionEngine.new(Roby.plan, Roby.control)
734
- end
735
-
736
- if Roby.control != Roby.engine.control
737
- raise "inconsistency between Roby.control and Roby.engine.control"
738
- elsif Roby.engine != Roby.plan.engine
739
- raise "inconsistency between Roby.engine and Roby.plan.engine"
740
- end
741
-
742
- if !Roby.engine.scheduler && Roby.scheduler
743
- Roby.engine.scheduler = Roby.scheduler
744
- end
745
- end
746
-
747
- # Guesses the type of +filename+ if it is a source suitable for
748
- # data display in this application
749
- def data_streams_of(filenames)
750
- if filenames.size == 1
751
- path = filenames.first
752
- path = if path =~ /-(events|timings)\.log$/
753
- $`
754
- elsif File.exists?("#{path}-events.log")
755
- path
756
- end
757
- if path
758
- return [Roby::Log::EventStream.new(path)]
759
- end
760
- end
761
-
762
- each_responding_plugin(:data_streams_of, true) do |config|
763
- if streams = config.data_streams_of(filenames)
764
- return streams
765
- end
766
- end
767
- nil
768
- end
769
-
770
- # Returns the list of data streams suitable for data display known
771
- # to the application
772
- def data_streams(log_dir = nil)
773
- log_dir ||= self.log_dir
774
- streams = []
775
- Dir.glob(File.join(log_dir, '*-events.log*')).each do |file|
776
- next unless file =~ /-events\.log$/
777
- streams << Roby::Log::EventStream.new($`)
778
- end
779
- each_responding_plugin(:data_streams, true) do |config|
780
- if s = config.data_streams(log_dir)
781
- streams += s
782
- end
783
- end
784
- streams
785
- end
786
-
787
- def self.find_data(name)
788
- Roby::State.datadirs.each do |dir|
789
- path = File.join(dir, name)
790
- return path if File.exists?(path)
791
- end
792
- raise Errno::ENOENT, "no file #{name} found in #{Roby::State.datadirs.join(":")}"
793
- end
794
-
795
- def self.register_plugin(name, mod, &init)
796
- caller(1)[0] =~ /^([^:]+):\d/
797
- dir = File.expand_path(File.dirname($1))
798
- Roby.app.available_plugins << [name, dir, mod, init]
799
- end
800
-
801
- @@reload_model_filter = []
802
- # Add a filter to model reloading. A task or planner model is
803
- # reinitialized only if all filter blocks return true for it
804
- def self.filter_reloaded_models(&block)
805
- @@reload_model_filter << block
806
- end
807
-
808
- def model?(model)
809
- (model <= Roby::Task) || (model.kind_of?(Roby::TaskModelTag)) ||
810
- (model <= Planning::Planner) || (model <= Planning::Library)
811
- end
812
-
813
- def reload_model?(model)
814
- @@reload_model_filter.all? { |filter| filter[model] }
815
- end
816
-
817
- def app_file?(path)
818
- (path =~ %r{(^|/)#{APP_DIR}(/|$)}) ||
819
- ((path[0] != ?/) && File.file?(File.join(APP_DIR, path)))
820
- end
821
- def framework_file?(path)
822
- if path =~ /roby\/.*\.rb$/
823
- true
824
- else
825
- Roby.app.plugins.any? do |name, _|
826
- _, dir, _, _ = Roby.app.plugin_definition(name)
827
- path =~ %r{(^|/)#{dir}(/|$)}
828
- end
829
- end
830
- end
831
-
832
- def reload
833
- # Always reload this file first. This ensure that one can use #reload
834
- # to fix the reload code itself
835
- load __FILE__
836
-
837
- # Clear all event definitions in task models that are filtered out by
838
- # Application.filter_reloaded_models
839
- ObjectSpace.each_object(Class) do |model|
840
- next unless model?(model)
841
- next unless reload_model?(model)
842
-
843
- model.clear_model
844
- end
845
-
846
- # Remove what we want to reload from LOADED_FEATURES and use
847
- # require. Do not use 'load' as the reload order should be the
848
- # require order.
849
- needs_reload = []
850
- $LOADED_FEATURES.delete_if do |feature|
851
- if framework_file?(feature) || app_file?(feature)
852
- needs_reload << feature
853
- end
854
- end
855
-
856
- needs_reload.each do |feature|
857
- begin
858
- require feature.gsub(/\.rb$/, '')
859
- rescue Exception => e
860
- STDERR.puts e.full_message
861
- end
862
- end
863
- end
864
- end
400
+ @search_path
401
+ end
402
+ end
865
403
 
866
- @app = Application.instance
867
- class << self
868
- # The one and only Application object
869
- attr_reader :app
404
+ # Logging options.
405
+ # events:: save a log of all events in the system. This log can be read using scripts/replay
406
+ # If this value is 'stats', only the data necessary for timing statistics is saved.
407
+ # levels:: a component => level hash of the minimum level of the messages that
408
+ # should be displayed on the console. The levels are DEBUG, INFO, WARN and FATAL.
409
+ # Roby: FATAL
410
+ # Roby::Interface: INFO
411
+ # dir:: the log directory. Uses $app_dir/log if not set
412
+ # results:: the
413
+ # filter_backtraces:: true if the framework code should be removed from the error backtraces
414
+ attr_config :log
870
415
 
871
- # The scheduler object to be used during execution. See
872
- # ExecutionEngine#scheduler.
416
+ # ExecutionEngine setup
417
+ attr_config :engine
418
+
419
+ # A [name, dir, file, module] array of available plugins, where 'name'
420
+ # is the plugin name, 'dir' the directory in which it is installed,
421
+ # 'file' the file which should be required to load the plugin and
422
+ # 'module' the Application-compatible module for configuration of the
423
+ # plug-in
424
+ attr_reader :available_plugins
425
+ # An [name, module] array of the loaded plugins
426
+ attr_reader :plugins
427
+
428
+ # The discovery options in multi-robot mode
429
+ attr_config :discovery
430
+
431
+ # @!method abort_on_exception?
432
+ # @!method abort_on_exception=(flag)
873
433
  #
874
- # This is only used during the configuration of the application, and
875
- # not afterwards. It is also possible to set per-engine through
876
- # ExecutionEngine#scheduler=
877
- attr_accessor :scheduler
878
- end
434
+ # Controls whether the application should quit if an unhandled plan
435
+ # exception is received
436
+ #
437
+ # The default is false
438
+ attr_predicate :abort_on_exception, true
439
+
440
+ # @!method abort_on_application_exception?
441
+ # @!method abort_on_application_exception=(flag)
442
+ #
443
+ # Controls whether the Roby app should quit if an application (i.e.
444
+ # non-plan) exception is received
445
+ #
446
+ # The default is true
447
+ attr_predicate :abort_on_application_exception, true
448
+
449
+ # @!method automatic_testing?
450
+ # @!method automatic_testing=(flag)
451
+ #
452
+ # True if user interaction is disabled during tests
453
+ attr_predicate :automatic_testing?, true
454
+
455
+ # @!method plugins_enabled?
456
+ # @!method plugins_enabled=(flag)
457
+ #
458
+ # True if plugins should be discovered, registered and loaded (true by
459
+ # default)
460
+ attr_predicate :plugins_enabled?, true
461
+
462
+ # @return [Array<String>] list of paths to files not in models/ that
463
+ # contain some models. This is mainly used by the command-line tools
464
+ # so that the user can load separate "model-based scripts" files.
465
+ attr_reader :additional_model_files
466
+
467
+ # @return [Array<#call>] list of objects called when the app gets
468
+ # initialized (i.e. just after init.rb is loaded)
469
+ attr_reader :init_handlers
470
+
471
+ # @return [Array<#call>] list of objects called when the app gets
472
+ # initialized (i.e. in {#setup} after {#base_setup})
473
+ attr_reader :setup_handlers
474
+
475
+ # @return [Array<#call>] list of objects called when the app gets
476
+ # to require its models (i.e. after {#require_models})
477
+ attr_reader :require_handlers
478
+
479
+ # @return [Array<#call>] list of objects called when the app is doing
480
+ # {#clear_models}
481
+ attr_reader :clear_models_handlers
482
+
483
+ # @return [Array<#call>] list of objects called when the app cleans up
484
+ # (it is the opposite of setup)
485
+ attr_reader :cleanup_handlers
486
+
487
+ # @return [Array<#call>] list of blocks that should be executed once the
488
+ # application is started
489
+ attr_reader :controllers
490
+
491
+ # @return [Array<#call>] list of blocks that should be executed once the
492
+ # application is started
493
+ attr_reader :action_handlers
494
+
495
+ # The list of log directories created by this app
496
+ #
497
+ # They are deleted on cleanup if {#public_logs?} is false. Unlike with
498
+ # {#created_log_base_dirs}, they are deleted even if they are not empty.
499
+ #
500
+ # @return [Array<String>]
501
+ attr_reader :created_log_dirs
502
+
503
+ # The list of directories created by this app in the paths to
504
+ # {#created_log_dirs}
505
+ #
506
+ # They are deleted on cleanup if {#public_logs?} is false. Unlike with
507
+ # {#created_log_dirs}, they are not deleted if they are not empty.
508
+ #
509
+ # @return [Array<String>]
510
+ attr_reader :created_log_base_dirs
511
+
512
+ # Additional metadata saved in log_dir/info.yml by the app
513
+ #
514
+ # Do not modify directly, use {#add_app_metadata} instead
515
+ attr_reader :app_extra_metadata
516
+
517
+ # Defines common configuration options valid for all Roby-oriented
518
+ # scripts
519
+ def self.common_optparse_setup(parser)
520
+ Roby.app.load_config_yaml
521
+ parser.on("--set=KEY=VALUE", String, "set a value on the Conf object") do |value|
522
+ Roby.app.argv_set << value
523
+ key, value = value.split('=')
524
+ path = key.split('.')
525
+ base_conf = path[0..-2].inject(Conf) { |c, name| c.send(name) }
526
+ base_conf.send("#{path[-1]}=", YAML.load(value))
527
+ end
528
+ parser.on("--log=SPEC", String, "configuration specification for text loggers. SPEC is of the form path/to/a/module:LEVEL[:FILE][,path/to/another]") do |log_spec|
529
+ log_spec.split(',').each do |spec|
530
+ mod, level, file = spec.split(':')
531
+ Roby.app.log_setup(mod, level, file)
532
+ end
533
+ end
534
+ parser.on('-r NAME', '--robot=NAME[,TYPE]', String, 'the robot name and type') do |name|
535
+ robot_name, robot_type = name.split(',')
536
+ Roby.app.setup_robot_names_from_config_dir
537
+ Roby.app.robot(robot_name, robot_type)
538
+ end
539
+ parser.on('--debug', 'run in debug mode') do
540
+ Roby.app.public_logs = true
541
+ Roby.app.filter_backtraces = false
542
+ require 'roby/app/debug'
543
+ end
544
+ parser.on_tail('-h', '--help', 'this help message') do
545
+ STDERR.puts parser
546
+ exit
547
+ end
548
+ end
549
+
550
+ # Sets up provided option parser to add the --host and --vagrant option
551
+ #
552
+ # When added, a :host entry will be added to the provided options hash
553
+ def self.host_options(parser, options)
554
+ options[:host] ||= Roby.app.shell_interface_host || 'localhost'
555
+ options[:port] ||= Roby.app.shell_interface_port || Interface::DEFAULT_PORT
556
+
557
+ parser.on('--host URL', String, "sets the host to connect to as hostname[:PORT]") do |url|
558
+ if url =~ /(.*):(\d+)$/
559
+ options[:host] = $1
560
+ options[:port] = Integer($2)
561
+ else
562
+ options[:host] = url
563
+ end
564
+ end
565
+ parser.on('--vagrant NAME[:PORT]', String, "connect to a vagrant VM") do |vagrant_name|
566
+ require 'roby/app/vagrant'
567
+ if vagrant_name =~ /(.*):(\d+)$/
568
+ vagrant_name, port = $1, Integer($2)
569
+ end
570
+ options[:host] = Roby::App::Vagrant.resolve_ip(vagrant_name)
571
+ options[:port] = port
572
+ end
573
+ end
574
+
575
+ # Array of regular expressions used to filter out backtraces
576
+ attr_reader :filter_out_patterns
577
+
578
+ # Configures a text logger in the system. It has to be called before
579
+ # #setup to have an effect.
580
+ #
581
+ # It overrides configuration from the app.yml file
582
+ #
583
+ # For instance,
584
+ #
585
+ # log_setup 'roby/execution_engine', 'DEBUG'
586
+ #
587
+ # will be equivalent to having the following entry in config/app.yml:
588
+ #
589
+ # log:
590
+ # levels:
591
+ # roby/execution_engine: DEBUG
592
+ def log_setup(mod_path, level, file = nil)
593
+ levels = (log_overrides['levels'] ||= Hash.new)
594
+ levels[mod_path] = [level, file].compact.join(":")
595
+ end
596
+
597
+ ##
598
+ # :method: filter_backtraces?
599
+ #
600
+ # True if we should remove the framework code from the error backtraces
601
+
602
+ ##
603
+ # :method: filter_backtraces=
604
+ #
605
+ # Override the value stored in configuration files for filter_backtraces?
606
+
607
+ overridable_configuration 'log', 'filter_backtraces', predicate: true
608
+
609
+ ##
610
+ # :method: log_server?
611
+ #
612
+ # True if the log server should be started
613
+
614
+ ##
615
+ # :method: log_server=
616
+ #
617
+ # Sets whether the log server should be started
618
+
619
+ overridable_configuration 'log', 'server', predicate: true, attr_name: 'log_server'
620
+
621
+ DEFAULT_OPTIONS = {
622
+ 'log' => Hash['events' => true, 'server' => true, 'levels' => Hash.new, 'filter_backtraces' => true],
623
+ 'discovery' => Hash.new,
624
+ 'engine' => Hash.new
625
+ }
626
+
627
+ # @!method public_rest_interface?
628
+ # @!method public_rest_interface=(flag)
629
+ #
630
+ # If set to true, this Roby application will publish a
631
+ # {Interface::REST::API} object
632
+ attr_predicate :public_rest_interface?, true
633
+
634
+ # The host to which the REST interface server should bind
635
+ #
636
+ # @return [String]
637
+ attr_accessor :rest_interface_host
638
+ # The port on which the REST interface server should be
639
+ #
640
+ # @return [Integer]
641
+ attr_accessor :rest_interface_port
642
+
643
+ # The host to which the shell interface server should bind
644
+ #
645
+ # @return [String]
646
+ attr_accessor :shell_interface_host
647
+ # The port on which the shell interface server should be
648
+ #
649
+ # @return [Integer]
650
+ attr_accessor :shell_interface_port
651
+ # Whether an unexpected (non-comm-related) failure in the shell should
652
+ # cause an abort
653
+ #
654
+ # The default is yes
655
+ attr_predicate :shell_abort_on_exception?, true
656
+ # The {Interface} bound to this app
657
+ # @return [Interface]
658
+ attr_reader :shell_interface
659
+
660
+ def initialize
661
+ @plan = ExecutablePlan.new
662
+ @argv_set = Array.new
663
+
664
+ @auto_load_all = false
665
+ @default_auto_load = true
666
+ @auto_load_models = nil
667
+ @app_name = nil
668
+ @module_name = nil
669
+ @app_dir = nil
670
+ @backward_compatible_naming = true
671
+ @development_mode = true
672
+ @search_path = nil
673
+ @plugins = Array.new
674
+ @plugins_enabled = true
675
+ @available_plugins = Array.new
676
+ @options = DEFAULT_OPTIONS.dup
677
+
678
+ @public_logs = false
679
+ @log_create_current = true
680
+ @created_log_dirs = []
681
+ @created_log_base_dirs = []
682
+ @additional_model_files = []
683
+ @restarting = false
684
+
685
+ @shell_interface = nil
686
+ @shell_interface_host = nil
687
+ @shell_interface_port = Interface::DEFAULT_PORT
688
+ @shell_abort_on_exception = true
689
+
690
+ @rest_interface = nil
691
+ @rest_interface_host = nil
692
+ @rest_interface_port = Interface::DEFAULT_REST_PORT
693
+
694
+ @automatic_testing = true
695
+ @registered_exceptions = []
696
+ @app_extra_metadata = Hash.new
697
+
698
+ @filter_out_patterns = [Roby::RX_IN_FRAMEWORK,
699
+ Roby::RX_IN_METARUBY,
700
+ Roby::RX_IN_UTILRB,
701
+ Roby::RX_REQUIRE]
702
+ self.abort_on_application_exception = true
703
+
704
+ @planners = []
705
+ @notification_listeners = Array.new
706
+ @ui_event_listeners = Array.new
707
+
708
+ @init_handlers = Array.new
709
+ @setup_handlers = Array.new
710
+ @require_handlers = Array.new
711
+ @clear_models_handlers = Array.new
712
+ @cleanup_handlers = Array.new
713
+ @controllers = Array.new
714
+ @action_handlers = Array.new
715
+ end
716
+
717
+ # Loads the base configuration
718
+ #
719
+ # This method loads the two most basic configuration files:
720
+ #
721
+ # * config/app.yml
722
+ # * config/init.rb
723
+ #
724
+ # It also calls the plugin's 'load' method
725
+ def load_base_config
726
+ load_config_yaml
727
+ setup_loggers(ignore_missing: true, redirections: false)
728
+
729
+ setup_robot_names_from_config_dir
730
+
731
+ # Get the application-wide configuration
732
+ if plugins_enabled?
733
+ register_plugins
734
+ end
735
+
736
+ update_load_path
737
+
738
+ if initfile = find_file('config', 'init.rb', order: :specific_first)
739
+ Application.info "loading init file #{initfile}"
740
+ require initfile
741
+ end
742
+
743
+ update_load_path
744
+
745
+ # Deprecated hook
746
+ call_plugins(:load, self, deprecated: "define 'load_base_config' instead")
747
+ call_plugins(:load_base_config, self)
748
+
749
+ update_load_path
750
+
751
+ if defined? Roby::Conf
752
+ Roby::Conf.datadirs = find_dirs('data', 'ROBOT', all: true, order: :specific_first)
753
+ end
754
+
755
+ if has_app?
756
+ require_robot_file
757
+ end
758
+
759
+ init_handlers.each(&:call)
760
+ update_load_path
761
+
762
+ # Define the app module if there is none, and define a root logger
763
+ # on it
764
+ app_module =
765
+ begin self.app_module
766
+ rescue NameError
767
+ Object.const_set(module_name, Module.new)
768
+ end
769
+ if !app_module.respond_to?(:logger)
770
+ module_name = self.module_name
771
+ app_module.class_eval do
772
+ extend ::Logger::Root(module_name, Logger::INFO)
773
+ end
774
+ end
775
+ end
776
+
777
+ def base_setup
778
+ STDOUT.sync = true
779
+
780
+ load_base_config
781
+ if !@log_dir
782
+ find_and_create_log_dir
783
+ end
784
+ setup_loggers(redirections: true)
785
+
786
+ # Set up the loaded plugins
787
+ call_plugins(:base_setup, self)
788
+ end
789
+
790
+ # The inverse of #base_setup
791
+ def base_cleanup
792
+ if !public_logs?
793
+ created_log_dirs.delete_if do |dir|
794
+ FileUtils.rm_rf dir
795
+ true
796
+ end
797
+ created_log_base_dirs.sort_by(&:length).reverse_each do |dir|
798
+ # .rmdir will ignore nonempty / nonexistent directories
799
+ FileUtils.rmdir(dir)
800
+ created_log_base_dirs.delete(dir)
801
+ end
802
+ end
803
+ end
804
+
805
+ # Does basic setup of the Roby environment. It loads configuration files
806
+ # and sets up singleton objects.
807
+ #
808
+ # After a call to #setup, the Roby services that do not require an
809
+ # execution loop to run should be available
810
+ #
811
+ # Plugins that define a setup(app) method will see their method called
812
+ # at this point
813
+ #
814
+ # The #cleanup method is the reverse of #setup
815
+ def setup
816
+ base_setup
817
+ # Set up the loaded plugins
818
+ call_plugins(:setup, self)
819
+ # And run the setup handlers
820
+ setup_handlers.each(&:call)
821
+
822
+ require_models
823
+
824
+ # Main is always included in the planner list
825
+ self.planners << app_module::Actions::Main
826
+
827
+ # Attach the global fault tables to the plan
828
+ self.planners.each do |planner|
829
+ if planner.respond_to?(:each_fault_response_table)
830
+ planner.each_fault_response_table do |table, arguments|
831
+ plan.use_fault_response_table table, arguments
832
+ end
833
+ end
834
+ end
835
+
836
+ rescue Exception
837
+ begin cleanup
838
+ rescue Exception => e
839
+ Roby.warn "failed to cleanup after #setup raised"
840
+ Roby.log_exception_with_backtrace(e, Roby, :warn)
841
+ end
842
+ raise
843
+ end
844
+
845
+ # The inverse of #setup. It gets called at the end of #run
846
+ def cleanup
847
+ # Run the cleanup handlers first, we want the plugins to still be
848
+ # active
849
+ cleanup_handlers.each(&:call)
850
+
851
+ call_plugins(:cleanup, self)
852
+ # Deprecated version of #cleanup
853
+ call_plugins(:reset, self, deprecated: "define 'cleanup' instead")
854
+
855
+ planners.clear
856
+ plan.execution_engine.gather_propagation do
857
+ plan.clear
858
+ end
859
+ clear_models
860
+ clear_config
861
+
862
+ stop_shell_interface
863
+ base_cleanup
864
+ end
865
+
866
+ # @api private
867
+ def prepare_event_log
868
+ require 'roby/droby/event_logger'
869
+ require 'roby/droby/logfile/writer'
870
+
871
+ logfile_path = File.join(log_dir, "#{robot_name}-events.log")
872
+ event_io = File.open(logfile_path, 'w')
873
+ logfile = DRoby::Logfile::Writer.new(event_io, plugins: plugins.map { |n, _| n })
874
+ plan.event_logger = DRoby::EventLogger.new(logfile)
875
+ plan.execution_engine.event_logger = plan.event_logger
876
+
877
+ Robot.info "logs are in #{log_dir}"
878
+ logfile_path
879
+ end
880
+
881
+ # Prepares the environment to actually run
882
+ def prepare
883
+ if public_shell_interface?
884
+ setup_shell_interface
885
+ end
886
+ if public_rest_interface?
887
+ setup_rest_interface
888
+ end
889
+
890
+ if public_logs? && log_create_current?
891
+ FileUtils.rm_f File.join(log_base_dir, "current")
892
+ FileUtils.ln_s log_dir, File.join(log_base_dir, 'current')
893
+ end
894
+
895
+ if log['events'] && public_logs?
896
+ logfile_path = prepare_event_log
897
+
898
+ # Start a log server if needed, and poll the log directory for new
899
+ # data sources
900
+ if log_server_options = (log.has_key?('server') ? log['server'] : Hash.new)
901
+ if !log_server_options.kind_of?(Hash)
902
+ log_server_options = Hash.new
903
+ end
904
+ plan.event_logger.sync = true
905
+ start_log_server(logfile_path, log_server_options)
906
+ Roby.info "log server started"
907
+ else
908
+ plan.event_logger.sync = false
909
+ Roby.warn "log server disabled"
910
+ end
911
+ end
912
+
913
+ call_plugins(:prepare, self)
914
+ end
915
+
879
916
 
880
- # Load the plugins 'main' files
881
- Roby.app.plugin_dir File.join(ROBY_ROOT_DIR, 'plugins')
882
- if plugin_path = ENV['ROBY_PLUGIN_PATH']
883
- plugin_path.split(':').each do |dir|
884
- if File.directory?(dir)
885
- Roby.app.plugin_dir File.expand_path(dir)
886
- end
887
- end
917
+ # The inverse of #prepare. It gets called either at the end of #run or
918
+ # at the end of #setup if there is an error during loading
919
+ def shutdown
920
+ call_plugins(:shutdown, self)
921
+ stop_log_server
922
+ stop_shell_interface
923
+ stop_rest_interface(join: true)
924
+ end
925
+
926
+ # The robot names configuration
927
+ #
928
+ # @return [App::RobotNames]
929
+ def robots
930
+ if !@robots
931
+ robots = App::RobotNames.new(options['robots'] || Hash.new)
932
+ robots.strict = !!options['robots']
933
+ @robots = robots
934
+ end
935
+ @robots
936
+ end
937
+
938
+ # Declares a block that should be executed when the Roby app gets
939
+ # initialized (i.e. just after init.rb gets loaded)
940
+ def on_init(&block)
941
+ if !block
942
+ raise ArgumentError, "missing expected block argument"
943
+ end
944
+ init_handlers << block
945
+ end
946
+
947
+ # Declares a block that should be executed when the Roby app is begin
948
+ # setup
949
+ def on_setup(&block)
950
+ if !block
951
+ raise ArgumentError, "missing expected block argument"
952
+ end
953
+ setup_handlers << block
954
+ end
955
+
956
+ # Declares a block that should be executed when the Roby app loads
957
+ # models (i.e. in {#require_models})
958
+ def on_require(&block)
959
+ if !block
960
+ raise ArgumentError, "missing expected block argument"
961
+ end
962
+ require_handlers << block
963
+ end
964
+
965
+ # @deprecated use {#on_setup} instead
966
+ def on_config(&block)
967
+ on_setup(&block)
968
+ end
969
+
970
+ # Declares that the following block should be used as the robot
971
+ # controller
972
+ def controller(&block)
973
+ controllers << block
974
+ end
975
+
976
+ # Declares that the following block should be used to setup the main
977
+ # action interface
978
+ def actions(&block)
979
+ action_handlers << block
980
+ end
981
+
982
+ # Declares that the following block should be called when
983
+ # {#clear_models} is called
984
+ def on_clear_models(&block)
985
+ if !block
986
+ raise ArgumentError, "missing expected block argument"
987
+ end
988
+ clear_models_handlers << block
989
+ end
990
+
991
+ # Declares that the following block should be called when
992
+ # {#clear_models} is called
993
+ def on_cleanup(&block)
994
+ if !block
995
+ raise ArgumentError, "missing expected block argument"
996
+ end
997
+ cleanup_handlers << block
998
+ end
999
+
1000
+ # Looks into subdirectories of +dir+ for files called app.rb and
1001
+ # registers them as Roby plugins
1002
+ def load_plugins_from_prefix(dir)
1003
+ dir = File.expand_path(dir)
1004
+ $LOAD_PATH.unshift dir
1005
+
1006
+ Dir.new(dir).each do |subdir|
1007
+ subdir = File.join(dir, subdir)
1008
+ next unless File.directory?(subdir)
1009
+ appfile = File.join(subdir, "app.rb")
1010
+ next unless File.file?(appfile)
1011
+ load_plugin_file(appfile)
1012
+ end
1013
+ ensure
1014
+ $LOAD_PATH.shift
1015
+ end
1016
+
1017
+ # Load the given Roby plugin file. It is usually called app.rb, and
1018
+ # should call register_plugin with the relevant information
1019
+ #
1020
+ # Note that the file should not do anything yet. The actions required to
1021
+ # have a functional plugin should be taken only in the block given to
1022
+ # register_plugin or in the relevant plugin methods.
1023
+ def load_plugin_file(appfile)
1024
+ begin
1025
+ require appfile
1026
+ rescue
1027
+ Roby.warn "cannot load plugin #{appfile}: #{$!.full_message}\n"
1028
+ end
1029
+ Roby.info "loaded plugin #{appfile}"
1030
+ end
1031
+
1032
+ # Returns true if +name+ is a loaded plugin
1033
+ def loaded_plugin?(name)
1034
+ plugins.any? { |plugname, _| plugname == name }
1035
+ end
1036
+
1037
+ # Returns the [name, dir, file, module] array definition of the plugin
1038
+ # +name+, or nil if +name+ is not a known plugin
1039
+ def plugin_definition(name)
1040
+ available_plugins.find { |plugname, *_| plugname == name }
1041
+ end
1042
+
1043
+ # True if +name+ is a plugin known to us
1044
+ def defined_plugin?(name)
1045
+ available_plugins.any? { |plugname, *_| plugname == name }
1046
+ end
1047
+
1048
+ # Enumerates all available plugins, yielding only the plugin module
1049
+ # (i.e. the plugin object itself)
1050
+ def each_plugin(on_available = false)
1051
+ plugins = self.plugins
1052
+ if on_available
1053
+ plugins = available_plugins.map { |name, _, mod, _| [name, mod] }
1054
+ end
1055
+ plugins.each do |_, mod|
1056
+ yield(mod)
1057
+ end
1058
+ end
1059
+
1060
+ # Yields each plugin object that respond to +method+
1061
+ def each_responding_plugin(method, on_available = false)
1062
+ each_plugin do |mod|
1063
+ yield(mod) if mod.respond_to?(method)
1064
+ end
1065
+ end
1066
+
1067
+ # Call +method+ on each loaded extension module which define it, with
1068
+ # arguments +args+
1069
+ def call_plugins(method, *args, deprecated: nil)
1070
+ each_responding_plugin(method) do |config_extension|
1071
+ if deprecated
1072
+ Roby.warn "#{config_extension} uses the deprecated .#{method} hook during setup and teardown, #{deprecated}"
1073
+ end
1074
+ config_extension.send(method, *args)
1075
+ end
1076
+ end
1077
+
1078
+ def register_plugins(force: false)
1079
+ if !plugins_enabled? && !force
1080
+ raise PluginsDisabled, "cannot call #register_plugins while the plugins are disabled"
1081
+ end
1082
+
1083
+ # Load the plugins 'main' files
1084
+ if plugin_path = ENV['ROBY_PLUGIN_PATH']
1085
+ plugin_path.split(':').each do |plugin|
1086
+ if File.directory?(plugin)
1087
+ load_plugins_from_prefix plugin
1088
+ elsif File.file?(plugin)
1089
+ load_plugin_file plugin
1090
+ end
1091
+ end
1092
+ end
1093
+ end
1094
+
1095
+ # Loads the plugins whose name are listed in +names+
1096
+ def using(*names, force: false)
1097
+ if !plugins_enabled? && !force
1098
+ raise PluginsDisabled, "plugins are disabled, cannot load #{names.join(", ")}"
1099
+ end
1100
+
1101
+ register_plugins(force: true)
1102
+ names.map do |name|
1103
+ name = name.to_s
1104
+ unless plugin = plugin_definition(name)
1105
+ raise ArgumentError, "#{name} is not a known plugin (available plugins: #{available_plugins.map { |n, *_| n }.join(", ")})"
1106
+ end
1107
+ name, dir, mod, init = *plugin
1108
+ if already_loaded = plugins.find { |n, m| n == name && m == mod }
1109
+ next(already_loaded[1])
1110
+ end
1111
+
1112
+ if dir
1113
+ filter_out_patterns.push(/#{Regexp.quote(dir)}/)
1114
+ end
1115
+
1116
+ if init
1117
+ begin
1118
+ $LOAD_PATH.unshift dir
1119
+ init.call
1120
+ mod.reset(self) if mod.respond_to?(:reset)
1121
+ rescue Exception => e
1122
+ Roby.fatal "cannot load plugin #{name}: #{e.full_message}"
1123
+ exit(1)
1124
+ ensure
1125
+ $LOAD_PATH.shift
1126
+ end
1127
+ end
1128
+
1129
+ add_plugin(name, mod)
1130
+ end
1131
+ end
1132
+
1133
+ def add_plugin(name, mod)
1134
+ plugins << [name, mod]
1135
+ extend mod
1136
+ # If +load+ has already been called, call it on the module
1137
+ if mod.respond_to?(:load) && options
1138
+ mod.load(self, options)
1139
+ end
1140
+
1141
+ # Refresh the relation sets in #plan to include relations
1142
+ # possibly added by the plugin
1143
+ plan.refresh_relations
1144
+
1145
+ mod
1146
+ end
1147
+
1148
+ # The robot name
1149
+ #
1150
+ # @return [String,nil]
1151
+ def robot_name
1152
+ if @robot_name then @robot_name
1153
+ else robots.default_robot_name
1154
+ end
1155
+ end
1156
+
1157
+ # The robot type
1158
+ #
1159
+ # @return [String,nil]
1160
+ def robot_type
1161
+ if @robot_type then @robot_type
1162
+ else robots.default_robot_type
1163
+ end
1164
+ end
1165
+
1166
+ # Sets up the name and type of the robot. This can be called only once
1167
+ # in a given Roby controller.
1168
+ def robot(name, type = nil)
1169
+ @robot_name, @robot_type = robots.resolve(name, type)
1170
+ end
1171
+
1172
+ # The base directory in which logs should be saved
1173
+ #
1174
+ # Logs are saved in log_base_dir/$time_tag by default, and a
1175
+ # log_base_dir/current symlink gets updated to reflect the most current
1176
+ # log directory.
1177
+ #
1178
+ # The path is controlled by the log/dir configuration variable. If the
1179
+ # provided value, it is interpreted relative to the application
1180
+ # directory. It defaults to "data".
1181
+ def log_base_dir
1182
+ maybe_relative_dir =
1183
+ if @log_base_dir ||= log['dir']
1184
+ @log_base_dir
1185
+ elsif global_base_dir = ENV['ROBY_BASE_LOG_DIR']
1186
+ File.join(global_base_dir, app_name)
1187
+ else
1188
+ 'logs'
1189
+ end
1190
+ File.expand_path(maybe_relative_dir, app_dir || Dir.pwd)
1191
+ end
1192
+
1193
+ # Sets the directory under which logs should be created
1194
+ #
1195
+ # This cannot be called after log_dir has been set
1196
+ def log_base_dir=(dir)
1197
+ @log_base_dir = dir
1198
+ end
1199
+
1200
+ # Create a log directory for the given time tag, and make it this app's
1201
+ # log directory
1202
+ #
1203
+ # The time tag given to this method also becomes the app's time tag
1204
+ #
1205
+ # @param [String] time_tag
1206
+ # @return [String] the path to the log directory
1207
+ def find_and_create_log_dir(time_tag = self.time_tag)
1208
+ base_dir = log_base_dir
1209
+ @time_tag = time_tag
1210
+
1211
+ while true
1212
+ log_dir = Roby::Application.unique_dirname(base_dir, '', time_tag)
1213
+ new_dirs = Array.new
1214
+
1215
+ dir = log_dir
1216
+ while !File.directory?(dir)
1217
+ new_dirs << dir
1218
+ dir = File.dirname(dir)
1219
+ end
1220
+
1221
+ # Create all paths necessary, but check for possible concurrency
1222
+ # issues with other Roby-based tools creating a log dir with the
1223
+ # same name
1224
+ failed = new_dirs.reverse.any? do |dir|
1225
+ begin FileUtils.mkdir(dir)
1226
+ false
1227
+ rescue Errno::EEXIST
1228
+ true
1229
+ end
1230
+ end
1231
+
1232
+ if !failed
1233
+ new_dirs.delete(log_dir)
1234
+ created_log_dirs << log_dir
1235
+ created_log_base_dirs.concat(new_dirs)
1236
+ @log_dir = log_dir
1237
+ log_save_metadata
1238
+ return log_dir
1239
+ end
1240
+ end
1241
+ end
1242
+
1243
+ # The directory in which logs are to be saved
1244
+ # Defaults to app_dir/data/$time_tag
1245
+ def log_dir
1246
+ if !@log_dir
1247
+ raise LogDirNotInitialized, "the log directory has not been initialized yet"
1248
+ end
1249
+ @log_dir
1250
+ end
1251
+
1252
+ # Reset the current log dir so that {#setup} picks a new one
1253
+ def reset_log_dir
1254
+ @log_dir = nil
1255
+ end
1256
+
1257
+ # Reset the plan to a new Plan object
1258
+ def reset_plan(plan = ExecutablePlan.new)
1259
+ @plan = plan
1260
+ end
1261
+
1262
+ # Explicitely set the log directory
1263
+ #
1264
+ # It is usually automatically created under {#log_base_dir} during
1265
+ # {#base_setup}
1266
+ def log_dir=(dir)
1267
+ if !File.directory?(dir)
1268
+ raise ArgumentError, "log directory #{dir} does not exist"
1269
+ end
1270
+ @log_dir = dir
1271
+ end
1272
+
1273
+ # The time tag. It is a time formatted as YYYYMMDD-HHMM used to mark log
1274
+ # directories
1275
+ def time_tag
1276
+ @time_tag ||= Time.now.strftime('%Y%m%d-%H%M')
1277
+ end
1278
+
1279
+ # Add some metadata to {#app_metadata}, and save it to the log dir's
1280
+ # info.yml if it is already created
1281
+ def add_app_metadata(metadata)
1282
+ app_extra_metadata.merge!(metadata)
1283
+ if created_log_dir?
1284
+ log_save_metadata
1285
+ end
1286
+ end
1287
+
1288
+ # Metadata used to describe the app
1289
+ #
1290
+ # It is saved in the app's log directory under info.yml
1291
+ #
1292
+ # @see add_app_metadata
1293
+ def app_metadata
1294
+ Hash['time' => time_tag, 'cmdline' => "#{$0} #{ARGV.join(" ")}",
1295
+ 'robot_name' => robot_name, 'robot_type' => robot_type,
1296
+ 'app_name' => app_name, 'app_dir' => app_dir].merge(app_extra_metadata)
1297
+ end
1298
+
1299
+ # Test whether this app already created its log directory
1300
+ def created_log_dir?
1301
+ @log_dir && File.directory?(@log_dir)
1302
+ end
1303
+
1304
+ # Save {#app_metadata} in the log directory
1305
+ #
1306
+ # @param [Boolean] append if true (the default), the value returned by
1307
+ # {#app_metadata} is appended to the existing data. Otherwise, it
1308
+ # replaces the last entry in the file
1309
+ def log_save_metadata(append: true)
1310
+ path = File.join(log_dir, 'info.yml')
1311
+
1312
+ info = Array.new
1313
+ current =
1314
+ if File.file?(path)
1315
+ YAML.load(File.read(path)) || Array.new
1316
+ else Array.new
1317
+ end
1318
+
1319
+ if append || current.empty?
1320
+ current << app_metadata
1321
+ else
1322
+ current[-1] = app_metadata
1323
+ end
1324
+ File.open(path, 'w') do |io|
1325
+ YAML.dump(current, io)
1326
+ end
1327
+ end
1328
+
1329
+ # Read the time tag from the current log directory
1330
+ def log_read_metadata
1331
+ dir = begin
1332
+ log_current_dir
1333
+ rescue ArgumentError
1334
+ end
1335
+
1336
+ if dir && File.exists?(File.join(dir, 'info.yml'))
1337
+ YAML.load(File.read(File.join(dir, 'info.yml')))
1338
+ else
1339
+ Array.new
1340
+ end
1341
+ end
1342
+
1343
+ def log_read_time_tag
1344
+ metadata = log_read_metadata.last
1345
+ metadata && metadata['time_tag']
1346
+ end
1347
+
1348
+ # The path to the current log directory
1349
+ #
1350
+ # If {#log_dir} is set, it is used. Otherwise, the current log directory
1351
+ # is inferred by the directory pointed to the 'current' symlink
1352
+ def log_current_dir
1353
+ if @log_dir
1354
+ @log_dir
1355
+ else
1356
+ current_path = File.join(log_base_dir, "current")
1357
+ self.class.read_current_dir(current_path)
1358
+ end
1359
+ end
1360
+
1361
+ class NoCurrentLog < RuntimeError; end
1362
+
1363
+ # The path to the current log file
1364
+ def log_current_file
1365
+ log_current_dir = self.log_current_dir
1366
+ metadata = log_read_metadata
1367
+ if metadata.empty?
1368
+ raise NoCurrentLog, "#{log_current_dir} is not a valid Roby log dir, it does not have an info.yml metadata file"
1369
+ elsif !(robot_name = metadata.map { |h| h['robot_name'] }.compact.last)
1370
+ raise NoCurrentLog, "#{log_current_dir}'s metadata does not specify the robot name"
1371
+ end
1372
+
1373
+ full_path = File.join(log_current_dir, "#{robot_name}-events.log")
1374
+ if !File.file?(full_path)
1375
+ raise NoCurrentLog, "inferred log file #{full_path} for #{log_current_dir}, but that file does not exist"
1376
+ end
1377
+ full_path
1378
+ end
1379
+
1380
+ # @api private
1381
+ #
1382
+ # Read and validate the 'current' dir by means of the 'current' symlink
1383
+ # that Roby maintains in its log base directory
1384
+ #
1385
+ # @param [String] current_path the path to the 'current' symlink
1386
+ def self.read_current_dir(current_path)
1387
+ if !File.symlink?(current_path)
1388
+ raise ArgumentError, "#{current_path} does not exist or is not a symbolic link"
1389
+ end
1390
+ resolved_path = File.readlink(current_path)
1391
+ if !File.exist?(resolved_path)
1392
+ raise ArgumentError, "#{current_path} points to #{resolved_path}, which does not exist"
1393
+ elsif !File.directory?(resolved_path)
1394
+ raise ArgumentError, "#{current_path} points to #{resolved_path}, which is not a directory"
1395
+ end
1396
+ resolved_path
1397
+ end
1398
+
1399
+ # A path => File hash, to re-use the same file object for different
1400
+ # logs
1401
+ attribute(:log_files) { Hash.new }
1402
+
1403
+ # Returns a unique directory name as a subdirectory of
1404
+ # +base_dir+, based on +path_spec+. The generated name
1405
+ # is of the form
1406
+ # <base_dir>/a/b/c/YYYYMMDD-HHMM-basename
1407
+ # if <tt>path_spec = "a/b/c/basename"</tt>. A .<number> suffix
1408
+ # is appended if the path already exists.
1409
+ def self.unique_dirname(base_dir, path_spec, date_tag = nil)
1410
+ if path_spec =~ /\/$/
1411
+ basename = ""
1412
+ dirname = path_spec
1413
+ else
1414
+ basename = File.basename(path_spec)
1415
+ dirname = File.dirname(path_spec)
1416
+ end
1417
+
1418
+ date_tag ||= Time.now.strftime('%Y%m%d-%H%M')
1419
+ if basename && !basename.empty?
1420
+ basename = date_tag + "-" + basename
1421
+ else
1422
+ basename = date_tag
1423
+ end
1424
+
1425
+ # Check if +basename+ already exists, and if it is the case add a
1426
+ # .x suffix to it
1427
+ full_path = File.expand_path(File.join(dirname, basename), base_dir)
1428
+ base_dir = File.dirname(full_path)
1429
+
1430
+ final_path, i = full_path, 0
1431
+ while File.exists?(final_path)
1432
+ i += 1
1433
+ final_path = full_path + ".#{i}"
1434
+ end
1435
+
1436
+ final_path
1437
+ end
1438
+
1439
+ class InvalidLoggerName < ArgumentError; end
1440
+
1441
+ # Sets up all the default loggers. It creates the logger for the Robot
1442
+ # module (accessible through Robot.logger), and sets up log levels as
1443
+ # specified in the <tt>config/app.yml</tt> file.
1444
+ def setup_loggers(ignore_missing: false, redirections: true)
1445
+ Robot.logger.progname = robot_name || 'Robot'
1446
+ return if !log['levels']
1447
+
1448
+ # Set up log levels
1449
+ log['levels'].each do |name, value|
1450
+ const_name = name.modulize
1451
+ mod =
1452
+ begin Kernel.constant(const_name)
1453
+ rescue NameError => e
1454
+ if ignore_missing
1455
+ next
1456
+ elsif name != const_name
1457
+ raise InvalidLoggerName, "cannot resolve logger #{name} (resolved as #{const_name}): #{e.message}"
1458
+ else
1459
+ raise InvalidLoggerName, "cannot resolve logger #{name}: #{e.message}"
1460
+ end
1461
+ end
1462
+
1463
+ if value =~ /^(\w+):(.+)$/
1464
+ value, file = $1, $2
1465
+ file = file.gsub('ROBOT', robot_name) if robot_name
1466
+ end
1467
+ level = Logger.const_get(value)
1468
+
1469
+ io = if redirections && file
1470
+ path = File.expand_path(file, log_dir)
1471
+ Robot.info "redirected logger for #{mod} to #{path} (level #{level})"
1472
+ io = File.open(path, 'w')
1473
+ io.sync = true
1474
+ log_files[path] ||= io
1475
+ else
1476
+ STDOUT
1477
+ end
1478
+ new_logger = Logger.new(io)
1479
+ new_logger.level = level
1480
+ new_logger.formatter = mod.logger.formatter
1481
+ new_logger.progname = [name, robot_name].compact.join(" ")
1482
+ mod.logger = new_logger
1483
+ end
1484
+ end
1485
+
1486
+ # Register a server port that can be discovered later
1487
+ def register_server(name, port)
1488
+ end
1489
+
1490
+ # Transforms +path+ into a path relative to an entry in +search_path+
1491
+ # (usually the application root directory)
1492
+ def make_path_relative(path)
1493
+ if !File.exists?(path)
1494
+ path
1495
+ elsif root_path = find_base_path_for(path)
1496
+ return Pathname.new(path).relative_path_from(root_path).to_s
1497
+ else
1498
+ path
1499
+ end
1500
+ end
1501
+
1502
+ def register_exception(e, reason = nil)
1503
+ registered_exceptions << [e, reason]
1504
+ end
1505
+
1506
+ def clear_exceptions
1507
+ registered_exceptions.clear
1508
+ end
1509
+
1510
+ def isolate_load_errors(message, logger = Application, level = :warn)
1511
+ yield
1512
+ rescue Interrupt
1513
+ raise
1514
+ rescue ::Exception => e
1515
+ register_exception(e, message)
1516
+ if ignore_all_load_errors?
1517
+ Robot.warn message
1518
+ Roby.log_exception_with_backtrace(e, logger, level)
1519
+ else raise
1520
+ end
1521
+ end
1522
+
1523
+ def require(absolute_path)
1524
+ # Make the file relative to the search path
1525
+ file = make_path_relative(absolute_path)
1526
+ Roby::Application.info "loading #{file} (#{absolute_path})"
1527
+ isolate_load_errors("ignored file #{file}") do
1528
+ if file != absolute_path
1529
+ Kernel.require(file)
1530
+ else
1531
+ Kernel.require absolute_path
1532
+ end
1533
+ end
1534
+ end
1535
+
1536
+ # Loads the models, based on the given robot name and robot type
1537
+ def require_models
1538
+ # Set up the loaded plugins
1539
+ call_plugins(:require_config, self, deprecated: "define 'require_models' instead")
1540
+ call_plugins(:require_models, self)
1541
+
1542
+ require_handlers.each do |handler|
1543
+ isolate_load_errors("while calling #{handler}") do
1544
+ handler.call
1545
+ end
1546
+ end
1547
+
1548
+ define_actions_module
1549
+ if auto_load_models?
1550
+ auto_require_planners
1551
+ end
1552
+ define_main_planner_if_needed
1553
+
1554
+ action_handlers.each do |act|
1555
+ isolate_load_errors("error in #{act}") do
1556
+ app_module::Actions::Main.class_eval(&act)
1557
+ end
1558
+ end
1559
+
1560
+ additional_model_files.each do |path|
1561
+ require File.expand_path(path)
1562
+ end
1563
+
1564
+ if auto_load_models?
1565
+ auto_require_models
1566
+ end
1567
+
1568
+ # Set up the loaded plugins
1569
+ call_plugins(:finalize_model_loading, self)
1570
+
1571
+ plan.refresh_relations
1572
+ end
1573
+
1574
+ def load_all_model_files_in(prefix_name, ignored_exceptions: Array.new)
1575
+ search_path = auto_load_search_path
1576
+ dirs = find_dirs(
1577
+ "models", prefix_name,
1578
+ path: search_path,
1579
+ all: true,
1580
+ order: :specific_last)
1581
+
1582
+ dirs.each do |dir|
1583
+ all_files = Set.new
1584
+ Find.find(dir) do |path|
1585
+ # Skip the robot-specific bits that don't apply on the
1586
+ # selected robot
1587
+ if File.directory?(path)
1588
+ suffix = File.basename(File.dirname(path))
1589
+ if robots.has_robot?(suffix) && ![robot_name, robot_type].include?(suffix)
1590
+ Find.prune
1591
+ end
1592
+ end
1593
+
1594
+ if File.file?(path) && path =~ /\.rb$/
1595
+ all_files << path
1596
+ end
1597
+ end
1598
+
1599
+ all_files.each do |path|
1600
+ begin
1601
+ require(path)
1602
+ rescue *ignored_exceptions => e
1603
+ ::Robot.warn "ignored file #{path}: #{e.message}"
1604
+ end
1605
+ end
1606
+ end
1607
+ end
1608
+
1609
+ def auto_require_models
1610
+ # Require all common task models and the task models specific to
1611
+ # this robot
1612
+ if auto_load_models?
1613
+ load_all_model_files_in('tasks')
1614
+
1615
+ if backward_compatible_naming?
1616
+ search_path = self.auto_load_search_path
1617
+ all_files = find_files_in_dirs('tasks', 'ROBOT', path: search_path, all: true, order: :specific_last, pattern: /\.rb$/)
1618
+ all_files.each do |p|
1619
+ require(p)
1620
+ end
1621
+ end
1622
+ call_plugins(:auto_require_models, self)
1623
+ end
1624
+ end
1625
+
1626
+ # Test if the given name is a valid robot name
1627
+ def robot_name?(name)
1628
+ !robots.strict? || robots.has_robot?(name)
1629
+ end
1630
+
1631
+ # Helper to the robot config files to load the root files in models/
1632
+ # (e.g. models/tasks.rb)
1633
+ def load_default_models
1634
+ ['tasks.rb', 'actions.rb'].each do |root_type|
1635
+ if path = find_file('models', root_type, path: [app_dir], order: :specific_first)
1636
+ require path
1637
+ end
1638
+ end
1639
+ call_plugins(:load_default_models, self)
1640
+ end
1641
+
1642
+ # Returns the downmost app file that was involved in the given model's
1643
+ # definition
1644
+ def definition_file_for(model)
1645
+ return if !model.respond_to?(:definition_location) || !model.definition_location
1646
+ model.definition_location.each do |location|
1647
+ file = location.absolute_path
1648
+ next if !(base_path = find_base_path_for(file))
1649
+ relative = Pathname.new(file).relative_path_from(base_path)
1650
+ split = relative.each_filename.to_a
1651
+ next if split[0] != 'models'
1652
+ return file
1653
+ end
1654
+ nil
1655
+ end
1656
+
1657
+ # Given a model class, returns the full path of an existing test file
1658
+ # that is meant to verify this model
1659
+ def test_files_for(model)
1660
+ return [] if !model.respond_to?(:definition_location) || !model.definition_location
1661
+
1662
+ test_files = Array.new
1663
+ model.definition_location.each do |location|
1664
+ file = location.absolute_path
1665
+ next if !(base_path = find_base_path_for(file))
1666
+ relative = Pathname.new(file).relative_path_from(base_path)
1667
+ split = relative.each_filename.to_a
1668
+ next if split[0] != 'models'
1669
+ split[0] = 'test'
1670
+ split[-1] = "test_#{split[-1]}"
1671
+ canonical_testpath = [base_path, *split].join(File::SEPARATOR)
1672
+ if File.exist?(canonical_testpath)
1673
+ test_files << canonical_testpath
1674
+ end
1675
+ end
1676
+ test_files
1677
+ end
1678
+
1679
+ def define_actions_module
1680
+ if !app_module.const_defined_here?(:Actions)
1681
+ app_module.const_set(:Actions, Module.new)
1682
+ end
1683
+ end
1684
+
1685
+ def define_main_planner_if_needed
1686
+ if !app_module::Actions.const_defined_here?(:Main)
1687
+ app_module::Actions.const_set(:Main, Class.new(Roby::Actions::Interface))
1688
+ end
1689
+ if backward_compatible_naming?
1690
+ if !Object.const_defined_here?(:Main)
1691
+ Object.const_set(:Main, app_module::Actions::Main)
1692
+ end
1693
+ end
1694
+ end
1695
+
1696
+ # Loads the planner models
1697
+ #
1698
+ # This method is called at the end of {#require_models}, before the
1699
+ # plugins' require_models hook is called
1700
+ def require_planners
1701
+ Roby.warn_deprecated "Application#require_planners is deprecated and has been renamed into #auto_require_planners"
1702
+ auto_require_planners
1703
+ end
1704
+
1705
+ def auto_require_planners
1706
+ search_path = self.auto_load_search_path
1707
+
1708
+ prefixes = ['actions']
1709
+ if backward_compatible_naming?
1710
+ prefixes << 'planners'
1711
+ end
1712
+ prefixes.each do |prefix|
1713
+ load_all_model_files_in(prefix)
1714
+ end
1715
+
1716
+ if backward_compatible_naming?
1717
+ main_files = find_files('planners', 'ROBOT', 'main.rb', all: true, order: :specific_first)
1718
+ main_files.each do |path|
1719
+ require path
1720
+ end
1721
+ planner_files = find_files_in_dirs('planners', 'ROBOT', all: true, order: :specific_first, pattern: /\.rb$/)
1722
+ planner_files.each do |path|
1723
+ require path
1724
+ end
1725
+ end
1726
+ call_plugins(:require_planners, self)
1727
+ end
1728
+
1729
+ def load_config_yaml
1730
+ file = find_file('config', 'app.yml', order: :specific_first)
1731
+ return if !file
1732
+
1733
+ Application.info "loading config file #{file}"
1734
+ options = YAML.load(File.open(file)) || Hash.new
1735
+
1736
+ if robot_name && (robot_config = options.delete('robots'))
1737
+ options = options.recursive_merge(robot_config[robot_name] || Hash.new)
1738
+ end
1739
+ options = options.map_value do |k, val|
1740
+ val || Hash.new
1741
+ end
1742
+ options = @options.recursive_merge(options)
1743
+ apply_config(options)
1744
+ @options = options
1745
+ end
1746
+
1747
+ # @api private
1748
+ #
1749
+ # Sets relevant configuration values from a configuration hash
1750
+ def apply_config(config)
1751
+ if host_port = config['interface']
1752
+ apply_config_interface(host_port)
1753
+ elsif host_port = config.fetch('droby', Hash.new)['host']
1754
+ Roby.warn_deprecated 'the droby.host configuration parameter in config/app.yml is deprecated, use "interface" at the toplevel instead'
1755
+ apply_config_interface(host_port)
1756
+ end
1757
+ end
1758
+
1759
+ # @api private
1760
+ #
1761
+ # Parses and applies the 'interface' value from a configuration hash
1762
+ #
1763
+ # It is a helper for {#apply_config}
1764
+ def apply_config_interface(host_port)
1765
+ if host_port !~ /:\d+$/
1766
+ host_port += ":#{Interface::DEFAULT_PORT}"
1767
+ end
1768
+
1769
+ match = /(.*):(\d+)$/.match(host_port)
1770
+ host = match[1]
1771
+ @shell_interface_host =
1772
+ if !host.empty?
1773
+ host
1774
+ end
1775
+ @shell_interface_port = Integer(match[2])
1776
+ end
1777
+
1778
+ def update_load_path
1779
+ search_path.reverse.each do |app_dir|
1780
+ $LOAD_PATH.delete(app_dir)
1781
+ $LOAD_PATH.unshift(app_dir)
1782
+ libdir = File.join(app_dir, 'lib')
1783
+ if File.directory?(libdir)
1784
+ $LOAD_PATH.delete(libdir)
1785
+ $LOAD_PATH.unshift(libdir)
1786
+ end
1787
+ end
1788
+
1789
+ find_dirs('lib', 'ROBOT', all: true, order: :specific_last).
1790
+ each do |libdir|
1791
+ if !$LOAD_PATH.include?(libdir)
1792
+ $LOAD_PATH.unshift libdir
1793
+ end
1794
+ end
1795
+ end
1796
+
1797
+ def setup_robot_names_from_config_dir
1798
+ robot_config_files = find_files_in_dirs 'config', 'robots',
1799
+ all: true,
1800
+ order: :specific_first,
1801
+ pattern: lambda { |p| File.extname(p) == ".rb" }
1802
+
1803
+ robots.strict = !robot_config_files.empty?
1804
+ robot_config_files.each do |path|
1805
+ robot_name = File.basename(path, ".rb")
1806
+ robots.robots[robot_name] ||= robot_name
1807
+ end
1808
+ end
1809
+
1810
+ def require_robot_file
1811
+ p = find_file('config', 'robots', "#{robot_name}.rb", order: :specific_first) ||
1812
+ find_file('config', 'robots', "#{robot_type}.rb", order: :specific_first)
1813
+
1814
+ if p
1815
+ @default_auto_load = false
1816
+ require p
1817
+ if !robot_type
1818
+ robot(robot_name, robot_name)
1819
+ end
1820
+ elsif !find_dir('config', 'robots', order: :specific_first) || (robot_name == robots.default_robot_name) || !robots.strict?
1821
+ Roby.warn "#{robot_name}:#{robot_type} is selected as the robot, but there is"
1822
+ if robot_name == robot_type
1823
+ Roby.warn "no file named config/robots/#{robot_name}.rb"
1824
+ else
1825
+ Roby.warn "neither config/robots/#{robot_name}.rb nor config/robots/#{robot_type}.rb"
1826
+ end
1827
+ Roby.warn "run roby gen robot #{robot_name} in your app to create one"
1828
+ Roby.warn "initialization will go on, but this behaviour is deprecated and will be removed in the future"
1829
+ else
1830
+ raise NoSuchRobot, "cannot find config file for robot #{robot_name} of type #{robot_type} in config/robots/"
1831
+ end
1832
+ end
1833
+
1834
+ # Publishes a shell interface
1835
+ #
1836
+ # This method publishes a Roby::Interface object using
1837
+ # {Interface::TCPServer}. It is published on {Interface::DEFAULT_PORT}
1838
+ # by default. This default can be overriden by setting
1839
+ # {#shell_interface_port} either in config/init.rb, or in a
1840
+ # {Robot.setup} block in the robot configuration file.
1841
+ #
1842
+ # The shell interface is started in #setup and stopped in #cleanup
1843
+ #
1844
+ # @see stop_shell_interface
1845
+ def setup_shell_interface
1846
+ require 'roby/interface'
1847
+
1848
+ if @shell_interface
1849
+ raise RuntimeError, "there is already a shell interface started, call #stop_shell_interface first"
1850
+ end
1851
+ @shell_interface = Interface::TCPServer.new(
1852
+ self, host: shell_interface_host, port: shell_interface_port)
1853
+ shell_interface.abort_on_exception = shell_abort_on_exception?
1854
+ if shell_interface_port != Interface::DEFAULT_PORT
1855
+ Robot.info "shell interface started on port #{shell_interface_port}"
1856
+ else
1857
+ Robot.debug "shell interface started on port #{shell_interface_port}"
1858
+ end
1859
+ end
1860
+
1861
+ # Stops a running shell interface
1862
+ #
1863
+ # This is a no-op if no shell interface is currently running
1864
+ def stop_shell_interface
1865
+ if @shell_interface
1866
+ @shell_interface.close
1867
+ @shell_interface = nil
1868
+ end
1869
+ end
1870
+
1871
+ # Publishes a REST API
1872
+ #
1873
+ # The REST API will long-term replace the shell interface. It is however
1874
+ # currently too limited for this purpose. Whether one should use one or
1875
+ # the other is up to the application, but prefer the REST API if it
1876
+ # suits your needs
1877
+ def setup_rest_interface
1878
+ require 'roby/interface/rest'
1879
+
1880
+ if @rest_interface
1881
+ raise RuntimeError, "there is already a REST interface started, call #stop_rest_interface first"
1882
+ end
1883
+ composite_api = Class.new(Grape::API)
1884
+ composite_api.mount Interface::REST::API
1885
+ call_plugins(:setup_rest_interface, self, composite_api)
1886
+
1887
+ @rest_interface = Interface::REST::Server.new(
1888
+ self, host: rest_interface_host, port: rest_interface_port,
1889
+ api: composite_api)
1890
+ @rest_interface.start
1891
+
1892
+ if rest_interface_port != Interface::DEFAULT_REST_PORT
1893
+ Robot.info "REST interface started on port #{@rest_interface.port(timeout: nil)}"
1894
+ else
1895
+ Robot.debug "REST interface started on port #{rest_interface_port}"
1896
+ end
1897
+ @rest_interface
1898
+ end
1899
+
1900
+ # Stops a running REST interface
1901
+ def stop_rest_interface(join: false)
1902
+ if @rest_interface
1903
+ # In case we're shutting down while starting up,
1904
+ # we must synchronize with the start to ensure that
1905
+ # EventMachine will be properly stopped
1906
+ @rest_interface.wait_start
1907
+ @rest_interface.stop
1908
+ @rest_interface.join if join
1909
+ end
1910
+ end
1911
+
1912
+ def run(thread_priority: 0, &block)
1913
+ prepare
1914
+
1915
+ engine_config = self.engine
1916
+ engine = self.plan.execution_engine
1917
+ plugins = self.plugins.map { |_, mod| mod if (mod.respond_to?(:start) || mod.respond_to?(:run)) }.compact
1918
+ engine.once do
1919
+ run_plugins(plugins, &block)
1920
+ end
1921
+ @thread = Thread.new do
1922
+ Thread.current.priority = thread_priority
1923
+ engine.run cycle: engine_config['cycle'] || 0.1
1924
+ end
1925
+ join
1926
+
1927
+ ensure
1928
+ shutdown
1929
+ @thread = nil
1930
+ if restarting?
1931
+ Kernel.exec *@restart_cmdline
1932
+ end
1933
+ end
1934
+
1935
+ def join
1936
+ @thread.join
1937
+
1938
+ rescue Exception => e
1939
+ if @thread.alive? && execution_engine.running?
1940
+ if execution_engine.forced_exit?
1941
+ raise
1942
+ else
1943
+ execution_engine.quit
1944
+ retry
1945
+ end
1946
+ else
1947
+ raise
1948
+ end
1949
+ end
1950
+
1951
+ # Whether we're inside {#run}
1952
+ def running?
1953
+ !!@thread
1954
+ end
1955
+
1956
+ # Whether {#run} should exec a new process on quit or not
1957
+ def restarting?
1958
+ !!@restarting
1959
+ end
1960
+
1961
+ # Quits this app and replaces with a new one after a proper cleanup
1962
+ #
1963
+ # @param [String] cmdline the command line to exec after quitting. If
1964
+ # not given, will restart using the same command line as the one that
1965
+ # started this process
1966
+ def restart(*cmdline)
1967
+ @restarting = true
1968
+ @restart_cmdline =
1969
+ if cmdline.empty?
1970
+ if defined? ORIGINAL_ARGV
1971
+ [$0, *ORIGINAL_ARGV]
1972
+ else
1973
+ [$0, *ARGV]
1974
+ end
1975
+ else cmdline
1976
+ end
1977
+ plan.execution_engine.quit
1978
+ end
1979
+
1980
+ # Helper for Application#run to call the plugin's run or start methods
1981
+ # while guaranteeing the system's cleanup
1982
+ #
1983
+ # This method recursively calls each plugin's #run method (if defined)
1984
+ # in block forms. This guarantees that the plugins can use a
1985
+ # begin/rescue/end mechanism to do their cleanup
1986
+ #
1987
+ # If no run-related cleanup is required, the plugin can define a #start(app)
1988
+ # method instead.
1989
+ #
1990
+ # Note that the cleanup we talk about here is related to running.
1991
+ # Cleanup required after #setup must be done in #cleanup
1992
+ def run_plugins(mods, &block)
1993
+ if mods.empty?
1994
+ yield if block_given?
1995
+ else
1996
+ mod = mods.shift
1997
+ if mod.respond_to?(:start)
1998
+ mod.start(self)
1999
+ run_plugins(mods, &block)
2000
+ else
2001
+ mod.run(self) do
2002
+ run_plugins(mods, &block)
2003
+ end
2004
+ end
2005
+ end
2006
+ end
2007
+
2008
+ def stop; call_plugins(:stop, self) end
2009
+
2010
+ def start_log_server(logfile, options = Hash.new)
2011
+ require 'roby/droby/logfile/server'
2012
+
2013
+ # Allocate a TCP server to get an ephemeral port, and pass it to
2014
+ # roby-display
2015
+ sampling_period = DRoby::Logfile::Server::DEFAULT_SAMPLING_PERIOD
2016
+ sampling_period = Float(options['sampling_period'] || sampling_period)
2017
+
2018
+ tcp_server = TCPServer.new(Integer(options['port'] || 0))
2019
+ server_flags = ["--fd=#{tcp_server.fileno}", "--sampling=#{sampling_period}", logfile]
2020
+ redirect_flags = Hash[tcp_server => tcp_server]
2021
+ if options['debug']
2022
+ server_flags << "--debug"
2023
+ elsif options['silent']
2024
+ redirect_flags[:out] = redirect_flags[:err] = :close
2025
+ end
2026
+
2027
+ @log_server_port = tcp_server.local_address.ip_port
2028
+ @log_server_pid = Kernel.spawn(
2029
+ Gem.ruby, File.join(Roby::BIN_DIR, "roby-display"),
2030
+ 'server', *server_flags, redirect_flags)
2031
+ ensure
2032
+ tcp_server.close if tcp_server
2033
+ end
2034
+
2035
+ def stop_log_server
2036
+ if @log_server_pid
2037
+ Process.kill('INT', @log_server_pid)
2038
+ @log_server_pid = nil
2039
+ end
2040
+ end
2041
+
2042
+ # @overload find_files_in_dirs(*path, options)
2043
+ #
2044
+ # Enumerates the subdirectories of paths in {#search_path} matching the
2045
+ # given path. The subdirectories are resolved using File.join(*path)
2046
+ # If one of the elements of the path is the string 'ROBOT', it gets
2047
+ # replaced by the robot name and type.
2048
+ #
2049
+ # @option options [Boolean] :all (true) if true, all matching
2050
+ # directories are returned. Otherwise, only the first one is (the
2051
+ # meaning of 'first' is controlled by the order option below)
2052
+ # @option options [:specific_first,:specific_last] :order if
2053
+ # :specific_first, the first returned match is the one that is most
2054
+ # specific. The sorting order is to first sort by ROBOT and then by
2055
+ # the place in search_dir. From the most specific to the least
2056
+ # specific, ROBOT is assigned the robot name, the robot type and
2057
+ # finally an empty string.
2058
+ # @return [Array<String>]
2059
+ #
2060
+ # Given a search dir of [app2, app1]
2061
+ #
2062
+ # app1/models/tasks/goto.rb
2063
+ # app1/models/tasks/v3/goto.rb
2064
+ # app2/models/tasks/asguard/goto.rb
2065
+ #
2066
+ # @example
2067
+ # find_dirs('tasks', 'ROBOT', all: true, order: :specific_first)
2068
+ # # returns [app1/models/tasks/v3,
2069
+ # # app2/models/tasks/asguard,
2070
+ # # app1/models/tasks/]
2071
+ #
2072
+ # @example
2073
+ # find_dirs('tasks', 'ROBOT', all: false, order: :specific_first)
2074
+ # # returns [app1/models/tasks/v3/goto.rb]
2075
+ def find_dirs(*dir_path)
2076
+ Application.debug { "find_dirs(#{dir_path.map(&:inspect).join(", ")})" }
2077
+ if dir_path.last.kind_of?(Hash)
2078
+ options = dir_path.pop
2079
+ end
2080
+ options = Kernel.validate_options(options || Hash.new, :all, :order, :path)
2081
+
2082
+ if dir_path.empty?
2083
+ raise ArgumentError, "no path given"
2084
+ end
2085
+
2086
+ search_path = options[:path] || self.search_path
2087
+ if !options.has_key?(:all)
2088
+ raise ArgumentError, "no :all argument given"
2089
+ elsif !options.has_key?(:order)
2090
+ raise ArgumentError, "no :order argument given"
2091
+ elsif ![:specific_first, :specific_last].include?(options[:order])
2092
+ raise ArgumentError, "expected either :specific_first or :specific_last for the :order argument, but got #{options[:order]}"
2093
+ end
2094
+
2095
+ relative_paths = []
2096
+
2097
+ base_dir_path = dir_path.dup
2098
+ base_dir_path.delete_if { |p| p =~ /ROBOT/ }
2099
+ relative_paths = [base_dir_path]
2100
+ if dir_path.any? { |p| p =~ /ROBOT/ } && robot_name && robot_type
2101
+ replacements = [robot_type]
2102
+ if robot_type != robot_name
2103
+ replacements << robot_name
2104
+ end
2105
+ replacements.each do |replacement|
2106
+ robot_dir_path = dir_path.map do |s|
2107
+ s.gsub('ROBOT', replacement)
2108
+ end
2109
+ relative_paths << robot_dir_path
2110
+ end
2111
+ end
2112
+
2113
+ root_paths = search_path.dup
2114
+ if options[:order] == :specific_first
2115
+ relative_paths = relative_paths.reverse
2116
+ else
2117
+ root_paths = root_paths.reverse
2118
+ end
2119
+
2120
+ result = []
2121
+ Application.debug { " relative paths: #{relative_paths.inspect}" }
2122
+ relative_paths.each do |rel_path|
2123
+ root_paths.each do |root|
2124
+ abs_path = File.expand_path(File.join(*rel_path), root)
2125
+ Application.debug { " absolute path: #{abs_path}" }
2126
+ if File.directory?(abs_path)
2127
+ Application.debug { " selected" }
2128
+ result << abs_path
2129
+ end
2130
+ end
2131
+ end
2132
+
2133
+ if result.empty?
2134
+ return result
2135
+ elsif !options[:all]
2136
+ return [result.first]
2137
+ else
2138
+ return result
2139
+ end
2140
+ end
2141
+
2142
+ # @overload find_files_in_dirs(*path, options)
2143
+ #
2144
+ # Enumerates the files that are present in subdirectories of paths in
2145
+ # {#search_path}. The subdirectories are resolved using File.join(*path)
2146
+ # If one of the elements of the path is the string 'ROBOT', it gets
2147
+ # replaced by the robot name and type.
2148
+ #
2149
+ # @option (see find_dirs)
2150
+ # @option options [#===] :pattern a filter to apply on the matching
2151
+ # results
2152
+ # @option options [Symbol] :all (false) if true, all files from all
2153
+ # matching directories are returned. Otherwise, only the files from
2154
+ # the first matching directory is searched
2155
+ # @return [Array<String>]
2156
+ #
2157
+ # Given a search dir of [app2, app1]
2158
+ #
2159
+ # app1/models/tasks/goto.rb
2160
+ # app1/models/tasks/v3/goto.rb
2161
+ # app2/models/tasks/asguard/goto.rb
2162
+ #
2163
+ # @example
2164
+ # find_files_in_dirs('tasks', 'ROBOT', all: true, order: :specific_first)
2165
+ # # returns [app1/models/tasks/v3/goto.rb,
2166
+ # # app2/models/tasks/asguard/goto.rb,
2167
+ # # app1/models/tasks/goto.rb]
2168
+ #
2169
+ # @example
2170
+ # find_files_in_dirs('tasks', 'ROBOT', all: false, order: :specific_first)
2171
+ # # returns [app1/models/tasks/v3/goto.rb,
2172
+ def find_files_in_dirs(*dir_path)
2173
+ Application.debug { "find_files_in_dirs(#{dir_path.map(&:inspect).join(", ")})" }
2174
+ if dir_path.last.kind_of?(Hash)
2175
+ options = dir_path.pop
2176
+ end
2177
+ options = Kernel.validate_options(options || Hash.new, :all, :order, :path, pattern: Regexp.new(""))
2178
+
2179
+ dir_search = dir_path.dup
2180
+ dir_search << { all: true, order: options[:order], path: options[:path] }
2181
+ search_path = find_dirs(*dir_search)
2182
+
2183
+ result = []
2184
+ search_path.each do |dirname|
2185
+ Application.debug { " dir: #{dirname}" }
2186
+ Dir.new(dirname).each do |file_name|
2187
+ file_path = File.join(dirname, file_name)
2188
+ Application.debug { " file: #{file_path}" }
2189
+ if File.file?(file_path) && options[:pattern] === file_name
2190
+ Application.debug " added"
2191
+ result << file_path
2192
+ end
2193
+ end
2194
+ break if !options[:all]
2195
+ end
2196
+ return result
2197
+ end
2198
+
2199
+ # @overload find_files(*path, options)
2200
+ #
2201
+ # Enumerates files based on their relative paths in {#search_path}.
2202
+ # The paths are resolved using File.join(*path)
2203
+ # If one of the elements of the path is the string 'ROBOT', it gets
2204
+ # replaced by the robot name and type.
2205
+ #
2206
+ # @option options [Boolean] :all (true) if true, all matching
2207
+ # directories are returned. Otherwise, only the first one is (the
2208
+ # meaning of 'first' is controlled by the order option below)
2209
+ # @option options [:specific_first,:specific_last] :order if
2210
+ # :specific_first, the first returned match is the one that is most
2211
+ # specific. The sorting order is to first sort by ROBOT and then by
2212
+ # the place in search_dir. From the most specific to the least
2213
+ # specific, ROBOT is assigned the robot name, the robot type and
2214
+ # finally an empty string.
2215
+ # @return [Array<String>]
2216
+ #
2217
+ # Given a search dir of [app2, app1], a robot name of v3 and a robot
2218
+ # type of asguard,
2219
+ #
2220
+ # app1/config/v3.rb
2221
+ # app2/config/asguard.rb
2222
+ #
2223
+ # @example
2224
+ # find_files('config', 'ROBOT.rb', all: true, order: :specific_first)
2225
+ # # returns [app1/config/v3.rb,
2226
+ # # app2/config/asguard.rb]
2227
+ #
2228
+ # @example
2229
+ # find_dirs('tasks', 'ROBOT', all: false, order: :specific_first)
2230
+ # # returns [app1/config/v3.rb]
2231
+ #
2232
+ def find_files(*file_path)
2233
+ if file_path.last.kind_of?(Hash)
2234
+ options = file_path.pop
2235
+ end
2236
+ options = Kernel.validate_options(options || Hash.new, :all, :order, :path)
2237
+
2238
+ if file_path.empty?
2239
+ raise ArgumentError, "no path given"
2240
+ end
2241
+
2242
+ # Remove the filename from the complete path
2243
+ filename = file_path.pop
2244
+ filename = filename.split('/')
2245
+ file_path.concat(filename[0..-2])
2246
+ filename = filename[-1]
2247
+
2248
+ if filename =~ /ROBOT/ && robot_name
2249
+ args = file_path + [options.merge(pattern: filename.gsub('ROBOT', robot_name))]
2250
+ robot_name_matches = find_files_in_dirs(*args)
2251
+
2252
+ robot_type_matches = []
2253
+ if robot_name != robot_type
2254
+ args = file_path + [options.merge(pattern: filename.gsub('ROBOT', robot_type))]
2255
+ robot_type_matches = find_files_in_dirs(*args)
2256
+ end
2257
+
2258
+ if options[:order] == :specific_first
2259
+ result = robot_name_matches + robot_type_matches
2260
+ else
2261
+ result = robot_type_matches + robot_name_matches
2262
+ end
2263
+ else
2264
+ args = file_path.dup
2265
+ args << options.merge(pattern: filename)
2266
+ result = find_files_in_dirs(*args)
2267
+ end
2268
+
2269
+ orig_path = Pathname.new(File.join(*file_path))
2270
+ orig_path += filename
2271
+ if orig_path.absolute? && File.file?(orig_path.to_s)
2272
+ if options[:order] == :specific_first
2273
+ result.unshift orig_path.to_s
2274
+ else
2275
+ result.push orig_path.to_s
2276
+ end
2277
+ end
2278
+
2279
+ return result
2280
+ end
2281
+
2282
+ # Returns the first match from {#find_files}, or nil if nothing matches
2283
+ def find_file(*args)
2284
+ if !args.last.kind_of?(Hash)
2285
+ args.push(Hash.new)
2286
+ end
2287
+ args.last.delete('all')
2288
+ args.last.merge!(all: true)
2289
+ find_files(*args).first
2290
+ end
2291
+
2292
+ # Returns the first match from {#find_dirs}, or nil if nothing matches
2293
+ def find_dir(*args)
2294
+ if !args.last.kind_of?(Hash)
2295
+ args.push(Hash.new)
2296
+ end
2297
+ args.last.delete('all')
2298
+ args.last.merge!(all: true)
2299
+ find_dirs(*args).first
2300
+ end
2301
+
2302
+ def setup_for_minimal_tooling
2303
+ self.public_logs = false
2304
+ self.auto_load_models = false
2305
+ self.single = true
2306
+ self.modelling_only = true
2307
+ setup
2308
+ end
2309
+
2310
+ # @!method public_shell_interface?
2311
+ # @!method public_shell_interface=(flag)
2312
+ #
2313
+ # If set to true, this Roby application will publish a
2314
+ # {Interface::Interface} object as a TCP server.
2315
+ attr_predicate :public_shell_interface?, true
2316
+
2317
+ # @!method public_logs?
2318
+ # @!method public_logs=(flag)
2319
+ #
2320
+ # If set to true, this Roby application will make its logs public, i.e.
2321
+ # will save the logs in logs/ and update the logs/current symbolic link
2322
+ # accordingly. Otherwise, the logs are saved in a folder in logs/ that
2323
+ # is deleted on teardown, and current is not updated
2324
+ #
2325
+ # Only the run modes have public logs by default
2326
+ attr_predicate :public_logs?, true
2327
+
2328
+ # @!method log_create_current?
2329
+ # @!method log_create_current=(flag)
2330
+ #
2331
+ # If set to true, this Roby application will create a 'current' entry in
2332
+ # {#log_base_dir} that points to the latest log directory. Otherwise, it
2333
+ # will not. It is false when 'roby run' is started with an explicit log
2334
+ # directory (the --log-dir option)
2335
+ #
2336
+ # This symlink will never be created if {#public_logs?} is false,
2337
+ # regardless of this setting.
2338
+ attr_predicate :log_create_current?, true
2339
+
2340
+ attr_predicate :simulation?, true
2341
+ def simulation; self.simulation = true end
2342
+
2343
+ attr_predicate :testing?, true
2344
+ def testing; self.testing = true end
2345
+ attr_predicate :shell?, true
2346
+ def shell; self.shell = true end
2347
+ attr_predicate :single?, true
2348
+ def single; @single = true end
2349
+
2350
+ attr_predicate :modelling_only?, true
2351
+ def modelling_only; self.modelling_only = true end
2352
+
2353
+ # @!method auto_load_all?
2354
+ # @!method auto_load_all=(flag)
2355
+ #
2356
+ # Controls whether Roby's auto-load feature should load all models in
2357
+ # {#search_path} or only the ones in {#app_dir}. It influences the
2358
+ # return value of {#auto_load_search_path}
2359
+ #
2360
+ # @return [Boolean]
2361
+ attr_predicate :auto_load_all?, true
2362
+
2363
+ # @!method auto_load_models?
2364
+ # @!method auto_load_models=(flag)
2365
+ #
2366
+ # Controls whether Roby should load the available the model files
2367
+ # automatically in {#require_models}
2368
+ #
2369
+ # @return [Boolean]
2370
+ attr_writer :auto_load_models
2371
+
2372
+ def auto_load_models?
2373
+ if @auto_load_models.nil?
2374
+ @default_auto_load
2375
+ else
2376
+ @auto_load_models
2377
+ end
2378
+ end
2379
+
2380
+ # @return [Array<String>] the search path for the auto-load feature. It
2381
+ # depends on the value of {#auto_load_all?}
2382
+ def auto_load_search_path
2383
+ if auto_load_all? then search_path
2384
+ elsif app_dir then [app_dir]
2385
+ else []
2386
+ end
2387
+ end
2388
+
2389
+ def find_data(*name)
2390
+ Application.find_data(*name)
2391
+ end
2392
+
2393
+ def self.find_data(*name)
2394
+ name = File.join(*name)
2395
+ Roby::Conf.datadirs.each do |dir|
2396
+ path = File.join(dir, name)
2397
+ return path if File.exists?(path)
2398
+ end
2399
+ raise Errno::ENOENT, "no file #{name} found in #{Roby::Conf.datadirs.join(":")}"
2400
+ end
2401
+
2402
+ def self.register_plugin(name, mod, &init)
2403
+ caller(1)[0] =~ /^([^:]+):\d/
2404
+ dir = File.expand_path(File.dirname($1))
2405
+ Roby.app.available_plugins.delete_if { |n| n == name }
2406
+ Roby.app.available_plugins << [name, dir, mod, init]
2407
+ end
2408
+
2409
+ # Returns the path in search_path that contains the given file or path
2410
+ #
2411
+ # @param [String] path
2412
+ # @return [nil,String]
2413
+ def find_base_path_for(path)
2414
+ if @find_base_path_rx_paths != search_path
2415
+ @find_base_path_rx = search_path.map { |app_dir| [Pathname.new(app_dir), app_dir, %r{(^|/)#{app_dir}(/|$)}] }.
2416
+ sort_by { |_, app_dir, _| app_dir.size }.
2417
+ reverse
2418
+ @find_base_path_rx_paths = search_path.dup
2419
+ end
2420
+
2421
+ longest_prefix_path, _ = @find_base_path_rx.find do |app_path, app_dir, rx|
2422
+ (path =~ rx) ||
2423
+ ((path[0] != ?/) && File.file?(File.join(app_dir, path)))
2424
+ end
2425
+ longest_prefix_path
2426
+ end
2427
+
2428
+ # Returns true if the given path points to a file under {#app_dir}
2429
+ def self_file?(path)
2430
+ find_base_path_for(path) == app_path
2431
+ end
2432
+
2433
+ # Returns true if the given path points to a file in the Roby app
2434
+ #
2435
+ # @param [String] path
2436
+ def app_file?(path)
2437
+ !!find_base_path_for(path)
2438
+ end
2439
+
2440
+ # Tests whether a path is within a framework library
2441
+ #
2442
+ # @param [String] path
2443
+ def framework_file?(path)
2444
+ if path =~ /roby\/.*\.rb$/
2445
+ true
2446
+ else
2447
+ Roby.app.plugins.any? do |name, _|
2448
+ _, dir, _, _ = Roby.app.plugin_definition(name)
2449
+ path =~ %r{(^|/)#{dir}(/|$)}
2450
+ end
2451
+ end
2452
+ end
2453
+
2454
+ # Ensure tha require'd files that match the given pattern can be
2455
+ # re-required
2456
+ def unload_features(*pattern)
2457
+ patterns = search_path.map { |p| Regexp.new(File.join(p, *pattern)) }
2458
+ patterns << Regexp.new("^#{File.join(*pattern)}")
2459
+ $LOADED_FEATURES.delete_if { |path| patterns.any? { |p| p =~ path } }
2460
+ end
2461
+
2462
+ def clear_config
2463
+ Conf.clear
2464
+ call_plugins(:clear_config, self)
2465
+ # Deprecated name for clear_config
2466
+ call_plugins(:reload_config, self)
2467
+ end
2468
+
2469
+ # Reload files in config/
2470
+ def reload_config
2471
+ clear_config
2472
+ unload_features("config", ".*\.rb$")
2473
+ if has_app?
2474
+ require_robot_file
2475
+ end
2476
+ call_plugins(:require_config, self)
2477
+ end
2478
+
2479
+ # Tests whether a model class has been defined in this app's code
2480
+ def model_defined_in_app?(model)
2481
+ model.definition_location.each do |location|
2482
+ return if location.label == 'require'
2483
+ return true if app_file?(location.absolute_path)
2484
+ end
2485
+ false
2486
+ end
2487
+
2488
+ # The list of model classes that allow to discover all models in this
2489
+ # app
2490
+ #
2491
+ # @return [Array<#each_submodel>]
2492
+ def root_models
2493
+ models = [Task, TaskService, TaskEvent, Actions::Interface, Actions::Library,
2494
+ Coordination::ActionScript, Coordination::ActionStateMachine, Coordination::TaskScript]
2495
+
2496
+ each_responding_plugin(:root_models) do |config_extension|
2497
+ models.concat(config_extension.root_models)
2498
+ end
2499
+ models
2500
+ end
2501
+
2502
+ # Enumerate all models registered in this app
2503
+ #
2504
+ # It basically enumerate all submodels of all models in {#root_models}
2505
+ #
2506
+ # @param [nil,#each_submodel] root_model if non-nil, limit the
2507
+ # enumeration to the submodels of this root
2508
+ # @yieldparam [#each_submodel]
2509
+ def each_model(root_model = nil)
2510
+ return enum_for(__method__, root_model) if !block_given?
2511
+
2512
+ if !root_model
2513
+ self.root_models.each { |m| each_model(m, &Proc.new) }
2514
+ return
2515
+ end
2516
+
2517
+ yield(root_model)
2518
+ root_model.each_submodel do |m|
2519
+ yield(m)
2520
+ end
2521
+ end
2522
+
2523
+ # Whether this model should be cleared in {#clear_models}
2524
+ #
2525
+ # The default implementation returns true for the models that are not
2526
+ # registered as constants (more precisely, for which MetaRuby's
2527
+ # #permanent_model? returns false) and for the models defined in this
2528
+ # app.
2529
+ def clear_model?(m)
2530
+ !m.permanent_model? ||
2531
+ (!testing? && model_defined_in_app?(m))
2532
+ end
2533
+
2534
+ # Clear all models for which {#clear_model?} returns true
2535
+ def clear_models
2536
+ root_models.each do |root_model|
2537
+ submodels = root_model.each_submodel.to_a.dup
2538
+ submodels.each do |m|
2539
+ if clear_model?(m)
2540
+ m.permanent_model = false
2541
+ m.clear_model
2542
+ end
2543
+ end
2544
+ end
2545
+ DRoby::V5::DRobyConstant.clear_cache
2546
+ clear_models_handlers.each { |b| b.call }
2547
+ call_plugins(:clear_models, self)
2548
+ end
2549
+
2550
+ # Reload model files in models/
2551
+ def reload_models
2552
+ clear_models
2553
+ unload_features("models", ".*\.rb$")
2554
+ additional_model_files.each do |path|
2555
+ unload_features(path)
2556
+ end
2557
+ require_models
2558
+ end
2559
+
2560
+ # Reload action models defined in models/actions/
2561
+ def reload_actions
2562
+ unload_features("actions", ".*\.rb$")
2563
+ unload_features("models", "actions", ".*\.rb$")
2564
+ planners.each do |planner_model|
2565
+ planner_model.clear_model
2566
+ end
2567
+ require_planners
2568
+ end
2569
+
2570
+ def reload_planners
2571
+ unload_features("planners", ".*\.rb$")
2572
+ unload_features("models", "planners", ".*\.rb$")
2573
+ planners.each do |planner_model|
2574
+ planner_model.clear_model
2575
+ end
2576
+ require_planners
2577
+ end
2578
+
2579
+ class ActionResolutionError < ArgumentError; end
2580
+
2581
+ # Find an action on the planning interface that can generate the given task
2582
+ # model
2583
+ #
2584
+ # @return [Actions::Models::Action]
2585
+ # @raise [ActionResolutionError] if there either none or more than one matching
2586
+ # action
2587
+ def action_from_model(model)
2588
+ candidates = []
2589
+ planners.each do |planner_model|
2590
+ planner_model.find_all_actions_by_type(model).each do |action|
2591
+ candidates << [planner_model, action]
2592
+ end
2593
+ end
2594
+ candidates = candidates.uniq
2595
+
2596
+ if candidates.empty?
2597
+ raise ActionResolutionError, "cannot find an action to produce #{model}"
2598
+ elsif candidates.size > 1
2599
+ raise ActionResolutionError, "more than one actions available produce #{model}: #{candidates.map { |pl, m| "#{pl}.#{m.name}" }.sort.join(", ")}"
2600
+ else
2601
+ candidates.first
2602
+ end
2603
+ end
2604
+
2605
+ # Find an action with the given name on the action interfaces registered on
2606
+ # {#planners}
2607
+ #
2608
+ # @return [(ActionInterface,Actions::Models::Action),nil]
2609
+ # @raise [ActionResolutionError] if more than one action interface provide an
2610
+ # action with this name
2611
+ def find_action_from_name(name)
2612
+ candidates = []
2613
+ planners.each do |planner_model|
2614
+ if m = planner_model.find_action_by_name(name)
2615
+ candidates << [planner_model, m]
2616
+ end
2617
+ end
2618
+ candidates = candidates.uniq
2619
+
2620
+ if candidates.size > 1
2621
+ raise ActionResolutionError, "more than one action interface provide the #{name} action: #{candidates.map { |pl, m| "#{pl}" }.sort.join(", ")}"
2622
+ else candidates.first
2623
+ end
2624
+ end
2625
+
2626
+ # Finds the action matching the given name
2627
+ #
2628
+ # Unlike {#find_action_from_name}, it raises if no matching action has
2629
+ # been found
2630
+ #
2631
+ # @return [Actions::Models::Action]
2632
+ # @raise [ActionResolutionError] if either none or more than one action
2633
+ # interface provide an action with this name
2634
+ def action_from_name(name)
2635
+ action = find_action_from_name(name)
2636
+ if !action
2637
+ available_actions = planners.map do |planner_model|
2638
+ planner_model.each_action.map(&:name)
2639
+ end.flatten
2640
+ if available_actions.empty?
2641
+ raise ActionResolutionError, "cannot find an action named #{name}, there are no actions defined"
2642
+ else
2643
+ raise ActionResolutionError, "cannot find an action named #{name}, available actions are: #{available_actions.sort.join(", ")}"
2644
+ end
2645
+ end
2646
+ action
2647
+ end
2648
+
2649
+ # Generate the plan pattern that will call the required action on the
2650
+ # planning interface, with the given arguments.
2651
+ #
2652
+ # This returns immediately, and the action is not yet deployed at that
2653
+ # point.
2654
+ #
2655
+ # @return task, planning_task
2656
+ def prepare_action(name, mission: false, **arguments)
2657
+ if name.kind_of?(Class)
2658
+ planner_model, m = action_from_model(name)
2659
+ else
2660
+ planner_model, m = action_from_name(name)
2661
+ end
2662
+
2663
+ if mission
2664
+ plan.add_mission_task(task = m.plan_pattern(arguments))
2665
+ else
2666
+ plan.add(task = m.plan_pattern(arguments))
2667
+ end
2668
+ return task, task.planning_task
2669
+ end
2670
+
2671
+ # @return [#call] the blocks that listen to ui events. They are
2672
+ # added with {#on_ui_event} and removed with
2673
+ # {#remove_ui_event}
2674
+ attr_reader :ui_event_listeners
2675
+
2676
+ # Enumerates the listeners currently registered through
2677
+ # #on_ui_event
2678
+ #
2679
+ # @yieldparam [#call] the job listener object
2680
+ def each_ui_event_listener(&block)
2681
+ ui_event_listeners.each(&block)
2682
+ end
2683
+
2684
+ # Sends a message to all UI event listeners
2685
+ def ui_event(name, *args)
2686
+ each_ui_event_listener do |block|
2687
+ block.call(name, *args)
2688
+ end
2689
+ end
2690
+
2691
+ # Registers a block to be called when a message needs to be
2692
+ # dispatched from {#ui_event}
2693
+ #
2694
+ # @yieldparam [String] name the event name
2695
+ # @yieldparam args the UI event listener arguments
2696
+ # @return [Object] the listener ID that can be given to
2697
+ # {#remove_ui_event_listener}
2698
+ def on_ui_event(&block)
2699
+ if !block
2700
+ raise ArgumentError, "missing expected block argument"
2701
+ end
2702
+ ui_event_listeners << block
2703
+ block
2704
+ end
2705
+
2706
+ # Removes a notification listener added with {#on_ui_event}
2707
+ #
2708
+ # @param [Object] listener the listener ID returned by
2709
+ # {#on_ui_event}
2710
+ def remove_ui_event_listener(listener)
2711
+ ui_event_listeners.delete(listener)
2712
+ end
2713
+
2714
+ # @return [#call] the blocks that listen to notifications. They are
2715
+ # added with {#on_notification} and removed with
2716
+ # {#remove_notification_listener}
2717
+ attr_reader :notification_listeners
2718
+
2719
+ # Enumerates the listeners currently registered through
2720
+ # #on_notification
2721
+ #
2722
+ # @yieldparam [#call] the job listener object
2723
+ def each_notification_listener(&block)
2724
+ notification_listeners.each(&block)
2725
+ end
2726
+
2727
+ # Sends a message to all notification listeners
2728
+ def notify(source, level, message)
2729
+ each_notification_listener do |block|
2730
+ block.call(source, level, message)
2731
+ end
2732
+ end
2733
+
2734
+ # Registers a block to be called when a message needs to be
2735
+ # dispatched from {#notify}
2736
+ #
2737
+ # @yieldparam [String] source the source of the message
2738
+ # @yieldparam [String] level the log level
2739
+ # @yieldparam [String] message the message itself
2740
+ # @return [Object] the listener ID that can be given to
2741
+ # {#remove_notification_listener}
2742
+ def on_notification(&block)
2743
+ if !block
2744
+ raise ArgumentError, "missing expected block argument"
2745
+ end
2746
+ notification_listeners << block
2747
+ block
2748
+ end
2749
+
2750
+ # Removes a notification listener added with {#on_notification}
2751
+ #
2752
+ # @param [Object] listener the listener ID returned by
2753
+ # {#on_notification}
2754
+ def remove_notification_listener(listener)
2755
+ notification_listeners.delete(listener)
2756
+ end
2757
+
2758
+ # Discover which tests should be run, and require them
2759
+ #
2760
+ # @param [Boolean] all if set, list all files in {#app_dir}/test.
2761
+ # Otherwise, list only the tests that are related to the loaded
2762
+ # models.
2763
+ # @param [Boolean] only_self if set, list only test files from within
2764
+ # {#app_dir}. Otherwise, consider test files from all over {#search_path}
2765
+ # @return [Array<String>]
2766
+ def discover_test_files(all: true, only_self: false)
2767
+ if all
2768
+ test_files = each_test_file_in_app.inject(Hash.new) do |h, k|
2769
+ h[k] = Array.new
2770
+ h
2771
+ end
2772
+ if !only_self
2773
+ test_files.merge!(Hash[each_test_file_for_loaded_models.to_a])
2774
+ end
2775
+ else
2776
+ test_files = Hash[each_test_file_for_loaded_models.to_a]
2777
+ if only_self
2778
+ test_files = test_files.find_all { |f, _| self_file?(f) }
2779
+ end
2780
+ end
2781
+ test_files
2782
+ end
2783
+
2784
+ # Hook for the plugins to filter out some paths that should not be
2785
+ # auto-loaded by {#each_test_file_in_app}. It does not affect
2786
+ # {#each_test_file_for_loaded_models}.
2787
+ #
2788
+ # @return [Boolean]
2789
+ def autodiscover_tests_in?(path)
2790
+ suffix = File.basename(path)
2791
+ if robots.has_robot?(suffix) && ![robot_name, robot_type].include?(suffix)
2792
+ false
2793
+ elsif defined? super
2794
+ super
2795
+ else
2796
+ true
2797
+ end
2798
+ end
2799
+
2800
+ # Enumerate all the test files in this app and for this robot
2801
+ # configuration
2802
+ def each_test_file_in_app
2803
+ return enum_for(__method__) if !block_given?
2804
+
2805
+ dir = File.join(app_dir, 'test')
2806
+ return if !File.directory?(dir)
2807
+
2808
+ Find.find(dir) do |path|
2809
+ # Skip the robot-specific bits that don't apply on the
2810
+ # selected robot
2811
+ if File.directory?(path)
2812
+ Find.prune if !autodiscover_tests_in?(path)
2813
+ end
2814
+
2815
+ if File.file?(path) && path =~ /test_.*\.rb$/
2816
+ yield(path)
2817
+ end
2818
+ end
2819
+ end
2820
+
2821
+ # Enumerate the test files that should be run to test the current app
2822
+ # configuration
2823
+ #
2824
+ # @yieldparam [String] path the file's path
2825
+ # @yieldparam [Array<Class<Roby::Task>>] models the models that are
2826
+ # meant to be tested by 'path'. It can be empty for tests that involve
2827
+ # lib/
2828
+ def each_test_file_for_loaded_models(&block)
2829
+ models_per_file = Hash.new { |h, k| h[k] = Set.new }
2830
+ each_model do |m|
2831
+ next if m.respond_to?(:has_ancestor?) && m.has_ancestor?(Roby::Event)
2832
+ next if m.respond_to?(:private_specialization?) && m.private_specialization?
2833
+ next if !m.name
2834
+
2835
+ test_files_for(m).each do |test_path|
2836
+ models_per_file[test_path] << m
2837
+ end
2838
+ end
2839
+
2840
+ find_files('test', 'actions', 'ROBOT', 'test_main.rb', order: :specific_first, all: true).each do |path|
2841
+ models_per_file[path] = Set[main_action_interface]
2842
+ end
2843
+
2844
+ find_dirs('test', 'lib', order: :specific_first, all: true).each do |path|
2845
+ Pathname.new(path).find do |p|
2846
+ if p.basename.to_s =~ /^test_.*.rb$/ || p.basename.to_s =~ /_test\.rb$/
2847
+ models_per_file[p.to_s] = Set.new
2848
+ end
2849
+ end
2850
+ end
2851
+
2852
+ models_per_file.each(&block)
2853
+ end
888
2854
  end
889
2855
  end
890
2856