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.
- checksums.yaml +7 -0
- data/.deep-cover.rb +3 -0
- data/.gitattributes +1 -0
- data/.gitignore +24 -0
- data/.simplecov +10 -0
- data/.travis.yml +17 -0
- data/.yardopts +4 -0
- data/Gemfile +15 -0
- data/README.md +11 -0
- data/Rakefile +47 -177
- data/benchmark/{alloc_misc.rb → attic/alloc_misc.rb} +2 -2
- data/benchmark/{discovery_latency.rb → attic/discovery_latency.rb} +19 -19
- data/benchmark/{garbage_collection.rb → attic/garbage_collection.rb} +9 -9
- data/benchmark/{genom.rb → attic/genom.rb} +0 -0
- data/benchmark/attic/transactions.rb +62 -0
- data/benchmark/plan_basic_operations.rb +28 -0
- data/benchmark/relations/graph.rb +63 -0
- data/benchmark/ruby/identity.rb +18 -0
- data/benchmark/ruby/set_intersect_vs_hash_merge.rb +39 -0
- data/benchmark/ruby/yield_vs_block.rb +35 -0
- data/benchmark/run +5 -0
- data/benchmark/synthetic_plan_modifications_with_transactions.rb +79 -0
- data/benchmark/transactions.rb +99 -51
- data/bin/roby +38 -197
- data/bin/roby-display +14 -0
- data/bin/roby-log +3 -176
- data/doc/guide/{src → attic}/abstraction/achieve_with.page +1 -1
- data/doc/guide/{src → attic}/abstraction/forwarding.page +1 -1
- data/doc/guide/{src → attic}/abstraction/hierarchy.page +1 -1
- data/doc/guide/{src → attic}/abstraction/index.page +1 -1
- data/doc/guide/{src → attic}/abstraction/task_models.page +1 -1
- data/doc/guide/{overview.rdoc → attic/cycle/api_overview.rdoc} +6 -1
- data/doc/guide/{src → attic}/cycle/cycle-overview.png +0 -0
- data/doc/guide/{src → attic}/cycle/cycle-overview.svg +0 -0
- data/doc/guide/attic/cycle/error_handling.page +98 -0
- data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.png +0 -0
- data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.svg +0 -0
- data/doc/guide/{src/cycle/error_handling.page → attic/cycle/error_sources.page} +46 -89
- data/doc/guide/{src → attic}/cycle/garbage_collection.page +1 -1
- data/doc/guide/{src → attic}/cycle/index.page +1 -1
- data/doc/guide/{src → attic}/cycle/propagation.page +11 -1
- data/doc/guide/{src → attic}/cycle/propagation_diamond.png +0 -0
- data/doc/guide/{src → attic}/cycle/propagation_diamond.svg +0 -0
- data/doc/guide/attic/plans/building_plans.page +89 -0
- data/doc/guide/attic/plans/code.page +192 -0
- data/doc/guide/{src/basics → attic/plans}/events.page +3 -4
- data/doc/guide/attic/plans/index.page +7 -0
- data/doc/guide/{plan_modifications.rdoc → attic/plans/plan_modifications.rdoc} +5 -3
- data/doc/guide/{src/basics → attic/plans}/plan_objects.page +2 -1
- data/doc/guide/attic/plans/querying_plans.page +5 -0
- data/doc/guide/{src/basics → attic/plans}/tasks.page +20 -20
- data/doc/guide/config.yaml +7 -4
- data/doc/guide/ext/extended_menu.rb +29 -0
- data/doc/guide/ext/init.rb +6 -0
- data/doc/guide/ext/rdoc_links.rb +7 -6
- data/doc/guide/src/advanced_concepts/history.page +5 -0
- data/doc/guide/src/advanced_concepts/index.page +11 -0
- data/doc/guide/src/advanced_concepts/recognizing_patterns.page +83 -0
- data/doc/guide/src/advanced_concepts/scheduling.page +87 -0
- data/doc/guide/src/advanced_concepts/transactions.page +5 -0
- data/doc/guide/src/advanced_concepts/unreachability.page +42 -0
- data/doc/guide/src/base.template +96 -0
- data/doc/guide/src/basics_shell_header.txt +5 -7
- data/doc/guide/src/building/action_coordination.page +96 -0
- data/doc/guide/src/building/actions.page +124 -0
- data/doc/guide/src/building/file_layout.page +71 -0
- data/doc/guide/src/building/index.page +50 -0
- data/doc/guide/src/building/patterns.page +86 -0
- data/doc/guide/src/building/patterns_forwarding.png +0 -0
- data/doc/guide/src/building/patterns_forwarding.svg +277 -0
- data/doc/guide/src/building/runtime.page +95 -0
- data/doc/guide/src/building/task_models.page +94 -0
- data/doc/guide/src/building/tasks.page +284 -0
- data/doc/guide/src/concepts/error_handling.page +100 -0
- data/doc/guide/src/concepts/exception_propagation.png +0 -0
- data/doc/guide/src/concepts/exception_propagation.svg +445 -0
- data/doc/guide/src/concepts/execution.page +85 -0
- data/doc/guide/src/concepts/execution.png +0 -0
- data/doc/guide/src/concepts/execution.svg +573 -0
- data/doc/guide/src/concepts/execution_cycle.png +0 -0
- data/doc/guide/src/concepts/garbage_collection.page +57 -0
- data/doc/guide/src/concepts/index.page +27 -0
- data/doc/guide/src/concepts/plans.page +101 -0
- data/doc/guide/src/concepts/policy.page +31 -0
- data/doc/guide/src/concepts/reactor.page +61 -0
- data/doc/guide/src/concepts/simple_plan_example.png +0 -0
- data/doc/guide/src/concepts/simple_plan_example.svg +376 -0
- data/doc/guide/src/default.template +9 -74
- data/doc/guide/src/event_relations/forward.page +71 -0
- data/doc/guide/src/event_relations/index.page +12 -0
- data/doc/guide/src/event_relations/scheduling_constraints.page +43 -0
- data/doc/guide/src/event_relations/signal.page +55 -0
- data/doc/guide/src/event_relations/temporal_constraints.page +77 -0
- data/doc/guide/src/htmldoc.metainfo +21 -8
- data/doc/guide/src/index.page +8 -3
- data/doc/guide/src/{introduction/install.page → installation/index.page} +37 -25
- data/doc/guide/src/installation/publications.page +14 -0
- data/doc/guide/src/{introduction → installation}/videos.page +14 -7
- data/doc/guide/src/interacting/index.page +16 -0
- data/doc/guide/src/interacting/run.page +33 -0
- data/doc/guide/src/interacting/shell.page +95 -0
- data/doc/guide/src/plugins/creating_plugins.page +72 -0
- data/doc/guide/src/plugins/index.page +27 -5
- data/doc/guide/src/plugins/{fault_tolerance.page → standard_plugins/fault_tolerance.page} +2 -2
- data/doc/guide/src/plugins/standard_plugins/index.page +11 -0
- data/doc/guide/src/plugins/{subsystems.page → standard_plugins/subsystems.page} +2 -2
- data/doc/guide/src/style_screen.css +687 -0
- data/doc/guide/src/task_relations/dependency.page +107 -0
- data/doc/guide/src/task_relations/executed_by.page +77 -0
- data/doc/guide/src/task_relations/index.page +12 -0
- data/doc/guide/src/task_relations/new_relations.page +119 -0
- data/doc/guide/src/task_relations/planned_by.page +46 -0
- data/doc/guide/src/tutorial/app.page +117 -0
- data/doc/guide/src/{basics → tutorial}/code_examples.page +6 -5
- data/doc/guide/src/{basics → tutorial}/dry.page +15 -15
- data/doc/guide/src/{basics → tutorial}/errors.page +43 -68
- data/doc/guide/src/tutorial/events.page +195 -0
- data/doc/guide/src/{basics → tutorial}/hierarchy.page +53 -52
- data/doc/guide/src/tutorial/index.page +13 -0
- data/doc/guide/src/tutorial/log_replay/goForward_1.png +0 -0
- data/doc/guide/src/tutorial/log_replay/goForward_2.png +0 -0
- data/doc/guide/src/tutorial/log_replay/goForward_3.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/goForward_4.png +0 -0
- data/doc/guide/src/tutorial/log_replay/goForward_5.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_1.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_2.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_3.png +0 -0
- data/doc/guide/src/tutorial/log_replay/moveto_code_error.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_1.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_2.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_3.png +0 -0
- data/doc/guide/src/tutorial/log_replay/plan_repair_4.png +0 -0
- data/doc/guide/src/tutorial/log_replay/roby_log_main_window.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/roby_log_relation_window.png +0 -0
- data/doc/guide/src/{basics → tutorial}/log_replay/roby_replay_event_representation.png +0 -0
- data/doc/guide/src/tutorial/relations_display.page +153 -0
- data/doc/guide/src/{basics → tutorial}/roby_cycle_overview.png +0 -0
- data/doc/guide/src/tutorial/shell.page +121 -0
- data/doc/guide/src/{basics → tutorial}/summary.page +1 -1
- data/doc/guide/src/tutorial/tasks.page +374 -0
- data/lib/roby.rb +102 -47
- data/lib/roby/actions.rb +17 -0
- data/lib/roby/actions/action.rb +80 -0
- data/lib/roby/actions/interface.rb +45 -0
- data/lib/roby/actions/library.rb +23 -0
- data/lib/roby/actions/models/action.rb +224 -0
- data/lib/roby/actions/models/coordination_action.rb +58 -0
- data/lib/roby/actions/models/interface.rb +22 -0
- data/lib/roby/actions/models/interface_base.rb +294 -0
- data/lib/roby/actions/models/library.rb +12 -0
- data/lib/roby/actions/models/method_action.rb +90 -0
- data/lib/roby/actions/task.rb +114 -0
- data/lib/roby/and_generator.rb +125 -0
- data/lib/roby/app.rb +2795 -829
- data/lib/roby/app/autotest_console_reporter.rb +138 -0
- data/lib/roby/app/base.rb +21 -0
- data/lib/roby/app/cucumber.rb +2 -0
- data/lib/roby/app/cucumber/controller.rb +439 -0
- data/lib/roby/app/cucumber/helpers.rb +280 -0
- data/lib/roby/app/cucumber/world.rb +32 -0
- data/lib/roby/app/debug.rb +136 -0
- data/lib/roby/app/gen.rb +2 -0
- data/lib/roby/app/rake.rb +178 -38
- data/lib/roby/app/robot_config.rb +9 -0
- data/lib/roby/app/robot_names.rb +115 -0
- data/lib/roby/app/run.rb +3 -2
- data/lib/roby/app/scripts.rb +72 -0
- data/lib/roby/app/scripts/autotest.rb +173 -0
- data/lib/roby/app/scripts/display.rb +2 -0
- data/lib/roby/app/scripts/restart.rb +52 -0
- data/lib/roby/app/scripts/results.rb +17 -8
- data/lib/roby/app/scripts/run.rb +155 -24
- data/lib/roby/app/scripts/shell.rb +147 -62
- data/lib/roby/app/scripts/test.rb +107 -22
- data/lib/roby/app/test_reporter.rb +74 -0
- data/lib/roby/app/test_server.rb +159 -0
- data/lib/roby/app/vagrant.rb +47 -0
- data/lib/roby/backports.rb +16 -0
- data/lib/roby/cli/display.rb +190 -0
- data/lib/roby/cli/exceptions.rb +17 -0
- data/lib/roby/cli/gen/actions/class.rb +5 -0
- data/lib/roby/cli/gen/actions/test.rb +6 -0
- data/lib/roby/cli/gen/app/.yardopts +6 -0
- data/lib/roby/cli/gen/app/README.md +28 -0
- data/lib/roby/cli/gen/app/Rakefile +15 -0
- data/{app → lib/roby/cli/gen/app}/config/app.yml +29 -39
- data/lib/roby/cli/gen/app/models/.gitattributes +1 -0
- data/{app → lib/roby/cli/gen/app/scripts}/controllers/.gitattributes +0 -0
- data/{app/data/.gitattributes → lib/roby/cli/gen/app/test/.gitignore} +0 -0
- data/lib/roby/cli/gen/class/class.rb +6 -0
- data/lib/roby/cli/gen/class/test.rb +7 -0
- data/lib/roby/cli/gen/helpers.rb +203 -0
- data/lib/roby/cli/gen/module/module.rb +5 -0
- data/lib/roby/cli/gen/module/test.rb +6 -0
- data/lib/roby/cli/gen/roby_app/config/init.rb +17 -0
- data/lib/roby/cli/gen/roby_app/config/robots/robot.rb +40 -0
- data/lib/roby/cli/gen/task/class.rb +44 -0
- data/lib/roby/cli/gen/task/test.rb +6 -0
- data/lib/roby/cli/gen_main.rb +120 -0
- data/lib/roby/cli/log.rb +276 -0
- data/lib/roby/cli/log/flamegraph.html +499 -0
- data/lib/roby/cli/log/flamegraph_renderer.rb +88 -0
- data/lib/roby/cli/main.rb +153 -0
- data/lib/roby/coordination.rb +60 -0
- data/lib/roby/coordination/action_script.rb +25 -0
- data/lib/roby/coordination/action_state_machine.rb +125 -0
- data/lib/roby/coordination/actions.rb +106 -0
- data/lib/roby/coordination/base.rb +145 -0
- data/lib/roby/coordination/calculus.rb +40 -0
- data/lib/roby/coordination/child.rb +28 -0
- data/lib/roby/coordination/event.rb +29 -0
- data/lib/roby/coordination/fault_handler.rb +25 -0
- data/lib/roby/coordination/fault_handling_task.rb +13 -0
- data/lib/roby/coordination/fault_response_table.rb +110 -0
- data/lib/roby/coordination/models/action_script.rb +64 -0
- data/lib/roby/coordination/models/action_state_machine.rb +224 -0
- data/lib/roby/coordination/models/actions.rb +191 -0
- data/lib/roby/coordination/models/arguments.rb +55 -0
- data/lib/roby/coordination/models/base.rb +176 -0
- data/lib/roby/coordination/models/capture.rb +86 -0
- data/lib/roby/coordination/models/child.rb +35 -0
- data/lib/roby/coordination/models/event.rb +41 -0
- data/lib/roby/coordination/models/exceptions.rb +42 -0
- data/lib/roby/coordination/models/fault_handler.rb +219 -0
- data/lib/roby/coordination/models/fault_response_table.rb +77 -0
- data/lib/roby/coordination/models/root.rb +22 -0
- data/lib/roby/coordination/models/script.rb +283 -0
- data/lib/roby/coordination/models/task.rb +184 -0
- data/lib/roby/coordination/models/task_from_action.rb +50 -0
- data/lib/roby/coordination/models/task_from_as_plan.rb +33 -0
- data/lib/roby/coordination/models/task_from_instanciation_object.rb +31 -0
- data/lib/roby/coordination/models/task_from_variable.rb +27 -0
- data/lib/roby/coordination/models/task_with_dependencies.rb +48 -0
- data/lib/roby/coordination/models/variable.rb +32 -0
- data/lib/roby/coordination/script.rb +200 -0
- data/lib/roby/coordination/script_instruction.rb +12 -0
- data/lib/roby/coordination/task.rb +45 -0
- data/lib/roby/coordination/task_base.rb +69 -0
- data/lib/roby/coordination/task_script.rb +293 -0
- data/lib/roby/coordination/task_state_machine.rb +308 -0
- data/lib/roby/decision_control.rb +33 -21
- data/lib/roby/distributed_object.rb +76 -0
- data/lib/roby/droby.rb +17 -0
- data/lib/roby/droby/droby_id.rb +6 -0
- data/lib/roby/droby/enable.rb +153 -0
- data/lib/roby/droby/event_logger.rb +189 -0
- data/lib/roby/droby/event_logging.rb +57 -0
- data/lib/roby/droby/exceptions.rb +14 -0
- data/lib/roby/droby/identifiable.rb +22 -0
- data/lib/roby/droby/logfile.rb +141 -0
- data/lib/roby/droby/logfile/client.rb +176 -0
- data/lib/roby/droby/logfile/file_format.md +97 -0
- data/lib/roby/droby/logfile/index.rb +117 -0
- data/lib/roby/droby/logfile/reader.rb +139 -0
- data/lib/roby/droby/logfile/server.rb +199 -0
- data/lib/roby/droby/logfile/writer.rb +114 -0
- data/lib/roby/droby/marshal.rb +264 -0
- data/lib/roby/droby/marshallable.rb +12 -0
- data/lib/roby/droby/null_event_logger.rb +25 -0
- data/lib/roby/droby/object_manager.rb +205 -0
- data/lib/roby/droby/peer_id.rb +6 -0
- data/lib/roby/droby/plan_rebuilder.rb +373 -0
- data/lib/roby/droby/rebuilt_plan.rb +160 -0
- data/lib/roby/droby/remote_droby_id.rb +6 -0
- data/lib/roby/droby/timepoints.rb +205 -0
- data/lib/roby/droby/timepoints_ctf.metadata.erb +101 -0
- data/lib/roby/droby/timepoints_ctf.rb +125 -0
- data/lib/roby/droby/v5.rb +14 -0
- data/lib/roby/droby/v5/builtin.rb +120 -0
- data/lib/roby/droby/v5/droby_class.rb +45 -0
- data/lib/roby/droby/v5/droby_constant.rb +81 -0
- data/lib/roby/droby/v5/droby_dump.rb +1026 -0
- data/lib/roby/droby/v5/droby_id.rb +44 -0
- data/lib/roby/droby/v5/droby_model.rb +82 -0
- data/lib/roby/droby/v5/peer_id.rb +10 -0
- data/lib/roby/droby/v5/remote_droby_id.rb +42 -0
- data/lib/roby/event.rb +79 -957
- data/lib/roby/event_constraints.rb +835 -0
- data/lib/roby/event_generator.rb +1047 -0
- data/lib/roby/event_structure/causal_link.rb +6 -0
- data/lib/roby/event_structure/forwarding.rb +6 -0
- data/lib/roby/event_structure/precedence.rb +7 -0
- data/lib/roby/event_structure/signal.rb +8 -0
- data/lib/roby/event_structure/temporal_constraints.rb +640 -0
- data/lib/roby/exceptions.rb +446 -152
- data/lib/roby/executable_plan.rb +549 -0
- data/lib/roby/execution_engine.rb +1997 -950
- data/lib/roby/filter_generator.rb +26 -0
- data/lib/roby/gui/chronicle_view.rb +225 -0
- data/lib/roby/gui/chronicle_widget.rb +925 -0
- data/lib/roby/gui/dot_id.rb +11 -0
- data/lib/roby/gui/exception_view.rb +44 -0
- data/lib/roby/gui/log_display.rb +273 -0
- data/lib/roby/gui/model_views.rb +2 -0
- data/lib/roby/gui/model_views/action_interface.rb +53 -0
- data/lib/roby/gui/model_views/task.rb +47 -0
- data/lib/roby/gui/model_views/task.rhtml +41 -0
- data/lib/roby/gui/object_info_view.rb +89 -0
- data/lib/roby/gui/plan_dot_layout.rb +427 -0
- data/lib/roby/gui/plan_rebuilder_widget.rb +357 -0
- data/lib/roby/gui/qt4_toMSecsSinceEpoch.rb +8 -0
- data/lib/roby/gui/relations_view.rb +278 -0
- data/lib/roby/gui/relations_view/relations.ui +139 -0
- data/lib/roby/gui/relations_view/relations_canvas.rb +1088 -0
- data/lib/roby/gui/relations_view/relations_config.rb +292 -0
- data/lib/roby/gui/relations_view/relations_view.ui +53 -0
- data/lib/roby/gui/scheduler_view.css +24 -0
- data/lib/roby/gui/scheduler_view.rb +46 -0
- data/lib/roby/gui/scheduler_view.rhtml +53 -0
- data/lib/roby/gui/stepping.rb +93 -0
- data/lib/roby/gui/stepping.ui +181 -0
- data/lib/roby/gui/styles.rb +81 -0
- data/lib/roby/gui/task_display_configuration.rb +42 -0
- data/lib/roby/gui/task_state_at.rb +38 -0
- data/lib/roby/hooks.rb +26 -0
- data/lib/roby/interface.rb +136 -469
- data/lib/roby/interface/async.rb +20 -0
- data/lib/roby/interface/async/action_monitor.rb +188 -0
- data/lib/roby/interface/async/interface.rb +498 -0
- data/lib/roby/interface/async/job_monitor.rb +213 -0
- data/lib/roby/interface/async/log.rb +238 -0
- data/lib/roby/interface/async/new_job_listener.rb +79 -0
- data/lib/roby/interface/async/ui_connector.rb +183 -0
- data/lib/roby/interface/client.rb +553 -0
- data/lib/roby/interface/command.rb +24 -0
- data/lib/roby/interface/command_argument.rb +16 -0
- data/lib/roby/interface/command_library.rb +92 -0
- data/lib/roby/interface/droby_channel.rb +174 -0
- data/lib/roby/interface/exceptions.rb +22 -0
- data/lib/roby/interface/interface.rb +655 -0
- data/lib/roby/interface/job.rb +47 -0
- data/lib/roby/interface/rest.rb +10 -0
- data/lib/roby/interface/rest/api.rb +29 -0
- data/lib/roby/interface/rest/helpers.rb +24 -0
- data/lib/roby/interface/rest/server.rb +212 -0
- data/lib/roby/interface/server.rb +154 -0
- data/lib/roby/interface/shell_client.rb +468 -0
- data/lib/roby/interface/shell_subcommand.rb +24 -0
- data/lib/roby/interface/subcommand_client.rb +35 -0
- data/lib/roby/interface/tcp.rb +168 -0
- data/lib/roby/models/arguments.rb +112 -0
- data/lib/roby/models/plan_object.rb +83 -0
- data/lib/roby/models/task.rb +835 -0
- data/lib/roby/models/task_event.rb +62 -0
- data/lib/roby/models/task_service.rb +78 -0
- data/lib/roby/or_generator.rb +88 -0
- data/lib/roby/plan.rb +1751 -864
- data/lib/roby/plan_object.rb +611 -0
- data/lib/roby/plan_service.rb +200 -0
- data/lib/roby/promise.rb +332 -0
- data/lib/roby/queries.rb +23 -0
- data/lib/roby/queries/and_matcher.rb +32 -0
- data/lib/roby/queries/any.rb +27 -0
- data/lib/roby/queries/code_error_matcher.rb +58 -0
- data/lib/roby/queries/event_generator_matcher.rb +9 -0
- data/lib/roby/queries/execution_exception_matcher.rb +165 -0
- data/lib/roby/queries/index.rb +165 -0
- data/lib/roby/queries/localized_error_matcher.rb +149 -0
- data/lib/roby/queries/matcher_base.rb +107 -0
- data/lib/roby/queries/none.rb +27 -0
- data/lib/roby/queries/not_matcher.rb +30 -0
- data/lib/roby/queries/op_matcher.rb +8 -0
- data/lib/roby/queries/or_matcher.rb +30 -0
- data/lib/roby/queries/plan_object_matcher.rb +363 -0
- data/lib/roby/queries/query.rb +188 -0
- data/lib/roby/queries/task_event_generator_matcher.rb +86 -0
- data/lib/roby/queries/task_matcher.rb +344 -0
- data/lib/roby/relations.rb +42 -678
- data/lib/roby/relations/bidirectional_directed_adjacency_graph.rb +492 -0
- data/lib/roby/relations/directed_relation_support.rb +268 -0
- data/lib/roby/relations/event_relation_graph.rb +19 -0
- data/lib/roby/relations/fork_merge_visitor.rb +154 -0
- data/lib/roby/relations/graph.rb +533 -0
- data/lib/roby/relations/models/directed_relation_support.rb +11 -0
- data/lib/roby/relations/models/graph.rb +75 -0
- data/lib/roby/relations/models/task_relation_graph.rb +18 -0
- data/lib/roby/relations/space.rb +380 -0
- data/lib/roby/relations/task_relation_graph.rb +20 -0
- data/lib/roby/robot.rb +85 -38
- data/lib/roby/schedulers/basic.rb +155 -25
- data/lib/roby/schedulers/null.rb +20 -0
- data/lib/roby/schedulers/reporting.rb +31 -0
- data/lib/roby/schedulers/state.rb +129 -0
- data/lib/roby/schedulers/temporal.rb +91 -0
- data/lib/roby/singletons.rb +87 -0
- data/lib/roby/standalone.rb +4 -2
- data/lib/roby/standard_errors.rb +405 -82
- data/lib/roby/state.rb +6 -3
- data/lib/roby/state/conf_model.rb +5 -0
- data/lib/roby/state/events.rb +181 -95
- data/lib/roby/state/goal_model.rb +77 -0
- data/lib/roby/state/open_struct.rb +591 -0
- data/lib/roby/state/open_struct_model.rb +68 -0
- data/lib/roby/state/pos.rb +45 -45
- data/lib/roby/state/shapes.rb +11 -11
- data/lib/roby/state/state_model.rb +303 -0
- data/lib/roby/state/task.rb +43 -0
- data/lib/roby/support.rb +88 -148
- data/lib/roby/task.rb +1361 -1750
- data/lib/roby/task_arguments.rb +428 -0
- data/lib/roby/task_event.rb +127 -0
- data/lib/roby/task_event_generator.rb +337 -0
- data/lib/roby/task_service.rb +6 -0
- data/lib/roby/task_structure/conflicts.rb +104 -0
- data/lib/roby/task_structure/dependency.rb +932 -0
- data/lib/roby/task_structure/error_handling.rb +118 -0
- data/lib/roby/task_structure/executed_by.rb +234 -0
- data/lib/roby/task_structure/planned_by.rb +90 -0
- data/lib/roby/tasks/aggregator.rb +37 -0
- data/lib/roby/tasks/external_process.rb +275 -0
- data/lib/roby/tasks/group.rb +27 -0
- data/lib/roby/tasks/null.rb +19 -0
- data/lib/roby/tasks/parallel.rb +43 -0
- data/lib/roby/tasks/sequence.rb +88 -0
- data/lib/roby/tasks/simple.rb +21 -0
- data/lib/roby/{thread_task.rb → tasks/thread.rb} +50 -24
- data/lib/roby/tasks/timeout.rb +17 -0
- data/lib/roby/tasks/virtual.rb +55 -0
- data/lib/roby/template_plan.rb +7 -0
- data/lib/roby/test/aruba_minitest.rb +74 -0
- data/lib/roby/test/assertion.rb +16 -0
- data/lib/roby/test/assertions.rb +490 -0
- data/lib/roby/test/common.rb +368 -591
- data/lib/roby/test/dsl.rb +149 -0
- data/lib/roby/test/error.rb +18 -0
- data/lib/roby/test/event_reporter.rb +83 -0
- data/lib/roby/test/execution_expectations.rb +1134 -0
- data/lib/roby/test/expect_execution.rb +151 -0
- data/lib/roby/test/minitest_helpers.rb +166 -0
- data/lib/roby/test/roby_app_helpers.rb +200 -0
- data/lib/roby/test/run_planners.rb +155 -0
- data/lib/roby/test/self.rb +112 -0
- data/lib/roby/test/spec.rb +198 -0
- data/lib/roby/test/tasks/empty_task.rb +4 -4
- data/lib/roby/test/tasks/goto.rb +28 -27
- data/lib/roby/test/teardown_plans.rb +100 -0
- data/lib/roby/test/testcase.rb +239 -307
- data/lib/roby/test/tools.rb +159 -155
- data/lib/roby/test/validate_state_machine.rb +75 -0
- data/lib/roby/transaction.rb +1125 -0
- data/lib/roby/transaction/event_generator_proxy.rb +63 -0
- data/lib/roby/transaction/plan_object_proxy.rb +99 -0
- data/lib/roby/transaction/plan_service_proxy.rb +43 -0
- data/lib/roby/transaction/proxying.rb +120 -0
- data/lib/roby/transaction/task_event_generator_proxy.rb +19 -0
- data/lib/roby/transaction/task_proxy.rb +135 -0
- data/lib/roby/until_generator.rb +30 -0
- data/lib/roby/version.rb +5 -0
- data/lib/roby/yard.rb +169 -0
- data/lib/yard-roby.rb +1 -0
- data/manifest.xml +32 -6
- data/roby.gemspec +59 -0
- metadata +788 -587
- data/Manifest.txt +0 -321
- data/NOTES +0 -4
- data/README.txt +0 -166
- data/TODO.txt +0 -146
- data/app/README.txt +0 -24
- data/app/Rakefile +0 -8
- data/app/config/ROBOT.rb +0 -5
- data/app/config/init.rb +0 -33
- data/app/config/roby.yml +0 -3
- data/app/controllers/ROBOT.rb +0 -2
- data/app/planners/ROBOT/main.rb +0 -6
- data/app/planners/main.rb +0 -5
- data/app/scripts/distributed +0 -3
- data/app/scripts/generate/bookmarks +0 -3
- data/app/scripts/replay +0 -3
- data/app/scripts/results +0 -3
- data/app/scripts/run +0 -3
- data/app/scripts/server +0 -3
- data/app/scripts/shell +0 -3
- data/app/scripts/test +0 -3
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby-shell +0 -25
- data/doc/guide/src/basics/app.page +0 -139
- data/doc/guide/src/basics/index.page +0 -11
- data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
- data/doc/guide/src/basics/relations_display.page +0 -203
- data/doc/guide/src/basics/shell.page +0 -102
- data/doc/guide/src/default.css +0 -319
- data/doc/guide/src/introduction/index.page +0 -29
- data/doc/guide/src/introduction/publications.page +0 -14
- data/doc/guide/src/relations/dependency.page +0 -89
- data/doc/guide/src/relations/index.page +0 -12
- data/ext/droby/dump.cc +0 -175
- data/ext/droby/extconf.rb +0 -3
- data/ext/graph/algorithm.cc +0 -746
- data/ext/graph/extconf.rb +0 -7
- data/ext/graph/graph.cc +0 -575
- data/ext/graph/graph.hh +0 -183
- data/ext/graph/iterator_sequence.hh +0 -102
- data/ext/graph/undirected_dfs.hh +0 -226
- data/ext/graph/undirected_graph.hh +0 -421
- data/lib/roby/app/scripts/generate/bookmarks.rb +0 -162
- data/lib/roby/app/scripts/replay.rb +0 -31
- data/lib/roby/app/scripts/server.rb +0 -18
- data/lib/roby/basic_object.rb +0 -151
- data/lib/roby/config.rb +0 -14
- data/lib/roby/distributed.rb +0 -36
- data/lib/roby/distributed/base.rb +0 -448
- data/lib/roby/distributed/communication.rb +0 -875
- data/lib/roby/distributed/connection_space.rb +0 -616
- data/lib/roby/distributed/distributed_object.rb +0 -206
- data/lib/roby/distributed/drb.rb +0 -62
- data/lib/roby/distributed/notifications.rb +0 -531
- data/lib/roby/distributed/peer.rb +0 -555
- data/lib/roby/distributed/protocol.rb +0 -529
- data/lib/roby/distributed/proxy.rb +0 -343
- data/lib/roby/distributed/subscription.rb +0 -311
- data/lib/roby/distributed/transaction.rb +0 -498
- data/lib/roby/external_process_task.rb +0 -225
- data/lib/roby/graph.rb +0 -160
- data/lib/roby/log.rb +0 -3
- data/lib/roby/log/chronicle.rb +0 -303
- data/lib/roby/log/console.rb +0 -74
- data/lib/roby/log/data_stream.rb +0 -275
- data/lib/roby/log/dot.rb +0 -279
- data/lib/roby/log/event_stream.rb +0 -161
- data/lib/roby/log/file.rb +0 -396
- data/lib/roby/log/gui/basic_display.ui +0 -83
- data/lib/roby/log/gui/basic_display_ui.rb +0 -89
- data/lib/roby/log/gui/chronicle.rb +0 -26
- data/lib/roby/log/gui/chronicle_view.rb +0 -40
- data/lib/roby/log/gui/chronicle_view.ui +0 -70
- data/lib/roby/log/gui/chronicle_view_ui.rb +0 -90
- data/lib/roby/log/gui/data_displays.rb +0 -171
- data/lib/roby/log/gui/data_displays.ui +0 -155
- data/lib/roby/log/gui/data_displays_ui.rb +0 -146
- data/lib/roby/log/gui/notifications.rb +0 -26
- data/lib/roby/log/gui/relations.rb +0 -269
- data/lib/roby/log/gui/relations.ui +0 -123
- data/lib/roby/log/gui/relations_ui.rb +0 -120
- data/lib/roby/log/gui/relations_view.rb +0 -185
- data/lib/roby/log/gui/relations_view.ui +0 -149
- data/lib/roby/log/gui/relations_view_ui.rb +0 -144
- data/lib/roby/log/gui/replay.rb +0 -366
- data/lib/roby/log/gui/replay_controls.rb +0 -206
- data/lib/roby/log/gui/replay_controls.ui +0 -282
- data/lib/roby/log/gui/replay_controls_ui.rb +0 -249
- data/lib/roby/log/gui/runtime.rb +0 -130
- data/lib/roby/log/hooks.rb +0 -186
- data/lib/roby/log/logger.rb +0 -203
- data/lib/roby/log/notifications.rb +0 -244
- data/lib/roby/log/plan_rebuilder.rb +0 -468
- data/lib/roby/log/relations.rb +0 -1084
- data/lib/roby/log/server.rb +0 -547
- data/lib/roby/log/sqlite.rb +0 -47
- data/lib/roby/log/timings.rb +0 -233
- data/lib/roby/plan-object.rb +0 -371
- data/lib/roby/planning.rb +0 -13
- data/lib/roby/planning/loops.rb +0 -309
- data/lib/roby/planning/model.rb +0 -1012
- data/lib/roby/planning/task.rb +0 -180
- data/lib/roby/query.rb +0 -655
- data/lib/roby/relations/conflicts.rb +0 -67
- data/lib/roby/relations/dependency.rb +0 -358
- data/lib/roby/relations/ensured.rb +0 -19
- data/lib/roby/relations/error_handling.rb +0 -22
- data/lib/roby/relations/events.rb +0 -7
- data/lib/roby/relations/executed_by.rb +0 -208
- data/lib/roby/relations/influence.rb +0 -10
- data/lib/roby/relations/planned_by.rb +0 -63
- data/lib/roby/state/information.rb +0 -55
- data/lib/roby/state/state.rb +0 -367
- data/lib/roby/task-operations.rb +0 -186
- data/lib/roby/task_index.rb +0 -80
- data/lib/roby/test/distributed.rb +0 -230
- data/lib/roby/test/tasks/simple_task.rb +0 -23
- data/lib/roby/transactions.rb +0 -507
- data/lib/roby/transactions/proxy.rb +0 -325
- data/plugins/fault_injection/History.txt +0 -4
- data/plugins/fault_injection/README.txt +0 -34
- data/plugins/fault_injection/Rakefile +0 -12
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +0 -52
- data/plugins/fault_injection/fault_injection.rb +0 -89
- data/plugins/fault_injection/test/test_fault_injection.rb +0 -78
- data/plugins/subsystems/README.txt +0 -37
- data/plugins/subsystems/Rakefile +0 -13
- data/plugins/subsystems/app.rb +0 -182
- data/plugins/subsystems/test/app/README +0 -24
- data/plugins/subsystems/test/app/Rakefile +0 -8
- data/plugins/subsystems/test/app/config/app.yml +0 -71
- data/plugins/subsystems/test/app/config/init.rb +0 -12
- data/plugins/subsystems/test/app/config/roby.yml +0 -3
- data/plugins/subsystems/test/app/planners/main.rb +0 -20
- data/plugins/subsystems/test/app/scripts/distributed +0 -3
- data/plugins/subsystems/test/app/scripts/replay +0 -3
- data/plugins/subsystems/test/app/scripts/results +0 -3
- data/plugins/subsystems/test/app/scripts/run +0 -3
- data/plugins/subsystems/test/app/scripts/server +0 -3
- data/plugins/subsystems/test/app/scripts/shell +0 -3
- data/plugins/subsystems/test/app/scripts/test +0 -3
- data/plugins/subsystems/test/app/tasks/services.rb +0 -15
- data/plugins/subsystems/test/test_subsystems.rb +0 -78
- data/test/distributed/test_communication.rb +0 -195
- data/test/distributed/test_connection.rb +0 -284
- data/test/distributed/test_execution.rb +0 -378
- data/test/distributed/test_mixed_plan.rb +0 -341
- data/test/distributed/test_plan_notifications.rb +0 -238
- data/test/distributed/test_protocol.rb +0 -525
- data/test/distributed/test_query.rb +0 -106
- data/test/distributed/test_remote_plan.rb +0 -491
- data/test/distributed/test_transaction.rb +0 -466
- data/test/mockups/external_process +0 -28
- data/test/mockups/tasks.rb +0 -27
- data/test/planning/test_loops.rb +0 -432
- data/test/planning/test_model.rb +0 -427
- data/test/planning/test_task.rb +0 -126
- data/test/relations/test_conflicts.rb +0 -42
- data/test/relations/test_dependency.rb +0 -324
- data/test/relations/test_ensured.rb +0 -38
- data/test/relations/test_executed_by.rb +0 -224
- data/test/relations/test_planned_by.rb +0 -56
- data/test/suite_core.rb +0 -29
- data/test/suite_distributed.rb +0 -10
- data/test/suite_planning.rb +0 -4
- data/test/suite_relations.rb +0 -8
- data/test/tasks/test_external_process.rb +0 -126
- data/test/tasks/test_thread_task.rb +0 -70
- data/test/test_bgl.rb +0 -528
- data/test/test_event.rb +0 -969
- data/test/test_exceptions.rb +0 -591
- data/test/test_execution_engine.rb +0 -987
- data/test/test_gui.rb +0 -20
- data/test/test_interface.rb +0 -43
- data/test/test_log.rb +0 -125
- data/test/test_log_server.rb +0 -133
- data/test/test_plan.rb +0 -418
- data/test/test_query.rb +0 -424
- data/test/test_relations.rb +0 -260
- data/test/test_state.rb +0 -432
- data/test/test_support.rb +0 -16
- data/test/test_task.rb +0 -1181
- data/test/test_testcase.rb +0 -138
- data/test/test_transactions.rb +0 -610
- 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
|
+
|
data/lib/roby/app.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
#
|
|
259
|
-
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
|
|
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
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
#
|
|
295
|
-
#
|
|
296
|
-
# is
|
|
297
|
-
|
|
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
|
-
#
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
179
|
+
plain
|
|
368
180
|
end
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
#
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
529
|
-
end
|
|
530
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
#
|
|
869
|
-
|
|
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
|
-
#
|
|
872
|
-
|
|
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
|
-
#
|
|
875
|
-
#
|
|
876
|
-
#
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|