roby 0.8.0 → 3.0.0

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