roby 0.8.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|