diff --git a/.gitignore b/.gitignore index 058cd6d3b..40156f55a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ coverage/ tmp/ syskit-0.1.gem + +lib/syskit/telemetry/agent/*_pb.rb +.test-results/ diff --git a/.rubocop-version b/.rubocop-version new file mode 100644 index 000000000..65ee09598 --- /dev/null +++ b/.rubocop-version @@ -0,0 +1 @@ +1.67.0 diff --git a/.rubocop.yml b/.rubocop.yml index 50471bf76..5f665c26d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -10,6 +10,9 @@ inherit_mode: AllCops: TargetRubyVersion: "2.5" + NewCops: enable + Exclude: + - lib/syskit/telemetry/agent/*_pb.rb Style/MultilineMemoization: EnforcedStyle: braces @@ -39,12 +42,13 @@ Naming/MethodParameterName: AllowedNames: - as - fd + - kw Security/MarshalLoad: Exclude: - - lib/syskit/roby_app/remote_processes/server.rb - - lib/syskit/roby_app/remote_processes/client.rb + - 'lib/syskit/process_managers/remote/manager.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' Style/MultilineBlockChain: Exclude: - - test/**/test_*.rb \ No newline at end of file + - test/**/test_*.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 470141000..2a5e34f0e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,22 +1,18 @@ # This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 99999` -# on 2020-04-09 10:05:22 -0300 using RuboCop version 0.80.1. +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit` +# on 2024-10-16 14:52:55 UTC using RuboCop version 1.67.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Need to disable this until the corresponding cops have been fixed -Lint/RedundantCopDisableDirective: - Exclude: - - 'lib/syskit/gui/batch_manager.rb' - -# Offense count: 2077 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# Offense count: 1892 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: Exclude: + - 'bin/syskit' - 'lib/syskit/actions/action_model.rb' - 'lib/syskit/actions/profile.rb' - 'lib/syskit/composition.rb' @@ -33,6 +29,7 @@ Layout/LineLength: - 'lib/syskit/coordination/plan_extension.rb' - 'lib/syskit/coordination/port_handling.rb' - 'lib/syskit/data_flow.rb' + - 'lib/syskit/dependency_injection.rb' - 'lib/syskit/dependency_injection_context.rb' - 'lib/syskit/deployment.rb' - 'lib/syskit/droby/v5/droby_dump.rb' @@ -55,11 +52,15 @@ Layout/LineLength: - 'lib/syskit/gui/page_extension.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/gui/state_label.rb' + - 'lib/syskit/gui/testing.rb' - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/instance_selection.rb' - 'lib/syskit/models/base.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/models/composition.rb' - 'lib/syskit/models/composition_child.rb' - 'lib/syskit/models/composition_specialization.rb' + - 'lib/syskit/models/data_service.rb' - 'lib/syskit/models/deployment.rb' - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/dynamic_data_service.rb' @@ -75,16 +76,17 @@ Layout/LineLength: - 'lib/syskit/network_generation/dataflow_dynamics.rb' - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/network_generation/merge_solver.rb' + - 'lib/syskit/network_generation/system_network_deployer.rb' - 'lib/syskit/network_generation/system_network_generator.rb' - 'lib/syskit/orogen_namespace.rb' - 'lib/syskit/properties.rb' - - 'lib/syskit/remote_state_getter.rb' + - 'lib/syskit/queries/port_matcher.rb' + - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/robot/robot_definition.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/logging_configuration.rb' - 'lib/syskit/roby_app/logging_group.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/ros/node.rb' @@ -95,17 +97,26 @@ Layout/LineLength: - 'lib/syskit/scripts/browse.rb' - 'lib/syskit/scripts/common.rb' - 'lib/syskit/scripts/connection_log.rb' - - 'lib/syskit/scripts/doc.rb' - 'lib/syskit/scripts/ide.rb' - 'lib/syskit/scripts/instanciate.rb' - 'lib/syskit/scripts/instanciate_gui.rb' - - 'lib/syskit/shell_interface.rb' - 'lib/syskit/task_configuration_manager.rb' + - 'lib/syskit/telemetry/ui/app_start_dialog.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/expanded_job_status.rb' + - 'lib/syskit/telemetry/ui/global_state_label.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/logging_configuration.rb' + - 'lib/syskit/telemetry/ui/logging_configuration_item.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/telemetry/ui/state_label.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/base.rb' + - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/test/profile_assertions.rb' - 'lib/syskit/test/profile_test.rb' - 'lib/syskit/test/ruby_task_context_test.rb' - 'lib/yard-syskit.rb' - - 'syskit.gemspec' - 'test/actions/test_action_model.rb' - 'test/actions/test_interface_model_extension.rb' - 'test/cli/test_gen_main.rb' @@ -134,6 +145,7 @@ Layout/LineLength: - 'test/models/test_specialization_manager.rb' - 'test/models/test_task_context.rb' - 'test/network_generation/test_async.rb' + - 'test/network_generation/test_dataflow_computation.rb' - 'test/network_generation/test_dataflow_dynamics.rb' - 'test/network_generation/test_engine.rb' - 'test/network_generation/test_logger.rb' @@ -152,8 +164,7 @@ Layout/LineLength: - 'test/ros/test_task_context.rb' - 'test/runtime/test_apply_requirement_modifications.rb' - 'test/runtime/test_connection_management.rb' - - 'test/runtime/test_update_deployment_state.rb' - - 'test/runtime/test_update_task_states.rb' + - 'test/test/test_execution_expectations.rb' - 'test/test/test_network_manipulation.rb' - 'test/test/test_spec.rb' - 'test/test_bound_data_service.rb' @@ -172,12 +183,11 @@ Layout/LineLength: - 'test/test_properties.rb' - 'test/test_property.rb' - 'test/test_remote_state_getter.rb' - - 'test/test_shell_interface.rb' - 'test/test_task_configuration_manager.rb' - 'test/test_task_context.rb' -# Offense count: 20 -# Cop supports --auto-correct. +# Offense count: 13 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented Layout/MultilineOperationIndentation: @@ -187,21 +197,22 @@ Layout/MultilineOperationIndentation: - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/models/deployment.rb' - 'lib/syskit/models/port.rb' - - 'lib/syskit/models/task_context.rb' - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/runtime/exceptions.rb' - 'lib/syskit/task_configuration_manager.rb' -# Offense count: 12 +# Offense count: 11 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - - 'test/models/test_component.rb' - 'test/network_generation/test_engine.rb' - 'test/network_generation/test_system_network_deployer.rb' - 'test/test_task_context.rb' -# Offense count: 23 +# Offense count: 19 +# This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousRegexpLiteral: Exclude: - 'test/cli/test_gen_main.rb' @@ -209,12 +220,11 @@ Lint/AmbiguousRegexpLiteral: - 'test/models/test_component.rb' - 'test/robot/test_device.rb' - 'test/roby_app/test_logging_group.rb' - - 'test/test_component.rb' - 'test/test_data_service.rb' - 'test/test_remote_state_getter.rb' - - 'test/test_shell_interface.rb' -# Offense count: 114 +# Offense count: 85 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Exclude: @@ -239,7 +249,6 @@ Lint/AssignmentInCondition: - 'lib/syskit/models/composition_child.rb' - 'lib/syskit/models/composition_specialization.rb' - 'lib/syskit/models/faceted_access.rb' - - 'lib/syskit/models/orogen_base.rb' - 'lib/syskit/models/placeholder.rb' - 'lib/syskit/models/port_access.rb' - 'lib/syskit/models/specialization_manager.rb' @@ -250,35 +259,142 @@ Lint/AssignmentInCondition: - 'lib/syskit/network_generation/system_network_generator.rb' - 'lib/syskit/port_access.rb' - 'lib/syskit/properties.rb' - - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/logging_configuration.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/runtime/connection_management.rb' - 'lib/syskit/scripts/common.rb' - 'lib/syskit/task_configuration_manager.rb' - - 'test/test/test_execution_expectations.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/state_label.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'test/test_remote_state_getter.rb' -# Offense count: 23 +# Offense count: 1 +Lint/BinaryOperatorWithIdenticalOperands: + Exclude: + - 'lib/syskit/models/specialization_manager.rb' + +# Offense count: 6 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'lib/syskit/roby_app/rest_api.rb' + - 'test/coordination/test_data_monitor.rb' + - 'test/process_managers/test_remote.rb' + - 'test/telemetry/agent/test_client_server.rb' + - 'test/test/test_spec.rb' + +# Offense count: 18 +# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. +Lint/DuplicateBranch: + Exclude: + - 'lib/syskit/dependency_injection.rb' + - 'lib/syskit/gui/batch_manager.rb' + - 'lib/syskit/gui/job_status_display.rb' + - 'lib/syskit/gui/runtime_state.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/models/deployment_group.rb' + - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/network_generation/logger.rb' + - 'lib/syskit/roby_app/logging_group.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + +# Offense count: 29 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'lib/syskit/logger_service.rb' + - 'test/actions/test_action_model.rb' + - 'test/actions/test_profile.rb' + - 'test/coordination/test_data_monitoring_table.rb' + - 'test/models/test_component.rb' + - 'test/models/test_dynamic_data_service.rb' + - 'test/models/test_specialization_manager.rb' + - 'test/robot/test_master_device_instance.rb' + - 'test/roby_app/test_logging_configuration.rb' + - 'test/telemetry/agent/test_client_server.rb' + - 'test/test/test_network_manipulation.rb' + - 'test/test/test_profile_assertions.rb' + - 'test/test/test_stubs.rb' + - 'test/test_task_context.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AutoCorrect, AllowComments. +Lint/EmptyConditionalBody: + Exclude: + - 'lib/syskit/deployment.rb' + +# Offense count: 2 +# Configuration parameters: AllowComments. +Lint/EmptyFile: + Exclude: + - 'lib/syskit/ros/roby_app/plugin.rb' + - 'lib/syskit/spec.rb' + +# Offense count: 2 +Lint/HashCompareByIdentity: + Exclude: + - 'lib/syskit/gui/testing.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/InterpolationCheck: + Exclude: + - 'lib/syskit/dependency_injection.rb' + +# Offense count: 30 +# Configuration parameters: AllowedParentClasses. +Lint/MissingSuper: + Exclude: + - 'lib/syskit/exceptions.rb' + - 'lib/syskit/models/composition_child.rb' + - 'lib/syskit/robot/master_device_instance.rb' + - 'lib/syskit/robot/slave_device_instance.rb' + - 'lib/syskit/telemetry/agent/server.rb' + - 'lib/syskit/telemetry/ui/name_service.rb' + - 'lib/syskit/test/stub_network.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/NonAtomicFileOperation: + Exclude: + - 'lib/syskit/process_managers/remote/server/server.rb' + - 'lib/syskit/roby_app/plugin.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods. +# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal? +Lint/RedundantSafeNavigation: + Exclude: + - 'lib/syskit/data_service.rb' + +# Offense count: 15 Lint/RescueException: Exclude: - 'lib/syskit/droby/v5/droby_dump.rb' - - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/instanciate.rb' - 'lib/syskit/network_generation/async.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/logging_group.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/runtime/connection_management.rb' - 'lib/syskit/runtime/update_deployment_states.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' - 'lib/syskit/test/base.rb' -# Offense count: 11 +# Offense count: 1 +Lint/SelfAssignment: + Exclude: + - 'lib/syskit/task_context.rb' + +# Offense count: 10 Lint/ShadowingOuterLocalVariable: Exclude: - 'lib/syskit/connection_graphs.rb' @@ -288,27 +404,36 @@ Lint/ShadowingOuterLocalVariable: - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/models/deployment.rb' - 'lib/syskit/models/specialization_manager.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' - 'test/models/test_specialization_manager.rb' -# Offense count: 16 -# Configuration parameters: AllowComments. +# Offense count: 12 +# Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: - - 'lib/syskit/deployment.rb' - 'lib/syskit/droby/v5/droby_dump.rb' - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/page_extension.rb' - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/scripts/common.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' - 'test/coordination/test_task_script.rb' - 'test/models/test_composition.rb' - 'test/models/test_data_services.rb' - 'test/models/test_deployment.rb' -# Offense count: 60 -# Cop supports --auto-correct. -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +# Offense count: 3 +# Configuration parameters: AllowedPatterns. +# AllowedPatterns: (?-mix:(exactly|at_least|at_most)\(\d+\)\.times) +Lint/UnreachableLoop: + Exclude: + - 'lib/syskit/data_flow.rb' + - 'lib/syskit/port.rb' + +# Offense count: 51 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Exclude: - 'lib/syskit/coordination/plan_extension.rb' @@ -330,20 +455,21 @@ Lint/UnusedBlockArgument: - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/ros/node.rb' - 'lib/syskit/runtime/connection_management.rb' - - 'lib/syskit/shell_interface.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/logging_configuration.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'test/coordination/test_data_monitoring_table.rb' - 'test/coordination/test_models_task_extension.rb' - 'test/models/test_specialization_manager.rb' - 'test/test_task_context.rb' -# Offense count: 53 -# Cop supports --auto-correct. -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. +# Offense count: 32 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. Lint/UnusedMethodArgument: Exclude: - 'lib/syskit/actions/profile.rb' - 'lib/syskit/data_flow.rb' - - 'lib/syskit/deployment.rb' - 'lib/syskit/exceptions.rb' - 'lib/syskit/graphviz.rb' - 'lib/syskit/gui/expanded_job_status.rb' @@ -351,39 +477,36 @@ Lint/UnusedMethodArgument: - 'lib/syskit/models/configured_deployment.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/network_generation/dataflow_dynamics.rb' - - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/port_access.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/ros/node.rb' + - 'lib/syskit/telemetry/ui/expanded_job_status.rb' - 'lib/syskit/test/base.rb' - 'lib/syskit/test/execution_expectations.rb' - 'lib/syskit/test/network_manipulation.rb' - - 'test/test_deployment.rb' -# Offense count: 184 +# Offense count: 176 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AutoCorrect. Lint/UselessAssignment: Exclude: - 'bin/syskit' - 'lib/syskit/connection_graphs.rb' - 'lib/syskit/coordination/models/fault_response_table_extension.rb' - - 'lib/syskit/deployment.rb' - 'lib/syskit/gui/ide.rb' - 'lib/syskit/gui/state_label.rb' - - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/runtime/update_deployment_states.rb' - 'lib/syskit/scripts/browse.rb' - 'lib/syskit/scripts/common.rb' - - 'lib/syskit/scripts/doc.rb' - 'lib/syskit/scripts/ide.rb' - 'lib/syskit/scripts/instanciate.rb' - 'lib/syskit/scripts/instanciate_gui.rb' - - 'test/actions/test_interface_model_extension.rb' + - 'lib/syskit/telemetry/ui/state_label.rb' - 'test/coordination/test_data_monitoring_table.rb' - 'test/coordination/test_task_script.rb' - - 'test/droby/test_droby_dump.rb' - 'test/models/test_bound_data_service.rb' - 'test/models/test_component.rb' - 'test/models/test_composition.rb' @@ -406,8 +529,16 @@ Lint/UselessAssignment: - 'test/test_dependency_injection_context.rb' - 'test/test_properties.rb' +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AutoCorrect. +Lint/UselessMethodDefinition: + Exclude: + - 'lib/syskit/models/task_context.rb' + - 'test/network_generation/test_logger.rb' + # Offense count: 380 -# Configuration parameters: Max. +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: - 'lib/syskit/actions/profile.rb' @@ -452,12 +583,14 @@ Metrics/AbcSize: - 'lib/syskit/gui/widget_list.rb' - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/instance_selection.rb' + - 'lib/syskit/interface/commands.rb' - 'lib/syskit/models/base.rb' - 'lib/syskit/models/bound_data_service.rb' - 'lib/syskit/models/component.rb' - 'lib/syskit/models/composition.rb' - 'lib/syskit/models/composition_child.rb' - 'lib/syskit/models/data_service.rb' + - 'lib/syskit/models/deployment.rb' - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/dynamic_data_service.rb' - 'lib/syskit/models/faceted_access.rb' @@ -475,43 +608,46 @@ Metrics/AbcSize: - 'lib/syskit/network_generation/system_network_generator.rb' - 'lib/syskit/orogen_namespace.rb' - 'lib/syskit/port.rb' + - 'lib/syskit/process_managers/remote/manager.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' - 'lib/syskit/properties.rb' - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/robot/robot_definition.rb' - 'lib/syskit/roby_app/configuration.rb' - - 'lib/syskit/roby_app/logging_group.rb' - 'lib/syskit/roby_app/log_transfer_server/spawn_server.rb' - - 'lib/syskit/roby_app/remote_processes/client.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' + - 'lib/syskit/roby_app/logging_group.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - - 'lib/syskit/roby_app/unmanaged_process.rb' - 'lib/syskit/runtime/apply_requirement_modifications.rb' - 'lib/syskit/runtime/connection_management.rb' - 'lib/syskit/runtime/exceptions.rb' - 'lib/syskit/runtime/update_deployment_states.rb' - - 'lib/syskit/runtime/update_task_states.rb' - 'lib/syskit/scripts/common.rb' - - 'lib/syskit/scripts/doc.rb' - - 'lib/syskit/shell_interface.rb' - 'lib/syskit/task_context.rb' + - 'lib/syskit/telemetry/ui/app_start_dialog.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/job_state_label.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/logging_configuration.rb' + - 'lib/syskit/telemetry/ui/logging_groups_item.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/network_manipulation.rb' - 'lib/syskit/test/profile_assertions.rb' - 'lib/syskit/test/self.rb' - 'lib/syskit/test/spec.rb' - - 'lib/syskit/test/stubs.rb' - 'lib/syskit/test/stub_network.rb' + - 'lib/syskit/test/stubs.rb' - 'lib/syskit/test/task_context_test.rb' - 'lib/yard-syskit.rb' - 'test/fixtures/simple_composition_model.rb' - 'test/gui/test_logging_configuration.rb' + - 'test/live/test_blocking_hooks.rb' - 'test/models/test_bound_data_service.rb' - 'test/models/test_data_services.rb' - 'test/models/test_deployment.rb' - - 'test/models/test_ruby_task_context.rb' - 'test/models/test_task_context.rb' - 'test/network_generation/test_engine.rb' - 'test/runtime/test_connection_management.rb' @@ -521,15 +657,13 @@ Metrics/AbcSize: - 'test/test_remote_state_getter.rb' - 'test/test_task_context.rb' -# Offense count: 412 -# Configuration parameters: CountComments, Max, ExcludedMethods. -# ExcludedMethods: refine +# Offense count: 25 +# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. +# AllowedMethods: refine Metrics/BlockLength: Exclude: - - '**/*.gemspec' - 'lib/syskit/data_flow.rb' - 'lib/syskit/dependency_injection.rb' - - 'lib/syskit/deployment.rb' - 'lib/syskit/exceptions.rb' - 'lib/syskit/gui/instanciate.rb' - 'lib/syskit/instance_selection.rb' @@ -540,22 +674,20 @@ Metrics/BlockLength: - 'lib/syskit/network_generation/dataflow_dynamics.rb' - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/network_generation/logger.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/runtime/connection_management.rb' - 'lib/syskit/scripts/instanciate.rb' - 'lib/syskit/task_context.rb' - 'lib/syskit/test/network_manipulation.rb' -# Offense count: 4 -# Configuration parameters: CountBlocks, Max. +# Offense count: 1 +# Configuration parameters: CountBlocks, CountModifierForms, Max. Metrics/BlockNesting: Exclude: - - 'lib/syskit/dependency_injection.rb' - 'lib/yard-syskit.rb' -# Offense count: 6 -# Configuration parameters: CountComments, Max. +# Offense count: 7 +# Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: Exclude: - 'lib/syskit/dependency_injection.rb' @@ -564,17 +696,20 @@ Metrics/ClassLength: - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/runtime/connection_management.rb' - 'lib/syskit/task_context.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' -# Offense count: 105 -# Configuration parameters: Max. +# Offense count: 122 +# Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: Exclude: + - 'lib/syskit/cli/main.rb' - 'lib/syskit/composition.rb' - 'lib/syskit/connection_graphs.rb' - 'lib/syskit/data_service.rb' - 'lib/syskit/dependency_injection.rb' - 'lib/syskit/deployment.rb' - 'lib/syskit/droby/v5/droby_dump.rb' + - 'lib/syskit/exceptions.rb' - 'lib/syskit/graphviz.rb' - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/component_network_base_view.rb' @@ -582,59 +717,67 @@ Metrics/CyclomaticComplexity: - 'lib/syskit/gui/ide.rb' - 'lib/syskit/gui/instanciate.rb' - 'lib/syskit/gui/job_status_display.rb' + - 'lib/syskit/gui/model_views/composition.rb' - 'lib/syskit/gui/model_views/data_service.rb' - 'lib/syskit/gui/model_views/profile.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/gui/testing.rb' - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/instance_selection.rb' + - 'lib/syskit/models/base.rb' - 'lib/syskit/models/bound_data_service.rb' - 'lib/syskit/models/component.rb' - 'lib/syskit/models/composition.rb' + - 'lib/syskit/models/configured_deployment.rb' - 'lib/syskit/models/data_service.rb' - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/placeholder.rb' - - 'lib/syskit/models/port.rb' - 'lib/syskit/models/specialization_manager.rb' - 'lib/syskit/models/task_context.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' + - 'lib/syskit/network_generation/dataflow_dynamics.rb' - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/network_generation/merge_solver.rb' - - 'lib/syskit/port.rb' - - 'lib/syskit/remote_state_getter.rb' + - 'lib/syskit/network_generation/system_network_deployer.rb' + - 'lib/syskit/network_generation/system_network_generator.rb' + - 'lib/syskit/process_managers/remote/manager.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/robot/robot_definition.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/logging_group.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - - 'lib/syskit/roby_app/remote_processes/client.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' + - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/runtime/apply_requirement_modifications.rb' - 'lib/syskit/runtime/connection_management.rb' - - 'lib/syskit/runtime/update_task_states.rb' + - 'lib/syskit/runtime/update_deployment_states.rb' - 'lib/syskit/scripts/common.rb' - - 'lib/syskit/scripts/doc.rb' - 'lib/syskit/task_context.rb' + - 'lib/syskit/telemetry/agent/server_peer.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/test/base.rb' - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/test/profile_assertions.rb' + - 'lib/syskit/test/stub_network.rb' - 'lib/yard-syskit.rb' # Offense count: 7 -# Configuration parameters: CountComments, Max, ExcludedMethods. +# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: - 'lib/syskit/graphviz.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/models/composition.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/network_generation/logger.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' - - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' -# Offense count: 9 -# Configuration parameters: CountComments, Max. +# Offense count: 4 +# Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: - 'lib/syskit/models/component.rb' @@ -642,8 +785,8 @@ Metrics/ModuleLength: - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/test/network_manipulation.rb' -# Offense count: 24 -# Configuration parameters: Max, CountKeywordArgs. +# Offense count: 31 +# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Exclude: - 'lib/syskit/deployment.rb' @@ -654,21 +797,20 @@ Metrics/ParameterLists: - 'lib/syskit/gui/instanciate.rb' - 'lib/syskit/gui/page_extension.rb' - 'lib/syskit/gui/testing.rb' - - 'lib/syskit/models/data_service.rb' + - 'lib/syskit/instance_selection.rb' - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/dynamic_data_service.rb' - 'lib/syskit/network_generation/dataflow_dynamics.rb' - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/process_managers/remote/manager.rb' - 'lib/syskit/robot/communication_bus.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/log_transfer_server/spawn_server.rb' - - 'lib/syskit/roby_app/remote_processes/client.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' - 'lib/syskit/runtime/connection_management.rb' -# Offense count: 99 -# Configuration parameters: Max. +# Offense count: 119 +# Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: Exclude: - 'lib/syskit/composition.rb' @@ -676,21 +818,25 @@ Metrics/PerceivedComplexity: - 'lib/syskit/data_service.rb' - 'lib/syskit/dependency_injection.rb' - 'lib/syskit/deployment.rb' - - 'lib/syskit/droby/v5/droby_dump.rb' + - 'lib/syskit/exceptions.rb' - 'lib/syskit/graphviz.rb' - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/component_network_base_view.rb' - 'lib/syskit/gui/component_network_view.rb' - 'lib/syskit/gui/ide.rb' + - 'lib/syskit/gui/instanciate.rb' + - 'lib/syskit/gui/model_views/composition.rb' - 'lib/syskit/gui/model_views/data_service.rb' - 'lib/syskit/gui/model_views/profile.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/gui/testing.rb' - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/instance_selection.rb' + - 'lib/syskit/models/base.rb' - 'lib/syskit/models/bound_data_service.rb' - 'lib/syskit/models/component.rb' - 'lib/syskit/models/composition.rb' + - 'lib/syskit/models/configured_deployment.rb' - 'lib/syskit/models/data_service.rb' - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/placeholder.rb' @@ -698,31 +844,36 @@ Metrics/PerceivedComplexity: - 'lib/syskit/models/specialization_manager.rb' - 'lib/syskit/models/task_context.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' + - 'lib/syskit/network_generation/dataflow_dynamics.rb' - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/network_generation/merge_solver.rb' + - 'lib/syskit/network_generation/system_network_deployer.rb' + - 'lib/syskit/network_generation/system_network_generator.rb' - 'lib/syskit/port.rb' + - 'lib/syskit/process_managers/remote/manager.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' - 'lib/syskit/properties.rb' - - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/robot/robot_definition.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/logging_group.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - - 'lib/syskit/roby_app/remote_processes/client.rb' - - 'lib/syskit/roby_app/remote_processes/server.rb' - 'lib/syskit/roby_app/rest_deployment_manager.rb' - 'lib/syskit/runtime/apply_requirement_modifications.rb' - 'lib/syskit/runtime/connection_management.rb' - - 'lib/syskit/runtime/update_task_states.rb' - - 'lib/syskit/scripts/common.rb' - - 'lib/syskit/scripts/doc.rb' + - 'lib/syskit/runtime/update_deployment_states.rb' - 'lib/syskit/task_context.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/test/profile_assertions.rb' + - 'lib/syskit/test/stub_network.rb' - 'lib/yard-syskit.rb' # Offense count: 7 +# Configuration parameters: AllowedNames. +# AllowedNames: module_parent Naming/ClassAndModuleCamelCase: Exclude: - 'lib/syskit/roby_app/rest_api.rb' @@ -733,16 +884,16 @@ Naming/ClassAndModuleCamelCase: - 'test/test_component.rb' - 'test/test_dependency_injection.rb' -# Offense count: 2 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/syskit/roby_app/rest_api.rb' - - 'lib/syskit/shell_interface.rb' -# Offense count: 21 -# Configuration parameters: EnforcedStyle, IgnoredPatterns. +# Offense count: 34 +# Configuration parameters: EnforcedStyle, AllowedPatterns. # SupportedStyles: snake_case, camelCase Naming/MethodName: Exclude: @@ -755,12 +906,17 @@ Naming/MethodName: - 'lib/syskit/gui/ruby_item.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/gui/widget_list.rb' - - 'lib/syskit/network_generation/logger.rb' + - 'lib/syskit/telemetry/ui/expanded_job_status.rb' + - 'lib/syskit/telemetry/ui/global_state_label.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/ruby_item.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/profile_assertions.rb' -# Offense count: 135 +# Offense count: 89 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'lib/syskit/actions/action.rb' @@ -784,7 +940,6 @@ Naming/MethodParameterName: - 'lib/syskit/models/bound_data_service.rb' - 'lib/syskit/models/composition.rb' - 'lib/syskit/models/data_service.rb' - - 'lib/syskit/models/dynamic_data_service.rb' - 'lib/syskit/models/faceted_access.rb' - 'lib/syskit/models/placeholder.rb' - 'lib/syskit/models/port.rb' @@ -797,19 +952,33 @@ Naming/MethodParameterName: - 'lib/syskit/queries/abstract_component_base.rb' - 'lib/syskit/robot/master_device_instance.rb' - 'lib/syskit/robot/robot_definition.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/roby_app/single_file_dsl.rb' - 'lib/syskit/runtime/connection_management.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/action_interface_test.rb' - - 'lib/syskit/test/network_manipulation.rb' - 'lib/syskit/test/profile_test.rb' - 'test/models/test_ruby_task_context.rb' - 'test/models/test_task_context.rb' +# Offense count: 1 +Security/CompoundHash: + Exclude: + - 'lib/syskit/port.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: MinBranchesCount. +Style/CaseLikeIf: + Exclude: + - 'lib/syskit/models/task_context.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' + # Offense count: 7 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: Exclude: @@ -821,19 +990,46 @@ Style/ClassAndModuleChildren: - 'lib/syskit/gui/model_views/task_context_base.rb' - 'lib/syskit/gui/model_views/type.rb' -# Offense count: 14 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, AllowedPatterns. +# AllowedMethods: ==, equal?, eql? +Style/ClassEqualityComparison: + Exclude: + - 'lib/syskit/test.rb' + +# Offense count: 12 Style/ClassVars: Exclude: - 'lib/syskit/deployment.rb' - 'lib/syskit/network_generation/engine.rb' - 'lib/syskit/network_generation/merge_solver.rb' - - 'lib/syskit/test/network_manipulation.rb' -# Offense count: 135 +# Offense count: 5 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CombinableLoops: + Exclude: + - 'lib/syskit/deployment.rb' + - 'lib/syskit/exceptions.rb' + - 'lib/syskit/models/deployment_group.rb' + - 'lib/syskit/network_generation/dataflow_dynamics.rb' + - 'lib/syskit/runtime/connection_management.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/ConcatArrayLiterals: + Exclude: + - 'lib/syskit/graphviz.rb' + +# Offense count: 1 +Style/DocumentDynamicEvalDefinition: + Exclude: + - 'lib/syskit/instance_requirements.rb' + +# Offense count: 129 +# Configuration parameters: AllowedConstants. Style/Documentation: Exclude: - - 'spec/**/*' - - 'test/**/*' - 'lib/syskit/abstract_component.rb' - 'lib/syskit/actions/profile.rb' - 'lib/syskit/base.rb' @@ -859,7 +1055,6 @@ Style/Documentation: - 'lib/syskit/graphviz.rb' - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/composer.rb' - - 'lib/syskit/gui/html_page.rb' - 'lib/syskit/gui/ide.rb' - 'lib/syskit/gui/instanciate.rb' - 'lib/syskit/gui/job_status_display.rb' @@ -871,7 +1066,6 @@ Style/Documentation: - 'lib/syskit/gui/model_views/task_context.rb' - 'lib/syskit/gui/model_views/task_context_base.rb' - 'lib/syskit/gui/model_views/type.rb' - - 'lib/syskit/gui/page.rb' - 'lib/syskit/gui/page_extension.rb' - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/gui/testing.rb' @@ -893,58 +1087,47 @@ Style/Documentation: - 'lib/syskit/queries/abstract_component_base.rb' - 'lib/syskit/roby_app.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/toplevel.rb' - 'lib/syskit/ros/node.rb' - 'lib/syskit/ros/roby_app/configuration.rb' - - 'lib/syskit/ruby_task_context.rb' - 'lib/syskit/runtime/apply_requirement_modifications.rb' - 'lib/syskit/runtime/exceptions.rb' - 'lib/syskit/runtime/update_deployment_states.rb' - 'lib/syskit/scripts/common.rb' - 'lib/syskit/scripts/connection_log.rb' - - 'lib/syskit/scripts/doc.rb' - - 'lib/syskit/shell_interface.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test.rb' - 'lib/syskit/test/base.rb' - 'lib/syskit/test/component_test.rb' - - 'lib/syskit/test/execution_expectations.rb' - 'lib/syskit/test/flexmock_extension.rb' - 'lib/syskit/test/network_manipulation.rb' - 'lib/syskit/test/ruby_task_context_test.rb' - 'lib/syskit/test/spec.rb' - 'lib/yard-syskit.rb' -# Offense count: 26 +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: allowed_in_returns, forbidden Style/DoubleNegation: Exclude: - - 'lib/syskit/actions/profile.rb' - - 'lib/syskit/coordination/models/port_handling.rb' - - 'lib/syskit/coordination/port_handling.rb' - - 'lib/syskit/data_flow.rb' - - 'lib/syskit/exceptions.rb' - - 'lib/syskit/instance_requirements.rb' - - 'lib/syskit/instance_requirements_task.rb' - - 'lib/syskit/models/orogen_base.rb' - - 'lib/syskit/models/specialization_manager.rb' - - 'lib/syskit/port_access.rb' - - 'lib/syskit/robot/master_device_instance.rb' - - 'lib/syskit/roby_app/configuration.rb' - - 'lib/syskit/task_configuration_manager.rb' - - 'lib/syskit/test/action_interface_test.rb' - 'test/gui/test_logging_configuration.rb' - 'test/roby_app/app/config/robots/reload_orogen.rb' - 'test/roby_app/app/config/robots/reload_ruby_task.rb' - 'test/roby_app/app/config/robots/reload_unmanaged_task.rb' # Offense count: 8 -# Configuration parameters: EnforcedStyle. +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: Exclude: - 'lib/syskit/gui/ide.rb' - 'lib/syskit/gui/runtime_state.rb' - - 'lib/syskit/scripts/common.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' # Offense count: 4 # Configuration parameters: AllowedVariables. @@ -954,21 +1137,16 @@ Style/GlobalVars: - 'lib/syskit/scripts/common.rb' - 'lib/syskit/scripts/ide.rb' -# Offense count: 180 -# Configuration parameters: MinBodyLength. +# Offense count: 105 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: Exclude: - 'lib/syskit/actions/profile.rb' - - 'lib/syskit/connection_graphs.rb' - 'lib/syskit/coordination/data_monitor.rb' - - 'lib/syskit/coordination/models/data_monitor.rb' - - 'lib/syskit/coordination/models/data_monitoring_table.rb' - - 'lib/syskit/coordination/models/fault_response_table_extension.rb' - - 'lib/syskit/coordination/models/port_handling.rb' - 'lib/syskit/coordination/port_handling.rb' - 'lib/syskit/data_flow.rb' - 'lib/syskit/dependency_injection_context.rb' - - 'lib/syskit/droby/v5/droby_dump.rb' - 'lib/syskit/exceptions.rb' - 'lib/syskit/gui/app_start_dialog.rb' - 'lib/syskit/gui/global_state_label.rb' @@ -980,18 +1158,13 @@ Style/GuardClause: - 'lib/syskit/gui/model_views/composition.rb' - 'lib/syskit/gui/model_views/task_context_base.rb' - 'lib/syskit/gui/runtime_state.rb' - - 'lib/syskit/gui/state_label.rb' - 'lib/syskit/gui/widget_list.rb' - 'lib/syskit/instance_requirements.rb' - - 'lib/syskit/instance_selection.rb' - - 'lib/syskit/models/base.rb' - 'lib/syskit/models/bound_dynamic_data_service.rb' - 'lib/syskit/models/composition_child.rb' - 'lib/syskit/models/composition_specialization.rb' - 'lib/syskit/models/deployment.rb' - - 'lib/syskit/models/deployment_group.rb' - 'lib/syskit/models/faceted_access.rb' - - 'lib/syskit/models/orogen_base.rb' - 'lib/syskit/models/placeholder.rb' - 'lib/syskit/models/port.rb' - 'lib/syskit/models/port_access.rb' @@ -999,12 +1172,8 @@ Style/GuardClause: - 'lib/syskit/models/task_context.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/network_generation/dataflow_dynamics.rb' - - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/network_generation/merge_solver.rb' - - 'lib/syskit/network_generation/system_network_generator.rb' - 'lib/syskit/port_access.rb' - - 'lib/syskit/properties.rb' - - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/logging_configuration.rb' - 'lib/syskit/roby_app/plugin.rb' @@ -1014,18 +1183,93 @@ Style/GuardClause: - 'lib/syskit/scripts/common.rb' - 'lib/syskit/scripts/connection_log.rb' - 'lib/syskit/task_configuration_manager.rb' - - 'lib/syskit/test/action_interface_test.rb' - - 'lib/syskit/test/action_test.rb' - - 'lib/syskit/test/component_test.rb' - - 'lib/syskit/test/profile_test.rb' + - 'lib/syskit/telemetry/ui/app_start_dialog.rb' + - 'lib/syskit/telemetry/ui/global_state_label.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/logging_configuration_item_base.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/task_context_test.rb' - 'lib/yard-syskit.rb' - 'test/runtime/test_connection_management.rb' -# Offense count: 77 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# Offense count: 268 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowSplatArgument. +Style/HashConversion: + Exclude: + - 'lib/syskit/graphviz.rb' + - 'lib/syskit/gui/component_network_view.rb' + - 'lib/syskit/gui/job_status_display.rb' + - 'lib/syskit/gui/model_views/component.rb' + - 'lib/syskit/gui/model_views/composition.rb' + - 'lib/syskit/gui/state_label.rb' + - 'lib/syskit/gui/testing.rb' + - 'lib/syskit/interface/v2/protocol.rb' + - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/network_generation/logger.rb' + - 'lib/syskit/roby_app/plugin.rb' + - 'lib/syskit/roby_app/rest_api.rb' + - 'lib/syskit/ros/roby_app/configuration.rb' + - 'lib/syskit/runtime/connection_management.rb' + - 'lib/syskit/scripts/process_server.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/state_label.rb' + - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/test/stubs.rb' + - 'test/actions/test_action_model.rb' + - 'test/actions/test_interface_model_extension.rb' + - 'test/coordination/test_models_task_extension.rb' + - 'test/models/test_component.rb' + - 'test/models/test_composition.rb' + - 'test/models/test_composition_child.rb' + - 'test/models/test_composition_specialization.rb' + - 'test/models/test_data_services.rb' + - 'test/models/test_deployment_group.rb' + - 'test/models/test_specialization_manager.rb' + - 'test/models/test_task_context.rb' + - 'test/network_generation/test_engine.rb' + - 'test/network_generation/test_logger.rb' + - 'test/network_generation/test_system_network_deployer.rb' + - 'test/network_generation/test_system_network_generator.rb' + - 'test/process_managers/test_remote.rb' + - 'test/roby_app/test_plugin.rb' + - 'test/roby_app/test_rest_api.rb' + - 'test/runtime/test_connection_management.rb' + - 'test/test_component.rb' + - 'test/test_connection_graph.rb' + - 'test/test_data_flow.rb' + - 'test/test_dependency_injection.rb' + - 'test/test_deployment.rb' + - 'test/test_instance_requirements.rb' + - 'test/test_instance_selection.rb' + - 'test/test_port.rb' + - 'test/test_task_context.rb' + +# Offense count: 22 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedReceivers. +# AllowedReceivers: Thread.current +Style/HashEachMethods: + Exclude: + - 'lib/syskit/actions/profile.rb' + - 'lib/syskit/cli/doc/gen.rb' + - 'lib/syskit/component.rb' + - 'lib/syskit/coordination/plan_extension.rb' + - 'lib/syskit/data_flow.rb' + - 'lib/syskit/gui/logging_groups_item.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/models/composition.rb' + - 'lib/syskit/models/deployment_group.rb' + - 'lib/syskit/models/specialization_manager.rb' + - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/robot/robot_definition.rb' + - 'lib/syskit/telemetry/ui/logging_groups_item.rb' + +# Offense count: 42 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys +# SupportedShorthandSyntax: always, never, either, consistent, either_consistent Style/HashSyntax: Exclude: - 'lib/syskit/coordination/data_monitoring_table.rb' @@ -1038,36 +1282,37 @@ Style/HashSyntax: - 'lib/syskit/roby_app/plugin.rb' - 'lib/syskit/ros/roby_app/configuration.rb' - 'lib/syskit/scripts/autodetect_interfaces.rb' - - 'lib/syskit/scripts/common.rb' - - 'lib/syskit/scripts/doc.rb' - - 'lib/syskit/shell_interface.rb' + - 'lib/syskit/telemetry/ui/logging_configuration.rb' - 'test/models/test_composition_specialization.rb' - 'test/models/test_deployment_group.rb' - 'test/models/test_specialization_manager.rb' - - 'test/network_generation/test_merge_solver.rb' - 'test/runtime/test_connection_management.rb' - 'test/test_connection_graphs.rb' - 'test/test_deployment.rb' - 'test/test_instance_selection.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashTransformValues: + Exclude: + - 'lib/syskit/component.rb' + # Offense count: 6 +# This cop supports unsafe autocorrection (--autocorrect-all). Style/IdenticalConditionalBranches: Exclude: - 'lib/syskit/gui/model_views/task_context.rb' - 'lib/syskit/instance_requirements.rb' - 'lib/syskit/roby_app/rest_api.rb' -# Offense count: 182 -# Cop supports --auto-correct. +# Offense count: 149 +# This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - - 'bin/syskit' - - 'lib/syskit/actions/action_model.rb' - 'lib/syskit/actions/profile.rb' - 'lib/syskit/connection_graph.rb' - 'lib/syskit/connection_graphs.rb' - 'lib/syskit/coordination/data_monitor.rb' - - 'lib/syskit/coordination/models/data_monitor.rb' - 'lib/syskit/coordination/plan_extension.rb' - 'lib/syskit/coordination/task_script_extension.rb' - 'lib/syskit/data_flow.rb' @@ -1096,12 +1341,10 @@ Style/IfUnlessModifier: - 'lib/syskit/models/specialization_manager.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/network_generation/dataflow_dynamics.rb' - - 'lib/syskit/network_generation/logger.rb' - 'lib/syskit/network_generation/merge_solver.rb' - 'lib/syskit/remote_state_getter.rb' - 'lib/syskit/roby_app/configuration.rb' - 'lib/syskit/roby_app/plugin.rb' - - 'lib/syskit/roby_app/process_server.rb' - 'lib/syskit/roby_app/single_file_dsl.rb' - 'lib/syskit/ros/roby_app/configuration.rb' - 'lib/syskit/runtime/connection_management.rb' @@ -1109,8 +1352,11 @@ Style/IfUnlessModifier: - 'lib/syskit/scripts/browse.rb' - 'lib/syskit/scripts/common.rb' - 'lib/syskit/scripts/instanciate.rb' - - 'lib/syskit/shell_interface.rb' - 'lib/syskit/task_configuration_manager.rb' + - 'lib/syskit/telemetry/ui/app_start_dialog.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/job_status_display.rb' + - 'lib/syskit/telemetry/ui/widget_list.rb' - 'lib/syskit/test/base.rb' - 'lib/syskit/test/flexmock_extension.rb' - 'lib/yard-syskit.rb' @@ -1118,35 +1364,64 @@ Style/IfUnlessModifier: - 'test/network_generation/test_engine.rb' - 'test/runtime/test_connection_management.rb' - 'test/test_deployment.rb' - - 'test/test_remote_state_getter.rb' - - 'test/test_shell_interface.rb' - 'test/test_task_context.rb' -# Offense count: 3 -Style/MethodMissingSuper: +# Offense count: 11 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToSet: Exclude: - - 'lib/syskit/gui/batch_manager.rb' - - 'lib/syskit/models/specialization_manager.rb' - - 'lib/syskit/properties.rb' + - 'lib/syskit/actions/profile.rb' + - 'lib/syskit/cli/doc/gen.rb' + - 'lib/syskit/graphviz.rb' + - 'lib/syskit/network_generation/dataflow_dynamics.rb' + - 'lib/syskit/roby_app/plugin.rb' + - 'lib/syskit/task_context.rb' + - 'test/cli/doc/test_each_model_file.rb' + - 'test/process_managers/test_remote.rb' -# Offense count: 3 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MinMaxComparison: + Exclude: + - 'lib/syskit/test/stubs.rb' + +# Offense count: 4 Style/MissingRespondToMissing: Exclude: - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/properties.rb' - 'lib/syskit/roby_app/single_file_dsl.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' -# Offense count: 18 +# Offense count: 1 Style/MultilineBlockChain: Exclude: - 'lib/syskit/deployment.rb' - - 'test/network_generation/test_engine.rb' - - 'test/runtime/test_connection_management.rb' - - 'test/test_deployment.rb' - - 'test/test_shell_interface.rb' -# Offense count: 15 -# Cop supports --auto-correct. +# Offense count: 20 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/syskit/gui/component_network_base_view.rb' + - 'lib/syskit/gui/instanciate.rb' + - 'lib/syskit/gui/logging_configuration_item_base.rb' + - 'lib/syskit/gui/model_views/data_service.rb' + - 'lib/syskit/gui/model_views/profile.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/models/dynamic_port_binding.rb' + - 'lib/syskit/models/port_access.rb' + - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/port_access.rb' + - 'lib/syskit/process_managers/status.rb' + - 'lib/syskit/remote_state_getter.rb' + - 'lib/syskit/roby_app/logging_group.rb' + - 'lib/syskit/scripts/common.rb' + - 'lib/syskit/telemetry/ui/logging_configuration_item_base.rb' + - 'test/gui/test_logging_configuration.rb' + +# Offense count: 14 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: Exclude: @@ -1154,40 +1429,101 @@ Style/PercentLiteralDelimiters: - 'lib/syskit/gui/runtime_state.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/scripts/common.rb' - - 'test/test_component.rb' - 'test/test_dependency_injection.rb' - 'test/test_instance_requirements.rb' -# Offense count: 21 -# Cop supports --auto-correct. +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Methods. +Style/RedundantArgument: + Exclude: + - 'lib/syskit/gui/instanciate.rb' + - 'lib/syskit/gui/testing.rb' + +# Offense count: 16 +# This cop supports safe autocorrection (--autocorrect). Style/RedundantBegin: Exclude: - 'lib/syskit/actions/profile.rb' - 'lib/syskit/coordination/port_handling.rb' - - 'lib/syskit/deployment.rb' - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/gui/logging_configuration.rb' - 'lib/syskit/network_generation/dataflow_computation.rb' - 'lib/syskit/port.rb' - 'lib/syskit/roby_app/rest_api.rb' - 'lib/syskit/runtime/connection_management.rb' - - 'lib/syskit/shell_interface.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' + - 'lib/syskit/telemetry/ui/logging_configuration.rb' - 'test/coordination/test_task_script.rb' - 'test/models/test_composition.rb' - 'test/test/test_task_context_test.rb' -# Offense count: 4 -# Cop supports --auto-correct. +# Offense count: 6 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/RedundantEach: + Exclude: + - 'lib/syskit/gui/ide.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/models/configured_deployment.rb' + - 'lib/syskit/process_managers/in_process/process.rb' + +# Offense count: 16 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, AllowedPatterns. +Style/ReturnNilInPredicateMethodDefinition: + Exclude: + - 'lib/syskit/component.rb' + - 'lib/syskit/gui/runtime_state.rb' + - 'lib/syskit/models/component.rb' + - 'lib/syskit/network_generation/engine.rb' + - 'lib/syskit/process_managers/remote/server/process.rb' + - 'lib/syskit/roby_app/plugin.rb' + - 'lib/syskit/runtime/update_task_states.rb' + - 'lib/syskit/task_context.rb' + - 'lib/syskit/telemetry/ui/runtime_state.rb' + - 'lib/syskit/test/instance_requirement_planning_handler.rb' + +# Offense count: 18 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'Rakefile' + - 'lib/syskit/droby/v5/droby_dump.rb' + - 'lib/syskit/graphviz.rb' + - 'lib/syskit/gui/model_views/profile.rb' + - 'lib/syskit/gui/page_extension.rb' + - 'lib/syskit/gui/testing.rb' + - 'lib/syskit/models/composition_specialization.rb' + - 'lib/syskit/network_generation/dataflow_dynamics.rb' + - 'lib/syskit/network_generation/merge_solver.rb' + - 'lib/syskit/process_managers/remote/server/server.rb' + - 'lib/syskit/roby_app/rest_api.rb' + - 'lib/syskit/test/execution_expectations.rb' + - 'lib/syskit/test/network_manipulation.rb' + - 'lib/syskit/test/spec.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. # AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym Style/TrivialAccessors: Exclude: - 'lib/syskit/gui/batch_manager.rb' - 'lib/syskit/roby_app/configuration.rb' + - 'lib/syskit/telemetry/ui/batch_manager.rb' -# Offense count: 2 -# Cop supports --auto-correct. +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). Style/WhileUntilModifier: Exclude: - 'test/ros/test_task_context.rb' - - 'test/test_remote_state_getter.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: forbid_for_all_comparison_operators, forbid_for_equality_operators_only, require_for_all_comparison_operators, require_for_equality_operators_only +Style/YodaCondition: + Exclude: + - 'lib/syskit/gui/app_start_dialog.rb' + - 'lib/syskit/telemetry/ui/app_start_dialog.rb' diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..d8e65a6ba --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(syskit) debug current file", + "type": "Ruby", + "request": "launch", + "program": "${file}" + } + ] +} diff --git a/Rakefile b/Rakefile index 4b929efdd..ad3d5f956 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,13 @@ # frozen_string_literal: true -require "bundler/gem_tasks" require "rake/testtask" task :default TESTOPTS = ENV.delete("TESTOPTS") || "" -RUBOCOP_REQUIRED = (ENV["RUBOCOP"] == "1") -USE_RUBOCOP = (ENV["RUBOCOP"] != "0") USE_JUNIT = (ENV["JUNIT"] == "1") +USE_GRPC = (ENV["SYSKIT_HAS_GRPC"] != "0") REPORT_DIR = ENV["REPORT_DIR"] || File.expand_path("test_reports", __dir__) def minitest_set_options(test_task, name) @@ -61,27 +59,24 @@ end task "test" => ["test:gui", "test:core", "test:live"] -if USE_RUBOCOP - begin - require "rubocop/rake_task" - RuboCop::RakeTask.new do |t| - if USE_JUNIT - t.formatters << "junit" - t.options << "-o" << "#{REPORT_DIR}/rubocop.junit.xml" - end - end - task "test" => "rubocop" - rescue LoadError - raise if RUBOCOP_REQUIRED - end +task "rubocop" do + raise "rubocop failed" unless system(ENV["RUBOCOP_CMD"] || "rubocop") end +task "test" => "rubocop" if ENV["RUBOCOP"] != "0" -begin - require "coveralls/rake/task" - Coveralls::RakeTask.new - task "test:coveralls" => ["test", "coveralls:push"] -rescue LoadError # rubocop:disable Lint/SuppressedException -end +protogen = + file "lib/syskit/telemetry/agent/agent_pb.rb" => + ["lib/syskit/telemetry/agent/agent.proto"] do + success = system( + "grpc_tools_ruby_protoc", + "syskit/telemetry/agent/agent.proto", + "--ruby_out=.", + "--grpc_out=.", + chdir: "lib" + ) + raise "grpc_tools_ruby_protoc call failed" unless success + end +task "default" => protogen if USE_GRPC # For backward compatibility with some scripts that expected hoe task "gem" => "build" diff --git a/bin/syskit b/bin/syskit index 349e87338..952b731ab 100755 --- a/bin/syskit +++ b/bin/syskit @@ -17,20 +17,23 @@ end require "syskit/cli/main" +def thor_command?(name) + Syskit::CLI::Main.all_commands.key?(name.tr("-", "_")) +end + # Transform 'syskit help ' to 'syskit --help' if mode is not # handled by thor if ARGV[0] == "help" - if (command_name = ARGV[1]) - unless Syskit::CLI::Main.all_commands.key?(command_name) - ARGV.shift - ARGV << "--help" - end + if (command_name = ARGV[1]) && !thor_command?(command_name) + ARGV.shift + ARGV << "--help" end # Transform 'syskit --help' to 'syskit help ' if mode is handled # by thor elsif ["-h", "--help"].include?(ARGV[1]) - STDERR.puts "WARN: syskit --help is deprecated, use syskit help instead" - if Syskit::CLI::Main.all_commands.key?(ARGV[0]) + $stderr.puts "WARN: syskit --help is deprecated, use syskit help instead" + if thor_command?(ARGV[0]) + ARGV.delete_at(1) ARGV.unshift "help" end end @@ -43,7 +46,7 @@ if ARGV.first Syskit::CLI::Main.start(ARGV) exit 0 rescue Roby::CLI::CLIException => e - STDERR.puts Roby.color(e.message, :red) + $stderr.puts Roby.color(e.message, :red) exit 1 end end @@ -52,7 +55,7 @@ end ORIGINAL_ARGV = ARGV.dup mode = ARGV.shift -SYSKIT_MODES = %w[ide process_server].freeze +SYSKIT_MODES = %w[ide process_server log_runtime_archive].freeze ROBY_MODES = %w[run shell test gen quit restart].freeze if [nil, "--help", "-h", "help"].include?(mode) thor_modes = Syskit::CLI::Main @@ -75,7 +78,7 @@ else begin require "roby/app/scripts/#{mode}" rescue LoadError - STDERR.puts "unknown mode '#{mode}'" + $stderr.puts "unknown mode '#{mode}'" exit(1) end end diff --git a/lib/syskit.rb b/lib/syskit.rb index b258989e4..08a8c1b43 100644 --- a/lib/syskit.rb +++ b/lib/syskit.rb @@ -23,6 +23,37 @@ require "syskit/instance_requirements" require "syskit/orogen_namespace" + +module Syskit + # @api private + # + # Definition of Syskit's built-in process managers + # + # Process managers are the facility that allow to execute components. {Deployment} + # is agnostic w.r.t the way the deployment is executed. The impedance layer is + # the process manager (which creates the actual deployments) and the process object + # (created by its manager) that manages the state of a running deployment. + module ProcessManagers + extend Logger::Hierarchy + include Logger::Hierarchy + end +end + +require "syskit/roby_app/log_transfer_server" +require "syskit/process_managers/process_base" +require "syskit/process_managers/status" +require "syskit/process_managers/remote/server/log_upload_state" +require "syskit/process_managers/remote/protocol" +require "syskit/process_managers/remote/loader" +require "syskit/process_managers/remote/manager" +require "syskit/process_managers/remote/process" +require "syskit/process_managers/in_process/manager" +require "syskit/process_managers/in_process/process" +require "syskit/process_managers/ruby_tasks/manager" +require "syskit/process_managers/ruby_tasks/process" +require "syskit/process_managers/unmanaged/manager" +require "syskit/process_managers/unmanaged/process" + require "syskit/roby_app" # Models diff --git a/lib/syskit/actions/action.rb b/lib/syskit/actions/action.rb index d4d19f4f4..8103ea0e1 100644 --- a/lib/syskit/actions/action.rb +++ b/lib/syskit/actions/action.rb @@ -8,7 +8,7 @@ class Action < Roby::Actions::Action # # @return [InstanceRequirements] def to_instance_requirements - model.to_instance_requirements(arguments) + model.to_instance_requirements(**arguments) end # Create a new action with the same arguments but the requirements @@ -28,7 +28,8 @@ def respond_to_missing?(m, include_private) def method_missing(m, *args, &block) if model.requirements.respond_to?(m) Action.new(model.public_send(m, *args, &block), arguments) - else super + else + super end end end diff --git a/lib/syskit/actions/action_model.rb b/lib/syskit/actions/action_model.rb index 09d79a6c6..bc252dd01 100644 --- a/lib/syskit/actions/action_model.rb +++ b/lib/syskit/actions/action_model.rb @@ -134,7 +134,8 @@ def method_missing(m, *args, **kw, &block) req = requirements.dup req.send(m, *args, **kw, &block) Actions::Models::Action.new(req, doc) - else super + else + super end end end diff --git a/lib/syskit/actions/interface_model_extension.rb b/lib/syskit/actions/interface_model_extension.rb index d042c6f24..46480fa45 100644 --- a/lib/syskit/actions/interface_model_extension.rb +++ b/lib/syskit/actions/interface_model_extension.rb @@ -16,7 +16,7 @@ def profile(name = nil, &block) if block Roby.warn_deprecated( - "calling profile do ... end in an action interface is "\ + "calling profile do ... end in an action interface is " \ "deprecated, call use_profile do .. end instead" ) use_profile(&block) @@ -142,7 +142,7 @@ def use_profile( elsif block unless tag_selection.empty? raise ArgumentError, - "cannot provide a tag selection and a block "\ + "cannot provide a tag selection and a block " \ "at the same time" end @@ -223,13 +223,13 @@ def profile def has_through_method_missing?(name) MetaRuby::DSLs.has_through_method_missing?( - profile, m, "_tag" => :has_tag? + profile, name, "_tag" => :has_tag? ) || super end def find_through_method_missing(name, args) MetaRuby::DSLs.find_through_method_missing( - profile, m, args, "_tag" => :find_tag + profile, name, args, "_tag" => :find_tag ) || super end diff --git a/lib/syskit/actions/profile.rb b/lib/syskit/actions/profile.rb index ef82199a2..e1669460f 100644 --- a/lib/syskit/actions/profile.rb +++ b/lib/syskit/actions/profile.rb @@ -53,6 +53,10 @@ def to_action_model(profile = self.profile, doc = self.doc) action_model.advanced = advanced? action_model end + + def to_s + "#{profile}.#{name}_def" + end end class Definition < ProfileInstanceRequirements @@ -108,6 +112,7 @@ def each_submodel; end # The profile name # @return [String] attr_reader :name + # The profile's basename def basename name.gsub(/.*::/, "") @@ -265,7 +270,7 @@ def resolved_dependency_injection end def to_s - "profile:#{name}" + name end # Promote requirements taken from another profile to this profile @@ -295,7 +300,8 @@ def resolve_tag_selection(profile, tags) tags.transform_keys do |key| if key.respond_to?(:to_str) profile.send("#{key.gsub(/_tag$/, '')}_tag") - else key + else + key end end end @@ -324,7 +330,11 @@ def each_used_profile(&block) # # @param [Profile] profile # @return [void] - def use_profile(profile, tags = {}, transform_names: ->(k) { k }) + def use_profile( + profile, tags = {}, + transform_names: ->(k) { k }, + prefer_deployed_tasks: nil + ) invalidate_dependency_injection tags = resolve_tag_selection(profile, tags) used_profiles.push([profile, tags]) @@ -337,6 +347,9 @@ def use_profile(profile, tags = {}, transform_names: ->(k) { k }) name = transform_names.call(name) req = promote_requirements(profile, req, tags) definition = register_definition(name, req, doc: req.doc) + if prefer_deployed_tasks + definition.prefer_deployed_tasks(prefer_deployed_tasks) + end new_definitions << definition end new_definitions.concat(robot.use_robot(profile.robot)) diff --git a/lib/syskit/actual_data_flow_graph.rb b/lib/syskit/actual_data_flow_graph.rb index 598a9af8d..1519db59c 100644 --- a/lib/syskit/actual_data_flow_graph.rb +++ b/lib/syskit/actual_data_flow_graph.rb @@ -46,8 +46,8 @@ def add_connections(source_task, sink_task, mappings) # :nodoc: mappings.each do |(source_port, sink_port), info| if info.size != 3 raise ArgumentError, - "ActualDataFlowGraph#add_connections expects "\ - "the mappings to be of the form (source_port,sink_port) "\ + "ActualDataFlowGraph#add_connections expects " \ + "the mappings to be of the form (source_port,sink_port) " \ "=> [policy, source_static, sink_static]" end diff --git a/lib/syskit/bound_data_service.rb b/lib/syskit/bound_data_service.rb index 380264021..bb5bd55ba 100644 --- a/lib/syskit/bound_data_service.rb +++ b/lib/syskit/bound_data_service.rb @@ -25,6 +25,7 @@ class BoundDataService # @return [Models::BoundDataService] # @see service_model attr_reader :model + # The data service name # # @return [String] diff --git a/lib/syskit/cli/doc/gen.rb b/lib/syskit/cli/doc/gen.rb index 127134ffc..71c8914d5 100644 --- a/lib/syskit/cli/doc/gen.rb +++ b/lib/syskit/cli/doc/gen.rb @@ -31,7 +31,7 @@ def self.generate(app, required_paths, target_path) # # @param [Pathname] target_path the root of the documentation output path # @param model the model to export - def self.save_model(target_path, model) # rubocop:disable Metrics/CyclomaticComplexity + def self.save_model(target_path, model) puts "Saving model #{model} in #{target_path}" case model @@ -82,7 +82,7 @@ def self.save_profile_definition(target_path, profile_def) save_profile_definition_graphs(target_path, task, profile_def) .map(&:to_s) rescue StandardError => e - Roby.warn "could not generate profile definition graph for "\ + Roby.warn "could not generate profile definition graph for " \ "#{profile_def.name}" Roby.log_exception_with_backtrace(e, Roby, :warn) # Make dataflow a different object than hierarchy, or psych @@ -131,7 +131,7 @@ def self.save_composition_model(target_path, composition_m) save_composition_graphs(target_path, composition_m) .map(&:to_s) rescue StandardError => e - Roby.warn "could not generate composition model graph for "\ + Roby.warn "could not generate composition model graph for " \ "#{composition_m.name}" Roby.log_exception_with_backtrace(e, Roby, :warn) hierarchy = { "error" => e.message } @@ -379,11 +379,9 @@ def self.render_plan( # Fixup a mixup in dot's SVG output. The URIs that contain < and > # are not properly escaped to < and > - svg = svg.gsub(/xlink:href="[^"]+"/) do |match| + svg.gsub(/xlink:href="[^"]+"/) do |match| match.gsub("<", "<").gsub(">", ">") end - - svg end end end diff --git a/lib/syskit/cli/doc_main.rb b/lib/syskit/cli/doc_main.rb index a0217e90d..7d75d2698 100644 --- a/lib/syskit/cli/doc_main.rb +++ b/lib/syskit/cli/doc_main.rb @@ -19,6 +19,9 @@ class DocMain < Thor option :exclude, type: :array, default: [], desc: "list of path patterns to exclude from documentation" + option :set, + type: :string, repeatable: true, + desc: "set some configuration parameters" def gen(target_path) MetaRuby.keep_definition_location = true roby_app_configure @@ -40,6 +43,8 @@ def roby_app end def roby_app_configure + apply_set_options(roby_app) + roby_app.require_app_dir roby_app.using "syskit" roby_app.development_mode = false @@ -49,6 +54,13 @@ def roby_app_configure roby_app.setup_for_minimal_tooling end + def apply_set_options(app) + (options[:set] || []).each do |kv| + app.argv_set << kv + Roby::Application.apply_conf_from_argv(kv) + end + end + def roby_autoload_orogen_projects (models_path / "orogen").glob("*.rb").each do |extension_file| project_name = extension_file.sub_ext("").basename.to_s diff --git a/lib/syskit/cli/gen/syskit_app/config/init.rb b/lib/syskit/cli/gen/syskit_app/config/init.rb index 33248f19b..c82375e34 100644 --- a/lib/syskit/cli/gen/syskit_app/config/init.rb +++ b/lib/syskit/cli/gen/syskit_app/config/init.rb @@ -20,6 +20,9 @@ # Set to false to disable old-style task model export (using constants) OroGen.syskit_model_constant_registration = true +# Whether Syskit's internal ruby task should be registered on the CORBA naming +# service. +Syskit.conf.register_self_on_name_server = false # Set the module's name. It is normally inferred from the app name, and the app # name is inferred from the base directory name (e.g. an app located in diff --git a/lib/syskit/cli/gen/syskit_app/config/orogen/.gitignore b/lib/syskit/cli/gen/syskit_app/config/orogen/.gitignore new file mode 100644 index 000000000..1998c294f --- /dev/null +++ b/lib/syskit/cli/gen/syskit_app/config/orogen/.gitignore @@ -0,0 +1 @@ +.cache \ No newline at end of file diff --git a/lib/syskit/cli/gen_main.rb b/lib/syskit/cli/gen_main.rb index cda9bc006..09857a1f2 100644 --- a/lib/syskit/cli/gen_main.rb +++ b/lib/syskit/cli/gen_main.rb @@ -171,7 +171,7 @@ def profile(name) gen_common("profile", name, "profiles", "Profiles") end - desc "orogen PROJECT_NAME", "generate an extension file for the tasks "\ + desc "orogen PROJECT_NAME", "generate an extension file for the tasks " \ "of an oroGen project" long_desc <<~OROGEN_DESCRIPTION Generates a new extension file for an oroGen project. It must be given the name @@ -188,7 +188,6 @@ def profile(name) def orogen(project_name) Roby.app.require_app_dir(needs_current: true) Roby.app.single = true - Roby.app.base_setup Syskit.conf.only_load_models = true Roby.app.setup @@ -210,7 +209,6 @@ def orogen(project_name) ) ensure Roby.app.cleanup - Roby.app.base_cleanup end no_commands do @@ -271,8 +269,8 @@ def orogenconf(model_name) rescue RuntimeError unless section raise Roby::CLI::CLICommandFailed, - "failed to start a component of model "\ - "#{task_model.orogen_model.name}, cannot create "\ + "failed to start a component of model " \ + "#{task_model.orogen_model.name}, cannot create " \ "a configuration file with default values" end end diff --git a/lib/syskit/cli/log_runtime_archive.rb b/lib/syskit/cli/log_runtime_archive.rb new file mode 100644 index 000000000..66e01a84e --- /dev/null +++ b/lib/syskit/cli/log_runtime_archive.rb @@ -0,0 +1,412 @@ +# frozen_string_literal: true + +require "archive/tar/minitar" +require "sys/filesystem" + +module Syskit + module CLI + # Exception raised when the zstd subprocess returns an error + class CompressionFailed < RuntimeError; end + + # Implementation of the `syskit log-runtime-archive` tool + # + # The tool archives Syskit log directories into tar archives in realtime, + # compressing files using zstd + # + # It depends on the syskit instance using log rotation + class LogRuntimeArchive + DEFAULT_MAX_ARCHIVE_SIZE = 10_000_000_000 # 10G + + def initialize( + root_dir, target_dir, + logger: LogRuntimeArchive.null_logger, + max_archive_size: DEFAULT_MAX_ARCHIVE_SIZE + ) + @last_archive_index = {} + @logger = logger + @root_dir = root_dir + @target_dir = target_dir + @max_archive_size = max_archive_size + end + + # Iterate over all datasets in a Roby log root folder and archive them + # + # The method assumes the last dataset is the current one (i.e. the running + # one), and will only archive already rotated files. + # + # @param [Pathname] root_dir the log root folder + # @param [Pathname] target_dir the folder in which to save the + # archived datasets + def process_root_folder + candidates = self.class.find_all_dataset_folders(@root_dir) + running = candidates.last + candidates.each do |child| + process_dataset(child, full: child != running) + end + end + + # Manages folder available space + # + # The method will check if there is enough space to save more log files + # according to pre-established threshold. + # + # @param [integer] free_space_low_limit: required free space threshold in + # bytes, at which the archiver starts deleting the oldest log files + # @param [integer] free_space_delete_until: post-deletion free space in bytes, + # at which the archiver stops deleting the oldest log files + def ensure_free_space(free_space_low_limit, free_space_delete_until) + if free_space_low_limit > free_space_delete_until + raise ArgumentError, + "cannot erase files: freed limit is smaller than " \ + "low limit space." + end + + stat = Sys::Filesystem.stat(@target_dir) + available_space = stat.bytes_available + + return if available_space > free_space_low_limit + + until available_space >= free_space_delete_until + files = @target_dir.each_child.select(&:file?) + if files.empty? + Roby.warn "Cannot erase files: the folder is empty but the " \ + "available space is smaller than the threshold." + break + end + + removed_file = files.min + size_removed_file = removed_file.size + removed_file.unlink + available_space += size_removed_file + end + end + + def process_dataset(child, full:) + use_existing = true + loop do + open_archive_for( + child.basename.to_s, use_existing: use_existing + ) do |io| + if io.tell > @max_archive_size + use_existing = false + break + end + + dataset_complete = self.class.archive_dataset( + io, child, + logger: @logger, full: full, + max_size: @max_archive_size + ) + return if dataset_complete + end + + use_existing = false + end + end + + # Create or open an archive + # + # The method will find an archive to open or create, do it and + # yield the corresponding IO. The archives are named #{basename}.${INDEX}.tar + # + # @param [Boolean] use_existing if false, always create a new + # archive. If true, reuse the last archive that was created, if + # present, or create ${basename}.0.tar otherwise. + def open_archive_for(basename, use_existing: true) + last_index = find_last_archive_index(basename) + + index, mode = + if !last_index + [0, "w"] + elsif use_existing + [last_index, "r+"] + else + [last_index + 1, "w"] + end + + archive_path = @target_dir / "#{basename}.#{index}.tar" + archive_path.open(mode) do |io| + io.seek(0, IO::SEEK_END) + yield(io) + end + end + + # Find the last archive index used for a given basename + # + # @param [String] basename the archive basename + def find_last_archive_index(basename) + i = @last_archive_index[basename] || 0 + last_i = nil + loop do + candidate = @target_dir / "#{basename}.#{i}.tar" + return last_i unless candidate.exist? + + @last_archive_index[basename] = last_i + last_i = i + i += 1 + end + end + + # Find all dataset-looking folders within a root log folder + def self.find_all_dataset_folders(root_dir) + candidates = root_dir.enum_for(:each_entry).map do |child| + next unless /^\d{8}-\d{4}(\.\d+)?$/.match?(child.basename.to_s) + + child = (root_dir / child) + next unless child.directory? + + child if (child / "info.yml").file? + end + + candidates.compact.sort_by { _1.basename.to_s } + end + + # Safely add an entry into an archive, compressing it with zstd + # + # @return [Boolean] true if the file was added successfully, false otherwise + def self.add_to_archive(archive_io, child_path, logger: null_logger) + logger.info "adding #{child_path}" + stat = child_path.stat + + start_pos = archive_io.tell + write_initial_header(archive_io, child_path, stat) + data_pos = archive_io.tell + exit_status = write_compressed_data(child_path, archive_io) + + unless exit_status.success? + raise CompressionFailed, "compression failed for #{child_path}" + end + + add_to_archive_commit( + archive_io, child_path, start_pos, data_pos, stat + ) + child_path.unlink + rescue Exception => e # rubocop:disable Lint/RescueException + Roby.display_exception($stdout, e) + if start_pos + add_to_archive_rollback(archive_io, start_pos, logger: logger) + end + raise + end + + # Finalize appending a file in the archive + def self.add_to_archive_commit( + archive_io, child_path, start_pos, data_pos, stat + ) + data_size = archive_io.tell - data_pos + write_padding(data_size, archive_io) + + # Update header + archive_io.seek(start_pos, IO::SEEK_SET) + write_final_header(archive_io, child_path, stat, data_size) + archive_io.seek(0, IO::SEEK_END) + end + + # Revert the addition of a file in the archive, after an error + def self.add_to_archive_rollback(archive_io, start_pos, logger:) + logger.warn "failed addition to archive, rolling back to known-good state" + archive_io.truncate(start_pos) + archive_io.seek(start_pos, IO::SEEK_SET) + end + + # Write a tar block header without the data size + def self.write_initial_header(archive_io, child_path, stat) + write_header( + archive_io, + "#{child_path.basename}.zst", + { mode: 0o644, uid: stat.uid, gid: stat.gid, + mtime: stat.mtime, size: 0 } + ) + end + + # Write the final tar block header at the given position + def self.write_final_header(archive_io, child_path, stat, size) + write_header( + archive_io, + "#{child_path.basename}.zst", + { mode: 0o644, uid: stat.uid, gid: stat.gid, + mtime: stat.mtime, size: size } + ) + end + + # Compress data and append it to the archive + def self.write_compressed_data(child_path, archive_io) + _, exit_status = child_path.open("r") do |io| + zstd_transfer_r, zstd_transfer_w = IO.pipe + pid = Process.spawn("zstd", "--stdout", in: io, out: zstd_transfer_w) + zstd_transfer_w.close + IO.copy_stream(zstd_transfer_r, archive_io) + Process.waitpid2(pid) + end + exit_status + end + + # Write necessary padding (tar requires multiples of 512 bytes) + def self.write_padding(size, io) + # Move to end, compute actual size, pad to 512 bytes blocks + remainder = ((size + 511) / 512 * 512) - size + io.write("\0" * remainder) + end + + # Create a logger that will display nothing + def self.null_logger + logger = Logger.new($stdout) + logger.level = Logger::FATAL + 1 + logger + end + + # Archive the given dataset + # + # @param [IO] archive_io the IO of the target archive + # @param [Pathname] path path to the dataset folder + # @param [Boolean] full whether we're arching the complete dataset (true), + # or only the files that we know are not being written to (for log + # directories of running Syskit instances) + # @return [Boolean] true if we're done processing this dataset. False + # if processing was interrupted by e.g. an archive that reached the + # max_archive_size limit + def self.archive_dataset( + archive_io, path, + full:, logger: null_logger, max_size: DEFAULT_MAX_ARCHIVE_SIZE + ) + logger.info( + "Archiving dataset #{path} in #{full ? 'full' : 'partial'} mode" + ) + candidates = each_file_from_path(path).to_a + complete, candidates = + if full + archive_filter_candidates_full(candidates) + else + archive_filter_candidates_partial(candidates) + end + + candidates.each_with_index do |child_path, i| + add_to_archive(archive_io, child_path, logger: logger) + + if archive_io.tell > max_size + return complete && (i == candidates.size - 1) + end + end + + complete + end + + # Enumerate the children of a path that are files + # + # @yieldparam [Pathname] file_path the full path to the file + def self.each_file_from_path(path) + return enum_for(:each_file_from_path, path) unless block_given? + + path.each_entry do |child_path| + full = path / child_path + yield(full) if full.file? + end + end + + # Filters all candidates for archiving to return the ones that should + # be archived in `full` mode at this point in time + # + # The method either returns the remaining rotated logs, or if there + # are none, the non-rotated files. This ensures that the archiver groups + # all non-rotated files in a single archive. + # + # @param [Array] candidates + # @return [(Boolean,Array)] a flag that tell whether the candidate + # array is complete or not, and the files that should be archived. If + # the flag is true, the assumption is that after having archived the files + # that were returned, the archiving loop should try archiving again. + def self.archive_filter_candidates_full(candidates) + per_file_and_idx = filter_and_group_pocolog_files(candidates) + rotated_logs = per_file_and_idx.each_value.flat_map(&:values) + unless rotated_logs.empty? + return [(candidates - rotated_logs).empty?, rotated_logs] + end + + [true, candidates] + end + + # Filters all candidates for archiving to return the ones that should + # be archived in `partial` mode at this point in time + # + # The method returns the rotated logs that are known to be complete. + # + # @param [Array] candidates + # @return [(true,Array)] files that should be archived. The + # boolean is here for consistency with {.archive_filter_candidates_full} + def self.archive_filter_candidates_partial(candidates) + per_file_and_idx = + filter_and_group_pocolog_files(candidates) + .each_value { |logs| logs.delete(logs.keys.max) } + + complete_log_files = per_file_and_idx.flat_map do |_, logs| + logs.keys.sort.map { |i| logs[i] } + end + + [true, complete_log_files] + end + + # Filter the pocolog files from the given candidates and sort them + # by basename and log index + # + # @param [Array] candidates + # @return [{String=>{Integer=>Pathname}}] + def self.filter_and_group_pocolog_files(candidates) + candidates.each_with_object({}) do |path, h| + name = path.basename.to_s + if (m = /\.(\d+)\.log$/.match(name)) + per_file = (h[m.pre_match] ||= {}) + per_file[Integer(m[1])] = path + end + end + end + + extend Archive::Tar::Minitar::ByteSize + + # Copy a tar header (copied from minitar) + def self.write_header(io, long_name, header) + short_name, prefix, needs_long_name = split_name(long_name) + + if needs_long_name + long_name_header = { + prefix: "", + name: Archive::Tar::Minitar::PosixHeader::GNU_EXT_LONG_LINK, + typeflag: "L", + size: long_name.length, + mode: 0 + } + io.write(Archive::Tar::Minitar::PosixHeader.new(long_name_header)) + io.write(long_name) + io.write("\0" * (512 - (long_name.length % 512))) + end + + new_header = header.merge({ name: short_name, prefix: prefix }) + io.write(Archive::Tar::Minitar::PosixHeader.new(new_header)) + end + + # Process a file name to determine whether it should use the GNU + # long file extension. Copied from minitar + def self.split_name(name) # rubocop:disable Metrics/AbcSize + if bytesize(name) <= 100 + prefix = "" + else + parts = name.split(%r{/}) + newname = parts.pop + + nxt = "" + + loop do + nxt = parts.pop || "" + break if bytesize(newname) + 1 + bytesize(nxt) >= 100 + + newname = "#{nxt}/#{newname}" + end + + prefix = (parts + [nxt]).join("/") + name = newname + end + + [name, prefix, bytesize(name) > 100 || bytesize(prefix) > 155] + end + end + end +end diff --git a/lib/syskit/cli/log_runtime_archive_main.rb b/lib/syskit/cli/log_runtime_archive_main.rb new file mode 100644 index 000000000..aa536b575 --- /dev/null +++ b/lib/syskit/cli/log_runtime_archive_main.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# NOTE: this is NOT integrated in the Thor-based CLI to make it more independent +# (i.e. not depending on actually having Syskit installed) + +require "pathname" +require "thor" +require "syskit/cli/log_runtime_archive" + +module Syskit + module CLI + # Command-line definition for the cli-archive-main syskit subcommand + class LogRuntimeArchiveMain < Thor + def self.exit_on_failure? + true + end + + desc "watch", "watch a dataset root folder and call archiver" + + option :period, + type: :numeric, default: 600, desc: "polling period in seconds" + option :max_size, + type: :numeric, default: 10_000, desc: "max log size in MB" + option :free_space_low_limit, + type: :numeric, default: 5_000, desc: "start deleting files if \ + available space is below this threshold (threshold in MB)" + option :free_space_freed_limit, + type: :numeric, default: 25_000, desc: "stop deleting files if \ + available space is above this threshold (threshold in MB)" + default_task def watch(root_dir, target_dir) + loop do + begin + archive(root_dir, target_dir) + rescue Errno::ENOSPC + next + end + + puts "Archived pending logs, sleeping #{options[:period]}s" + sleep options[:period] + end + end + + desc "archive", "archive the datasets and manages disk space" + option :max_size, + type: :numeric, default: 10_000, desc: "max log size in MB" + option :free_space_low_limit, + type: :numeric, default: 5_000, desc: "start deleting files if \ + available space is below this threshold (threshold in MB)" + option :free_space_freed_limit, + type: :numeric, default: 25_000, desc: "stop deleting files if \ + available space is above this threshold (threshold in MB)" + def archive(root_dir, target_dir) + root_dir = validate_directory_exists(root_dir) + target_dir = validate_directory_exists(target_dir) + archiver = make_archiver(root_dir, target_dir) + + archiver.ensure_free_space( + options[:free_space_low_limit] * 1_000_000, + options[:free_space_freed_limit] * 1_000_000 + ) + archiver.process_root_folder + end + + no_commands do + def validate_directory_exists(dir) + dir = Pathname.new(dir) + unless dir.directory? + raise ArgumentError, "#{dir} does not exist, or is not a " \ + "directory" + end + + dir + end + + def make_archiver(root_dir, target_dir) + logger = Logger.new($stdout) + + Syskit::CLI::LogRuntimeArchive.new( + root_dir, target_dir, + logger: logger, max_archive_size: options[:max_size] * (1024**2) + ) + end + end + end + end +end diff --git a/lib/syskit/cli/main.rb b/lib/syskit/cli/main.rb index aadb4213d..47b5f8ed3 100644 --- a/lib/syskit/cli/main.rb +++ b/lib/syskit/cli/main.rb @@ -3,6 +3,7 @@ require "roby/cli/main" require "syskit/cli/gen_main" require "syskit/cli/doc_main" +require "syskit/telemetry/cli" module Syskit module CLI @@ -18,6 +19,7 @@ class Main < Roby::CLI::Main option :workdir, type: :string, default: nil option :logs, type: :string, default: nil option :logs_base, type: :string, default: nil + option :log, type: :string, repeatable: true, default: [] def orogen_test(*args) syskit_path = File.expand_path("../../../bin/syskit", __dir__) minitest_args, files = args.partition { |p| p.start_with?("-") } @@ -28,11 +30,16 @@ def orogen_test(*args) extra_args = ["--keep-logs"] extra_args << "--logs" << options[:logs] if options[:logs] extra_args << "--logs-base" << options[:logs_base] if options[:logs_base] + extra_args.concat(options[:log].map { |l| "--log=#{l}" }) system(syskit_path, "gen", "app", workdir) unless File.directory?(workdir) Process.exec(syskit_path, "test", "--live", *extra_args, *files, "--", *minitest_args, chdir: workdir) end + + desc "telemetry", + "commands related to monitoring and commanding a running Syskit system" + subcommand "telemetry", Telemetry::CLI end end end diff --git a/lib/syskit/component.rb b/lib/syskit/component.rb index d19816bb9..b66cf72ef 100644 --- a/lib/syskit/component.rb +++ b/lib/syskit/component.rb @@ -53,12 +53,7 @@ def initialize(**arguments) super @requirements = InstanceRequirements.new - @registered_data_writers = - instanciate_data_accessors(model.each_data_writer) - @registered_data_readers = - instanciate_data_accessors(model.each_data_reader) - @data_readers = @registered_data_readers.values - @data_writers = @registered_data_writers.values + setup_data_accessors end def initialize_copy(source) @@ -66,6 +61,16 @@ def initialize_copy(source) @requirements = @requirements.dup specialize if source.specialized_model? duplicate_missing_services_from(source) + setup_data_accessors + end + + def setup_data_accessors + @registered_data_writers = + instanciate_data_accessors(model.each_data_writer) + @registered_data_readers = + instanciate_data_accessors(model.each_data_reader) + @data_readers = @registered_data_readers.values + @data_writers = @registered_data_writers.values end def create_fresh_copy @@ -177,8 +182,8 @@ def meets_configurationg_precedence_constraints? if waiting_precedence_relation debug do - "#{self} not ready for setup: "\ - "waiting on #{waiting_precedence_relation}" + "#{self} not ready for setup: " \ + "waiting on #{waiting_precedence_relation}" end end @@ -190,8 +195,8 @@ def meets_configurationg_precedence_constraints? def ready_for_setup? # :nodoc: if garbage? debug do - "#{self} not ready for setup: "\ - "garbage collected but not yet finalized" + "#{self} not ready for setup: " \ + "garbage collected but not yet finalized" end return false elsif failed_to_start? @@ -268,7 +273,7 @@ def setup_failed!(exception) start_event.emit_failed(exception) else Roby.execution_engine.add_framework_error( - e, "#{self} got finalized before the setting_up! "\ + e, "#{self} got finalized before the setting_up! " \ "error handler was called" ) end @@ -387,7 +392,7 @@ def duplicate_missing_services_from(task) missing_services.each do |_, srv| unless srv.respond_to?(:dynamic_service) raise InternalError, - "attempting to duplicate static service "\ + "attempting to duplicate static service " \ "#{srv.name} from #{task} to #{self}" end @@ -534,7 +539,7 @@ def data_writer(port, *args, as: nil, **policy) if port.respond_to?(:to_str) if as raise ArgumentError, - "cannot provide the 'as' option to the deprecated "\ + "cannot provide the 'as' option to the deprecated " \ "string-based call to #data_writer" end @@ -561,6 +566,13 @@ def register_data_writer(writer, as: nil) writer.update end + # Deregister a data writer register + # + # @param [String] register writer name + def deregister_data_writer(name) + @registered_data_writers.delete(name) + end + # Set of {DynamicPortBinding::BoundOutputReader} registered on self # # They are added on creation from {Models::Component#data_readers}, or @@ -630,7 +642,7 @@ def data_reader(port, *args, as: nil, pull: true, **policy) if port.respond_to?(:to_str) if as raise ArgumentError, - "cannot provide the 'as' option to the deprecated "\ + "cannot provide the 'as' option to the deprecated " \ "string-based call to #data_reader" end @@ -659,6 +671,13 @@ def register_data_reader(reader, as: nil) reader.update end + # Deregister a data reader register + # + # @param [String] register reader name + def deregister_data_reader(name) + @registered_data_readers.delete(name) + end + on :start do |_event| @data_readers.each do |reader| reader.attach_to_task(self) if reader.respond_to?(:attach_to_task) diff --git a/lib/syskit/composition.rb b/lib/syskit/composition.rb index 2ac7b1418..0732054ff 100644 --- a/lib/syskit/composition.rb +++ b/lib/syskit/composition.rb @@ -51,8 +51,8 @@ def conf(names) if names.size != 1 raise ArgumentError, - "unlike with ConfigurationManager, only one configuration can "\ - "be selected on compositions. Attempted to select #{names} on "\ + "unlike with ConfigurationManager, only one configuration can " \ + "be selected on compositions. Attempted to select #{names} on " \ "#{self}" end @@ -66,13 +66,13 @@ def conf(names) unless found_something if names == ["default"] ConfigurationManager.info \ - "required default configuration on composition #{task}, "\ - "but #{task.model.short_name} has no registered "\ + "required default configuration on composition #{task}, " \ + "but #{task.model.short_name} has no registered " \ "default configurations" return {} else - raise ArgumentError, "#{self} has no declared configuration "\ - "called #{names.join(', ')}" + raise ArgumentError, "#{self} has no declared configuration " \ + "called #{names.join(', ')}" end end result @@ -98,7 +98,8 @@ def resolve_port(port_name) actual_port_name = child_selection[child_name].port_mappings[export.name] if child.respond_to?(:resolve_port) child.resolve_port(actual_port_name) - else child.find_port(actual_port_name) + else + child.find_port(actual_port_name) end end @@ -167,7 +168,8 @@ def find_required_composition_child_from_role(role, from_model = model) if task.model <= selected_srv.component_model selected_srv - else selected_srv.as_real_model + else + selected_srv.as_real_model end end selected_service_m.bind(task).as(target_srv.model) @@ -180,7 +182,7 @@ def find_required_composition_child_from_role(role, from_model = model) def required_composition_child_from_role(role) selected = find_required_composition_child_from_role(role) unless selected - raise ArgumentError, "#{role} does not seem to be a proper child "\ + raise ArgumentError, "#{role} does not seem to be a proper child " \ "of this composition" end selected @@ -327,7 +329,6 @@ def respond_to_missing?(m, include_private) (m =~ /_port$/) || super end - # rubocop:disable Style/MethodMissingSuper def method_missing(m, *args, &block) unless (matched_port = /_port$/.match(m)) return @child_task.send(m, *args, &block) @@ -340,13 +341,12 @@ def method_missing(m, *args, &block) @child_task.find_output_port(mapped_port_name) unless port - raise NoMethodError, "task #{@child_task}, child #{@child_name} "\ - "of #{@composition_task}, has no port "\ - "called #{port_name}" + raise NoMethodError, "task #{@child_task}, child #{@child_name} " \ + "of #{@composition_task}, has no port " \ + "called #{port_name}" end port end - # rubocop:enable Style/MethodMissingSuper end end end diff --git a/lib/syskit/connection_graphs.rb b/lib/syskit/connection_graphs.rb index 2f174f5c5..ab185dac6 100644 --- a/lib/syskit/connection_graphs.rb +++ b/lib/syskit/connection_graphs.rb @@ -113,12 +113,10 @@ def self.resolve_connections(output_ports, input_ports) # input only if it is a multiplexing port outputs_per_input = {} result.each do |out_port, in_port| - if outputs_per_input[in_port] - unless in_port.multiplexes? - candidates = result.map { |o, i| o if i == in_port } - .compact - raise AmbiguousAutoConnection.new(in_port, candidates) - end + if outputs_per_input[in_port] && !in_port.multiplexes? + candidates = result.map { |o, i| o if i == in_port } + .compact + raise AmbiguousAutoConnection.new(in_port, candidates) end outputs_per_input[in_port] = out_port end @@ -155,12 +153,14 @@ def self.connect(source, sink, policy) output_ports = if source.respond_to?(:each_output_port) source.each_output_port.to_a - else [source] + else + [source] end input_ports = if sink.respond_to?(:each_input_port) sink.each_input_port.to_a - else [sink] + else + [sink] end connections = resolve_connections(output_ports, input_ports) diff --git a/lib/syskit/coordination/data_monitor.rb b/lib/syskit/coordination/data_monitor.rb index dbc79a9d9..e821cc0ba 100644 --- a/lib/syskit/coordination/data_monitor.rb +++ b/lib/syskit/coordination/data_monitor.rb @@ -20,6 +20,7 @@ class DataMonitor # @return [Array] the set of events that # should be emitted when this monitor triggers attr_reader :emitted_events + # @return [Boolean] whether this monitor should generate a # {DataMonitoringError} when it triggers attr_predicate :raises?, true diff --git a/lib/syskit/coordination/data_monitoring_error.rb b/lib/syskit/coordination/data_monitoring_error.rb index 163907e46..63f79d596 100644 --- a/lib/syskit/coordination/data_monitoring_error.rb +++ b/lib/syskit/coordination/data_monitoring_error.rb @@ -5,9 +5,7 @@ module Coordination # Exception issued by the data monitors in {DataMonitor#poll} when # predicate#finalize returns true class DataMonitoringError < Roby::LocalizedError - attr_reader :monitor - attr_reader :time - attr_reader :samples + attr_reader :monitor, :time, :samples def initialize(task, monitor, time, samples) super(task) diff --git a/lib/syskit/coordination/data_monitoring_table.rb b/lib/syskit/coordination/data_monitoring_table.rb index 647cd0a97..af37b7e3b 100644 --- a/lib/syskit/coordination/data_monitoring_table.rb +++ b/lib/syskit/coordination/data_monitoring_table.rb @@ -55,7 +55,7 @@ class DataMonitoringTable < Roby::Coordination::Base # (see Roby::Coordination::Base) def initialize(root_task, arguments = {}, options = {}) - super(root_task, arguments, options) + super options, = Kernel.filter_options options, :on_replace => :drop @poll_id = root_task.poll(options) do poll diff --git a/lib/syskit/coordination/fault_response_table_extension.rb b/lib/syskit/coordination/fault_response_table_extension.rb index c25313a89..9ce2a2cc2 100644 --- a/lib/syskit/coordination/fault_response_table_extension.rb +++ b/lib/syskit/coordination/fault_response_table_extension.rb @@ -34,7 +34,8 @@ def attach_to(plan) data_args = tbl.arguments.transform_values do |fault_arg| if fault_arg.kind_of?(Roby::Coordination::Models::Variable) arguments[fault_arg.name] - else fault_arg + else + fault_arg end end data_monitoring_tables << plan.use_data_monitoring_table(tbl.table, data_args) diff --git a/lib/syskit/coordination/models/data_monitor.rb b/lib/syskit/coordination/models/data_monitor.rb index 8e1147630..73967790c 100644 --- a/lib/syskit/coordination/models/data_monitor.rb +++ b/lib/syskit/coordination/models/data_monitor.rb @@ -19,6 +19,7 @@ class DataMonitor # return an object that matches the description of # {Coordination::DataMonitor#predicate} attr_reader :predicate + # @return [Boolean] whether instances of this model should # generate a {DataMonitoringError} when it triggers # @see #raise_exception diff --git a/lib/syskit/coordination/models/data_monitor_predicate_from_block.rb b/lib/syskit/coordination/models/data_monitor_predicate_from_block.rb index a4140aa09..fc767cf19 100644 --- a/lib/syskit/coordination/models/data_monitor_predicate_from_block.rb +++ b/lib/syskit/coordination/models/data_monitor_predicate_from_block.rb @@ -16,6 +16,7 @@ class DataMonitorPredicateFromBlock # from a reader model to the index in the list of samples # (and, therefore, in the block's argument list) attr_reader :stream_to_index + # @return [Boolean] indicates whether #call has been received # with a new sample since the last call to #finalize def has_new_sample? diff --git a/lib/syskit/coordination/models/data_monitoring_table.rb b/lib/syskit/coordination/models/data_monitoring_table.rb index 4418f1cf9..a18bf38ce 100644 --- a/lib/syskit/coordination/models/data_monitoring_table.rb +++ b/lib/syskit/coordination/models/data_monitoring_table.rb @@ -50,7 +50,8 @@ def monitor(name, *data_streams) obj.reader elsif obj.respond_to?(:port) obj - else raise ArgumentError, "#{obj} does not seem to be a valid data source. Expected a port or a data reader" + else + raise ArgumentError, "#{obj} does not seem to be a valid data source. Expected a port or a data reader" end end diff --git a/lib/syskit/coordination/task_script_extension.rb b/lib/syskit/coordination/task_script_extension.rb index adf1a101b..929eb0efd 100644 --- a/lib/syskit/coordination/task_script_extension.rb +++ b/lib/syskit/coordination/task_script_extension.rb @@ -15,14 +15,16 @@ def wait_until_ready(writer) def respond_to_missing?(m, include_private) if m.to_s =~ /_port$/ instance_for(model.root).respond_to?(m) - else super + else + super end end def method_missing(m, *args, &block) if m.to_s =~ /_port$/ instance_for(model.root).send(m, *args, &block) - else super + else + super end end end diff --git a/lib/syskit/data_service.rb b/lib/syskit/data_service.rb index c17616d1e..d294ab886 100644 --- a/lib/syskit/data_service.rb +++ b/lib/syskit/data_service.rb @@ -72,13 +72,14 @@ def find_device_attached_to(service = nil) unless (service = find_data_service(service)) known_services = each_data_service.map(&:name).sort.join(", ") raise ArgumentError, - "#{service} is not a known service of #{self}, "\ + "#{service} is not a known service of #{self}, " \ "known services are: #{known_services}" end elsif !service || service.kind_of?(Syskit::Models::DataServiceModel) driver_services = if service then model.find_all_data_services_from_type(service) - else model.each_master_driver_service.to_a + else + model.each_master_driver_service.to_a end if driver_services.empty? raise ArgumentError, "#{self} is not attached to any device" @@ -87,7 +88,7 @@ def find_device_attached_to(service = nil) if driver_services.size > 1 driver_service_names = driver_services.map(&:name).sort.join(", ") raise ArgumentError, - "#{self} handles more than one device, you must "\ + "#{self} handles more than one device, you must " \ "specify one of #{driver_service_names} explicitely" end @@ -152,13 +153,11 @@ def each_com_bus_device # @yieldparam device [DeviceInstance] a device that is using self as # a communication bus # @see each_attached_device - def each_declared_attached_device + def each_declared_attached_device(&block) return enum_for(:each_declared_attached_device) unless block_given? each_com_bus_device do |combus| - combus.each_attached_device do |dev| - yield(dev) - end + combus.each_attached_device(&block) end end @@ -191,15 +190,15 @@ def attach(task) client_in_srv = dev.combus_client_in_srv client_out_srv = dev.combus_client_out_srv - if !combus_m.lazy_dispatch? + if combus_m.lazy_dispatch? + bus_srv = combus.require_dynamic_service_for_device(self, dev) + else unless (bus_srv = find_data_service(dev.name)) raise ArgumentError, - "combus task #{self} was expected to have "\ - "a service named #{dev.name} to connect "\ + "combus task #{self} was expected to have " \ + "a service named #{dev.name} to connect " \ "to the device of the same name, but has none" end - else - bus_srv = combus.require_dynamic_service_for_device(self, dev) end client_out_srv.bind(task).connect_to bus_srv if dev.client_to_bus? diff --git a/lib/syskit/dependency_injection.rb b/lib/syskit/dependency_injection.rb index cebc58be3..7240ff629 100644 --- a/lib/syskit/dependency_injection.rb +++ b/lib/syskit/dependency_injection.rb @@ -6,8 +6,7 @@ class DependencyInjection extend Logger::Hierarchy extend Logger::Forward - attr_reader :explicit - attr_reader :defaults + attr_reader :explicit, :defaults, :resolved def hash [explicit, defaults].hash @@ -33,8 +32,6 @@ def initialize(*base) add(*base) unless base.empty? end - attr_reader :resolved - def initialize_copy(from) super @explicit = from.explicit.dup @@ -142,7 +139,8 @@ def add_explicit(mappings) # However, when this happens, we can simply ignore the # identity selection if v2 == k then v1 - else v2 + else + v2 end end @@ -190,7 +188,7 @@ def self.normalize_selection(selection) (!key.kind_of?(Class) || !(key <= Component)) raise ArgumentError, - 'found #{value} as a selection key, but only names, '\ + 'found #{value} as a selection key, but only names, ' \ "component models and data service models are allowed" end @@ -204,13 +202,11 @@ def self.normalize_selection(selection) next end - if value.respond_to?(:fullfills?) - unless value.fullfills?(key) - raise ArgumentError, - "found #{value.name}(of class #{value.class}) "\ - "as a selection for #{key.name}, but "\ - "#{value.name} does not fullfill #{key.name}" - end + if value.respond_to?(:fullfills?) && !value.fullfills?(key) + raise ArgumentError, + "found #{value.name}(of class #{value.class}) " \ + "as a selection for #{key.name}, but " \ + "#{value.name} does not fullfill #{key.name}" end if key <= Component @@ -338,8 +334,8 @@ def selection_for(name, requirements) selected_instance ||= sel_task if selected_instance != sel_task raise ArgumentError, - "task instances #{selected_instance} and #{sel_m} "\ - "are both selected for #{required_m || requirements}, "\ + "task instances #{selected_instance} and #{sel_m} " \ + "are both selected for #{required_m || requirements}, " \ "but they are not compatible" end end @@ -368,7 +364,7 @@ def selection_for(name, requirements) selected_instance.fullfills?(requirements, requirements.arguments) unless valid_selected_instance raise ArgumentError, - "explicitly selected #{selected_instance}, "\ + "explicitly selected #{selected_instance}, " \ "but it does not fullfill the required #{requirements}" end @@ -444,17 +440,19 @@ def resolve_names(mapping = {}) if v.respond_to?(:to_str) result = DependencyInjection .find_name_resolution(v, mapping) - if !result + if result + result + else unresolved << v v - else result end elsif v.respond_to?(:resolve_names) # The value is e.g. an InstanceRequirements unresolved |= v.resolve_names(mapping) v - else v + else + v end end unresolved @@ -470,7 +468,8 @@ def remove_unresolved map! do |value| if value.respond_to?(:to_str) nil - else value + else + value end end end @@ -490,7 +489,7 @@ def map(&block) # This method yields the [selection_key, selected_instance] pairs to # a block that must return a new value for the for # +selected_instance+. It modifies +self+ - def map! + def map!(&block) # Invalidate the @resolved cached @resolved = nil changed = false @@ -504,9 +503,7 @@ def map! .resolve_recursive_selection_mapping(explicit) end - @defaults.map! do |v| - yield(v) - end + @defaults.map!(&block) self end @@ -562,10 +559,8 @@ def self.resolve_selection_recursively(value, spec) component_model = value.component_model if (selected = spec[component_model]) && - !selected.respond_to?(:to_str) - if selected != component_model - new_value = selected.selected_for(value).selected_model - end + !selected.respond_to?(:to_str) && selected != (component_model) + new_value = selected.selected_for(value).selected_model end when Module new_value = spec[value] @@ -587,7 +582,7 @@ def self.resolve_selection_recursively(value, spec) def self.normalize_selected_object(value, key = nil) unless value raise ArgumentError, - "found nil as selection for #{key}, "\ + "found nil as selection for #{key}, " \ "but it is not an acceptable selection value anymore" end @@ -607,15 +602,15 @@ def self.normalize_selected_object(value, key = nil) value = value.to_instance_requirements elsif key raise ArgumentError, - "found #{value}(of class #{value.class}) as a selection "\ - "for #{key}, but only names, component models, "\ - "components, data service models and bound data services "\ + "found #{value}(of class #{value.class}) as a selection " \ + "for #{key}, but only names, component models, " \ + "components, data service models and bound data services " \ "are allowed" else raise ArgumentError, - "found #{value}(of class #{value.class}) as a selection, "\ - "for #{key}, but only names, component models, "\ - "components, data service models and bound data services "\ + "found #{value}(of class #{value.class}) as a selection, " \ + "for #{key}, but only names, component models, " \ + "components, data service models and bound data services " \ "are allowed" end end @@ -654,11 +649,11 @@ def self.resolve_default_selections(using_spec, default_selections) next if IGNORED_MODELS.include?(m) if selection.respond_to?(:find_all_data_services_from_type) && - m.kind_of?(Models::DataServiceModel) + m.kind_of?(Models::DataServiceModel) && selection.find_all_data_services_from_type(m).size != (1) # Ignore if it is provided multiple times by the # selection - next if selection.find_all_data_services_from_type(m).size != 1 + next end if using_spec[m] diff --git a/lib/syskit/dependency_injection_context.rb b/lib/syskit/dependency_injection_context.rb index bdcf2445d..eab12a2d6 100644 --- a/lib/syskit/dependency_injection_context.rb +++ b/lib/syskit/dependency_injection_context.rb @@ -55,7 +55,7 @@ def initialize(base = nil) nil else raise ArgumentError, - "expected either a selection hash or a DependencyInjection "\ + "expected either a selection hash or a DependencyInjection " \ "object as base selection, got #{base}" end end @@ -100,15 +100,15 @@ def concat(context) # when the execution quits the block # @return [void] def save - if !block_given? - @savepoints << stack.size - else + if block_given? save begin yield ensure restore end + else + @savepoints << stack.size end end diff --git a/lib/syskit/deployment.rb b/lib/syskit/deployment.rb index 15ddeead3..d830a1337 100644 --- a/lib/syskit/deployment.rb +++ b/lib/syskit/deployment.rb @@ -33,6 +33,8 @@ class Deployment < ::Roby::Task # rubocop:disable Metrics/ClassLength argument :ready_polling_period, default: 0.1 argument :logger_task, default: nil argument :logger_name, default: nil + argument :logging_enabled, default: nil + argument :register_on_name_server, default: nil argument :read_only, default: nil # The underlying process object @@ -153,17 +155,13 @@ def deployed_orogen_model_by_name(name) mappings = name_mappings .map { |k, v| "#{k} => #{v}" }.join(", ") raise ArgumentError, - "no task called #{name} in "\ - "#{self.class.deployment_name}, available tasks are "\ + "no task called #{name} in " \ + "#{self.class.deployment_name}, available tasks are " \ "#{available} using name mappings #{mappings}" end orogen_task_deployment end - def deployed_model_by_orogen_model(orogen_model) - TaskContext.model_for(orogen_model.task_model) - end - # @api private # # Create and add a task model supported by this deployment @@ -188,34 +186,32 @@ def create_deployed_task( syskit_task_model, scheduler_task, auto_conf: false ) mapped_name = name_mappings[orogen_task_deployment_model.name] - if ready? - unless (remote_handles = remote_task_handles[mapped_name]) - raise InternalError, - "no remote handle describing #{mapped_name} in #{self}"\ - "(got #{remote_task_handles.keys.sort.join(', ')})" - end + if ready? && !(remote_handles = remote_task_handles[mapped_name]) + raise InternalError, + "no remote handle describing #{mapped_name} in #{self}" \ + "(got #{remote_task_handles.keys.sort.join(', ')})" end if task_context_in_fatal?(mapped_name) raise TaskContextInFatal.new(self, mapped_name), - "trying to create task for FATAL_ERROR component #{mapped_name} "\ + "trying to create task for FATAL_ERROR component #{mapped_name} " \ "from #{self}" end - base_syskit_task_model = deployed_model_by_orogen_model( + base_syskit_task_model = model.resolve_syskit_model_for_deployed_task( orogen_task_deployment_model ) syskit_task_model ||= base_syskit_task_model unless syskit_task_model <= base_syskit_task_model raise ArgumentError, - "incompatible explicit selection of task model "\ - "#{syskit_task_model} for the model of #{mapped_name} in #{self}, "\ + "incompatible explicit selection of task model " \ + "#{syskit_task_model} for the model of #{mapped_name} in #{self}, " \ "expected #{base_syskit_task_model} or one of its subclasses" end - is_read_only_task = read_only.include?(mapped_name) - plan.add(task = syskit_task_model - .new(orocos_name: mapped_name, read_only: is_read_only_task)) + task = syskit_task_model + .new(orocos_name: mapped_name, read_only: read_only?(mapped_name)) + plan.add(task) task.executed_by self if scheduler_task task.depends_on scheduler_task, role: "scheduler" @@ -228,6 +224,10 @@ def create_deployed_task( task end + def read_only?(mapped_name) + read_only.include?(mapped_name) + end + # Returns an task instance that represents the given task in this # deployment. # @@ -239,7 +239,7 @@ def create_deployed_task( def task(name, syskit_task_model = nil) if finishing? || finished? raise InvalidState, - "#{self} is either finishing or already "\ + "#{self} is either finishing or already " \ "finished, you cannot call #task" end @@ -289,20 +289,14 @@ def task(name, syskit_task_model = nil) spawn_options = spawn_options.merge( output: "%m-%p.txt", - wait: false, - cmdline_args: options + cmdline_args: options, + register_on_name_server: register_on_name_server? ) - if log_dir - spawn_options = spawn_options.merge(working_directory: log_dir) - else - spawn_options.delete(:working_directory) - end - Deployment.info do - "starting deployment #{process_name} using "\ - "#{model.deployment_name} on #{arguments[:on]} with "\ - "#{spawn_options} and mappings #{name_mappings}" + "starting deployment #{process_name} using " \ + "#{model.deployment_name} on #{arguments[:on]} with " \ + "#{spawn_options} and mappings #{name_mappings}" end @orocos_process = process_server_config.client.start( @@ -365,10 +359,11 @@ def logger_name # @return [TaskContext,nil] either the logging task, or nil if this # deployment has none def logger_task + return unless logging_enabled? + if arguments[:logger_task] @logger_task = arguments[:logger_task] elsif @logger_task&.reusable? - @logger_task elsif (logger_name = self.logger_name) logger_task = each_executed_task .find { |t| t.orocos_name == logger_name } @@ -380,8 +375,9 @@ def logger_task end @logger_task&.default_logger = true - @logger_task end + + @logger_task ||= process_server_config.default_logger_task(plan) end # Instanciates a new default logger @@ -395,7 +391,7 @@ def instanciate_default_logger_task(logger_name) return end - syskit_model = deployed_model_by_orogen_model(orogen_model) + syskit_model = model.resolve_syskit_model_for_deployed_task(orogen_model) return unless syskit_model.fullfills?(LoggerService) # Automatic setup by @@ -468,16 +464,6 @@ def log_port?(port) end end - on :start do |_event| - handles_from_plan = {} - each_parent_object(Roby::TaskStructure::ExecutionAgent) do |task| - if orocos_task = task.orocos_task - handles_from_plan[task.orocos_name] = orocos_task - end - end - schedule_ready_event_monitor(handles_from_plan) - end - # @api private # # Event used to quit the ready monitor started by @@ -487,49 +473,58 @@ def log_port?(port) attr_reader :quit_ready_event_monitor # @api private - # - # Schedule a promise to resolve the task handles - # - # It will reschedule itself until the process is ready, and will - # emit the ready event when it happens - def schedule_ready_event_monitor( - handles_from_plan, ready_polling_period: self.ready_polling_period - ) - distance_to_syskit = self.distance_to_syskit - promise = execution_engine.promise(description: "#{self}:ready_event_monitor") do - resolve_remote_task_handles(handles_from_plan) + # This schedules an asynchronous process to connect to the tasks before + # ready is emitted. I.e. the process is not ready after the method returns. + # + # @param [{String=>String}] ior_mappings the mappings of the form + # { task_name => ior }. This is necessary because the Syskit remote process has + # no information about the IORs until now. + def update_remote_tasks(ior_mappings) + orocos_process.define_ior_mappings(ior_mappings) + begin + remote_tasks = orocos_process.resolve_all_tasks + rescue StandardError => e + ready_event.emit_failed(e) + return end - promise.on_success(description: "#{self}#schedule_ready_event_monitor#emit") do |remote_tasks| - if running? && !finishing? && remote_tasks - @remote_task_handles = remote_tasks + + promise = execution_engine.promise( + description: "#{self}#update_remote_tasks#resolve handles" + ) do + resolve_remote_task_handles(remote_tasks) + end + promise.on_success( + description: "#{self}#update_remote_tasks#success" + ) do |remote_task_handles| + if running? && !finishing? + @remote_task_handles = remote_task_handles ready_event.emit end end promise.on_error(description: "#{self}#emit_failed") do |reason| - ready_event.emit_failed(reason) if !finishing? || !finished? + ready_event.emit_failed(reason) unless finishing? || finished? end + ready_event.pending([]) ready_event.achieve_asynchronously( promise, emit_on_success: false, on_failure: :nothing ) end - def resolve_remote_task_handles( - handles_from_plan, ready_polling_period: self.ready_polling_period - ) - until (handles = orocos_process.resolve_all_tasks(handles_from_plan)) - return if quit_ready_event_monitor.set? + def resolve_remote_task_handles(remote_tasks) + return if quit_ready_event_monitor.set? - sleep ready_polling_period - end - - handles.transform_values do |remote_task| - state_reader, state_getter = create_state_access(remote_task, distance: distance_to_syskit) + remote_tasks.transform_values do |remote_task| + state_reader, state_getter = + create_state_access(remote_task, distance: distance_to_syskit) properties = remote_task.property_names.map do |p_name| p = remote_task.raw_property(p_name) [p, p.raw_read.freeze] end current_configuration = CurrentTaskConfiguration.new(nil, [], Set.new) - RemoteTaskHandles.new(remote_task, state_reader, state_getter, properties, false, current_configuration) + RemoteTaskHandles.new( + remote_task, state_reader, state_getter, + properties, false, current_configuration + ) end end @@ -709,12 +704,12 @@ def mark_changed_configuration_as_not_reusable(changed) # @api private def setup_task_handles(remote_tasks) model.each_orogen_deployed_task_context_model do |act| - name = orocos_process.get_mapped_name(act.name) + name = orocos_process.mapped_name_of(act.name) unless remote_tasks.key?(name) raise InternalError, - "expected #{orocos_process}'s reported tasks to "\ - "include '#{name}' (mapped from '#{act.name}'), "\ - "but got handles only for "\ + "expected #{orocos_process}'s reported tasks to " \ + "include '#{name}' (mapped from '#{act.name}'), " \ + "but got handles only for " \ "#{remote_tasks.keys.sort.join(' ')}" end end @@ -728,8 +723,8 @@ def setup_task_handles(remote_tasks) task.initialize_remote_handles(remote_handles) else root_exception = InternalError.exception( - "#{task} is supported by #{self} but there does "\ - "not seem to be any task called #{task.orocos_name} "\ + "#{task} is supported by #{self} but there does " \ + "not seem to be any task called #{task.orocos_name} " \ "on this deployment" ) task.failed_to_start!( @@ -819,7 +814,9 @@ def stop_prepare def stop_cleanly(promise, remote_task_handles) promise.then(description: "#{self}.stop_event - cleaning RTT tasks") do - remote_task_handles.each_value do |remote_task| + remote_task_handles.each do |mapped_name, remote_task| + next if read_only?(mapped_name) + begin if remote_task.handle.rtt_state == :STOPPED remote_task.handle.cleanup(false) @@ -841,7 +838,7 @@ def stop_kill(promise, remote_task_handles, hard: true) end.on_success(description: "#{self}#stop_event - kill") do ready_to_die! begin - orocos_process.kill(false, cleanup: false, hard: hard) + orocos_process.kill(hard: hard) rescue Orocos::ComError # The underlying process server cannot be reached. Just emit # failed ourselves @@ -924,5 +921,21 @@ def opportunistic_recovery_from_quarantine # cleaned up, but stopped). Kill the process to avoid further damage kill! if running? end + + def logging_enabled? + if logging_enabled.nil? + process_server_config.logging_enabled? + else + logging_enabled + end + end + + def register_on_name_server? + if register_on_name_server.nil? + process_server_config.register_on_name_server? + else + register_on_name_server + end + end end end diff --git a/lib/syskit/droby/v5/droby_dump.rb b/lib/syskit/droby/v5/droby_dump.rb index 487b1312a..b648ce95d 100644 --- a/lib/syskit/droby/v5/droby_dump.rb +++ b/lib/syskit/droby/v5/droby_dump.rb @@ -89,7 +89,7 @@ def use_global_loader=(flag) return if flag == @use_global_loader raise ArgumentError, - "cannot change use_global_loader after the loader has "\ + "cannot change use_global_loader after the loader has " \ "been created" end @@ -171,8 +171,7 @@ module ComBusDumper include Roby::DRoby::V5::ModelDumper class DRoby < Roby::DRoby::V5::DRobyModel - attr_reader :message_type - attr_reader :lazy_dispatch + attr_reader :message_type, :lazy_dispatch def initialize(message_type, lazy_dispatch, *args) @message_type = message_type @@ -220,8 +219,8 @@ def droby_dump(peer) module TypelibTypeDumper # Marshalling representation of a typelib value class DRoby - attr_reader :byte_array - attr_reader :type + attr_reader :byte_array, :type + def initialize(byte_array, type) @byte_array = byte_array @type = type @@ -242,6 +241,7 @@ module TypelibTypeModelDumper # Class used to transfer the definition of a type class DRoby attr_reader :name, :xml + def initialize(name, xml) @name = name @xml = xml diff --git a/lib/syskit/dynamic_port_binding.rb b/lib/syskit/dynamic_port_binding.rb index 35c4151e5..5b00430c6 100644 --- a/lib/syskit/dynamic_port_binding.rb +++ b/lib/syskit/dynamic_port_binding.rb @@ -99,6 +99,10 @@ def attach_to_task(task) # the port was updated, and false otherwise. The tuple's second element # is the new resolved port which may be nil if no ports can be found def update + if @resolved_port && @port_resolver&.current_selection_valid?(@resolved_port) + return false, @resolved_port + end + port = @port_resolver&.update return false, @resolved_port if @resolved_port == port @@ -209,7 +213,7 @@ def create_accessor(port) # @return [nil,Typelib::Type] nil if there is has never been any data, or if # there are no underlying port. A data sample otherwise. def read(sample = nil) - return unless (sample = @resolved_accessor&.read(sample)) + return if (sample = @resolved_accessor&.read(sample)).nil? @value_resolver.__resolve(sample) end @@ -221,7 +225,7 @@ def read(sample = nil) # @return [nil,Typelib::Type] nil if there is no new data or if there are # no underlying port. A data sample otherwise. def read_new(sample = nil) - return unless (sample = @resolved_accessor&.read_new(sample)) + return if (sample = @resolved_accessor&.read_new(sample)).nil? @value_resolver.__resolve(sample) end @@ -294,6 +298,10 @@ def initialize(plan, matcher) @last_provider_task = nil end + def current_selection_valid?(port) + @matcher === port + end + def update port = @matcher.each_in_plan(@plan).first port&.to_actual_port diff --git a/lib/syskit/exceptions.rb b/lib/syskit/exceptions.rb index 68410dded..25085e706 100644 --- a/lib/syskit/exceptions.rb +++ b/lib/syskit/exceptions.rb @@ -23,9 +23,7 @@ class InvalidState < RuntimeError; end # Raised when a provides declaration does not match the underlying task # interface class InvalidProvides < SpecError - attr_reader :original_error - attr_reader :model - attr_reader :required_service + attr_reader :original_error, :model, :required_service def initialize(model, required_service, original_error = nil) @model = model @@ -87,10 +85,7 @@ def pretty_print(pp) # Exception raised during instanciation if there is an ambiguity for a # composition child class AmbiguousIndirectCompositionSelection < Ambiguous - attr_reader :composition_model - attr_reader :child_name - attr_reader :selection - attr_reader :candidates + attr_reader :composition_model, :child_name, :selection, :candidates def initialize(composition_model, child_name, selection, candidates) @composition_model = composition_model @@ -185,8 +180,7 @@ def pretty_print(pp) # Refinement of NoMatchingService for a composition child. It adds the # information of the composition / child name class NoMatchingServiceForCompositionChild < NoMatchingService - attr_reader :composition_model - attr_reader :child_name + attr_reader :composition_model, :child_name def initialize(composition_model, child_name, task_model, required_service) @composition_model = composition_model @@ -238,8 +232,7 @@ def pretty_print(pp) # Exception raised in composition instanciations if a selected component # model provides multipe services that fullfills a child's model class AmbiguousServiceMapping < AmbiguousServiceSelection - attr_reader :composition_model - attr_reader :child_name + attr_reader :composition_model, :child_name def initialize(composition_model, child_name, task_model, required_service, candidates) super(task_model, required_service, candidates) @@ -257,9 +250,7 @@ def pretty_print(pp) # Exception raised during the merge steps, if a merge is possible (i.e. # a task provides the required service), but ambiguous class AmbiguousImplicitServiceSelection < AmbiguousServiceSelection - attr_reader :task - attr_reader :merged_task - attr_reader :compositions + attr_reader :task, :merged_task, :compositions def initialize(task, merged_task, required_service, candidates) super(task.model, required_service, candidates) @@ -333,7 +324,7 @@ def pretty_print(pp) abstract_tasks.each do |task, (parents, candidates)| pp.breakable - pp.text task.to_s.gsub(/Syskit::/, "").to_s + pp.text task.to_s.gsub("Syskit::", "").to_s pp.nest(2) do pp.breakable if candidates @@ -352,7 +343,7 @@ def pretty_print(pp) pp.breakable pp.seplist(parents) do |parent| role, parent = parent - pp.text "child #{role.to_a.first} of #{parent.to_s.gsub(/Syskit::/, '')}" + pp.text "child #{role.to_a.first} of #{parent.to_s.gsub('Syskit::', '')}" end end end @@ -415,13 +406,13 @@ def pretty_print(pp) candidates = self.candidates[task] pp.breakable - pp.text "for #{task.to_s.gsub(/Syskit::/, '')}" + pp.text "for #{task.to_s.gsub('Syskit::', '')}" pp.nest(2) do unless parents.empty? pp.breakable pp.seplist(parents) do |parent| role, parent = parent - pp.text "child #{role.to_a.first} of #{parent.to_s.gsub(/Syskit::/, '')}" + pp.text "child #{role.to_a.first} of #{parent.to_s.gsub('Syskit::', '')}" end end @@ -446,46 +437,48 @@ def pretty_print(pp) end class ConflictingDeviceAllocation < SpecError - attr_reader :device - attr_reader :tasks - attr_reader :inputs + attr_reader :device, :tasks, :inputs def can_merge? !!@can_merge end - def initialize(device, task0, task1) + def initialize(device, task0, task1, toplevel_tasks_to_requirements = {}) @device = device @tasks = [task0, task1] - @can_merge = task0.can_merge?(task1) || task1.can_merge?(task0) - if can_merge? - # Mismatching inputs ... gather more info - @inputs = [] - @inputs[0] = task0.each_concrete_input_connection.to_a - @inputs[1] = task1.each_concrete_input_connection.to_a + + solver = NetworkGeneration::MergeSolver.new(task0.plan) + @merge_result = solver.resolve_merge(task0, task1, {}) + @involved_definitions = @tasks.map do |t| + find_all_related_syskit_actions(t, toplevel_tasks_to_requirements) end end + def find_all_related_syskit_actions(task, toplevel_tasks_to_requirements) + result = [] + while task + result.concat(toplevel_tasks_to_requirements[task] || []) + task = task.each_parent_task.first + end + result + end + def pretty_print(pp) - pp.text "device #{device.name} is assigned to two tasks" - if can_merge? - pp.text " that have mismatching inputs" - tasks.each_with_index do |t, i| - pp.breakable - t.pretty_print(pp) - pp.breakable - pp.text "#{inputs[i].size} input(s):" - inputs[i].each do |source_task, source_port, sink_port| + pp.text "device '#{device.name}' of type #{device.model} is assigned " + pp.text "to two tasks that cannot be merged" + pp.breakable + @merge_result.pretty_print_failure(pp) + @involved_definitions.each_with_index do |defs, i| + next if defs.empty? + + pp.breakable + pp.text "Chain #{i + 1} is needed by the following definitions:" + pp.nest(2) do + defs.each do |d| pp.breakable - pp.text " #{source_task}.#{source_port} -> #{sink_port}" + pp.text d.to_s end end - else - pp.text " that cannot be merged" - tasks.each do |t| - pp.breakable - t.pretty_print(pp) - end end end end @@ -559,8 +552,8 @@ def pretty_print(pp) orogen_model = deployed_task.configured_deployment .orogen_model pp.text( - "task #{deployed_task.mapped_task_name} from deployment "\ - "#{orogen_model.name} defined in "\ + "task #{deployed_task.mapped_task_name} from deployment " \ + "#{orogen_model.name} defined in " \ "#{orogen_model.project.name} on #{process_server_name}" ) pp.nest(2) do @@ -579,6 +572,27 @@ def pretty_print(pp) end end + # Exception raised at the end of #resolve if some tasks are referring to non-existing + # configurations + class MissingConfigurationSection < SpecError + # Association of tasks and the sections that are missing + attr_reader :missing_sections_by_task + + def initialize(missing_sections_by_task) + @missing_sections_by_task = missing_sections_by_task + end + + def pretty_print(pp) + pp.text "the following configuration sections are used but do not exist" + @missing_sections_by_task.each do |task, sections| + pp.breakable + pp.text "'#{sections.join('\', \'')}', in use by:" + pp.breakable + pp.text " #{task} (#{task.orogen_model.name})" + end + end + end + class InstanciationError < SpecError # The instanciation chain, i.e. an array of composition models that # were being instanciated @@ -665,10 +679,10 @@ class NameResolutionError < InstanciationError def initialize(missing_names) super() @missing_names = - if !missing_names.respond_to?(:each) - [missing_names] - else + if missing_names.respond_to?(:each) missing_names.to_a + else + [missing_names] end end @@ -685,6 +699,7 @@ def pretty_print(pp) # could be found class InvalidAutoConnection < RuntimeError attr_reader :source, :sink + def initialize(source, sink) @source = source @sink = sink @@ -784,7 +799,8 @@ def pretty_print(pp) value = value.each_required_model value = value.map do |v| if v.respond_to?(:short_name) then v.short_name - else v.to_s + else + v.to_s end end @@ -807,8 +823,7 @@ def pretty_print(pp) # # See Models.merge_model_lists class IncompatibleComponentModels < RuntimeError - attr_reader :model_a - attr_reader :model_b + attr_reader :model_a, :model_b def initialize(model_a, model_b) @model_a = model_a @@ -824,6 +839,7 @@ def pretty_print(pp) # port directions class WrongPortConnectionDirection < RuntimeError attr_reader :source, :sink + def initialize(source, sink) @source = source @sink = sink @@ -838,13 +854,14 @@ def pretty_print(pp) # port types class WrongPortConnectionDataTypes < RuntimeError attr_reader :source, :sink + def initialize(source, sink) @source = source @sink = sink end def pretty_print(pp) - pp.text "cannot connect output port #{source} to input port #{sink}: "\ + pp.text "cannot connect output port #{source} to input port #{sink}: " \ "data types mismatch (resp. #{source.type} and #{sink.type})" end end @@ -861,6 +878,7 @@ class UnsupportedConnectionType < RuntimeError # of the same component class SelfConnection < RuntimeError attr_reader :source, :sink + def initialize(source, sink) @source = source @sink = sink @@ -874,9 +892,8 @@ def pretty_print(pp) # Exception raised when port mappings cannot be computed because two # source ports have the same name class AmbiguousPortMappings < Ambiguous - attr_reader :model_a - attr_reader :model_b - attr_reader :port_name + attr_reader :model_a, :model_b, :port_name + def initialize(model_a, model_b, port_name) @model_a = model_a @model_b = model_b @@ -916,6 +933,7 @@ def pretty_print(pp) # The reason is in the exception message class InvalidDynamicServiceBlock < RuntimeError attr_reader :dynamic_service + def initialize(dynamic_service) @dynamic_service = dynamic_service end @@ -927,6 +945,7 @@ def initialize(dynamic_service) # services have ports with that name class AmbiguousPortOnCompositeModel < Ambiguous attr_reader :model, :models, :port_name, :candidates + def initialize(model, models, port_name, candidates) @model = model @models = models @@ -983,6 +1002,7 @@ def pretty_print(pp) # been called on a port that is being modified class ModifyingFinalizedPortInfo < ArgumentError attr_reader :task, :port_name, :done_at + def initialize(task, port_name, done_at, propagation_class_name) @task = task @port_name = port_name @@ -1011,6 +1031,7 @@ def pretty_print(pp) class DataflowPropagationError < Roby::ExceptionBase attr_reader :task, :port_name + def initialize(exception, task, port_name) @task = task @port_name = port_name diff --git a/lib/syskit/graphviz.rb b/lib/syskit/graphviz.rb index f64eaffd7..bf50a327f 100644 --- a/lib/syskit/graphviz.rb +++ b/lib/syskit/graphviz.rb @@ -6,6 +6,7 @@ module Syskit # Used by the to_dot* methods for color allocation attr_reader :current_color + # A set of colors to be used in graphiz graphs COLOR_PALETTE = %w[#FF9955 #FF0000 #bb9c21 #37c637 #62816e #2A7FFF #AA00D4 #D40055 #0000FF].freeze @@ -104,12 +105,12 @@ def uri_for(type) end def escape_dot_uri(string) - string.gsub(//, ">") + string.gsub("<", "<") + .gsub(">", ">") end def escape_dot(string) - escape_dot_uri(string).gsub(/[^\[\]&;:\w\. ]/, "_") + escape_dot_uri(string).gsub(/[^\[\]&;:\w. ]/, "_") end def annotate_tasks(annotations) @@ -220,7 +221,7 @@ def to_file(kind, format, output_io, graphviz_tool: "dot", **display_options) pattern = "syskit_graphviz_%i.dot" i += 1 while File.file?(format(pattern, i)) path = format(pattern, i) - File.open(path, "w") { |io| io.write send(kind, display_options) } + File.write(path, send(kind, display_options)) "saved graphviz input in #{path}" end raise DotFailedError, "dot reported an error generating the graph" @@ -277,7 +278,7 @@ def relation_to_dot(accessor:, dot_edge_mark: "->", label << "#{key}=#{format_edge_info(edge_info[key])}" end all_tasks << child_task - result << " #{task.dot_id} #{dot_edge_mark} "\ + result << " #{task.dot_id} #{dot_edge_mark} " \ "#{child_task.dot_id} [label=\"#{label.join('\n')}\"];" end end @@ -285,8 +286,8 @@ def relation_to_dot(accessor:, dot_edge_mark: "->", all_tasks.each do |task| attributes = [] task_label = format_task_label(task) - label = format(' '\ + label = format('
' \ "%s
", task_label: task_label) attributes << "label=<#{label}>" attributes << "href=\"plan://syskit/#{task.dot_id}\"" if make_links? @@ -542,8 +543,8 @@ def dataflow(remove_compositions: false, if deployment result << " subgraph cluster_#{deployment.dot_id} {" task_label, = format_task_label(deployment, task_colors) - label = ' '\ + label = '
' \ "#{task_label}
" result << " label=< #{label} >;" end @@ -605,7 +606,7 @@ def dot_id(object, context = nil) end raise ArgumentError, - "don't know how to generate a dot ID for #{object} "\ + "don't know how to generate a dot ID for #{object} " \ "in context #{context}" end end @@ -634,10 +635,10 @@ def render_task(task, input_ports, output_ports, style = nil) end task_label, = format_task_label(task) - task_label = ' '\ + task_label = '
' \ "#{task_label}
" - result << " label#{task.dot_id} [#{task_link},shape=none,"\ + result << " label#{task.dot_id} [#{task_link},shape=none," \ "label=< #{task_label} >];" unless input_ports.empty? @@ -691,8 +692,8 @@ def format_annotations(annotations, key = nil, include_empty: false) values = values.map { |v| v.tr("<>", "[]") } values = values.map { |v| v.tr("{}", "[]") } - "#{category}"\ + "#{category}" \ "#{values.first}\n" + values[1..-1].map { |v| "#{v}" }.join("\n") end.flatten diff --git a/lib/syskit/gui/app_start_dialog.rb b/lib/syskit/gui/app_start_dialog.rb index 7ec772fa4..0d1f2128f 100644 --- a/lib/syskit/gui/app_start_dialog.rb +++ b/lib/syskit/gui/app_start_dialog.rb @@ -15,6 +15,12 @@ class AppStartDialog < Qt::Dialog # @return [Qt::CheckBox] attr_reader :start_controller + # The checkbox allowing to choose whether Roby app should be run + # on single mode. For more information, check [Roby::Application#single] + # + # @return [Qt::CheckBox] + attr_reader :single + # Text used to allow the user to not load any robot configuration NO_ROBOT = " -- None -- " @@ -37,6 +43,9 @@ def initialize(names, parent = nil, default_robot_name: "default") layout.add_widget(@start_controller = Qt::CheckBox.new("Start controller")) start_controller.checked = true + layout.add_widget(@single = Qt::CheckBox.new("Run on single mode")) + single.checked = false + button_box = Qt::DialogButtonBox.new( Qt::DialogButtonBox::Ok | Qt::DialogButtonBox::Cancel ) @@ -51,9 +60,10 @@ def initialize(names, parent = nil, default_robot_name: "default") # configuration should be loaded def selected_name txt = robot_names.current_text - if txt != NO_ROBOT + if txt == NO_ROBOT + "" + else txt - else "" end end @@ -64,6 +74,13 @@ def start_controller? start_controller.checked? end + # Whether Roby app should be run on single mode + # + # @return [Boolean] + def single? + single.checked? + end + # Executes a {AppStartDialog} in a modal way and returns the result # # @return [nil,(String,Boolean)] either nil if the dialog was @@ -74,7 +91,7 @@ def start_controller? def self.exec(names, parent = nil, default_robot_name: "default") dialog = new(names, parent, default_robot_name: default_robot_name) if Qt::Dialog::Accepted == dialog.exec - [dialog.selected_name, dialog.start_controller?] + [dialog.selected_name, dialog.start_controller?, dialog.single?] end end end diff --git a/lib/syskit/gui/batch_manager.rb b/lib/syskit/gui/batch_manager.rb index 0a3017a1b..2e3f7d20b 100644 --- a/lib/syskit/gui/batch_manager.rb +++ b/lib/syskit/gui/batch_manager.rb @@ -43,11 +43,11 @@ def process begin batch.__process rescue Exception => e # rubocop:disable Lint/RescueException - Roby.display_exception(STDOUT, e) + Roby.display_exception($stdout, e) Qt::MessageBox.critical( self, "Syskit Process Batch", - "failed to process batch: #{e.message}, "\ + "failed to process batch: #{e.message}, " \ "see console output for more details" ) end @@ -96,7 +96,7 @@ def start_job(action_name, action_arguments) def create_new_job(action_name, arguments = {}) action_model = @syskit.actions.find { |m| m.name == action_name } unless action_model - raise ArgumentError, "no action named #{action_name} found" + ::Kernel.raise ArgumentError, "no action named #{action_name} found" end if action_model.arguments.empty? @@ -137,7 +137,8 @@ def create_new_job(action_name, arguments = {}) action_name, action_options = dialog.result start_job(action_name, action_options) true - else false + else + false end end end @@ -159,7 +160,7 @@ def self.convert(obj) if parser.instance_eval(as_string) == obj as_string end - rescue Exception + rescue StandardError end end end @@ -189,7 +190,7 @@ def initialize(parent = nil, text = "") @error_message.hide @result = Parser.parse(self.text) accept - rescue Exception => e + rescue StandardError => e @error_message.text = e.message @error_message.show end diff --git a/lib/syskit/gui/component_network_base_view.rb b/lib/syskit/gui/component_network_base_view.rb index 32e76c452..75fcf2375 100644 --- a/lib/syskit/gui/component_network_base_view.rb +++ b/lib/syskit/gui/component_network_base_view.rb @@ -335,9 +335,7 @@ def save_svg(id) ) next unless file_name - File.open(file_name, "w") do |file| - file.write f.html - end + File.write(file_name, f.html) end end diff --git a/lib/syskit/gui/ide.rb b/lib/syskit/gui/ide.rb index cf5dfadcc..245763475 100644 --- a/lib/syskit/gui/ide.rb +++ b/lib/syskit/gui/ide.rb @@ -12,13 +12,7 @@ module Syskit module GUI # The main Syskit IDE window class IDE < Qt::Widget - attr_reader :layout - attr_reader :btn_reload_models - attr_reader :tab_widget - attr_reader :model_browser - attr_reader :runtime_state - attr_reader :connection_state - attr_reader :testing + attr_reader :layout, :btn_reload_models, :tab_widget, :model_browser, :runtime_state, :connection_state, :testing def initialize(parent = nil, runtime_only: false, diff --git a/lib/syskit/gui/instanciate.rb b/lib/syskit/gui/instanciate.rb index cc4254c7d..0f8ebab2e 100644 --- a/lib/syskit/gui/instanciate.rb +++ b/lib/syskit/gui/instanciate.rb @@ -9,16 +9,7 @@ module Syskit module GUI class Instanciate < Qt::Widget - attr_reader :apply_btn - attr_reader :instance_txt - - attr_reader :display - attr_reader :page - attr_reader :rendering - - attr_reader :exception_view - - attr_reader :permanent + attr_reader :apply_btn, :instance_txt, :display, :page, :rendering, :exception_view, :permanent def plan rendering.plan diff --git a/lib/syskit/gui/job_status_display.rb b/lib/syskit/gui/job_status_display.rb index ea8817514..b23368bc9 100644 --- a/lib/syskit/gui/job_status_display.rb +++ b/lib/syskit/gui/job_status_display.rb @@ -4,17 +4,7 @@ module Syskit module GUI class JobStatusDisplay < Qt::Widget - attr_reader :job - - attr_reader :ui_job_actions - attr_reader :ui_start - attr_reader :ui_restart - attr_reader :ui_drop - attr_reader :ui_clear - attr_reader :ui_state - attr_reader :exceptions - attr_reader :notifications - attr_reader :ui_notifications + attr_reader :job, :ui_job_actions, :ui_start, :ui_restart, :ui_drop, :ui_clear, :ui_state, :exceptions, :notifications, :ui_notifications attr_predicate :show_actions?, true @@ -39,7 +29,7 @@ def initialize(job, batch_manager) ].freeze def label - "##{job.job_id} #{job.action_name}" + "##{job.job_id} #{job.job_name}" end def create_ui @@ -144,21 +134,21 @@ def connect_to_hooks @batch_manager.process end end - ui_restart.connect(SIGNAL("clicked()")) do - arguments = job.action_arguments.dup - arguments.delete(:job_id) - if @batch_manager.create_new_job(job.action_name, arguments) - @batch_manager.drop_job(self) - if @actions_immediate - @batch_manager.process + if job.action_name + ui_restart.connect(SIGNAL("clicked()")) do + arguments = job.action_arguments.dup + arguments.delete(:job_id) + if @batch_manager.create_new_job(job.action_name, arguments) + @batch_manager.drop_job(self) + if @actions_immediate + @batch_manager.process + end end end - end - ui_start.connect(SIGNAL("clicked()")) do - arguments = job.action_arguments.dup - arguments.delete(:job_id) - if @batch_manager.create_new_job(job.action_name, arguments) - if @actions_immediate + ui_start.connect(SIGNAL("clicked()")) do + arguments = job.action_arguments.dup + arguments.delete(:job_id) + if @batch_manager.create_new_job(job.action_name, arguments) && @actions_immediate @batch_manager.process end end diff --git a/lib/syskit/gui/logging_configuration.rb b/lib/syskit/gui/logging_configuration.rb index 4642121a8..29bd46adc 100644 --- a/lib/syskit/gui/logging_configuration.rb +++ b/lib/syskit/gui/logging_configuration.rb @@ -4,7 +4,7 @@ require "vizkit/vizkit_items" require "vizkit/tree_view" require "Qt4" -require "syskit/shell_interface" +require "syskit/interface" require "syskit/gui/logging_configuration_item" require "roby/interface/exceptions" @@ -14,6 +14,7 @@ module GUI # manage basic Syskit's logging configuration class LoggingConfiguration < Qt::Widget attr_reader :model, :tree_view, :syskit, :item_name, :item_value, :pending_call + def initialize(syskit, parent = nil) super(parent) main_layout = Qt::VBoxLayout.new(self) diff --git a/lib/syskit/gui/logging_configuration_item.rb b/lib/syskit/gui/logging_configuration_item.rb index 40c000d1a..5664aa611 100644 --- a/lib/syskit/gui/logging_configuration_item.rb +++ b/lib/syskit/gui/logging_configuration_item.rb @@ -10,13 +10,8 @@ module GUI # A QStandardItem that displays a Sysit::ShellInterface::LoggingConfiguration # in a tree view class LoggingConfigurationItem < LoggingConfigurationItemBase - attr_reader :options - attr_reader :conf_logs_item_name - attr_reader :conf_logs_item_value - attr_reader :port_logs_item_name - attr_reader :port_logs_item_value - attr_reader :groups_item_name - attr_reader :groups_item_value + attr_reader :options, :conf_logs_item_name, :conf_logs_item_value, :port_logs_item_name, :port_logs_item_value, :groups_item_name, :groups_item_value + def initialize(logging_configuration, options = {}) super(logging_configuration) @options = options diff --git a/lib/syskit/gui/logging_configuration_item_base.rb b/lib/syskit/gui/logging_configuration_item_base.rb index 76b07d050..0012034d9 100644 --- a/lib/syskit/gui/logging_configuration_item_base.rb +++ b/lib/syskit/gui/logging_configuration_item_base.rb @@ -9,8 +9,8 @@ module GUI # Base class for most items in the LoggingConfiguration widget with # common functionality class LoggingConfigurationItemBase < Vizkit::VizkitItem - attr_reader :current_model - attr_reader :editing_model + attr_reader :current_model, :editing_model + def initialize(model) super() @current_model = deep_copy(model) @@ -33,7 +33,7 @@ def add_conf_item(label, accessor = nil) end item2.setter do |value| - @editing_model.method("#{accessor}=".to_sym).call value + @editing_model.method(:"#{accessor}=").call value end end diff --git a/lib/syskit/gui/logging_groups_item.rb b/lib/syskit/gui/logging_groups_item.rb index b7182378a..9f1e7d926 100644 --- a/lib/syskit/gui/logging_groups_item.rb +++ b/lib/syskit/gui/logging_groups_item.rb @@ -10,6 +10,7 @@ module GUI # in a tree view class LoggingGroupsItem < LoggingConfigurationItemBase attr_reader :items_name, :items_value + def initialize(logging_groups, label = "") super(logging_groups) diff --git a/lib/syskit/gui/model_browser.rb b/lib/syskit/gui/model_browser.rb index b06208dd5..fc81c2a45 100644 --- a/lib/syskit/gui/model_browser.rb +++ b/lib/syskit/gui/model_browser.rb @@ -48,7 +48,7 @@ def split_name(model) def each_submodel(model) if model == Syskit::TaskContext model.each_submodel do |m| - excluded = (!m.name || m.private_specialization?) + excluded = !m.name || m.private_specialization? yield(m, excluded) end end diff --git a/lib/syskit/gui/model_views/composition.rb b/lib/syskit/gui/model_views/composition.rb index 2489ce759..9f04c623b 100644 --- a/lib/syskit/gui/model_views/composition.rb +++ b/lib/syskit/gui/model_views/composition.rb @@ -11,11 +11,10 @@ module ModelViews # In addition to the plain component network, it visualizes the # specializations and allows to select them dynamically class Composition < ComponentNetworkView - attr_reader :specializations - attr_reader :task_model_view + attr_reader :specializations, :task_model_view def initialize(page) - super(page) + super @specializations = {} @task_model_view = Roby::GUI::ModelViews::Task.new(page) end diff --git a/lib/syskit/gui/model_views/data_service.rb b/lib/syskit/gui/model_views/data_service.rb index b24e20418..2d94cf238 100644 --- a/lib/syskit/gui/model_views/data_service.rb +++ b/lib/syskit/gui/model_views/data_service.rb @@ -4,7 +4,7 @@ module Syskit::GUI module ModelViews class DataService < Component def initialize(page) - super(page) + super buttons = [] buttons.concat(self.class.common_graph_buttons("interface")) interface_options[:buttons] = buttons diff --git a/lib/syskit/gui/model_views/profile.rb b/lib/syskit/gui/model_views/profile.rb index 8ad308ede..9947158f8 100644 --- a/lib/syskit/gui/model_views/profile.rb +++ b/lib/syskit/gui/model_views/profile.rb @@ -157,7 +157,7 @@ class Profile < MetaRuby::GUI::HTML::Collection attr_accessor :instanciation_method def initialize(page) - super(page) + super @instanciation_method = :compute_system_network register_type Syskit::InstanceRequirements, ProfileElementView.new(page) diff --git a/lib/syskit/gui/model_views/task_context_base.rb b/lib/syskit/gui/model_views/task_context_base.rb index 7443abe41..d7975555d 100644 --- a/lib/syskit/gui/model_views/task_context_base.rb +++ b/lib/syskit/gui/model_views/task_context_base.rb @@ -3,11 +3,10 @@ module Syskit::GUI module ModelViews class TaskContextBase < Component - attr_reader :orogen_rendering - attr_reader :task_model_view + attr_reader :orogen_rendering, :task_model_view def initialize(page) - super(page) + super @task_model_view = Roby::GUI::ModelViews::Task.new(page) @orogen_rendering = OroGen::HTML::TaskContext.new(page) buttons = [] diff --git a/lib/syskit/gui/model_views/type.rb b/lib/syskit/gui/model_views/type.rb index 851064d37..a7ce4f5e8 100644 --- a/lib/syskit/gui/model_views/type.rb +++ b/lib/syskit/gui/model_views/type.rb @@ -4,8 +4,7 @@ module Syskit::GUI module ModelViews class Type < Qt::Object - attr_reader :page - attr_reader :type_rendering + attr_reader :page, :type_rendering def initialize(page) super() diff --git a/lib/syskit/gui/page_extension.rb b/lib/syskit/gui/page_extension.rb index 968051731..8bbd90701 100644 --- a/lib/syskit/gui/page_extension.rb +++ b/lib/syskit/gui/page_extension.rb @@ -54,18 +54,16 @@ def push_plan(title, kind, plan, buttons: [], svg = svg.gsub(/xlink:href="[^"]+"/) { |match| match.gsub("<", "<").gsub(">", ">") } begin - if match = /svg width=\"(\d+)(\w+)\" height=\"(\d+)(\w+)\"/.match(svg) + if match = /svg width="(\d+)(\w+)" height="(\d+)(\w+)"/.match(svg) width, w_unit, height, h_unit = *match.captures - svg = match.pre_match + "svg width=\"#{(Float(width) * zoom * 0.6)}#{w_unit}\" height=\"#{(Float(height) * zoom * 0.6)}#{h_unit}\"" + match.post_match + svg = match.pre_match + "svg width=\"#{Float(width) * zoom * 0.6}#{w_unit}\" height=\"#{Float(height) * zoom * 0.6}#{h_unit}\"" + match.post_match end rescue ArgumentError end if pattern = external_objects - file = pattern % kind + ".svg" - File.open(file, "w") do |io| - io.write(svg) - end + file = (pattern % kind) + ".svg" + File.write(file, svg) push(title, "", id: id, buttons: buttons) else diff --git a/lib/syskit/gui/runtime_state.rb b/lib/syskit/gui/runtime_state.rb index 8144c57cf..0f9220244 100644 --- a/lib/syskit/gui/runtime_state.rb +++ b/lib/syskit/gui/runtime_state.rb @@ -68,7 +68,7 @@ class RuntimeState < Qt::Widget # Checkboxes to select widgets options attr_reader :ui_hide_loggers - attr_reader :ui_show_expanded_job + attr_reader :ui_show_expanded_job, :syskit_poll define_hooks :on_connection_state_changed define_hooks :on_progress @@ -81,8 +81,8 @@ def sizeHint(option, index) main = index.data.toString doc = index.data(Qt::UserRole).to_string || "" Qt::Size.new( - [fm.width(main), fm.width(doc)].max + 2 * OUTER_MARGIN, - fm.height * 2 + OUTER_MARGIN * 2 + INTERLINE + [fm.width(main), fm.width(doc)].max + (2 * OUTER_MARGIN), + (fm.height * 2) + (OUTER_MARGIN * 2) + INTERLINE ) end @@ -100,7 +100,7 @@ def paint(painter, option, index) fm = option.font_metrics painter.draw_text( - Qt::Rect.new(option.rect.x + OUTER_MARGIN, option.rect.y + OUTER_MARGIN, option.rect.width - 2 * OUTER_MARGIN, fm.height), + Qt::Rect.new(option.rect.x + OUTER_MARGIN, option.rect.y + OUTER_MARGIN, option.rect.width - (2 * OUTER_MARGIN), fm.height), Qt::AlignLeft, main, text_bounds ) @@ -108,7 +108,7 @@ def paint(painter, option, index) font.italic = true painter.font = font painter.draw_text( - Qt::Rect.new(option.rect.x + OUTER_MARGIN, text_bounds.bottom + INTERLINE, option.rect.width - 2 * OUTER_MARGIN, fm.height), + Qt::Rect.new(option.rect.x + OUTER_MARGIN, text_bounds.bottom + INTERLINE, option.rect.width - (2 * OUTER_MARGIN), fm.height), Qt::AlignLeft, doc, text_bounds ) ensure @@ -287,7 +287,7 @@ def remote_name end def app_start(robot_name: "default", port: nil) - robot_name, start_controller = AppStartDialog.exec( + robot_name, start_controller, single = AppStartDialog.exec( Roby.app.robots.names, self, default_robot_name: robot_name ) return unless robot_name @@ -296,6 +296,7 @@ def app_start(robot_name: "default", port: nil) extra_args << "-r" << robot_name unless robot_name.empty? extra_args << "-c" if start_controller extra_args << "--port=#{port}" if port + extra_args << "--single" if single extra_args.concat( Roby.app.argv_set.flat_map { |arg| ["--set", arg] } ) @@ -324,7 +325,7 @@ def logger_task?(t) @logger_m ||= Syskit::TaskContext .find_model_from_orogen_name("logger::Logger") || false - t.kind_of?(@logger_m) + t.kind_of?(@logger_m) if @logger_m end def update_tasks_info @@ -334,7 +335,7 @@ def update_tasks_info .first return unless job_task - placeholder_task = job_task.planned_task + placeholder_task = job_task.planned_task || job_task return unless placeholder_task dependency = placeholder_task.relation_graph_for(Roby::TaskStructure::Dependency) @@ -360,16 +361,16 @@ def update_tasks_info elsif logger_task?(t) @known_loggers << t false - else true + else + true end end end all_tasks.merge(tasks) tasks.each do |job| if job.kind_of?(Roby::Interface::Job) - if placeholder_task = job.planned_task - all_job_info[placeholder_task] = job - end + placeholder_task = job.planned_task || job + all_job_info[placeholder_task] = job end end update_orocos_tasks @@ -431,7 +432,8 @@ def create_ui job_summary_layout.add_widget(@batch_manager) @batch_manager.connect(SIGNAL("active(bool)")) do |active| if active then @batch_manager.show - else @batch_manager.hide + else + @batch_manager.hide end end @batch_manager.hide @@ -474,7 +476,7 @@ def create_ui @ui_hide_loggers.checked = false @ui_hide_loggers.connect SIGNAL("toggled(bool)") do |checked| @known_loggers = nil - update_tasks_info + update_tasks_info if syskit_log_stream end @ui_show_expanded_job.checked = true @ui_show_expanded_job.connect SIGNAL("toggled(bool)") do |checked| @@ -587,8 +589,6 @@ def create_ui_new_job new_job_layout end - attr_reader :syskit_poll - # @api private # # Sets up polling on a given syskit interface diff --git a/lib/syskit/gui/state_label.rb b/lib/syskit/gui/state_label.rb index a66e85ed3..ac32934ac 100644 --- a/lib/syskit/gui/state_label.rb +++ b/lib/syskit/gui/state_label.rb @@ -194,7 +194,8 @@ def update_text(text = current_text) @current_text = text self.text = if name then format(TEXT_WITH_NAME, name, text) - else format(TEXT_WITHOUT_NAME, text) + else + format(TEXT_WITHOUT_NAME, text) end end diff --git a/lib/syskit/gui/testing.rb b/lib/syskit/gui/testing.rb index 18cfe2361..6c7f4c1bb 100644 --- a/lib/syskit/gui/testing.rb +++ b/lib/syskit/gui/testing.rb @@ -35,10 +35,8 @@ class Testing < Qt::Widget # The item model that represents the subprocess state attr_reader :item_model - attr_reader :test_list_ui - attr_reader :test_result_ui - attr_reader :test_result_page - attr_reader :exception_rendering + attr_reader :test_list_ui, :test_result_ui, :test_result_page, + :exception_rendering # The timer used to call {#manager}.poll periodically attr_reader :poll_timer @@ -151,7 +149,8 @@ def create_status_bar_ui start_stop_button.connect(SIGNAL("clicked()")) do if running? stop - else start + else + start end end @@ -215,7 +214,7 @@ def display_item_details(item) ) items << ["", link] - models = (item.slave.name[:models] || []) + models = item.slave.name[:models] || [] unless models.empty? items << %w[title Models] models.sort.each do |model_name| @@ -257,21 +256,22 @@ def display_item_details(item) item.each_test_result do |r| name = "#{r.test_case_name}::#{r.test_name}" info = format( - "#{r.skip_count} skips, #{r.failure_count} failures "\ + "#{r.skip_count} skips, #{r.failure_count} failures " \ "and #{r.assertions} assertions executed in %.3fs", duration: r.time ) color = if r.failure_count > 0 then :red elsif r.skip_count > 0 then :orange - else :green + else + :green end color = SubprocessItem.html_color(color) style = "padding: .1em; background-color: #{color}" test_result_page.push( - nil, "
"\ - "#{MetaRuby::GUI::HTML.escape_html(name)}: "\ - "#{MetaRuby::GUI::HTML.escape_html(info)}"\ + nil, "
" \ + "#{MetaRuby::GUI::HTML.escape_html(name)}: " \ + "#{MetaRuby::GUI::HTML.escape_html(info)}" \ "
" ) all_exceptions = r.failures.flat_map do |e| @@ -349,13 +349,14 @@ def stats def update_status_label(status_label) stats = self.stats state_name = if running? then "RUNNING" - else "STOPPED" + else + "STOPPED" end status_label.update_state( state_name, - text: "#{stats.executed_count} of #{stats.test_count} test files "\ - "executed, #{stats.run_count} runs, #{stats.skip_count} "\ - "skips, #{stats.failure_count} failures and "\ + text: "#{stats.executed_count} of #{stats.test_count} test files " \ + "executed, #{stats.run_count} runs, #{stats.skip_count} " \ + "skips, #{stats.failure_count} failures and " \ "#{stats.assertions_count} assertions" ) end @@ -400,19 +401,8 @@ def self.html_color(name) :failure_count, :failures, :assertions, :time ) - attr_reader :slave - attr_reader :name - attr_reader :test_results - attr_reader :exceptions - - attr_reader :start_time - attr_reader :assertions_count - attr_reader :failure_count - attr_reader :skip_count - - attr_reader :slave_exit_status - - attr_reader :total_run_count + attr_reader :slave, :name, :test_results, :exceptions, :start_time, + :assertions_count, :failure_count, :skip_count, :slave_exit_status, :total_run_count # The count of exceptions def exception_count @@ -428,7 +418,7 @@ def initialize(app, slave) @executed = false @slave = slave @total_run_count = 0 - name = (slave.name[:path] || "Robot: #{app.robot_name}") + name = slave.name[:path] || "Robot: #{app.robot_name}" if (base_path = app.find_base_path_for(name)) base_path = base_path.to_s name = File.basename(base_path) + ":" + @@ -465,7 +455,7 @@ def executed? end def finished? - !!@runtime # rubocop:disable Style/DoubleNegation + !!@runtime end def start @@ -507,19 +497,19 @@ def finished(slave_exit_status) def status_text text = [] if has_tested? - text << "#{test_results.size} runs, #{exception_count} "\ - "exceptions, #{failure_count} failures and "\ + text << "#{test_results.size} runs, #{exception_count} " \ + "exceptions, #{failure_count} failures and " \ "#{assertions_count} assertions" end return text unless slave_exit_status && !slave_exit_status.success? if slave_exit_status.signaled? - text << "Test process terminated with signal "\ + text << "Test process terminated with signal " \ "#{slave_exit_status.termsig}" elsif slave_exit_status.exitstatus != 1 || !has_tested? || (exception_count == 0 && failure_count == 0) - text << "Test process finished with exit code "\ + text << "Test process finished with exit code " \ "#{slave_exit_status.exitstatus}" end text @@ -568,7 +558,8 @@ def add_test_result( failures.each do |e| if e.kind_of?(Minitest::Skip) skip_count += 1 - else failure_count += 1 + else + failure_count += 1 end end @skip_count += skip_count diff --git a/lib/syskit/gui/widget_list.rb b/lib/syskit/gui/widget_list.rb index f71223a41..ef722ce9d 100644 --- a/lib/syskit/gui/widget_list.rb +++ b/lib/syskit/gui/widget_list.rb @@ -48,7 +48,8 @@ def add_before(widget, before, permanent: false) if i = @widgets.index { |w| w.widget == before } @main_layout.insert_widget(i, widget) @widgets.insert(i, ListItem.new(widget, permanent)) - else Kernel.raise ArgumentError, "#{before} is not part of #{self}" + else + Kernel.raise ArgumentError, "#{before} is not part of #{self}" end end @@ -60,7 +61,8 @@ def add_after(widget, after, permanent: false) if i = @widgets.index { |w| w.widget == after } @main_layout.insert_widget(i + 1, widget) @widgets.insert(i + 1, ListItem.new(widget, permanent)) - else Kernel.raise ArgumentError, "#{after} is not part of #{self}" + else + Kernel.raise ArgumentError, "#{after} is not part of #{self}" end end @@ -77,7 +79,8 @@ def children_size_updated s = size new_height = @widgets.inject(0) do |h, w| h + if w.widget.hidden? then 0 - else w.widget.contents_height + else + w.widget.contents_height end end if new_height != s.height diff --git a/lib/syskit/instance_requirements.rb b/lib/syskit/instance_requirements.rb index 4c894d3ec..8a0ec184a 100644 --- a/lib/syskit/instance_requirements.rb +++ b/lib/syskit/instance_requirements.rb @@ -184,13 +184,9 @@ class TemplatePlan < Roby::TemplatePlan # @yieldparam selection the selected model/task in the use flag # @yieldreturn the value that should replaces selection # @return [self] - def map_use_selections! - selections.map! do |value| - yield(value) - end - pushed_selections.map! do |value| - yield(value) - end + def map_use_selections!(&block) + selections.map!(&block) + pushed_selections.map!(&block) invalidate_dependency_injection invalidate_template self @@ -206,15 +202,15 @@ def to_component_model # @deprecated use {#bind} instead def resolve(object) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "InstanceRequirements#bind instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "InstanceRequirements#bind instead" bind(object) end # @deprecated use {#try_bind} instead def try_resolve(task) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "InstanceRequirements#try_bind instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "InstanceRequirements#try_bind instead" try_bind(task) end @@ -237,7 +233,8 @@ def component_model model = self.model.to_component_model if model.placeholder? model.proxied_component_model - else model + else + model end end @@ -428,7 +425,8 @@ def find_port(name) def port_by_name(name) if p = find_port(name) p - else raise ArgumentError, "#{self} has no port called #{name}, known ports are: #{each_port.map(&:name).sort.join(', ')}" + else + raise ArgumentError, "#{self} has no port called #{name}, known ports are: #{each_port.map(&:name).sort.join(', ')}" end end @@ -630,7 +628,8 @@ def use(*mappings) def simplest_model_representation if plain? model - else self + else + self end end @@ -694,8 +693,8 @@ def with_arguments(deprecated_arguments = nil, **arguments) deprecated_from_kw ||= Roby.sanitize_keywords(arguments) if deprecated_arguments || !deprecated_from_kw.empty? Roby.warn_deprecated( - "InstanceRequirements#with_arguments: providing arguments using "\ - "a string is not supported anymore use key: value instead of "\ + "InstanceRequirements#with_arguments: providing arguments using " \ + "a string is not supported anymore use key: value instead of " \ "'key' => value" ) deprecated_arguments&.each { |key, arg| arguments[key.to_sym] = arg } @@ -975,7 +974,8 @@ def post_instanciation_setup(task) !sel.kind_of?(DependencyInjection::SpecialDIValue) sel.to_instance_requirements - else sel + else + sel end end task.update_requirements(task_requirements, @@ -1037,10 +1037,10 @@ def to_s end def pretty_print(pp) - if model != base_model - pp.text "#{model}(from #{base_model})" - else + if model == base_model pp.text model.to_s + else + pp.text "#{model}(from #{base_model})" end pp.nest(2) do unless pushed_selections.empty? @@ -1063,7 +1063,7 @@ def each_child return enum_for(__method__) unless block_given? unless composition_model? - raise "cannot call #each_child on #{self} as it does not "\ + raise "cannot call #each_child on #{self} as it does not " \ "represent a composition model" end @@ -1112,19 +1112,18 @@ def each_required_service_model end end - def each_required_model + def each_required_model(&block) return enum_for(:each_required_model) unless block_given? - model.each_required_model do |m| - yield(m) - end + model.each_required_model(&block) end # Tests if these requirements explicitly point to a component model def component_model? if model.placeholder? model.proxied_component_model != Syskit::Component - else true + else + true end end @@ -1196,7 +1195,8 @@ def instanciate(_plan, variables = {}) arguments = @requirements.arguments.transform_values do |value| if value.respond_to?(:evaluate) value.evaluate(variables) - else value + else + value end end @requirements.as_plan(**arguments) @@ -1239,7 +1239,7 @@ def to_action_model(doc = self.doc) if optional action_model.optional_arg(arg_name, arg.doc || "#{arg_name} argument of #{task_model.name}", default_argument) else - action_model.required_arg(arg_name, arg.doc || "#{arg_name} argument of #{task_model.name}") + action_model.required_arg(arg_name, arg.doc || "#{arg_name} argument of #{task_model.name}", example: arg.example) end end action_model diff --git a/lib/syskit/instance_selection.rb b/lib/syskit/instance_selection.rb index f3c92ac42..2222a83ca 100644 --- a/lib/syskit/instance_selection.rb +++ b/lib/syskit/instance_selection.rb @@ -68,10 +68,12 @@ def autoselect_service_if_needed(selected, required, mappings) else selected.find_data_service_from_type(required_srv) end - else selected.dup + else + selected.dup end - else selected.dup + else + selected.dup end end @@ -145,14 +147,15 @@ def port_mappings mappings = {} service_selection.each do |req_m, sel_m| mappings.merge!(sel_m.port_mappings_for(req_m)) do |req_name, sel_name1, sel_name2| - if sel_name1 != sel_name2 + if sel_name1 == sel_name2 + sel_name1 + else # need to find who has the same port ... service_selection.each_key do |other_m| if req_m.has_port?(req_name) raise AmbiguousPortMappings.new(other_m, req_m, req_name) end end - else sel_name1 end end end @@ -168,7 +171,8 @@ def instanciate(plan, context = Syskit::DependencyInjectionContext.new, **option component = plan[self.component] if selected_service = selected.service selected_service.bind(component) - else component + else + component end else selected.instanciate(plan, context, **options) diff --git a/lib/syskit/interface.rb b/lib/syskit/interface.rb new file mode 100644 index 000000000..3135af052 --- /dev/null +++ b/lib/syskit/interface.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "syskit/interface/commands" diff --git a/lib/syskit/interface/commands.rb b/lib/syskit/interface/commands.rb new file mode 100644 index 000000000..b39f6c983 --- /dev/null +++ b/lib/syskit/interface/commands.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require "roby/interface/core" +require "roby/robot" + +module Syskit + module Interface + # Definition of the syskit-specific interface commands + class Commands < Roby::Interface::CommandLibrary + # Return information about deployments + # + # @return [Protocol::Deployment] + def deployments + plan.find_tasks(Syskit::Deployment).to_a + end + command :deployments, + "returns information about running deployments" + + # Save the configuration of all running tasks of the given model to disk + # + # @param [String,nil] name the section name for the new configuration. + # If nil, the task's orocos name will be used + # @param [String] path the directory in which the files should be saved + # @return [nil] + def dump_task_config(task_model, path, name = nil) + FileUtils.mkdir_p(path) + plan.find_tasks(task_model) + .each do |t| + Orocos.conf.save(t.orocos_task, path, name || t.orocos_task.name) + end + nil + end + command( + :dump_task_config, + "saves configuration from running tasks into yaml files", + model: "the model of the tasks that should be saved", + path: "the directory in which the configuration files should be saved", + name: "(optional) if given, the name of the section for the new " \ + "configuration. Defaults to the orocos task names" + ) + + # Saves the configuration of all running tasks to disk + # + # @param [String] name the section name for the new configuration + # @param [String] path the directory in which the files should be saved + # @return [nil] + def dump_all_config(path, name = nil) + dump_task_config(Syskit::TaskContext, path, name) + nil + end + command( + :dump_all_config, + "saves the configuration of all running tasks into yaml files", + path: "the directory in which the configuration files should be saved", + name: "(optional) if given, the name of the section for the new " \ + "configuration. Defaults to the orocos task names" + ) + + # Task used to tell the engine that we want the deployments restarted without + # killing the dependent networks + class ShellDeploymentRestart < Roby::Task + event :start, controlable: true + + poll do + if redeploy_event.pending? && !plan.syskit_has_async_resolution? + redeploy_event.emit + end + end + + event :redeploy do |_context| + Runtime.apply_requirement_modifications(plan, force: true) + end + + forward redeploy: :stop + end + + # Stops deployment processes + # + # @param [Array>] models if non-empty, only the + # deployments matching this model will be stopped, otherwise all + # deployments are stopped. + def stop_deployments(*models) + models << Syskit::Deployment if models.empty? + models.each do |m| + plan.find_tasks(m) + .each do |task| + if task.kind_of?(Syskit::TaskContext) + task.execution_agent.stop! + else + task.stop! + end + end + end + end + command :stop_deployments, "stops deployment processes", + models: "(optional) if given, a list of task or deployment models " \ + "pointing to what should be stopped. If not given, all " \ + "deployments are stopped" + + # Restarts deployment processes + # + # @param [Array,Model>] models if + # non-empty, only the deployments matching this model or the deployments + # supporting tasks matching the models will be restarted, otherwise all + # deployments are restarted. + def restart_deployments(*models) + models << Syskit::Deployment if models.empty? + deployments = restart_discover_deployment_tasks(models) + protection = restart_setup_protection(deployments) + + done = Roby::AndGenerator.new + done.signals protection.redeploy_event + deployments.each do |task| + done << task.stop_event + task.stop! + end + nil + end + command :restart_deployments, "restarts deployment processes", + models: "(optional) if given, a list of task or deployment models " \ + "pointing to what should be restarted. If not given, all " \ + "deployments are restarted" + + # @api private + # + # Helper for {#restart_deployments} that lists the deployment tasks which + # should be restarted + def restart_discover_deployment_tasks(models) + tasks = models.flat_map do |m| + plan.find_tasks(m).map do |task| + if task.kind_of?(Syskit::TaskContext) + task.execution_agent + else + task + end + end + end + tasks.uniq + end + + # @api private + # + # Helper for {#restart_deployments} that sets up a error handler to avoid + # killing tasks while we restart the deployments + def restart_setup_protection(deployment_tasks) + protection = ShellDeploymentRestart.new + plan.add(protection) + protection.start! + + deployment_tasks.each do |task| + task.each_executed_task do |executed_task| + executed_task.stop_event.handle_with(protection) + end + end + protection + end + + # (see Application#syskit_reload_config) + def reload_config + app.syskit_reload_config + end + command :reload_config, "reloads YAML configuration files from disk", + "You need to call the redeploy command to apply the new configuration" + + # (see Application#syskit_pending_reloaded_configurations) + def pending_reloaded_configurations + app.syskit_pending_reloaded_configurations + end + command :pending_reloaded_configurations, + "returns the list of TaskContext names " \ + "that are marked as needing reconfiguration", + "They will be reconfigured on the next redeploy or system transition" + + # Require the engine to redeploy the current network + # + # It must be called after {#reload_config} to apply the new + # configuration(s) + def redeploy + Runtime.apply_requirement_modifications(plan, force: true) + nil + end + command :redeploy, "redeploys the current network", + "It is mostly used to apply the configuration " \ + "loaded with reload_config" + + def enable_log_group(string) + Syskit.conf.logs.enable_log_group(string) + redeploy + nil + end + command :enable_logging_of, "enables a log group", + name: "the log group name" + + # @deprecated use enable_log_group instead + def enable_logging_of(string) + enable_log_group(string) + end + + def disable_log_group(string) + Syskit.conf.logs.disable_log_group(string) + redeploy + nil + end + command :disable_log_group, "disables a log group", + name: "the log group name" + + # @deprecated use disable_log_group instead + def disable_logging_of(string) + disable_log_group(string) + end + + LoggingGroup = Struct.new(:name, :enabled) + LoggingConfiguration = + Struct.new(:port_logs_enabled, :conf_logs_enabled, :groups) + def logging_conf + conf = LoggingConfiguration.new(false, false, {}) + conf.port_logs_enabled = Syskit.conf.logs.port_logs_enabled? + conf.conf_logs_enabled = Syskit.conf.logs.conf_logs_enabled? + Syskit.conf.logs.groups.each_pair do |key, group| + conf.groups[key] = LoggingGroup.new(key, group.enabled?) + end + conf + end + command :logging_conf, "gets the current logging configuration" + + def update_logging_conf(conf) + logs_conf = Syskit.conf.logs + if conf.port_logs_enabled + logs_conf.enable_port_logging + else + logs_conf.disable_port_logging + end + + if conf.conf_logs_enabled + logs_conf.enable_conf_logging + else + logs_conf.disable_conf_logging + end + + conf.groups.each_pair do |name, group| + logs_conf.group_by_name(name).enabled = group.enabled + rescue ArgumentError + Syskit.warn "tried to update a group that does not exist: #{name}" + end + redeploy + end + command :update_logging_conf, "updates the current logging configuration", + conf: "the new logging settings" + end + end +end + +module Robot # :nodoc: + # Syskit subcommand for the shell + def self.syskit + @syskit_interface ||= Syskit::Interface::Commands.new(Roby.app) # rubocop:disable Naming/MemoizedInstanceVariableName + end +end + +Roby::Interface::Interface.subcommand( + "syskit", Syskit::Interface::Commands, "Commands specific to Syskit" +) diff --git a/lib/syskit/interface/v2.rb b/lib/syskit/interface/v2.rb new file mode 100644 index 000000000..0016a9c4f --- /dev/null +++ b/lib/syskit/interface/v2.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require "syskit/interface/v2/protocol" +require "syskit/interface/commands" diff --git a/lib/syskit/interface/v2/protocol.rb b/lib/syskit/interface/v2/protocol.rb new file mode 100644 index 000000000..36a32bb8e --- /dev/null +++ b/lib/syskit/interface/v2/protocol.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "roby/interface/v2" + +module Syskit + module Interface + module V2 + # Syskit extensions to Roby's v2 interface wire protocol + module Protocol + ROBY_TASK_MEMBERS = Roby::Interface::V2::Protocol::Task.new.members + + Deployment = Struct.new( + *ROBY_TASK_MEMBERS, + :pid, :ready_since, :deployed_tasks, keyword_init: true + ) do + def pretty_print(pp) + roby_task = ROBY_TASK_MEMBERS.map do |name| + [name, self[name]] + end + Roby::Interface::V2::Protocol::Task + .new(**Hash[roby_task]) + .pretty_print(pp) + pp.breakable + pp.text "PID: #{pid}" + pp.breakable + names = deployed_tasks.map(&:name) + pp.text "Deployed tasks: #{names.join(', ')}" + end + end + + DeployedTask = Struct.new( + :name, :ior, :orogen_model_name, keyword_init: true + ) + + def self.register_marshallers(protocol) + protocol.add_marshaller( + Syskit::Deployment, &method(:marshal_deployment_task) + ) + protocol.allow_objects( + Orocos::RubyTasks::TaskContext, + Orocos::RubyTasks::StubTaskContext + ) + end + + def self.marshal_remote_task_handle(name, remote_task_handle) + ior = remote_task_handle.handle.ior + model_name = remote_task_handle.handle.model.name + DeployedTask.new( + name: name, ior: ior, orogen_model_name: model_name + ) + end + + def self.marshal_deployment_task(channel, task) + deployed_tasks = + task.remote_task_handles.map do |name, remote_task_handle| + marshal_remote_task_handle(name, remote_task_handle) + end + + roby_task = Roby::Interface::V2::Protocol.marshal_task(channel, task) + Deployment.new( + **roby_task.to_h, + pid: task.pid, + ready_since: task.ready_event.last&.time, + deployed_tasks: deployed_tasks + ) + end + end + end + end +end diff --git a/lib/syskit/models/bound_data_service.rb b/lib/syskit/models/bound_data_service.rb index df872ed0a..e5477a4a5 100644 --- a/lib/syskit/models/bound_data_service.rb +++ b/lib/syskit/models/bound_data_service.rb @@ -162,7 +162,7 @@ def attach(new_component_model, verify: true) if verify && !new_component_model.fullfills?(component_model) raise ArgumentError, - "cannot attach #{self} on #{new_component_model}: "\ + "cannot attach #{self} on #{new_component_model}: " \ "does not fullfill #{component_model}" end @@ -315,7 +315,7 @@ def bind(task) unless task.fullfills?(component_model) raise ArgumentError, - "cannot bind #{self} on #{task}: "\ + "cannot bind #{self} on #{task}: " \ "does not fullfill #{component_model}" end @@ -334,9 +334,9 @@ def bind(task) resolved = task.find_data_service(name) if !resolved || !resolved.model.same_service?(self) raise InternalError, - "#{component_model} is fullfilled by #{task}, "\ - "but is not inherited by its model #{task.model}. "\ - "I didn't manage to resolve this, either as a "\ + "#{component_model} is fullfilled by #{task}, " \ + "but is not inherited by its model #{task.model}. " \ + "I didn't manage to resolve this, either as a " \ "task-to-placeholder mapping, or as a dynamic service" end @@ -347,7 +347,7 @@ def bind(task) # @deprecated use {#bind} instead def resolve(task) Roby.warn_deprecated( - "#{__method__} is deprecated, use "\ + "#{__method__} is deprecated, use " \ "BoundDataService#bind instead" ) bind(task) diff --git a/lib/syskit/models/component.rb b/lib/syskit/models/component.rb index 00e3aa9ba..fba9db145 100644 --- a/lib/syskit/models/component.rb +++ b/lib/syskit/models/component.rb @@ -41,7 +41,7 @@ def clear_model super data_services.clear dynamic_services.clear - # Note: the placeholder_models cache is cleared separately. The + # NOTE: the placeholder_models cache is cleared separately. The # reason is that we need to clear it on permanent and # non-permanent models alike, including component models that # are defined in syskit. The normal procedure is to call @@ -109,7 +109,7 @@ def find_matching_service(target_model, pattern = nil) pattern_msg += " matching name hint #{pattern}" if pattern raise AmbiguousServiceSelection.new( self, target_model, master_matching_services - ), "there is more than one service of type #{target_model.name} "\ + ), "there is more than one service of type #{target_model.name} " \ "in #{name}#{pattern_msg}" end @@ -355,11 +355,11 @@ def normalize_port_mappings_argument(service_model, srv_to_self) if !srv_name.respond_to?(:to_str) raise ArgumentError, - "unexpected value given in port mapping: #{srv_name}, "\ + "unexpected value given in port mapping: #{srv_name}, " \ "expected a string" elsif !self_name.respond_to?(:to_str) raise ArgumentError, - "unexpected value given in port mapping: #{self_name}, "\ + "unexpected value given in port mapping: #{self_name}, " \ "expected a string" elsif !service_model.find_port(srv_name) raise InvalidPortMapping, @@ -380,7 +380,7 @@ def compute_output_port_mappings(service_model, given_srv_to_self) ) unless self_port_name raise InvalidPortMapping, - "cannot find an equivalent output port for "\ + "cannot find an equivalent output port for " \ "#{srv_port.name}[#{srv_port.type_name}] on #{short_name}" end @@ -395,7 +395,7 @@ def compute_input_port_mappings(service_model, given_srv_to_self) ) unless self_port_name raise InvalidPortMapping, - "cannot find an equivalent input port for "\ + "cannot find an equivalent input port for " \ "#{srv_port.name}[#{srv_port.type_name}] on #{short_name}" end @@ -419,10 +419,10 @@ def check_collisions_in_computed_port_mappings( .join(", ") raise InvalidPortMapping, - "automatic port mapping would map multiple service ports "\ - "#{srv_port_names.sort.join(', ')} to the same component port "\ - "#{self_port_name}. This is possible, but must be specified "\ - "explicitly by passing this mapping: "\ + "automatic port mapping would map multiple service ports " \ + "#{srv_port_names.sort.join(', ')} to the same component port " \ + "#{self_port_name}. This is possible, but must be specified " \ + "explicitly by passing this mapping: " \ "#{explicit_mapping_s} explicitly" end end @@ -464,15 +464,15 @@ def find_directional_port_mapping_by_name( if self_port raise InvalidPortMapping, - "invalid port mapping provided from #{srv_port} to "\ + "invalid port mapping provided from #{srv_port} to " \ "#{self_port}: type mismatch" else known_ports = send("each_#{direction}_port") .map { |p| "#{p.name}[#{p.type.name}]" } raise InvalidPortMapping, - "invalid port mapping \"#{srv_port.name}\" => "\ - "\"#{self_port_name}\": #{self_port_name} is not a "\ - "#{direction} port in #{short_name}. "\ + "invalid port mapping \"#{srv_port.name}\" => " \ + "\"#{self_port_name}\": #{self_port_name} is not a " \ + "#{direction} port in #{short_name}. " \ "Known #{direction} ports are #{known_ports.sort.join(', ')}" end end @@ -487,8 +487,8 @@ def find_directional_port_mapping_by_type(direction, srv_port) return srv_port_name if candidates.any? { |p| p.name == srv_port_name } raise InvalidPortMapping, - "there are multiple candidates to map "\ - "#{srv_port.name}[#{srv_port.type.name}]: "\ + "there are multiple candidates to map " \ + "#{srv_port.name}[#{srv_port.type.name}]: " \ "#{candidates.map(&:name).sort.join(', ')}" end @@ -563,24 +563,24 @@ def promote_dynamic_service(_name, dyn) # # setup the task to create the required service # end # end - def dynamic_service( # rubocop:disable Metrics/ParameterLists + def dynamic_service( model, as: nil, addition_requires_reconfiguration: true, remove_when_unused: true, **backward, &block ) if !as raise ArgumentError, - "no name given to the dynamic service, "\ + "no name given to the dynamic service, " \ "please provide one with the :as option" elsif !block_given? raise ArgumentError, - "no block given to #dynamic_service, "\ + "no block given to #dynamic_service, " \ "one must be provided and must call provides()" end if backward.key?(:dynamic) - Roby.warn_deprecated "the dynamic argument to #dynamic_service has "\ - "been renamed into "\ + Roby.warn_deprecated "the dynamic argument to #dynamic_service has " \ + "been renamed into " \ "addition_requires_reconfiguration" addition_requires_reconfiguration = !backward[:dynamic] end @@ -630,9 +630,9 @@ def require_dynamic_service(dynamic_service_name, as:, **dyn_options) return srv if srv.fullfills?(dyn.service_model) raise ArgumentError, - "there is already a service #{service_name}, but it is "\ - "of type #{srv.model.short_name} while the dynamic "\ - "service #{dynamic_service_name} expects "\ + "there is already a service #{service_name}, but it is " \ + "of type #{srv.model.short_name} while the dynamic " \ + "service #{dynamic_service_name} expects " \ "#{dyn.service_model.short_name}" end dyn.instanciate(service_name, **dyn_options) @@ -646,8 +646,8 @@ def dynamic_service_by_name(name) each_dynamic_service.map { |n, _| n }.sort.join(", ") raise ArgumentError, - "#{short_name} has no dynamic service called "\ - "#{name}, available dynamic services "\ + "#{short_name} has no dynamic service called " \ + "#{name}, available dynamic services " \ "are: #{dynamic_service_list}" end @@ -742,7 +742,7 @@ def provides_for_task_service?(model, as: nil) unless model.kind_of?(Roby::Models::TaskServiceModel) raise ArgumentError, - "expected either a task service model or a data service model "\ + "expected either a task service model or a data service model " \ "as argument, and got #{model}" end @@ -756,7 +756,7 @@ def provides_for_task_service?(model, as: nil) def provides_resolve_name(as:, slave_of: nil) unless as raise ArgumentError, - "#provides requires a name to be provided through "\ + "#provides requires a name to be provided through " \ "the 'as' option" end @@ -767,7 +767,7 @@ def provides_resolve_name(as:, slave_of: nil) master_srv = find_data_service(slave_of) unless master_srv raise ArgumentError, - "master data service #{slave_of} is not "\ + "master data service #{slave_of} is not " \ "registered on #{self}" end @@ -782,7 +782,7 @@ def provides_validate_possible_overload(model, full_name) # Get the source name and the source model if data_services[full_name] raise ArgumentError, - "there is already a data service named '#{full_name}' "\ + "there is already a data service named '#{full_name}' " \ "defined on '#{short_name}'" end @@ -792,7 +792,7 @@ def provides_validate_possible_overload(model, full_name) unless model <= parent_type raise ArgumentError, - "#{self} has a data service named #{full_name} of type "\ + "#{self} has a data service named #{full_name} of type " \ "#{parent_type}, which is not a parent type of #{model}" end @@ -819,7 +819,7 @@ def provides_compute_port_mappings(service, given_srv_to_self = {}) ) rescue InvalidPortMapping => e raise InvalidProvides.new(self, service_m, e), - "#{short_name} does not provide the '#{service_m.name}' "\ + "#{short_name} does not provide the '#{service_m.name}' " \ "service's interface. #{e.message}", e.backtrace end @@ -910,7 +910,8 @@ def implicit_fullfilled_model def ensure_model_is_specialized if private_specialization? self - else specialize + else + specialize end end @@ -967,15 +968,15 @@ def bind(object) # @deprecated use {#bind} instead def resolve(task) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "Models::Component#bind instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "Models::Component#bind instead" bind(task) end # @deprecated use {#try_bind} instead def try_resolve(task) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "Models::Component#try_bind instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "Models::Component#try_bind instead" try_bind(task) end @@ -1039,14 +1040,14 @@ def deregister_submodels(set) # @deprecated use {Models::Placeholder.create_for} instead def create_proxy_task_model(service_models, as: nil, extension: Placeholder) - Roby.warn_deprecated "Component.create_proxy_task_model is deprecated, "\ + Roby.warn_deprecated "Component.create_proxy_task_model is deprecated, " \ "use Syskit::Models::Placeholder.create_for instead" extension.create_for(service_models, component_model: self, as: as) end # @deprecated use {Models::Placeholder.for} instead def proxy_task_model(service_models, as: nil, extension: Placeholder) - Roby.warn_deprecated "Component.proxy_task_model is deprecated, "\ + Roby.warn_deprecated "Component.proxy_task_model is deprecated, " \ "use Syskit::Models::Placeholder.for instead" extension.for(service_models, component_model: self, as: as) end @@ -1114,7 +1115,8 @@ def fullfills?(object) object_real_model = if object.respond_to?(:concrete_model) object.concrete_model - else object + else + object end return super if self_real_model == self @@ -1156,10 +1158,10 @@ def can_merge?(target_model) def can_merge_service?(self_srv, target_srv) if target_srv.model != self_srv.model NetworkGeneration::MergeSolver.debug do - "rejecting #{self_srv}.merge(#{target_srv}): dynamic "\ - "service #{self_srv.name} is of model "\ - "#{self_srv.model.short_name} on self and of "\ - "model #{target_srv.model.short_name} on the candidate task" + "rejecting #{self_srv}.merge(#{target_srv}): dynamic " \ + "service #{self_srv.name} is of model " \ + "#{self_srv.model.short_name} on self and of " \ + "model #{target_srv.model.short_name} on the candidate task" end false elsif target_srv.dynamic? && self_srv.dynamic? @@ -1169,19 +1171,19 @@ def can_merge_service?(self_srv, target_srv) true else NetworkGeneration::MergeSolver.debug do - "rejecting #{self_srv}.merge(#{target_srv}): dynamic "\ - "service #{self_srv.name} has options "\ - "#{target_srv.dynamic_service_options} on self and "\ - "#{self_srv.dynamic_service_options} "\ - "on the candidate task" + "rejecting #{self_srv}.merge(#{target_srv}): dynamic " \ + "service #{self_srv.name} has options " \ + "#{target_srv.dynamic_service_options} on self and " \ + "#{self_srv.dynamic_service_options} " \ + "on the candidate task" end false end elsif target_srv.dynamic? || self_srv.dynamic? NetworkGeneration::MergeSolver.debug do - "rejecting #{self_srv}.merge(#{target_srv}): "\ - "#{self_srv.name} is a dynamic service on self, "\ - "but a static one on the candidate task" + "rejecting #{self_srv}.merge(#{target_srv}): " \ + "#{self_srv.name} is a dynamic service on self, " \ + "but a static one on the candidate task" end false else @@ -1194,14 +1196,17 @@ def apply_missing_dynamic_services_from(from, specialize_if_needed = true) !find_data_service(srv.full_name) end - if !missing_services.empty? + if missing_services.empty? + self + else # We really really need to specialize self. The reason is # that self.model, even though it has private # specializations, might be a reusable model from the system # designer's point of view. With the singleton class, we # know that it is not base_model = if specialize_if_needed then specialize - else self + else + self end missing_services.each do |_, srv| dynamic_service_options = @@ -1211,7 +1216,6 @@ def apply_missing_dynamic_services_from(from, specialize_if_needed = true) ) end base_model - else self end end @@ -1240,7 +1244,7 @@ def merge(other_model) else raise IncompatibleComponentModels.new(self, other_model), - "models #{short_name} and #{other_model.short_name} "\ + "models #{short_name} and #{other_model.short_name} " \ "are not compatible" end end @@ -1265,7 +1269,7 @@ def merge_service_model(service_model, port_mappings) "#{self} cannot dynamically create ports" elsif p.type != self_p.type raise InvalidPortMapping, - "#{self} already has a port named #{self_name} of type "\ + "#{self} already has a port named #{self_name} of type " \ "#{self_p.type}, cannot dynamically map #{p} onto it" end end @@ -1278,7 +1282,7 @@ def merge_service_model(service_model, port_mappings) "#{self} cannot dynamically create ports" elsif p.type != self_p.type raise InvalidPortMapping, - "#{self} already has a port named #{self_name} of "\ + "#{self} already has a port named #{self_name} of " \ "type #{self_p.type}, cannot dynamically map #{p} onto it" end end @@ -1310,15 +1314,30 @@ def match # @example create a data reader for a composition child # data_reader some_child.out_port, as: 'pose' # + # @example inject another class to act as the output reader + # class Foo < Syskit::DynamicPortBinding::BoundOutputReader + # def read_new + # return unless (sample = super) + # + # sample + 42 + # end + # end + # + # data_reader some_child.out_port, as: "pose", klass: Foo + # # @return [DynamicPortBinding::BoundOutputReader] - def data_reader(port, as:) + def data_reader( + port, as:, klass: Syskit::DynamicPortBinding::BoundOutputReader, **policy + ) port = DynamicPortBinding.create(port) unless port.output? raise ArgumentError, "expected an output port, but #{port} seems to be an input" end - data_readers[as] = port.to_bound_data_accessor(as, self) + data_readers[as] = DynamicPortBinding::BoundOutputReader.new( + as, self, port, klass: klass, **policy + ) end # The data writers defined on this task, as a mapping from the writer's @@ -1344,14 +1363,14 @@ def data_reader(port, as:) # data_reader some_child.cmd_in_port, as: 'cmd_in' # # @return [DynamicPortBinding::BoundInputWriter] - def data_writer(port, as:) + def data_writer(port, as:, **policy) port = DynamicPortBinding.create(port) if port.output? raise ArgumentError, "expected an input port, but #{port} seems to be an output" end - data_writers[as] = port.to_bound_data_accessor(as, self) + data_writers[as] = port.to_bound_data_accessor(as, self, **policy) end def has_through_method_missing?(name) @@ -1377,7 +1396,7 @@ def find_through_method_missing(name, args) ruby2_keywords def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing if name == :orogen_model raise NoMethodError, - "tried to use a method to access an oroGen model, "\ + "tried to use a method to access an oroGen model, " \ "but none exists on #{self}" end diff --git a/lib/syskit/models/composition.rb b/lib/syskit/models/composition.rb index 6c43ed527..e1b61233d 100644 --- a/lib/syskit/models/composition.rb +++ b/lib/syskit/models/composition.rb @@ -54,18 +54,20 @@ def promote_explicit_connection(connections) mappings_out = if (child_out = children[child_out_name]) child_out.port_mappings - else {} + else + {} end mappings_in = if (child_in = children[child_in_name]) child_in.port_mappings - else {} + else + {} end mapped = {} mappings.each do |(port_name_out, port_name_in), options| - port_name_out = (mappings_out[port_name_out] || port_name_out) - port_name_in = (mappings_in[port_name_in] || port_name_in) + port_name_out = mappings_out[port_name_out] || port_name_out + port_name_in = mappings_in[port_name_in] || port_name_in mapped[[port_name_out, port_name_in]] = options end [[child_out_name, child_in_name], mapped] @@ -124,8 +126,8 @@ def specialize(options = {}, &block) options = options.transform_keys do |key| if key.respond_to?(:to_str) || key.respond_to?(:to_sym) - Roby.warn_deprecated "calling #specialize with child names "\ - "is deprecated, use _child accessors "\ + Roby.warn_deprecated "calling #specialize with child names " \ + "is deprecated, use _child accessors " \ "instead (i.e. #{key}_child here)", 5 key elsif key.respond_to?(:child_name) @@ -213,13 +215,15 @@ def add_child(name, child_models, dependency_options) @exported_outputs = exported_outputs.transform_values do |port| if port.component_model.child_name == name child_model.find_port(port.name) - else port + else + port end end @exported_inputs = exported_inputs.transform_values do |port| if port.component_model.child_name == name child_model.find_port(port.name) - else port + else + port end end @@ -238,7 +242,7 @@ def add_child(name, child_models, dependency_options) def overload(child, model, **options) child = child.child_name if child.respond_to?(:child_name) unless find_child(child) - raise ArgumentError, "#{child} is not an existing child "\ + raise ArgumentError, "#{child} is not an existing child " \ "of #{short_name}" end @@ -306,7 +310,7 @@ def add(models, as: nil, **dependency_options) end unless as - raise ArgumentError, "you must provide an explicit name with "\ + raise ArgumentError, "you must provide an explicit name with " \ "the :as option" end @@ -451,15 +455,15 @@ def export(port, as: port.name) existing = find_exported_input(name) || find_exported_output(name) if existing if port.to_component_port != existing - raise ArgumentError, "#{port} is already exported as #{name} "\ - "on #{short_name}, cannot override "\ + raise ArgumentError, "#{port} is already exported as #{name} " \ + "on #{short_name}, cannot override " \ "with #{port}." end return end unless child_port?(port) - raise ArgumentError, "#{port} is not a port of one of "\ + raise ArgumentError, "#{port} is not a port of one of " \ "#{self}'s children" end @@ -469,7 +473,7 @@ def export(port, as: port.name) when OutputPort exported_outputs[name] = port.to_component_port else - raise TypeError, "invalid attempt to export port #{port} "\ + raise TypeError, "invalid attempt to export port #{port} " \ "of type #{port.class}" end find_port(name) @@ -581,7 +585,8 @@ def require_dynamic_service(dynamic_service_name, def respond_to_missing?(m, include_private) if m.to_s =~ /_port$/ @child.respond_to?(m) - else super + else + super end end @@ -625,7 +630,8 @@ def overload(*args, **options, &block) def respond_to_missing?(m, include_private) if m =~ /_child$/ component_model.respond_to?(m) - else super + else + super end end @@ -896,7 +902,8 @@ def try_resolve_child_references_in_use_flags(self_task, selected_child) return unless task # rubocop:disable Lint/NonLocalExitFromIterator task - else sel + else + sel end end true @@ -1009,7 +1016,8 @@ def instanciate(plan, context = DependencyInjectionContext.new, plan.add(self_task = new(**task_arguments)) conf = if self_task.has_argument?(:conf) self_task.conf(self_task.arguments[:conf]) - else {} + else + {} end # This is the part of the context that is directly associated @@ -1086,7 +1094,7 @@ def instanciate(plan, context = DependencyInjectionContext.new, if remaining_children_models.size == current_size remaining_children_names = remaining_children_models .map(&:first).sort.join(", ") - raise InternalError, "cannot resolve children "\ + raise InternalError, "cannot resolve children " \ "#{remaining_children_names}" end end @@ -1105,7 +1113,7 @@ def to_dot(io) connections.each do |(source, sink), mappings| mappings.each do |(source_port, sink_port), _policy| - io << "C#{id}#{source}:#{source_port} -> "\ + io << "C#{id}#{source}:#{source_port} -> " \ "C#{id}#{sink}:#{sink_port};" end end @@ -1118,8 +1126,8 @@ def to_dot(io) specialized_model.parent_models.each do |parent_compositions| parent_id = parent_compositions.object_id specialized_id = specialized_model.object_id - io << "C#{parent_id} -> C#{specialized_id} "\ - "[ltail=cluster_#{parent_id} "\ + io << "C#{parent_id} -> C#{specialized_id} " \ + "[ltail=cluster_#{parent_id} " \ "lhead=cluster_#{specialized_id} weight=2];" end end @@ -1136,15 +1144,15 @@ def to_dot(io) io << " Cinterface#{id} [label=\"#{label}\",color=blue,fontsize=15];" exported_outputs.each do |exported_name, port| - io << "C#{id}#{port.component_model.child_name}:"\ - "#{port.port.name} -> "\ - "Cinterface#{id}:#{exported_name} "\ + io << "C#{id}#{port.component_model.child_name}:" \ + "#{port.port.name} -> " \ + "Cinterface#{id}:#{exported_name} " \ "[style=dashed];" end exported_inputs.each do |exported_name, port| - io << "Cinterface#{id}:#{exported_name} -> "\ - "C#{id}#{port.component_model.child_name}:"\ - "#{port.port.name} "\ + io << "Cinterface#{id}:#{exported_name} -> " \ + "C#{id}#{port.component_model.child_name}:" \ + "#{port.port.name} " \ "[style=dashed];" end end @@ -1174,7 +1182,7 @@ def to_dot(io) if child_model.any? { |m| !m.fullfills?(Component) || m.abstract? } color = ', color="red"' end - io << " C#{id}#{child_name} "\ + io << " C#{id}#{child_name} " \ "[label=\"#{label}\"#{color},fontsize=15];" end io << "}" @@ -1310,9 +1318,9 @@ def conf(name, mappings = {}) mappings = mappings.transform_keys do |child| if child.respond_to?(:to_str) conf = mappings[child] - Roby.warn_deprecated "providing the child as string in #conf "\ - "is deprecated, use the _child accessors "\ - "instead (here #{child}_child => [#{conf.join(', ')}])" + Roby.warn_deprecated "providing the child as string in #conf " \ + "is deprecated, use the _child accessors " \ + "instead (here #{child}_child => [#{conf.join(', ')}])" child else child.child_name @@ -1331,7 +1339,7 @@ def merge(other_model) end if needed_specializations.empty? - super(other_model) + super else base_model = root_model.merge(other_model) # If base_model is a placeholder model, we apply the @@ -1367,7 +1375,7 @@ def fullfills?(models) other_model end end - super(models) + super end end end diff --git a/lib/syskit/models/composition_child.rb b/lib/syskit/models/composition_child.rb index d0c1dddf9..8a5f21ef3 100644 --- a/lib/syskit/models/composition_child.rb +++ b/lib/syskit/models/composition_child.rb @@ -14,6 +14,7 @@ class CompositionChild < InstanceRequirements # Set which contains at most one Component model and any number # of data service models attr_accessor :dependency_options + # [InstanceSelection] information needed to update the composition's # parent models about the child (mainly port mappings) def overload_info @@ -55,22 +56,22 @@ def eql?(other) # @deprecated use {#try_resolve_and_bind_child} instead def try_resolve_child(task) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "CompositionChild#try_resolve_and_bind_child instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "CompositionChild#try_resolve_and_bind_child instead" try_resolve_and_bind_child(task) end # @deprecated use {#try_resolve_and_bind_child_recursive} instead def try_resolve_child_recursive(root) - Roby.warn_deprecated "#{__method__} is deprecated, use "\ - "CompositionChild#try_resolve_and_bind_child_recursive instead" + Roby.warn_deprecated "#{__method__} is deprecated, use " \ + "CompositionChild#try_resolve_and_bind_child_recursive instead" try_resolve_and_bind_child_recursive(root) end # @deprecated use {#resolve_and_bind_child} instead def resolve_child(task) - Roby.warn_deprecated "#{__method__} is deprecated, "\ - "use #resolve_and_bind_child instead" + Roby.warn_deprecated "#{__method__} is deprecated, " \ + "use #resolve_and_bind_child instead" resolve_and_bind_child(task) end @@ -171,7 +172,7 @@ def bind(component_or_service) # "right" child, and that it is actually compatible with # the child model bind_resolve_parent(component_or_service.component) - super(component_or_service) + super else parent = bind_resolve_parent(component_or_service) resolve_and_bind_child(composition_model.bind(parent)) @@ -192,12 +193,12 @@ def bind_resolve_parent(component) if compositions.empty? raise ArgumentError, - "cannot bind #{self} to #{component}: it is not the child "\ + "cannot bind #{self} to #{component}: it is not the child " \ "of any #{composition_model} composition" else raise ArgumentError, - "cannot bind #{self} to #{component}: it is the child of "\ - "one or more #{composition_model} compositions, but not "\ + "cannot bind #{self} to #{component}: it is the child of " \ + "one or more #{composition_model} compositions, but not " \ "with the role '#{child_name}'" end end @@ -308,10 +309,7 @@ def to_instance_requirements end class InvalidCompositionChildPort < RuntimeError - attr_reader :composition_model - attr_reader :child_name - attr_reader :port_name - attr_reader :existing_ports + attr_reader :composition_model, :child_name, :port_name, :existing_ports def initialize(composition_model, child_name, port_name) @composition_model = composition_model diff --git a/lib/syskit/models/composition_specialization.rb b/lib/syskit/models/composition_specialization.rb index e77c26833..3afea9f06 100644 --- a/lib/syskit/models/composition_specialization.rb +++ b/lib/syskit/models/composition_specialization.rb @@ -13,6 +13,7 @@ def is_specialization? # The root composition model in the specialization chain attr_accessor :root_model + # The set of definition blocks that have been applied on +self+ in # the process of specializing +root_model+ attribute(:definition_blocks) { [] } @@ -134,12 +135,8 @@ def compatible_with?(spec) end def find_specialization(child_name, model) - if selected_models = specialized_children[child_name] - if matches = selected_models.find_all { |m| m.fullfills?(model) } - unless matches.empty? - matches - end - end + if (selected_models = specialized_children[child_name]) && (matches = selected_models.find_all { |m| m.fullfills?(model) }) && !matches.empty? + matches end end @@ -211,7 +208,8 @@ def weak_match?(selection) if this_selection = selection[child_name] has_match = true this_selection.fullfills?(child_models) - else true + else + true end end has_match && result diff --git a/lib/syskit/models/configured_deployment.rb b/lib/syskit/models/configured_deployment.rb index 6658a0e7f..7823ca84c 100644 --- a/lib/syskit/models/configured_deployment.rb +++ b/lib/syskit/models/configured_deployment.rb @@ -24,8 +24,7 @@ class ConfiguredDeployment # @return [String] the name of the logger to override the default logger name. attr_reader :logger_name - # rubocop: disable Metrics/ParameterLists - def initialize( + def initialize( # rubocop: disable Metrics/ParameterLists process_server_name, model, name_mappings = {}, process_name = model.name, spawn_options = {}, read_only: false, logger_name: nil, **spawn_options_kw @@ -42,10 +41,13 @@ def initialize( @name_mappings = default_mappings.merge(name_mappings) @process_name = process_name @spawn_options = spawn_options.merge(spawn_options_kw) - @read_only = resolve_read_only(read_only, @name_mappings) + @read_only = resolve_read_only(read_only, non_logger_task_names) @logger_name = logger_name end - # rubocop: enable Metrics/ParameterLists + + def non_logger_task_names + @name_mappings.values.grep_v(/_Logger$/) + end # @api private # @@ -53,19 +55,19 @@ def initialize( # read_only. # # @return [Array] - def resolve_read_only(read_only, mappings) - non_logger_names = mappings.values.reject { |name| /_Logger$/ === name } - return non_logger_names if read_only == true + def resolve_read_only(read_only, task_names) return [] unless read_only - read_only = [read_only] unless read_only.kind_of?(Array) - selection = non_logger_names.select do |task_name| - read_only.any? { |task_name_pattern| task_name_pattern === task_name } + return task_names if read_only == true + + read_only = Array(read_only) + selection = task_names.select do |task_name| + read_only.any? { |pattern| pattern === task_name } end if selection.empty? && !read_only.empty? raise ArgumentError, - "#{read_only} is not a valid deployed task name or pattern. "\ - "The valid deployed task names are #{non_logger_names}." + "#{read_only} is not a valid deployed task name or pattern. " \ + "The valid deployed task names are #{task_names}." end selection end @@ -75,7 +77,7 @@ def resolve_read_only(read_only, mappings) # Filters out options that are part of the spawn options but not of # the command-line generation options def filter_command_line_options( - oro_logfile: nil, wait: nil, output: nil, + oro_logfile: nil, output: nil, **command_line_options ) command_line_options @@ -158,7 +160,7 @@ def new(**options) model.new(**options) end - # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize def ==(other) return unless other.kind_of?(ConfiguredDeployment) @@ -170,7 +172,7 @@ def ==(other) read_only == other.read_only && logger_name == other.logger_name end - # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize def hash [process_name, model].hash diff --git a/lib/syskit/models/data_service.rb b/lib/syskit/models/data_service.rb index dad423058..fdc07dec4 100644 --- a/lib/syskit/models/data_service.rb +++ b/lib/syskit/models/data_service.rb @@ -36,7 +36,7 @@ def match end def clear_model - super() + super @orogen_model = OroGen::Spec::TaskContext.new(@orogen_model.project) port_mappings.clear end @@ -104,6 +104,7 @@ def each_required_model # DataServiceModel#apply_block class BlockInstanciator < BasicObject attr_reader :name + def initialize(model, name = nil) unless model.orogen_model raise InternalError, "no interface for #{model.short_name}" @@ -119,7 +120,7 @@ def respond_to_missing?(m, _include_private) @model.respond_to?(m) end - ruby2_keywords def method_missing(m, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing + ruby2_keywords def method_missing(m, *args, &block) if @orogen_model.respond_to?(m) @orogen_model.public_send(m, *args, &block) else @@ -154,7 +155,7 @@ def apply_block(name = nil, &block) # @raise [ArgumentError] if self does not provide service_type def port_mappings_for(service_type) unless (result = port_mappings[service_type]) - raise ArgumentError, "#{service_type.short_name} is not "\ + raise ArgumentError, "#{service_type.short_name} is not " \ "provided by #{short_name}" end @@ -185,9 +186,9 @@ def provides(service_model, new_port_mappings = {}) # can be provided unless kind_of?(service_model.class) raise ArgumentError, - "a #{self.class.name} cannot provide a "\ - "#{service_model.class.name}. If this is really "\ - "what you mean, declare #{name} as a "\ + "a #{self.class.name} cannot provide a " \ + "#{service_model.class.name}. If this is really " \ + "what you mean, declare #{name} as a " \ "#{service_model.class.name} first" end @@ -200,10 +201,10 @@ def provides(service_model, new_port_mappings = {}) service_model.each_port do |p| if find_port(p.name) && !new_port_mappings[p.name] raise SpecError, - "port collision: #{self} and #{service_model} both "\ - "have a port named #{p.name}. If you mean to tell "\ - "syskit that this is the same port, you must provide "\ - "the mapping explicitely by adding "\ + "port collision: #{self} and #{service_model} both " \ + "have a port named #{p.name}. If you mean to tell " \ + "syskit that this is the same port, you must provide " \ + "the mapping explicitely by adding " \ "'#{p.name}' => '#{p.name}' to the provides statement" end end @@ -211,7 +212,7 @@ def provides(service_model, new_port_mappings = {}) new_port_mappings.each do |service_name, self_name| unless (source_port = service_model.find_port(service_name)) raise SpecError, - "#{service_name} is not a port of "\ + "#{service_name} is not a port of " \ "#{service_model.name}" end unless (target_port = find_port(self_name)) @@ -220,20 +221,20 @@ def provides(service_model, new_port_mappings = {}) end if target_port.type != source_port.type raise SpecError, - "invalid port mapping #{service_name} => #{self_name} in "\ - "#{name}.provides("\ - "#{service_model.name}): port #{source_port.name} "\ - "on #{name} is of type "\ - "#{source_port.type.name} and #{target_port.name} on "\ - "#{service_model.name} is of type "\ + "invalid port mapping #{service_name} => #{self_name} in " \ + "#{name}.provides(" \ + "#{service_model.name}): port #{source_port.name} " \ + "on #{name} is of type " \ + "#{source_port.type.name} and #{target_port.name} on " \ + "#{service_model.name} is of type " \ "#{target_port.type.name}" elsif source_port.class != target_port.class raise SpecError, - "invalid port mapping #{service_name} => #{self_name} in "\ - "#{name}.provides("\ - "#{service_model.name}): port #{source_port.name} "\ - "on #{name} is a #{target_port.class.name} "\ - "and #{target_port.name} on #{service_model.name} "\ + "invalid port mapping #{service_name} => #{self_name} in " \ + "#{name}.provides(" \ + "#{service_model.name}): port #{source_port.name} " \ + "on #{name} is a #{target_port.class.name} " \ + "and #{target_port.name} on #{service_model.name} " \ "is of a #{source_port.class.name}" end end @@ -280,14 +281,14 @@ def provides(service_model, new_port_mappings = {}) # @deprecated use {#placeholder_model} instead def proxy_task_model - Roby.warn_deprecated "DataService.proxy_task_model is deprecated, "\ + Roby.warn_deprecated "DataService.proxy_task_model is deprecated, " \ "use .placeholder_model instead" placeholder_model end # @deprecated use {#create_placeholder_task} instead def create_proxy_task - Roby.warn_deprecated "DataService.create_proxy_task is deprecated, "\ + Roby.warn_deprecated "DataService.create_proxy_task is deprecated, " \ "use .create_placeholder_task instead" create_placeholder_task end @@ -391,7 +392,7 @@ def to_dot(io) parent_id = parent_m.object_id.abs (parent_m.each_input_port.to_a + parent_m.each_output_port.to_a) .each do |parent_p| - io << " C#{parent_id}:#{parent_p.name} -> "\ + io << " C#{parent_id}:#{parent_p.name} -> " \ "C#{id}:#{port_mappings_for(parent_m)[parent_p.name]};" end end @@ -507,14 +508,8 @@ def bus_to_client? client_in_srv end - attr_reader :bus_base_srv - attr_reader :bus_in_srv - attr_reader :bus_out_srv - attr_reader :bus_srv - - attr_reader :client_in_srv - attr_reader :client_out_srv - attr_reader :client_srv + attr_reader :bus_base_srv, :bus_in_srv, :bus_out_srv, :bus_srv, + :client_in_srv, :client_out_srv, :client_srv attr_predicate :lazy_dispatch?, true @@ -554,13 +549,13 @@ def setup_submodel( model.override_policy = override_policy if !message_type && !model.message_type raise ArgumentError, - "com bus types must either have a message_type or provide "\ + "com bus types must either have a message_type or provide " \ "another com bus type that does" elsif message_type && model.message_type if message_type != model.message_type raise ArgumentError, - "cannot override message types. The current message type "\ - "of #{model.name} is #{model.message_type}, which "\ + "cannot override message types. The current message type " \ + "of #{model.name} is #{model.message_type}, which " \ "might come from another provided com bus" end elsif !model.message_type @@ -611,8 +606,8 @@ def included(mod) client_to_bus = options.fetch(:client_to_bus) bus_to_client = options.fetch(:bus_to_client) rescue KeyError - raise ArgumentError, "you must provide both the client_to_bus "\ - "and bus_to_client option when "\ + raise ArgumentError, "you must provide both the client_to_bus " \ + "and bus_to_client option when " \ "instanciating a com bus dynamic service" end @@ -630,21 +625,19 @@ def included(mod) elsif bus_to_client provides combus_m.bus_out_srv, "from_bus" => out_name else - raise ArgumentError, "at least one of bus_to_client or "\ + raise ArgumentError, "at least one of bus_to_client or " \ "client_to_bus must be true" end end end def provides(service_model, new_port_mappings = {}) - if service_model.respond_to?(:message_type) - if message_type && service_model.message_type && - (message_type != service_model.message_type) - raise ArgumentError, - "#{name} cannot provide #{service_model.name} "\ - "as their message type differs (resp. #{message_type} "\ - "and #{service_model.message_type}" - end + if service_model.respond_to?(:message_type) && (message_type && service_model.message_type && + (message_type != service_model.message_type)) + raise ArgumentError, + "#{name} cannot provide #{service_model.name} " \ + "as their message type differs (resp. #{message_type} " \ + "and #{service_model.message_type}" end super diff --git a/lib/syskit/models/deployment.rb b/lib/syskit/models/deployment.rb index 9a9e0fb02..e48c7acef 100644 --- a/lib/syskit/models/deployment.rb +++ b/lib/syskit/models/deployment.rb @@ -42,6 +42,40 @@ def instanciate(plan, arguments = {}) task end + # @api private + # + # Context object used to evaluate the block given to new_submodel + class OroGenEvaluationContext < BasicObject + attr_reader :task_name_to_syskit_model + + def initialize(task_m) + @task_m = task_m + @orogen_model = task_m.orogen_model + @task_name_to_syskit_model = {} + end + + def task(name, model) + if model.respond_to?(:orogen_model) + deployed_task = @orogen_model.task(name, model.orogen_model) + @task_name_to_syskit_model[name] = model + else + deployed_task = @orogen_model.task(name, model) + @task_name_to_syskit_model[name] = + ::Syskit::TaskContext.model_for(deployed_task.task_model) + end + + deployed_task + end + + def respond_to_missing?(name, _private = false) + @orogen_model.respond_to?(name) + end + + def method_missing(name, *args, **kw) + @orogen_model.send(name, *args, **kw) + end + end + # Creates a new deployment model # # @option options [OroGen::Spec::Deployment] orogen_model the oroGen @@ -54,8 +88,17 @@ def new_submodel(name: nil, orogen_model: nil, **options, &block) klass = super(name: name, **options) do self.orogen_model = orogen_model || Models.create_orogen_deployment_model(name) + + @task_name_to_syskit_model = {} + self.orogen_model.task_activities.each do |act| + @task_name_to_syskit_model[act.name] = + ::Syskit::TaskContext.model_for(act.task_model) + end + if block - self.orogen_model.instance_eval(&block) + ctxt = OroGenEvaluationContext.new(self) + ctxt.instance_eval(&block) + @task_name_to_syskit_model.merge!(ctxt.task_name_to_syskit_model) end end klass.each_deployed_task_name do |name| @@ -91,7 +134,7 @@ def tasks orogen_model.task_activities end - # Enumerate the names of the tasks deployed by self + # Enumerate the unmapped names of the tasks deployed by self def each_deployed_task_name return enum_for(__method__) unless block_given? @@ -100,15 +143,36 @@ def each_deployed_task_name end end + # @api private + # + # Resolve the Syskit task model for one of this deployment's tasks + def resolve_syskit_model_for_deployed_task(deployed_task) + task_name = deployed_task.name + if (registered = @task_name_to_syskit_model[task_name]) + return registered + end + + # If this happens, it is most probably because the caller modified + # the underlying orogen model directly. Warn about that + Roby.warn_deprecated( + "Modifying the orogen model of a deployment is deprecated. Define " \ + "the orogen model before the syskit model creation, or use " \ + "Deployment.define_deployed_task" + ) + + @task_name_to_syskit_model[task_name] = + ::Syskit::TaskContext.model_for(deployed_task.task_model) + end + # Enumerate the tasks that are deployed in self # - # @yieldparam [String] name the task name + # @yieldparam [String] name the unmapped task name # @yieldparam [Models::TaskContext] model the deployed task model def each_deployed_task_model return enum_for(__method__) unless block_given? each_orogen_deployed_task_context_model do |deployed_task| - task_model = Syskit::TaskContext.model_for(deployed_task.task_model) + task_model = resolve_syskit_model_for_deployed_task(deployed_task) yield(deployed_task.name, task_model) end end diff --git a/lib/syskit/models/deployment_group.rb b/lib/syskit/models/deployment_group.rb index 5b72cf278..058469340 100644 --- a/lib/syskit/models/deployment_group.rb +++ b/lib/syskit/models/deployment_group.rb @@ -75,7 +75,7 @@ def use_group(other) .merge(other.deployed_tasks) do |task_name, self_d, other_d| if self_d != other_d raise TaskNameAlreadyInUse.new(task_name, self_d, other_d), - "there is already a deployment that "\ + "there is already a deployment that " \ "provides #{task_name}" end self_d @@ -226,11 +226,11 @@ def register_configured_deployment(configured_deployment) # Enumerates all the deployments registered on self # # @yieldparam [ConfiguredDeployment] - def each_configured_deployment + def each_configured_deployment(&block) return enum_for(__method__) unless block_given? deployments.each_value do |set| - set.each { |c| yield(c) } + set.each(&block) end end @@ -310,6 +310,102 @@ def use_ruby_tasks( end end + # Deploy oroGen-generated tasks within the Syskit process itself + # + # Valid values for the `activity` parameter: + # * type: "periodic", period: PERIOD_IN_SECONDS + # * type: "triggered" + # * type: "slave" + # * type: "fd_driven" + # + # @param [Hash] model_to_name mapping of an syskit task context model to + # the desired task name, e.g. `OroGen.orogen_syskit_tests.Task => "task"` + # @param [Hash] activity specification of an activity if the task's default + # activity needs to be overriden. + # @param [String] on the name of the process manager that should + # be used + # @param process_managers the object that maintains the set of + # process managers + # @param [Boolean|#===|Array<#===>] read_only set the deployment or some of + # the deployed tasks as read only. To set the whole deployment as read only, + # one should pass read_only: true. To set some tasks, pass a regex that + # matches the deployed task names. Defaults to false. + # @return [[ConfiguredDeployment]] + def use_in_process_tasks( + model_to_name = {}, on: "in_process_tasks", + activity: {}, read_only: false, **model_to_name_kw + ) + unless model_to_name.respond_to?(:each_key) + raise ArgumentError, "mappings should be given as model => name" + end + + model_to_name = model_to_name.merge(model_to_name_kw) + model_to_name.each_key do |task_model| + validate_task_model_is_plain(task_model) + end + + model_to_name.map do |task_model, name| + task_name, deployment_m = in_process_tasks_create_deployment_model( + task_model, activity + ) + + configured_deployment = + Models::ConfiguredDeployment + .new(on, deployment_m, { task_name => name }, name, + read_only: read_only) + register_configured_deployment(configured_deployment) + configured_deployment + end + end + + # @api private + # + # oroGen spec for a task model's default deployment + # + # @param [Class] task_model the task model of interest + # @param [OroGen::Loaders::Base] loader the orogen loader used to resolve + # the model + # @return [OroGen::Spec::TaskDeployment] the task model's default deployment + def in_process_tasks_create_deployment_model( + task_model, activity + ) + default_deployment_name = + OroGen::Spec::Project + .default_deployment_name(task_model.orogen_model.name) + + deployed_task = nil + syskit_deployment_m = Syskit::Deployment.new_submodel do + deployed_task = task(default_deployment_name, task_model) + end + in_process_tasks_override_activity(deployed_task, activity) + [default_deployment_name, syskit_deployment_m] + end + + # @api private + # + # Update a deployed task's activity based on {#use_in_process_tasks}' + # activity parameter + # + # @param [OroGen::Spec::TaskDeployment] deployed_task_model the orogen + # specification for the deployed task + # @param [Hash] activity + def in_process_tasks_override_activity(deployed_task_model, activity) + return if activity.empty? + + case activity[:type].to_sym + when :periodic + deployed_task_model.periodic(activity.fetch(:period)) + when :triggered + deployed_task_model.triggered + when :fd_driven + deployed_task_model.fd_driven + when :slave + deployed_task_model.slave + else + raise ArgumentError, "invalid activity type #{activity}" + end + end + # Declare tasks that are going to be started by some other process, # but whose tasks are going to be integrated in the syskit network # @@ -325,8 +421,8 @@ def use_unmanaged_task(mappings, model_to_name = mappings.map do |task_model, name| if task_model.respond_to?(:to_str) Roby.warn_deprecated( - "specifying the task model as string "\ - "is deprecated. Load the task library and use Syskit's "\ + "specifying the task model as string " \ + "is deprecated. Load the task library and use Syskit's " \ "task class" ) task_model_name = task_model @@ -341,15 +437,7 @@ def use_unmanaged_task(mappings, end model_to_name.each do |task_model, _name| - is_pure_task_context_model = - task_model.kind_of?(Class) && - (task_model <= Syskit::TaskContext) && - !(task_model <= Syskit::RubyTaskContext) - unless is_pure_task_context_model - raise ArgumentError, - "expected a mapping from a task context "\ - "model to a name, but got #{task_model}" - end + validate_task_model_is_plain(task_model) end model_to_name.map do |task_model, name| @@ -369,6 +457,26 @@ def use_unmanaged_task(mappings, end end + # Raise if a given task model is not a "plain" orogen-generated task context + def validate_task_model_is_plain(task_m) + return if plain_task_context_model?(task_m) + + raise ArgumentError, + "expected a mapping from a task context " \ + "model to a name, but got #{task_m}" + end + + # Tests whether a given task model is a "plain" orogen-generated task context + def plain_task_context_model?(model) + model.kind_of?(Class) && + (model <= Syskit::TaskContext) && + !(model <= Syskit::RubyTaskContext) + end + + def deployment_model?(model) + model.kind_of?(Class) && model <= Syskit::Deployment + end + # @api private # # Helper to {#use_deployment} and {#use_deployments_from} to resolve @@ -412,6 +520,7 @@ def use_deployment( process_managers: Syskit.conf, read_only: false, logger_name: nil, + execution_mode: nil, **run_options ) deployment_spec = {} @@ -433,8 +542,8 @@ def use_deployment( "only deployment models can be given without a name" elsif n <= Syskit::TaskContext && !(n <= Syskit::RubyTaskContext) raise TaskNameRequired, - "you must provide a task name when starting a "\ - "component by type, as e.g. use_deployment "\ + "you must provide a task name when starting a " \ + "component by type, as e.g. use_deployment " \ "OroGen.xsens_imu.Task => 'imu'" elsif !(n <= Syskit::Deployment) raise ArgumentError, @@ -442,20 +551,17 @@ def use_deployment( end deployments_by_name[n.orogen_model.name] = n n.orogen_model - else n + else + n end end deployment_spec = deployment_spec.transform_keys do |k| if k.respond_to?(:to_str) k else - is_valid = - k.kind_of?(Class) && - (k <= Syskit::TaskContext || k <= Syskit::Deployment) && - !(k <= Syskit::RubyTaskContext) - unless is_valid + unless plain_task_context_model?(k) || deployment_model?(k) raise ArgumentError, - "only deployment and task context "\ + "only deployment and task context " \ "models can be deployed by use_deployment, got #{k}" end deployments_by_name[k.orogen_model.name] = k @@ -467,6 +573,13 @@ def use_deployment( *names, deployment_spec, loader: loader, **run_options ) new_deployments.map do |deployment_name, name_mappings, name, spawn_options| + # Workaround until we get rid of parse_run_options + # + # In Syskit's case, both log dir and working directory are controlled + # by the process server config + spawn_options.delete(:working_directory) + spawn_options.delete(:wait) + spawn_options[:execution_mode] = execution_mode if execution_mode unless (model = deployments_by_name[deployment_name]) orogen_model = loader.deployment_model_from_name(deployment_name) model = Syskit::Deployment.find_model_by_orogen(orogen_model) diff --git a/lib/syskit/models/dynamic_data_service.rb b/lib/syskit/models/dynamic_data_service.rb index 9d46e29ff..10d290d6d 100644 --- a/lib/syskit/models/dynamic_data_service.rb +++ b/lib/syskit/models/dynamic_data_service.rb @@ -13,6 +13,7 @@ class DynamicDataService attr_reader :service_model # The service definition block attr_reader :block + # Whether this service can be dynamically added to a # configured/running task attr_predicate :addition_requires_reconfiguration? @@ -98,21 +99,21 @@ def driver_for(device_model, port_mappings = {}, **options) def provides(service_model, port_mappings = {}, as: nil, **arguments) if service raise ArgumentError, - "this dynamic service instantiation block already "\ + "this dynamic service instantiation block already " \ "created one new service" end unless service_model.fullfills?(dynamic_service.service_model) raise ArgumentError, - "#{service_model.short_name} does not fullfill the "\ - "model for the dynamic service #{dynamic_service.name}, "\ + "#{service_model.short_name} does not fullfill the " \ + "model for the dynamic service #{dynamic_service.name}, " \ "#{dynamic_service.service_model.short_name}" end if as && as != name raise ArgumentError, - "the as: argument was given (with value #{as}) but it "\ - "is required to be #{name}. Note that it can be omitted "\ + "the as: argument was given (with value #{as}) but it " \ + "is required to be #{name}. Note that it can be omitted " \ "in a dynamic service block" end diff --git a/lib/syskit/models/dynamic_port_binding.rb b/lib/syskit/models/dynamic_port_binding.rb index 4c5b54868..b1c74941e 100644 --- a/lib/syskit/models/dynamic_port_binding.rb +++ b/lib/syskit/models/dynamic_port_binding.rb @@ -12,7 +12,7 @@ class DynamicPortBinding # This resolver's data type attr_reader :type - def initialize(port_model, type, output: port_model.output?, port_resolver:) + def initialize(port_model, type, port_resolver:, output: port_model.output?) @port_model = port_model @type = type @output = output @@ -85,21 +85,19 @@ def self.create_from_matcher(matcher, direction: :auto) unless %i[auto input output].include?(direction) raise ArgumentError, - "'#{direction}' is not a valid value for the 'direction' "\ + "'#{direction}' is not a valid value for the 'direction' " \ "option. Should be one of :auto, :input or :output" end - if direction == :auto - unless (direction = matcher.try_resolve_direction) - raise ArgumentError, - "cannot create a dynamic data source from a matcher "\ - "whose direction cannot be inferred" - end + if direction == :auto && !(direction = matcher.try_resolve_direction) + raise ArgumentError, + "cannot create a dynamic data source from a matcher " \ + "whose direction cannot be inferred" end unless (type = matcher.try_resolve_type) raise ArgumentError, - "cannot create a dynamic data source from a matcher "\ + "cannot create a dynamic data source from a matcher " \ "whose type cannot be inferred" end @@ -164,7 +162,7 @@ def respond_to_missing?(name, _) @root_resolver.respond_to?(name) end - def method_missing(name, *args, **keywords) # rubocop:disable Style/MethodMissingSuper + def method_missing(name, *args, **keywords) @root_resolver.__send__(name, *args, **keywords) end @@ -197,9 +195,18 @@ def initialize(name, component_model, *arguments, **kw_arguments) class BoundOutputReader < OutputReader include BoundAccessor + def initialize( + name, component_model, *arguments, + klass: Syskit::DynamicPortBinding::BoundOutputReader, **kw_arguments + ) + super(name, component_model, *arguments, **kw_arguments) + + @klass = klass + end + def instanciate(component, value_resolver: IdentityValueResolver.new) port_binding = @port_binding.instanciate - Syskit::DynamicPortBinding::BoundOutputReader.new( + @klass.new( name, component, port_binding, value_resolver: value_resolver, **policy ) @@ -338,7 +345,7 @@ def __array_access(args) if @type.length <= args.first ::Kernel.raise ::ArgumentError, - "element #{args.first} out of bound in "\ + "element #{args.first} out of bound in " \ "an array of #{@type.length}" end @@ -365,7 +372,7 @@ def __indexed_access(args) def __compound_access(name, args) unless args.empty? ::Kernel.raise ::ArgumentError, - "expected zero arguments to `#{name}`, "\ + "expected zero arguments to `#{name}`, " \ "got #{args.size}" end diff --git a/lib/syskit/models/faceted_access.rb b/lib/syskit/models/faceted_access.rb index 532d68b19..22eda3fb9 100644 --- a/lib/syskit/models/faceted_access.rb +++ b/lib/syskit/models/faceted_access.rb @@ -45,10 +45,10 @@ def find_data_service(name) def find_data_service_from_type(type) srv = required.find_data_service_from_type(type) - if !required.each_required_model.to_a.include?(srv.model) - find_data_service(srv.name) - else + if required.each_required_model.to_a.include?(srv.model) srv + else + find_data_service(srv.name) end end @@ -107,20 +107,16 @@ def each_port_helper(each_method) end end - def each_input_port + def each_input_port(&block) return enum_for(:each_input_port) unless block_given? - each_port_helper :each_input_port do |p| - yield(p) - end + each_port_helper :each_input_port, &block end - def each_output_port + def each_output_port(&block) return enum_for(:each_output_port) unless block_given? - each_port_helper :each_output_port do |p| - yield(p) - end + each_port_helper :each_output_port, &block end def each_port diff --git a/lib/syskit/models/orogen_base.rb b/lib/syskit/models/orogen_base.rb index 71e5112c3..f23cc0c7f 100644 --- a/lib/syskit/models/orogen_base.rb +++ b/lib/syskit/models/orogen_base.rb @@ -20,6 +20,18 @@ def find_model_from_orogen_name(name) nil end + # Return all syskit models subclasses of self that represent the + # given oroGen model + # + # @param orogen_model the oroGen model + # @return [Array] the + # corresponding syskit model, or nil if there are none registered + def find_all_models_by_orogen(orogen_model) + each_submodel.find_all do |syskit_model| + syskit_model.orogen_model == orogen_model + end + end + # Return the syskit model that represents the given oroGen model # # @param orogen_model the oroGen model @@ -34,13 +46,34 @@ def find_model_by_orogen(orogen_model) nil end - # Returns the syskit model for the given oroGen model + # Returns the syskit model subclass of self for the given oroGen model # # @raise ArgumentError if no syskit model exists + # @raise ArgumentError if more than one matching syskit model exists, + # and Conf.syskit.strict_model_for is set to true def model_for(orogen_model) - if m = find_model_by_orogen(orogen_model) + if Syskit.conf.strict_model_for? + strict_model_for(orogen_model) + elsif (m = find_model_by_orogen(orogen_model)) m - else raise ArgumentError, "there is no syskit model for #{orogen_model.name}" + else + raise ArgumentError, "there is no Syskit model for #{orogen_model.name}" + end + end + + # @api private + # + # Implementation of {#model_for} when Syskit.conf.strict_model_for is set + def strict_model_for(orogen_model) + matches = find_all_models_by_orogen(orogen_model) + if matches.size == 1 + matches.first + elsif matches.empty? + raise ArgumentError, + "there is no Syskit model for #{orogen_model.name}" + else + raise ArgumentError, + "more than one Syskit model matches #{orogen_model.name}" end end end diff --git a/lib/syskit/models/placeholder.rb b/lib/syskit/models/placeholder.rb index 54cd2b621..dffcc4dec 100644 --- a/lib/syskit/models/placeholder.rb +++ b/lib/syskit/models/placeholder.rb @@ -48,15 +48,13 @@ def fullfilled_model result end - def each_required_model + def each_required_model(&block) return enum_for(:each_required_model) unless block_given? if component_model? yield(proxied_component_model) end - proxied_data_service_models.each do |m| - yield(m) - end + proxied_data_service_models.each(&block) end def merge(other_model) @@ -87,27 +85,27 @@ def merge(other_model) Placeholder.for(model_list, component_model: task_model) end - def each_output_port + def each_output_port(&block) return enum_for(:each_output_port) unless block_given? @output_port_models.each_value do |list| - list.each { |p| yield(p) } + list.each(&block) end end - def each_input_port + def each_input_port(&block) return enum_for(:each_input_port) unless block_given? @input_port_models.each_value do |list| - list.each { |p| yield(p) } + list.each(&block) end end - def each_port + def each_port(&block) return enum_for(:each_port) unless block_given? - each_output_port { |p| yield(p) } - each_input_port { |p| yield(p) } + each_output_port(&block) + each_input_port(&block) end def find_output_port(name) @@ -138,7 +136,7 @@ def update_proxy_mappings m.each_output_port do |port| (@output_port_models[port.name] ||= []) << port.attach(self) end - m.each_input_port do |port| + m.each_input_port do |port| (@input_port_models[port.name] ||= []) << port.attach(self) end end @@ -265,7 +263,8 @@ def for(models, component_model: nil, as: nil) if service service.attach(proxy_component_model) - else proxy_component_model + else + proxy_component_model end end @@ -308,7 +307,8 @@ def resolve_models_argument(models, component_model: nil) service = m m.component_model - else m + else + m end end task_models, service_models = models.partition { |t| t <= Syskit::Component } diff --git a/lib/syskit/models/port.rb b/lib/syskit/models/port.rb index df34b582d..959907074 100644 --- a/lib/syskit/models/port.rb +++ b/lib/syskit/models/port.rb @@ -78,7 +78,8 @@ def attach(model) def try_to_component_port if component_model.respond_to?(:self_port_to_component_port) component_model.self_port_to_component_port(self) - else self + else + self end end @@ -92,7 +93,8 @@ def try_to_component_port def to_component_port if component_model.respond_to?(:self_port_to_component_port) component_model.self_port_to_component_port(self) - else raise ArgumentError, "cannot resolve a port of #{component_model.short_name} into a component port" + else + raise ArgumentError, "cannot resolve a port of #{component_model.short_name} into a component port" end end @@ -152,7 +154,8 @@ def can_connect_to?(sink_port) def respond_to_missing?(m, include_private) if !OROGEN_MODEL_EXCLUDED_FORWARDINGS.include?(m) && orogen_model.respond_to?(m) true - else super + else + super end end @@ -161,7 +164,8 @@ def respond_to_missing?(m, include_private) def method_missing(m, *args, &block) if !OROGEN_MODEL_EXCLUDED_FORWARDINGS.include?(m) && orogen_model.respond_to?(m) orogen_model.public_send(m, *args, &block) - else super + else + super end end @@ -271,8 +275,7 @@ def input? end class OutputReader - attr_reader :port - attr_reader :policy + attr_reader :port, :policy def initialize(port, policy = {}) @port = port @@ -307,8 +310,7 @@ def ==(other) end class InputWriter - attr_reader :port - attr_reader :policy + attr_reader :port, :policy def initialize(port, policy = {}) @port = port diff --git a/lib/syskit/models/port_access.rb b/lib/syskit/models/port_access.rb index f4af5e695..3521b6514 100644 --- a/lib/syskit/models/port_access.rb +++ b/lib/syskit/models/port_access.rb @@ -31,7 +31,8 @@ def find_port(name) def port_by_name(name) if p = find_port(name) p - else raise ArgumentError, "#{self} has no port called #{name}, known ports are: #{each_port.map(&:name).sort.join(', ')}" + else + raise ArgumentError, "#{self} has no port called #{name}, known ports are: #{each_port.map(&:name).sort.join(', ')}" end end diff --git a/lib/syskit/models/ruby_task_context.rb b/lib/syskit/models/ruby_task_context.rb index 33ad69419..4b9b99a0b 100644 --- a/lib/syskit/models/ruby_task_context.rb +++ b/lib/syskit/models/ruby_task_context.rb @@ -27,11 +27,11 @@ def deployed_as(name, **options) # # The deployment is created with a single task named 'task' def deployment_model - orogen_model = self.orogen_model deployment_name = "Deployments::RubyTasks::#{name}" + task_model = self @deployment_model ||= Syskit::Deployment.new_submodel(name: deployment_name) do - task "task", orogen_model + task "task", task_model end end end diff --git a/lib/syskit/models/specialization_manager.rb b/lib/syskit/models/specialization_manager.rb index 6d79c1a84..169091d46 100644 --- a/lib/syskit/models/specialization_manager.rb +++ b/lib/syskit/models/specialization_manager.rb @@ -54,12 +54,10 @@ def empty? # Enumerates all specializations defined on {#composition_model} # # @yield [CompositionSpecialization] - def each_specialization + def each_specialization(&block) return enum_for(:each_specialization) unless block_given? - specializations.each_value do |spec| - yield(spec) - end + specializations.each_value(&block) end # Specifies a modification that should be applied on @@ -348,7 +346,8 @@ def instanciated_specializations root = composition_model.root_model if root == composition_model (@instanciated_specializations ||= {}) - else root.specializations.instanciated_specializations + else + root.specializations.instanciated_specializations end end @@ -411,7 +410,7 @@ def child_name def initialize(model, reference_model) @model = model @reference_model = reference_model - @overload_info = ::Hash.new + @overload_info = {} end def apply_block(block) @@ -425,12 +424,12 @@ def respond_to_missing?(symbol, include_private) model.respond_to?(symbol) || super end - ruby2_keywords def method_missing(m, *args, &block) # rubocop:disable Style/MissingRespondToMissing + ruby2_keywords def method_missing(m, *args, &block) unless m =~ /_child$/ return model.send(m, *args, &block) end - child_name = $` + child_name = ::Regexp.last_match.pre_match if (info = overload_info[child_name]) return info end @@ -646,7 +645,7 @@ def matching_specialized_model( specialized_model = specialized_model(*candidates.first) Models.debug do if specialized_model != composition_model - Models.debug "using specialization #{specialized_model.short_name} "\ + Models.debug "using specialization #{specialized_model.short_name} " \ "of #{composition_model.short_name}" end break diff --git a/lib/syskit/models/task_context.rb b/lib/syskit/models/task_context.rb index f79d77b93..2f2cad387 100644 --- a/lib/syskit/models/task_context.rb +++ b/lib/syskit/models/task_context.rb @@ -93,25 +93,26 @@ def make_state_events # Creates a subclass of TaskContext that represents the given task # specification. The class is registered as # Roby::Orogen::ProjectName::ClassName. - def define_from_orogen(orogen_model, register: false) - if model = find_model_by_orogen(orogen_model) # already defined, probably because of dependencies + def define_from_orogen(orogen_model, supermodel: nil, register: false, **kw) + # Already defined, probably because of dependencies + if (model = find_model_by_orogen(orogen_model)) return model end - superclass = orogen_model.superclass - supermodel = - if superclass # we are defining a root model - find_model_by_orogen(superclass) || - define_from_orogen(superclass, register: register) - else - self - end - klass = supermodel.new_submodel(orogen_model: orogen_model) - + supermodel ||= supermodel_from_orogen(orogen_model) + klass = supermodel.new_submodel(orogen_model: orogen_model, **kw) klass.register_model if register && orogen_model.name klass end + def supermodel_from_orogen(orogen_model, register: false) + superclass = orogen_model.superclass + return self unless superclass # we are defining a root model + + find_model_by_orogen(superclass) || + define_from_orogen(superclass, register: register) + end + def register_model OroGen.syskit_model_toplevel_constant_registration = Roby.app.backward_compatible_naming? @@ -232,11 +233,11 @@ def each_event_port(&block) # @return [TaskConfigurationManager] def configuration_manager unless @configuration_manager - if !concrete_model? - manager = concrete_model.configuration_manager - else + if concrete_model? manager = TaskConfigurationManager.new(Roby.app, self) manager.reload + else + manager = concrete_model.configuration_manager end @configuration_manager = manager end diff --git a/lib/syskit/network_generation/async.rb b/lib/syskit/network_generation/async.rb index f776fcc00..106f5fbf1 100644 --- a/lib/syskit/network_generation/async.rb +++ b/lib/syskit/network_generation/async.rb @@ -42,9 +42,7 @@ def transaction_committed? # @api private class Resolution < Concurrent::Future - attr_reader :plan - attr_reader :requirement_tasks - attr_reader :engine + attr_reader :plan, :requirement_tasks, :engine def initialize(plan, event_logger, requirement_tasks, **options, &block) @plan = plan @@ -128,7 +126,7 @@ class InvalidState < RuntimeError; end def apply unless future.complete? raise InvalidState, - "attempting to call Async#apply while processing "\ + "attempting to call Async#apply while processing " \ "is in progress" end diff --git a/lib/syskit/network_generation/dataflow_computation.rb b/lib/syskit/network_generation/dataflow_computation.rb index 3af7909dd..2b0cf0c30 100644 --- a/lib/syskit/network_generation/dataflow_computation.rb +++ b/lib/syskit/network_generation/dataflow_computation.rb @@ -21,15 +21,7 @@ module NetworkGeneration # * triggering_inputs(task) # * propagate_task(task) class DataFlowComputation - attr_reader :result - - attr_reader :triggering_connections - - attr_reader :triggering_dependencies - - attr_reader :missing_ports - - attr_reader :done_ports + attr_reader :result, :triggering_connections, :triggering_dependencies, :missing_ports, :done_ports extend Logger::Hierarchy include Logger::Hierarchy @@ -66,10 +58,8 @@ def task_info(task) # # @raise ArgumentError if there are no information stored for the given port def port_info(task, port_name) - if result.key?(task) - if result[task].key?(port_name) - return result[task][port_name] - end + if result.key?(task) && result[task].key?(port_name) + return result[task][port_name] end if port_name raise ArgumentError, "no information currently available for #{task.orocos_name}.#{port_name}" @@ -154,6 +144,20 @@ def pretty_print(pp) end end + # @api private + # + # For testing purposes only + def changed? + @changed + end + + # @api private + # + # For testing purposes only + def reset_changed + @changed = false + end + def reset(tasks = []) @result = Hash.new { |h, k| h[k] = {} } # Internal variable that is used to detect whether an iteration @@ -262,7 +266,9 @@ def propagate(tasks) end end - if !missing_ports.empty? + if missing_ports.empty? + debug "done computing all required port information" + else debug do debug "found fixed point, breaking out of propagation loop with #{missing_ports.size} missing ports" debug "removing partial port information" @@ -287,18 +293,16 @@ def propagate(tasks) end port_info.empty? end - else - debug "done computing all required port information" end result end def set_port_info(task, port_name, info) - if !has_information_for_port?(task, port_name) - add_port_info(task, port_name, info) - else + if has_information_for_port?(task, port_name) @result[task][port_name] = info + else + add_port_info(task, port_name, info) end end @@ -313,15 +317,15 @@ def add_port_info(task, port_name, info) raise ModifyingFinalizedPortInfo.new(task, port_name, done_at, self.class.name), "trying to change port information for #{task}.#{port_name} after done_port_info has been called" end - if !has_information_for_port?(task, port_name) - @changed = true - @result[task][port_name] = info - else + if has_information_for_port?(task, port_name) begin - @changed = @result[task][port_name].merge(info) + @changed |= @result[task][port_name].merge(info) rescue Exception => e raise e, "while adding information to port #{port_name} on #{task}, #{e.message}", e.backtrace end + else + @changed = true + @result[task][port_name] = info end end @@ -343,10 +347,8 @@ def done_port_info(task, port_name) unless done_ports[task].include?(port_name) @changed = true - if has_information_for_port?(task, port_name) - if port_info(task, port_name).empty? - remove_port_info(task, port_name) - end + if has_information_for_port?(task, port_name) && port_info(task, port_name).empty? + remove_port_info(task, port_name) end done_ports[task] << port_name diff --git a/lib/syskit/network_generation/dataflow_dynamics.rb b/lib/syskit/network_generation/dataflow_dynamics.rb index 51e1a0007..9ce1cfa88 100644 --- a/lib/syskit/network_generation/dataflow_dynamics.rb +++ b/lib/syskit/network_generation/dataflow_dynamics.rb @@ -20,7 +20,7 @@ class << self def buffer_size_margin=(value) value = Float(value) if value < 0 - raise ArgumentError, "only positive values can be used as "\ + raise ArgumentError, "only positive values can be used as " \ "buffer_size_margin, got #{value}" end @buffer_size_margin = Float(value) @@ -56,11 +56,7 @@ class PortDynamics attr_reader :triggers class Trigger - attr_reader :name - attr_reader :period - attr_reader :sample_count - - attr_reader :hash + attr_reader :name, :period, :sample_count, :hash def initialize(name, period, sample_count) @name = name.to_str @@ -95,8 +91,8 @@ def add_trigger(name, period, sample_count) return if sample_count == 0 DataFlowDynamics.debug do - " [#{self.name}]: adding trigger from #{name} -"\ - " #{period} #{sample_count}" + " [#{self.name}]: adding trigger from #{name} - " \ + "#{period} #{sample_count}" end triggers << Trigger.new(name, period, sample_count) end @@ -154,9 +150,7 @@ def pretty_print(pp) # default policies for each of the existing connections in +plan+. The # resulting information is stored in #dynamics class DataFlowDynamics < DataFlowComputation - attr_reader :plan - - attr_reader :triggers + attr_reader :plan, :triggers # Mapping from a deployed task name to the corresponding Roby task object # @@ -365,10 +359,8 @@ def initial_information(task) # Computes a task's slaves initial information def initial_slaves_information(task) task.orogen_model.slaves.each do |orogen_slave_task| - if slave_task = task_from_name[orogen_slave_task.name] - unless has_information_for_task?(slave_task) - initial_task_information(slave_task) - end + if (slave_task = task_from_name[orogen_slave_task.name]) && !has_information_for_task?(slave_task) + initial_task_information(slave_task) end end end @@ -478,9 +470,9 @@ def compute_info_for(task, port_name) port_info(trigger_task, trigger_port) else DataFlowDynamics.debug do - DataFlowDynamics.debug " missing info on "\ - "#{trigger_task}.#{trigger_port} to compute "\ - "#{task}.#{port_name}" + DataFlowDynamics.debug " missing info on " \ + "#{trigger_task}.#{trigger_port} to compute " \ + "#{task}.#{port_name}" break end return false @@ -594,19 +586,19 @@ def policy_for( unless source_port raise InternalError, - "#{source_port_name} is not an output port "\ + "#{source_port_name} is not an output port " \ "of #{source_task}" end unless sink_port raise InternalError, - "#{sink_port_name} is not an input port "\ + "#{sink_port_name} is not an input port " \ "of #{sink_task}" end DataFlowDynamics.debug do - " #{source_task}:#{source_port.name} => "\ - "#{sink_task}:#{sink_port.name}" + " #{source_task}:#{source_port.name} => " \ + "#{sink_task}:#{sink_port.name}" end sink_port_m = sink_port.model @@ -624,8 +616,8 @@ def policy_for( policy else raise UnsupportedConnectionType, - "unknown required connection type "\ - "#{sink_port_m.required_connection_type} "\ + "unknown required connection type " \ + "#{sink_port_m.required_connection_type} " \ "on #{sink_port}" end end @@ -660,8 +652,8 @@ def handle_missing_connection_policy_input( warn do if has_source_dynamics warn "#{sink_task} has no minimal period" - warn "This is needed to compute the reading latency on "\ - "#{sink_port.name}" + warn "This is needed to compute the reading latency on " \ + "#{sink_port.name}" warn "Specified fallback policy #{fallback_policy} will be used" else warn "Cannot compute the period information for output port" @@ -676,13 +668,13 @@ def handle_missing_connection_policy_input( fallback_policy.dup elsif !has_source_dynamics raise SpecError, - "period information for output port #{source_task}:"\ - "#{source_port.name} cannot be computed. This is needed "\ - "to compute the policy to connect to "\ + "period information for output port #{source_task}:" \ + "#{source_port.name} cannot be computed. This is needed " \ + "to compute the policy to connect to " \ "#{sink_task}:#{sink_port.name}" else raise SpecError, - "#{sink_task} has no minimal period, needed to compute "\ + "#{sink_task} has no minimal period, needed to compute " \ "reading latency on #{sink_port.name}" end end @@ -705,12 +697,12 @@ def compute_buffer_policy(source_dynamics, reading_latency) source_dynamics.queue_size(reading_latency) debug do - debug " input_period:#{source_dynamics.minimal_period} => "\ - "reading_latency:#{reading_latency}" + debug " input_period:#{source_dynamics.minimal_period} => " \ + "reading_latency:#{reading_latency}" debug " sample_size:#{source_dynamics.sample_size}" source_dynamics.triggers.each do |tr| - debug " trigger(#{tr.name}): period=#{tr.period} "\ - "count=#{tr.sample_count}" + debug " trigger(#{tr.name}): period=#{tr.period} " \ + "count=#{tr.sample_count}" end break end diff --git a/lib/syskit/network_generation/engine.rb b/lib/syskit/network_generation/engine.rb index d969caa7d..4d294e535 100644 --- a/lib/syskit/network_generation/engine.rb +++ b/lib/syskit/network_generation/engine.rb @@ -137,6 +137,8 @@ def apply_deployed_network_to_plan finalize_deployed_tasks end + sever_old_plan_from_new_plan + if @dataflow_dynamics @dataflow_dynamics.apply_merges(merge_solver) log_timepoint "apply_merged_to_dataflow_dynamics" @@ -147,6 +149,42 @@ def apply_deployed_network_to_plan end end + # "Cut" relations between the "old" plan and the new one + # + # At this stage, old components (task contexts and compositions) + # that are not part of the new plan may still be child of bits of + # the new plan. This happens if they are added as children of other + # task contexts. The transformer does this to register dynamic + # transformation producers + # + # This pass looks for all proxies of compositions and task contexts + # that are not the target of a merge operation. When this happens, + # we know that the component is not being reused, and we remove all + # dependency relations where it is child and where the parent is + # "useful" + # + # Note that we do this only for relations between Syskit + # components. Relations with "plan" Roby tasks are updated because + # we replace toplevel tasks. + def sever_old_plan_from_new_plan + old_tasks = + work_plan + .find_local_tasks(Syskit::Component) + .find_all(&:transaction_proxy?) + + merge_leaves = merge_solver.each_merge_leaf.to_set + old_tasks.each do |old_task| + next if merge_leaves.include?(old_task) + + parents = + old_task + .each_parent_task + .find_all { |t| merge_leaves.include?(t) } + + parents.each { |t| t.remove_child(old_task) } + end + end + class << self # Set of blocks registered with # register_instanciation_postprocessing @@ -334,7 +372,7 @@ def finalize_deployed_tasks if existing_deployment_tasks.size > 1 raise InternalError, - "more than one task for #{process_name} "\ + "more than one task for #{process_name} " \ "present in the plan: #{existing_deployment_tasks}" end @@ -381,8 +419,8 @@ def finalize_deployed_tasks # otherwise (can't have the same deployment running twice) def handle_required_deployment(required, usable, not_reusable) debug do - debug " looking to reuse a deployment for "\ - "#{required.process_name} (#{required})" + debug " looking to reuse a deployment for " \ + "#{required.process_name} (#{required})" debug " candidate: #{usable}" debug " not reusable deployment: #{not_reusable}" break @@ -425,7 +463,7 @@ def validate_usable_deployment(required, usable, non_reusable) return [nil, usable] unless non_reusable raise InternalError, - "non-nil non_reusable_deployment found in #{__method__} while "\ + "non-nil non_reusable_deployment found in #{__method__} while " \ "existing_deployment_needs_restart? returned true" end @@ -492,10 +530,10 @@ def import_existing_tasks(used_tasks) t.find_input_port(sink_port) !both_output && !both_input end - if !connections.empty? - dataflow_graph.set_edge_info(source_t, t, connections) - else + if connections.empty? dataflow_graph.remove_edge(source_t, t) + else + dataflow_graph.set_edge_info(source_t, t, connections) end end end @@ -545,8 +583,8 @@ def reconfigure_tasks_on_static_port_modification(deployed_tasks) next unless t.transaction_modifies_static_ports? debug do - "#{t} was selected as deployment, but it would require "\ - "modifications on static ports, spawning a new task" + "#{t} was selected as deployment, but it would require " \ + "modifications on static ports, spawning a new task" end new_task = t.execution_agent.task(t.orocos_name, t.concrete_model) @@ -573,7 +611,7 @@ def find_current_deployed_task(deployed_tasks) if tasks.size > 1 raise InternalError, - "could not find the current task in "\ + "could not find the current task in " \ "#{deployed_tasks.map(&:to_s).sort.join(', ')}" end @@ -602,18 +640,16 @@ def adapt_existing_deployment(deployment_task, existing_deployment_task) deployed_tasks.each do |task| existing_tasks = orocos_name_to_existing[task.orocos_name] || [] - unless existing_tasks.empty? - existing_task = find_current_deployed_task(existing_tasks) - end + existing_task = find_current_deployed_task(existing_tasks) if !existing_task || !task.can_be_deployed_by?(existing_task) debug do - if !existing_task - " task #{task.orocos_name} has not yet been deployed" + if existing_task + " task #{task.orocos_name} has been deployed, but " \ + "I can't merge with the existing deployment " \ + "(#{existing_task})" else - " task #{task.orocos_name} has been deployed, but "\ - "I can't merge with the existing deployment "\ - "(#{existing_task})" + " task #{task.orocos_name} has not yet been deployed" end end @@ -625,18 +661,10 @@ def adapt_existing_deployment(deployment_task, existing_deployment_task) existing_tasks.each do |previous_task| debug do - " #{new_task} needs to wait for #{existing_task} "\ - "to finish before reconfiguring" + " #{new_task} needs to wait for #{existing_task} " \ + "to finish before reconfiguring" end - parent_task_contexts = - previous_task - .each_parent_task - .find_all { |t| t.kind_of?(Syskit::TaskContext) } - - parent_task_contexts.each do |t| - t.remove_child(previous_task) - end new_task.should_configure_after(previous_task.stop_event) end existing_task = new_task @@ -881,9 +909,9 @@ def handle_resolution_exception(e, on_error: :discard) dataflow_path, hierarchy_path = Engine.autosave_plan_to_dot(work_plan, Roby.app.log_dir) fatal "the generated plan has been saved" - fatal "use dot -Tsvg #{dataflow_path} > #{dataflow_path}.svg "\ + fatal "use dot -Tsvg #{dataflow_path} > #{dataflow_path}.svg " \ "to convert the dataflow to SVG" - fatal "use dot -Tsvg #{hierarchy_path} > #{hierarchy_path}.svg "\ + fatal "use dot -Tsvg #{hierarchy_path} > #{hierarchy_path}.svg " \ "to convert to SVG" rescue Exception => e # rubocop:disable Lint/RescueException Roby.log_exception_with_backtrace(e, self, :fatal) @@ -903,7 +931,7 @@ def validate_final_network( required_instances.each do |_req_task, task| if task.transaction_proxy? raise InternalError, - "instance definition #{instance} contains a transaction "\ + "instance definition #{instance} contains a transaction " \ "proxy: #{instance.task}" elsif !task.plan raise InternalError, @@ -924,9 +952,7 @@ def self.autosave_plan_to_dot( dot_index, mode) path = File.join(dir, basename) - File.open(path, "w") do |io| - io.write Graphviz.new(plan).send(mode, dot_options) - end + File.write(path, Graphviz.new(plan).send(mode, dot_options)) path end end diff --git a/lib/syskit/network_generation/logger.rb b/lib/syskit/network_generation/logger.rb index c7f7ced2c..ac0b96c21 100644 --- a/lib/syskit/network_generation/logger.rb +++ b/lib/syskit/network_generation/logger.rb @@ -65,7 +65,7 @@ def create_logging_port(sink_port_name, logged_task, logged_port) def configure super - each_input_connection do |source_task, source_port_name, sink_port_name, policy| + each_concrete_input_connection do |source_task, source_port_name, sink_port_name, policy| source_port = source_task.find_output_port(source_port_name) create_logging_port(sink_port_name, source_task, source_port) end @@ -76,11 +76,11 @@ def self.logger_dynamic_port_of(task_model) .find_all { |p| !p.type } if ports.size > 1 raise InternalError, - "valid logger task contexts should have only one catch-all "\ + "valid logger task contexts should have only one catch-all " \ "dynamic input port, #{task_model} got #{ports.size}" elsif ports.empty? raise InternalError, - "valid logger task contexts should have one catch-all "\ + "valid logger task contexts should have one catch-all " \ "dynamic input port, and #{task_model} has none" end @@ -103,6 +103,7 @@ def self.add_logging_to_network(engine, work_plan) required_loggers = [] engine.deployment_tasks.each do |deployment| next unless deployment.plan + next unless deployment.logging_enabled? required_logging_ports = [] required_connections = [] @@ -127,9 +128,9 @@ def self.add_logging_to_network(engine, work_plan) end unless (logger_task = deployment.logger_task) - warn "deployment #{deployment.process_name} has no usable "\ - "logger (default logger name would be "\ - "#{deployment.process_name}_Logger))" + warn "deployment #{deployment.process_name} has no usable " \ + "logger (default logger name would be " \ + "#{deployment.process_name}_Logger)" next end logger_task = work_plan[deployment.logger_task] diff --git a/lib/syskit/network_generation/merge_solver.rb b/lib/syskit/network_generation/merge_solver.rb index e59b750e4..5040f4034 100644 --- a/lib/syskit/network_generation/merge_solver.rb +++ b/lib/syskit/network_generation/merge_solver.rb @@ -286,11 +286,9 @@ def merge_task_contexts each_task_context_merge_candidate(task) do |merged_task| # Try to resolve the merge - can_merge, mappings = - resolve_merge(merged_task, task, merged_task => task) - - if can_merge - apply_merge_group(mappings) + result = resolve_merge(merged_task, task, merged_task => task) + if result.can_merge? + apply_merge_group(result.mappings) else invalid_merges << [merged_task, task] end @@ -412,44 +410,126 @@ def merge_compositions end end + # Representation of the result of {#resolve_merge} + MergeResolution = Struct.new( + :mappings, :merged_task, :task, :merged_failure_chain, :failure_chain + ) do + # Whether the merge resolution was successful + def can_merge? + !failure_chain + end + + def pretty_print_failure(pp) + pp.text "Chain 1 cannot be merged in chain 2:" + [[merged_task, merged_failure_chain], [task, failure_chain]] + .each_with_index do |(task, chain), i| + pp.breakable + pp.text "Chain #{i + 1}:" + pp.nest(2) do + pp.breakable + task.pretty_print(pp) + chain.each do |connection| + pp.breakable + pp.text "sink #{connection.sink_port}_port connected " + pp.text "via policy #{connection.policy} to source " + pp.text "#{connection.source_port}_port of" + pp.breakable + connection.source_task.pretty_print(pp) + end + end + end + end + end + + Connection = Struct.new( + :source_task, :source_port, :policy, :sink_port, :sink_task + ) + + # Resolve merge between N tasks with the given tasks as seeds + # + # The method will cycle through the task's mismatching inputs (if + # there are any) and recursively resolve them, until it determines + # if the whole group can or cannot be merged + # + # @param [Syskit::TaskContext] merged_task task that is being considered + # to be merged into 'task' + # @param [Syskit::TaskContext] task the task into which merged_task will + # be merged. I.e. the operation is `task.merge(merged_task)`` + # @param [{Syskit::TaskContext=>Syskit::TaskContext}] mappings hash + # of "known good" merges that do not consider the dataflow (i.e. only + # considering intrinsic properties of the tasks themselves) + # + # @return [MergeResolution] def resolve_merge(merged_task, task, mappings) - mismatched_inputs = log_nest(2) { resolve_input_matching(merged_task, task) } + unless may_merge_task_contexts?(merged_task, task) + return MergeResolution.new(mappings, merged_task, task, [], []) + end + + mismatched_inputs, failed_connections = log_nest(2) do + resolve_input_matching(merged_task, task) + end + unless mismatched_inputs - # Incompatible inputs - return false, mappings + return MergeResolution.new( + mappings, merged_task, task, + [failed_connections[0]], [failed_connections[1]] + ) end - mismatched_inputs.each do |sink_port, merged_source_task, source_task| - info do - info " looking to pair the inputs of port #{sink_port} of" - info " #{merged_source_task}" - info " -- and --" - info " #{source_task}" - break - end + mappings = mappings.merge({ merged_task => task }) + mismatched_inputs.each do |connection, m_connection| + next unless (result = process_port_mismatch(connection, m_connection, mappings)) + return result unless result.can_merge? - if mappings[merged_source_task] == source_task - info " are already paired in the merge resolution: matching" - next - elsif !may_merge_task_contexts?(merged_source_task, source_task) - info " rejected: may not be merged" - return false, mappings - end + mappings = result.mappings + end - can_merge, mappings = log_nest(2) do - resolve_merge(merged_source_task, source_task, - mappings.merge(merged_source_task => source_task)) - end + MergeResolution.new(mappings, merged_task, task) + end - if can_merge - info " resolved" - else - info " rejected: cannot find mapping to merge both tasks" - return false, mappings - end + # @api private + # + # Process the mismatch between two connections as returned by + # {#resolve_input_matching} + # + # @param [Connection] connection the mismatching connection on the + # merge-target side + # @param [Connection] connection the mismatching connection on the + # to-be-merged side + # @return [MergeResolution] merge result + def process_port_mismatch(connection, m_connection, mappings) + sink_port = connection.sink_port + merged_source_task = m_connection.source_task + source_task = connection.source_task + + info do + info " looking to pair the inputs of port #{sink_port} of" + info " #{merged_source_task}" + info " -- and --" + info " #{source_task}" + break + end + + if mappings[merged_source_task] == source_task + info " are already paired in the merge resolution: matching" + return + end + + resolution = log_nest(2) do + resolve_merge(merged_source_task, source_task, mappings) end - [true, mappings] + if resolution.can_merge? + info " resolved" + resolution + else + info " rejected: cannot find mapping to merge both tasks" + MergeResolution.new( + resolution.mappings, m_connection.sink_task, connection.sink_task, + [connection] + resolution.failure_chain, + [m_connection] + resolution.merged_failure_chain + ) + end end def compatible_policies?(policy, other_policy) @@ -469,81 +549,86 @@ def compatible_policies?(policy, other_policy) # Otherwise, the set of mismatching inputs is returned, in which # each mismatch is a tuple (port_name,source_port,task_source,target_source). def resolve_input_matching(merged_task, task) - return [] if merged_task.equal?(task) + return [], nil if merged_task.equal?(task) m_inputs = Hash.new { |h, k| h[k] = {} } merged_task.each_concrete_input_connection do |m_source_task, m_source_port, sink_port, m_policy| - m_inputs[sink_port][[m_source_task, m_source_port]] = m_policy + m_inputs[sink_port][[m_source_task, m_source_port]] = + Connection.new(m_source_task, m_source_port, m_policy, + sink_port, merged_task) end - task.each_concrete_input_connection - .filter_map do |source_task, source_port, sink_port, policy| + mismatched_inputs = + task.each_concrete_input_connection + .filter_map do |source_task, source_port, sink_port, policy| # If merged_task has no connection on sink_port, the merge # is always valid next unless m_inputs.key?(sink_port) + connection = Connection.new( + source_task, source_port, policy, sink_port, task + ) + port_model = merged_task.model.find_input_port(sink_port) - resolved = + resolved, m_failed_connection = if port_model&.multiplexes? resolve_multiplexing_input( - sink_port, source_task, source_port, policy, - m_inputs[sink_port] + connection, m_inputs[sink_port] ) else - resolve_input( - sink_port, source_task, source_port, policy, - m_inputs[sink_port] - ) + resolve_input(connection, m_inputs[sink_port]) end - break unless resolved + unless resolved + return nil, [m_failed_connection, connection] + end resolved unless resolved.empty? end + + [mismatched_inputs, nil] end - def resolve_multiplexing_input( - sink_port, source_task, source_port, policy, m_inputs - ) - return [] unless (m_policy = m_inputs[[source_task, source_port]]) + def resolve_multiplexing_input(connection, m_inputs) + m_connection = m_inputs[[connection.source_task, connection.source_port]] + return [], nil unless m_connection # Already connected to the same task and port, we # just need to check whether the connections are # compatible - return [] if compatible_policies?(policy, m_policy) + return [], nil if compatible_policies?(connection.policy, m_connection.policy) debug do "rejected: incompatible policies on #{sink_port}" end - nil + [nil, m_connection] end - def resolve_input( - sink_port, source_task, source_port, policy, m_inputs - ) + def resolve_input(connection, m_inputs) # If we are not multiplexing, there can be only one source # for merged_task - (m_source_task, m_source_port), m_policy = m_inputs.first + m_connection = m_inputs.first.last - if m_source_port != source_port + if m_connection.source_port != connection.source_port debug do - "rejected: sink #{sink_port} is connected to a port "\ - "named #{m_source_port}, expected #{source_port}" + "rejected: sink #{sink_port} is connected to a port " \ + "named #{m_connection.source_port}, expected " \ + "#{connection.source_port}" end - return + return nil, m_connection end - unless compatible_policies?(policy, m_policy) + unless compatible_policies?(connection.policy, m_connection.policy) debug do - "rejected: incompatible policies on #{sink_port}" + "rejected: incompatible policies on #{connection.sink_port}" end - return + return nil, m_connection end - if m_source_task == source_task - [] + if m_connection.source_task == connection.source_task + [[], nil] else - [sink_port, m_source_task, source_task] + [[connection, m_connection], nil] end end @@ -572,6 +657,14 @@ def display_merge_graph(title, merge_graph) break end end + + def each_merge_leaf + return enum_for(__method__) unless block_given? + + task_replacement_graph.each_vertex do |v| + yield(v) if task_replacement_graph.leaf?(v) + end + end end end end diff --git a/lib/syskit/network_generation/system_network_deployer.rb b/lib/syskit/network_generation/system_network_deployer.rb index 7277ddfaf..f4c12f912 100644 --- a/lib/syskit/network_generation/system_network_deployer.rb +++ b/lib/syskit/network_generation/system_network_deployer.rb @@ -111,8 +111,8 @@ def find_suitable_deployment_for(task) return candidates.first if candidates.size <= 1 debug do - "#{candidates.size} deployments available for #{task} "\ - "(#{task.concrete_model}), trying to resolve" + "#{candidates.size} deployments available for #{task} " \ + "(#{task.concrete_model}), trying to resolve" end selected = log_nest(2) do resolve_deployment_ambiguity(candidates, task) @@ -122,8 +122,8 @@ def find_suitable_deployment_for(task) selected else debug do - " deployment of #{task} (#{task.concrete_model}) "\ - "is ambiguous" + " deployment of #{task} (#{task.concrete_model}) " \ + "is ambiguous" end nil end @@ -153,7 +153,7 @@ def select_deployments(tasks) elsif used_deployments.include?(selected) debug do machine, configured_deployment, task_name = *selected - "#{task} resolves to #{configured_deployment}.#{task_name} "\ + "#{task} resolves to #{configured_deployment}.#{task_name} " \ "on #{machine} for its deployment, but it is already used" end missing_deployments << task @@ -198,8 +198,11 @@ def apply_selected_deployments(selected_deployments) # is valid # # @raise [MissingDeployments] if some tasks could not be deployed + # @raise [MissingConfigurationSection] if some configuration sections are + # used but do not exist def validate_deployed_network verify_all_tasks_deployed + verify_all_configurations_exist end # Verifies that all tasks in the plan are deployed @@ -228,10 +231,38 @@ def verify_all_tasks_deployed tasks_with_candidates[task] = candidates end raise MissingDeployments.new(tasks_with_candidates), - "there are tasks for which it exists no deployed equivalent: "\ + "there are tasks for which it exists no deployed equivalent: " \ "#{not_deployed.map { |m| "#{m}(#{m.orogen_model.name})" }}" end + # Verifies that all selected configuration sections exist + # + # @raise [MissingConfigurationSection] + def verify_all_configurations_exist + tasks = plan.find_local_tasks(TaskContext) + .not_finished.not_abstract + + missing_sections = tasks.map do |t| + conf_manager = t.model.configuration_manager + Array(t.conf).find_all do |name| + # 'default' always exists + next if name == "default" + + !conf_manager.sections[name.to_s] + end + end + + missing = + tasks.zip(missing_sections).each_with_object({}) do |(t, sections), h| + h[t] = sections unless sections.empty? + end + + return if missing.empty? + + raise MissingConfigurationSection.new(missing), + "some configuration sections are used but not defined" + end + # Try to resolve a set of deployment candidates for a given task # # @param [Array<(String,Model,String)>] candidates set @@ -273,8 +304,8 @@ def resolve_deployment_ambiguity(candidates, task) candidates.each do |deployed_task| deployment = deployed_task.configured_deployment info do - " #{deployed_task.mapped_task_name} of #{deployment.model} "\ - "on #{deployment.process_server_name}" + " #{deployed_task.mapped_task_name} of #{deployment.model} " \ + "on #{deployment.process_server_name}" end end break diff --git a/lib/syskit/network_generation/system_network_generator.rb b/lib/syskit/network_generation/system_network_generator.rb index 85273aaef..77b15e946 100644 --- a/lib/syskit/network_generation/system_network_generator.rb +++ b/lib/syskit/network_generation/system_network_generator.rb @@ -11,9 +11,7 @@ class SystemNetworkGenerator include Logger::Hierarchy include Roby::DRoby::EventLogging - attr_reader :plan - attr_reader :event_logger - attr_reader :merge_solver + attr_reader :plan, :event_logger, :merge_solver def initialize(plan, event_logger: plan.event_logger, @@ -97,6 +95,15 @@ def instanciate(instance_requirements) log_timepoint "instanciate_requirements" toplevel_tasks = instance_requirements.each_with_index.map do |requirements, i| task = requirements.instanciate(plan).to_task + debug do + debug "Instanciated task " + log_nest(2) do + log_pp :debug, task + end + debug "for requirements " + log_pp :debug, requirements + nil + end # We add all these tasks as permanent tasks, to use # #static_garbage_collect to cleanup #plan. plan.add_permanent_task(task) @@ -187,9 +194,10 @@ def self.remove_abstract_composition_optional_children(plan) def compute_system_network(instance_requirements, garbage_collect: true, validate_abstract_network: true, validate_generated_network: true) - toplevel_tasks = log_timepoint_group "instanciate" do + @toplevel_tasks = log_timepoint_group "instanciate" do instanciate(instance_requirements) end + @toplevel_instance_requirements = instance_requirements merge_solver.merge_identical_tasks log_timepoint "merge" @@ -237,12 +245,20 @@ def compute_system_network(instance_requirements, garbage_collect: true, self.validate_abstract_network log_timepoint "validate_abstract_network" end + if validate_generated_network self.validate_generated_network log_timepoint "validate_generated_network" end - toplevel_tasks + @toplevel_tasks + end + + def toplevel_tasks_to_requirements + (@toplevel_tasks || []) + .map { |t| merge_solver.replacement_for(t) } + .zip(@toplevel_instance_requirements || []) + .each_with_object({}) { |(t, ir), h| (h[t] ||= []) << ir } end # Verifies that the task allocation is complete @@ -257,7 +273,7 @@ def self.verify_task_allocation( return if still_abstract.empty? raise TaskAllocationFailed.new(self, still_abstract), - "could not find implementation for the following abstract "\ + "could not find implementation for the following abstract " \ "tasks: #{still_abstract}" end @@ -278,9 +294,9 @@ def self.verify_no_multiplexing_connections(plan) if seen[sink_port] seen_task, seen_port = seen[sink_port] if [source_task, source_port] != [seen_task, seen_port] - raise SpecError, "#{task}.#{sink_port} is connected "\ - "multiple times, at least to "\ - "#{source_task}.#{source_port} and "\ + raise SpecError, "#{task}.#{sink_port} is connected " \ + "multiple times, at least to " \ + "#{source_task}.#{source_port} and " \ "#{seen_task}.#{seen_port}" end end @@ -298,7 +314,7 @@ def self.verify_no_multiplexing_connections(plan) # attached to any device # @raise [SpecError] if some devices are assigned to more than one # task - def self.verify_device_allocation(plan) + def self.verify_device_allocation(plan, toplevel_tasks_to_requirements = {}) components = plan.find_local_tasks(Syskit::Device).to_a # Check that all devices are properly assigned @@ -308,7 +324,7 @@ def self.verify_device_allocation(plan) end unless missing_devices.empty? raise DeviceAllocationFailed.new(plan, missing_devices), - "could not allocate devices for the following tasks: "\ + "could not allocate devices for the following tasks: " \ "#{missing_devices}" end @@ -317,7 +333,9 @@ def self.verify_device_allocation(plan) task.each_master_device do |dev| device_name = dev.full_name if (old_task = devices[device_name]) - raise ConflictingDeviceAllocation.new(dev, task, old_task) + raise ConflictingDeviceAllocation.new( + dev, task, old_task, toplevel_tasks_to_requirements + ) else devices[device_name] = task end @@ -337,7 +355,7 @@ def validate_abstract_network # Validates the network generated by {#compute_system_network} def validate_generated_network self.class.verify_task_allocation(plan) - self.class.verify_device_allocation(plan) + self.class.verify_device_allocation(plan, toplevel_tasks_to_requirements) super if defined? super end end diff --git a/lib/syskit/orogen_namespace.rb b/lib/syskit/orogen_namespace.rb index e0ee2eaa6..8eda54fb1 100644 --- a/lib/syskit/orogen_namespace.rb +++ b/lib/syskit/orogen_namespace.rb @@ -60,8 +60,8 @@ def method_missing(m, *args, &block) end super rescue ::NoMethodError => e - ::Kernel.raise e, "no task #{m} on #{project_name}, available tasks: "\ - "#{@registered_objects.keys.map(&:to_s).sort.join(', ')}" + ::Kernel.raise e, "no task #{m} on #{project_name}, available tasks: " \ + "#{@registered_objects.keys.map(&:to_s).sort.join(', ')}" end end @@ -118,7 +118,7 @@ def method_missing(name, *args, **kw) if (m = @deployments[name.to_s]) unless args.empty? && kw.empty? raise ArgumentError, - "wrong number of arguments, given #{args.size} and #{kw.size} "\ + "wrong number of arguments, given #{args.size} and #{kw.size} " \ "keyword arguments, expected 0" end @@ -129,7 +129,7 @@ def method_missing(name, *args, **kw) rescue NoMethodError deployments_s = @deployments.keys.join(", ") raise NoMethodError.new(name), - "no deployment registered with the name '#{name}', "\ + "no deployment registered with the name '#{name}', " \ "available deployments are: #{deployments_s}" end end @@ -185,7 +185,7 @@ def method_missing(m, *args, &block) project rescue NoMethodError => e - raise e, "#{e.message}, available OroGen projects: "\ + raise e, "#{e.message}, available OroGen projects: " \ "#{@project_namespaces.keys.map(&:to_s).join(', ')}" end diff --git a/lib/syskit/port.rb b/lib/syskit/port.rb index 352b2a604..45dadc9ca 100644 --- a/lib/syskit/port.rb +++ b/lib/syskit/port.rb @@ -96,15 +96,15 @@ def connect_to(in_port, policy = {}) in_port = in_port.to_component_port if !output? raise WrongPortConnectionDirection.new(self, in_port), - "cannot connect #{self} to #{in_port}: "\ + "cannot connect #{self} to #{in_port}: " \ "#{self} is not an output port" elsif !in_port.input? raise WrongPortConnectionDirection.new(self, in_port), - "cannot connect #{self} to #{in_port}: "\ + "cannot connect #{self} to #{in_port}: " \ "#{in_port} is not an input port" elsif component == in_port.component raise SelfConnection.new(self, in_port), - "cannot connect #{self} to #{in_port}: "\ + "cannot connect #{self} to #{in_port}: " \ "they are both ports of the same component" elsif type != in_port.type raise WrongPortConnectionTypes.new(self, in_port), diff --git a/lib/syskit/port_access.rb b/lib/syskit/port_access.rb index 61f8236d8..509dddec2 100644 --- a/lib/syskit/port_access.rb +++ b/lib/syskit/port_access.rb @@ -68,11 +68,11 @@ def each_input_port end # Enumerates all of this component's ports - def each_port + def each_port(&block) return enum_for(:each_port) unless block_given? - each_output_port { |p| yield(p) } - each_input_port { |p| yield(p) } + each_output_port(&block) + each_input_port(&block) end # Returns true if +name+ is a valid output port name for instances diff --git a/lib/syskit/process_managers/in_process/manager.rb b/lib/syskit/process_managers/in_process/manager.rb new file mode 100644 index 000000000..615dd23a6 --- /dev/null +++ b/lib/syskit/process_managers/in_process/manager.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + # The in-process process manager allows to instanciate orogen-created tasks + # inside the Ruby process itself + # + # @see Configuration#use_in_process_tasks DeploymentGroup#use_in_process_tasks + module InProcess + # Manager of in-process deployments + class Manager + # Exception raised if one attempts to do name mappings in an + # unmanaged process server + class NameMappingsForbidden < ArgumentError; end + + # The set of processes started so far + # + # @return [Hash] mapping from process name + # to the process object + attr_reader :processes + + # The model loader + # + # @return [OroGen::Loaders::PkgConfig] + attr_reader :loader + + def initialize(component_loader: Roby.app.syskit_component_loader) + @component_loader = component_loader + @loader = component_loader.pkgconfig_loader + @processes = {} + @deployments = {} + end + + def disconnect; end + + # Register a new deployment model on this server. + # + # If name mappings are needed, they must have been done in the + # model. {#start} does not support name mappings + def register_deployment_model(model) + loader.register_deployment_model(model) + end + + # Start a registered deployment + # + # @param [String] name the desired process name + # @param [String,OroGen::Spec::Deployment] deployment_name either + # the name of a deployment model on {#loader}, or the deployment + # model itself + # @param [Hash] name_mappings name mappings. This is provided for + # compatibility with the process server API, but should always be + # empty + # @param [String] prefix a prefix to be added to all tasks in the + # deployment. This is provided for + # compatibility with the process server API, but should always be + # nil + # @param [Hash] options additional spawn options. This is provided + # for compatibility with the process server API, but is ignored + # @return [UnmanagedProcess] + def start( + name, deployment_name = name, name_mappings = {}, prefix: nil, **_ + ) + if processes[name] + raise ArgumentError, "#{name} is already started in #{self}" + end + + model = resolve_deployment_model(deployment_name) + process = Process.new(self, name, model) + apply_name_mappings(process, model, prefix, name_mappings) + process.spawn + processes[name] = process + end + + def resolve_deployment_model(model_or_name) + return model_or_name unless model_or_name.respond_to?(:to_str) + + loader.deployment_model_from_name(model_or_name) + end + + def apply_name_mappings(process, model, prefix, name_mappings) + prefix_mappings = Orocos::ProcessBase.resolve_prefix(model, prefix) + name_mappings = prefix_mappings.merge(name_mappings) + name_mappings.each { process.map_name(_1, _2) } + end + + # Requests that the process server moves the log directory at +log_dir+ + # to +results_dir+ + def save_log_dir(log_dir, results_dir); end + + # Creates a new log dir, and save the given time tag in it (used later + # on by save_log_dir) + def create_log_dir(log_dir, time_tag, metadata = {}); end + + # Waits for processes to terminate. +timeout+ is the number of + # milliseconds we should wait. If set to nil, the call will block until + # a process terminates + # + # Returns a hash that maps deployment names to the Status + # object that represents their exit status. + def wait_termination(_timeout = nil) + dead_processes = {} + processes.delete_if do |_, process| + next unless process.dead? + + dead_processes[process] = Status.new + true + end + dead_processes + end + + def wait_running(*process_names) + result = {} + process_names.each do |name| + if (p = processes[name]) + result[name] = { iors: p.wait_running(0) } + else + result[name] = + { error: "#{name} was not found of the processes list" } + end + end + result + end + + # Requests to stop the given deployment + # + # The call does not block until the process has quit. You will have to + # call #wait_termination to wait for the process end. + def stop(process_name) + processes[process_name]&.kill + end + + DEFAULT_LOGGER_NAME = "syskit_in_process_logger" + + def default_logger_task(plan, app: Roby.app) + self.class.default_logger_task(plan, app: app) + end + + def self.find_default_logger_task(plan, app: Roby.app) + return unless (logger_m = app.syskit_logger_m) + + plan.find_tasks(logger_m) + .with_arguments(orocos_name: DEFAULT_LOGGER_NAME) + .permanent.first + end + + def self.find_default_logger_deployment_task(plan, app: Roby.app) + return unless app.syskit_logger_m + return unless (deployment = app.syskit_in_process_logger_deployment) + + plan.find_tasks(deployment.model) + .permanent.first + end + + # The logger task + def self.default_logger_task(plan, app: Roby.app) + return unless (deployment = app.syskit_in_process_logger_deployment) + + if (t = find_default_logger_task(plan)) + return t + end + + deployment_t = find_default_logger_deployment_task(plan, app: app) + unless deployment_t + plan.add_permanent_task(deployment_t = deployment.new) + end + + plan.add_permanent_task( + logger_t = deployment_t.task(DEFAULT_LOGGER_NAME) + ) + if logger_t.respond_to?(:default_logger=) + logger_t.default_logger = true + end + logger_t + end + + def self.register_default_logger_deployment(app, conf: Syskit.conf) + return unless (logger_m = app.syskit_logger_m) + + d = conf.use_in_process_tasks(logger_m => DEFAULT_LOGGER_NAME).first + app.syskit_in_process_logger_deployment = d + end + + def self.deregister_default_logger_deployment(app, conf: Syskit.conf) + return unless (d = app.syskit_in_process_logger_deployment) + + conf.deregister_configured_deployment(d) + app.syskit_in_process_logger_deployment = nil + end + end + end + end +end diff --git a/lib/syskit/process_managers/in_process/process.rb b/lib/syskit/process_managers/in_process/process.rb new file mode 100644 index 000000000..8a699b8ad --- /dev/null +++ b/lib/syskit/process_managers/in_process/process.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + module InProcess + # Representation of a single running in-process deployment + class Process < ProcessBase + extend Logger::Hierarchy + include Logger::Hierarchy + + # The {Manager} which created this process + # + # If non-nil, the object's #dead_deployment will be called when self + # is stopped + # + # @return [#dead_deployment,nil] + attr_reader :manager + + # The set of deployed tasks + # + # @return [{String=>TaskContext}] mapping from the deployed task name as + # defined in {model} to the actual {Orocos::TaskContext} + attr_reader :deployed_tasks + + # The host on which this process' tasks run. + # + # @return [String] + attr_reader :host_id + + # Whether the tasks in this process are running on the same machine than + # the ruby process + # + # This is always true as ruby tasks are instanciated inside the ruby + # process + # + # @return [Boolean] + def on_localhost? + host_id == "localhost" + end + + # The PID of the process in which the tasks run + # + # This is always Process.pid as in-process tasks are instanciated inside + # the ruby process + # + # @return [Integer] + attr_reader :pid + + # @api private + # + # The thread that monitors the tasks availability + # + # It is spawned the first time {#wait_running} returns true + attr_reader :monitor_thread + + # Creates a new object managing tasks that are running in-process + # + # @param [nil,#dead_deployment] process_manager the process manager + # which created this process. If non-nil, its #dead_deployment method + # will be called when {stop} is called + # @param [String] name the process name + # @param [OroGen::Spec::Deployment] model the deployment model + def initialize(manager, name, model, host_id: "localhost") + @manager = manager + @host_id = host_id + super(name, model) + + @component_loader = Roby.app.syskit_component_loader + @default_logger = false + end + + # "Starts" this process + # + # It actually instanciates the underlying task + # + # @return [void] + def spawn(_options = {}) + @deployed_tasks = + model.each_task.each_with_object({}) do |deployed, h| + mapped_name = mapped_name_of(deployed.name) + h[mapped_name] = spawn_deployed_task(mapped_name, deployed) + end + @killed = false + end + + # Module used to extend Orocos::TaskContext for in-process tasks + module InProcessTask + # The local task handle, used to dispose of the local task when done + # + # @return [Orocos::LocalTask] + attr_reader :local_task + + def dispose + local_task.dispose + end + + def execute + local_task.execute + end + end + + def spawn_deployed_task(mapped_name, orogen_deployed_task) + @component_loader.load_task_library( + orogen_deployed_task.task_model.project.name + ) + + task_model = orogen_deployed_task.task_model + local_task = @component_loader.create_local_task_context( + mapped_name, task_model.name, false + ) + local_task_activity_setup(local_task, orogen_deployed_task) + + task = Orocos.allow_blocking_calls do + Orocos::TaskContext.new( + local_task.ior, name: mapped_name, model: task_model + ) + end + + task.extend InProcessTask + # protect the local task against GC + task.instance_variable_set :@local_task, local_task + task + end + + def local_task_activity_setup(local_task, orogen_deployed_task) + if orogen_deployed_task.triggered? + local_task.make_triggered + elsif orogen_deployed_task.periodic? + local_task.make_periodic(orogen_deployed_task.period) + elsif orogen_deployed_task.fd_driven? + local_task.make_fd_driven + elsif orogen_deployed_task.slave? + local_task.make_slave + else + activity_name = orogen_deployed_task.activity_type.name + raise ArgumentError, "unsupported activity #{activity_name}" + end + end + + # Returns the deployed tasks.The deployed tasks are resolved on + # wait_running, which is called by `update_deployment_states.rb` + # when making the deployment ready. + # + # @returns [Hash] + def resolve_all_tasks + @deployed_tasks + end + + def define_ior_mappings(ior_mappings) + @ior_mappings = ior_mappings + end + + # Returns the component object for the given name + # + # @raise [RuntimeError] if the process is not running yet + # @raise [ArgumentError] if the name is not the name of a task on + # self + def task(task_name) + raise "process not running yet" unless ready? + + @deployed_tasks.fetch(task_name) + end + + # Waits until all the tasks are resolved or the timeout is due, + # registering the IORs of the resolved tasks and starting the monitor. + # + # @raises RuntimeError + # @raises Orocos::CORBA::ComError + # @return [Hash] the ior mappings + def wait_running(_timeout = nil) + @deployed_tasks.each_value.map(&:ior) + end + + # "Kill" this process + # + # It shuts down the tasks that are part of it + def kill(**) + @deployed_tasks.each_value(&:dispose) + @deployed_tasks = {} + @killed = true + end + + # Returns true if the process died + def dead? + @killed + end + + # Returns true if the tasks have been successfully discovered + def ready? + true + end + + # True if the process is running. This is an alias for running? + def alive? + !dead? + end + + # True if the process is running. This is an alias for alive? + def running? + alive? + end + + def join + raise NotImplementedError, "UnmanagedProcess#join is not implemented" + end + end + end + end +end diff --git a/lib/syskit/process_managers/process_base.rb b/lib/syskit/process_managers/process_base.rb new file mode 100644 index 000000000..feb2f0c77 --- /dev/null +++ b/lib/syskit/process_managers/process_base.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + # Exception raised when there is no IOR registered for a given task name. + class IORNotRegisteredError < Orocos::NotFound; end + + # Interface for processes, that is the representation of a running deployment + # + # This is the interface needed by {Deployment} + class ProcessBase + # The deployment name + attr_reader :name + + # The deployment model + attr_reader :model + + def initialize(name, model) + @name = name + @model = model + @name_mappings = {} + end + + # Require that to rename the task called +old+ in this deployment to + # +new+ during execution + # + # @see name_mappings name_mappings= + def map_name(old, new) + @name_mappings[old] = new + end + + # @api private + # + # use a mapping if exists + def mapped_name_of(name) + @name_mappings[name] || name + end + + # Returns the name of the tasks that are running in this process + # + # See also #each_task + def mapped_task_names + unless model + raise Orocos::NotOrogenComponent, + "#{name} does not seem to have been generated by orogen" + end + + model.task_activities.map do |deployed_task| + name = deployed_task.name + mapped_name_of(name) + end + end + end + end +end diff --git a/lib/syskit/roby_app/remote_processes/loader.rb b/lib/syskit/process_managers/remote/loader.rb similarity index 93% rename from lib/syskit/roby_app/remote_processes/loader.rb rename to lib/syskit/process_managers/remote/loader.rb index d73413328..6a476cece 100644 --- a/lib/syskit/roby_app/remote_processes/loader.rb +++ b/lib/syskit/process_managers/remote/loader.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true module Syskit - module RobyApp - module RemoteProcesses + module ProcessManagers + module Remote # A loader object that allows to load models from a remote process # server class Loader < OroGen::Loaders::Base - attr_reader :client - - attr_reader :available_projects - attr_reader :available_deployments - attr_reader :available_typekits + attr_reader :client, :available_projects, :available_deployments, + :available_typekits def initialize(client, root_loader = self) @client = client @@ -33,7 +30,7 @@ def project_model_text_from_name(name) end raise OroGen::ProjectNotFound, - "#{client} has no project called #{name}, "\ + "#{client} has no project called #{name}, " \ "available projects: #{available_projects.keys.sort.join(', ')}" end diff --git a/lib/syskit/roby_app/remote_processes/client.rb b/lib/syskit/process_managers/remote/manager.rb similarity index 78% rename from lib/syskit/roby_app/remote_processes/client.rb rename to lib/syskit/process_managers/remote/manager.rb index 758194c47..0c1d49945 100644 --- a/lib/syskit/roby_app/remote_processes/client.rb +++ b/lib/syskit/process_managers/remote/manager.rb @@ -1,17 +1,25 @@ # frozen_string_literal: true -require "orocos" -require "syskit/roby_app/remote_processes/loader" - module Syskit - module RobyApp - module RemoteProcesses - # Client-side API to a {Server} instance + module ProcessManagers + # The remote process manager allows to manage orogen-created deployments managed + # by a Syskit process server + # + # The syskit process servers are started with `syskit process-server`. Even + # local orogen processes are managed this way, through a syskit-started local + # process server + # + # @see Configuration#use_deployment DeploymentGroup#use_deployment + module Remote + # Type transferred between the server and the manager to report on log updates # - # Process servers allow to start/stop and monitor processes on remote - # machines. Instances of this class provides access to remote process - # servers. - class Client + # Defined here to make sure it is actually defined. Otherwise, the log + # state reporting would fail at runtime, and unit-testing for this is + # very hard. + LogUploadState = Server::LogUploadState + + # Syskit-side interface to the remote process server + class Manager # Emitted when an operation fails class Failed < RuntimeError; end class StartupFailed < RuntimeError; end @@ -36,7 +44,7 @@ class StartupFailed < RuntimeError; end # Mapping from deployment names to the corresponding XML type registry # for the typekits available on the process server attr_reader :available_typekits - # Mapping from a deployment name to the corresponding RemoteProcess + # Mapping from a deployment name to the corresponding {Process} # instance, for processes that have been started by this client. attr_reader :processes @@ -48,12 +56,9 @@ class StartupFailed < RuntimeError; end attr_reader :server_pid # A string that allows to uniquely identify this process server attr_reader :host_id - # The name service object that allows to resolve tasks from this process - # server - attr_reader :name_service def to_s - "#" + "#<#{self.class} #{host}:#{port}>" end def inspect @@ -62,16 +67,14 @@ def inspect # Connects to the process server at +host+:+port+ # - # @option options [Orocos::NameService] :name_service - # (Orocos.name_service). The name service object that should be used - # to resolve tasks started by this process server # @option options [OroGen::Loaders::Base] :root_loader # (Orocos.default_loader). The loader object that should be used as # root for this client's loader def initialize( host = "localhost", port = DEFAULT_PORT, - response_timeout: 10, root_loader: Orocos.default_loader, - name_service: Orocos.name_service + response_timeout: 10, + root_loader: Orocos.default_loader, + register_on_name_server: true ) @host = host @port = port @@ -79,14 +82,13 @@ def initialize( begin TCPSocket.new(host, port) rescue Errno::ECONNREFUSED => e raise e.class, - "cannot contact process server at "\ + "cannot contact process server at " \ "'#{host}:#{port}': #{e.message}" end - socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) - socket.fcntl(Fcntl::FD_CLOEXEC, 1) + @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) + @socket.fcntl(Fcntl::FD_CLOEXEC, 1) - @name_service = name_service begin @server_pid = pid rescue EOFError @@ -98,6 +100,7 @@ def initialize( @processes = {} @death_queue = [] @host_id = "#{host}:#{port}:#{server_pid}" + @register_on_name_server = register_on_name_server @response_timeout = response_timeout end @@ -115,16 +118,12 @@ def pid(timeout: @response_timeout) def info(timeout: @response_timeout) socket.write(COMMAND_GET_INFO) unless select([socket], [], [], timeout) - raise "timeout while reading process server "\ + raise "timeout while reading process server " \ "at '#{host}:#{port}'" end Marshal.load(socket) end - def disconnect - socket.close - end - class TimeoutError < RuntimeError end @@ -163,14 +162,14 @@ def wait_for_ack # Starts the given deployment on the remote server, without waiting for # it to be ready. # - # Returns a RemoteProcess instance that represents the process on the + # Returns a {Process} instance that represents the process on the # remote side. # # Raises Failed if the server reports a startup failure def start(process_name, deployment, name_mappings = {}, options = {}) if processes[process_name] raise ArgumentError, - "this client already started a process "\ + "this client already started a process " \ "called #{process_name}" end @@ -179,16 +178,19 @@ def start(process_name, deployment, name_mappings = {}, options = {}) loader.root_loader.deployment_model_from_name(deployment) unless loader.has_deployment?(deployment) raise OroGen::DeploymentModelNotFound, - "deployment #{deployment} exists locally but not "\ + "deployment #{deployment} exists locally but not " \ "on the remote process server #{self}" end - else deployment_model = deployment + else + deployment_model = deployment end prefix_mappings = Orocos::ProcessBase.resolve_prefix( deployment_model, options.delete(:prefix) ) name_mappings = prefix_mappings.merge(name_mappings) + options[:register_on_name_server] = + options.fetch(:register_on_name_server, @register_on_name_server) socket.write(COMMAND_START) Marshal.dump( @@ -199,13 +201,15 @@ def start(process_name, deployment, name_mappings = {}, options = {}) if pid_s == RET_NO msg = Marshal.load(socket) raise Failed, - "failed to start #{deployment_model.name}: #{msg}" + "failed to start #{process_name}: #{msg}" elsif pid_s == RET_STARTED_PROCESS pid = Marshal.load(socket) process = Process.new( process_name, deployment_model, self, pid ) - process.name_mappings = name_mappings + name_mappings.each do |a, b| + process.map_name(a, b) + end processes[process_name] = process return process else @@ -217,9 +221,10 @@ def start(process_name, deployment, name_mappings = {}, options = {}) # Creates a new log dir, and save the given time tag in it (used later # on by save_log_dir) - def create_log_dir(log_dir, time_tag, metadata = {}) + def create_log_dir(time_tag, metadata = {}) socket.write(COMMAND_CREATE_LOG) - Marshal.dump([log_dir, time_tag, metadata], socket) + Marshal.dump([time_tag, metadata], socket) + wait_for_ack end def queue_death_announcement @@ -230,10 +235,15 @@ def queue_death_announcement # # The transfer is asynchronous, use {#upload_state} to track the # upload progress - def log_upload_file(host, port, certificate, user, password, localfile) + def log_upload_file( + host, port, certificate, user, password, localfile, + max_upload_rate: Float::INFINITY, + implicit_ftps: RobyApp::LogTransferServer.use_implicit_ftps? + ) socket.write(COMMAND_LOG_UPLOAD_FILE) Marshal.dump( - [host, port, certificate, user, password, localfile], socket + [host, port, certificate, user, password, localfile, + max_upload_rate, implicit_ftps], socket ) wait_for_ack @@ -279,14 +289,14 @@ def wait_termination(timeout = nil) result = {} @death_queue.each do |name, status| - RemoteProcesses.debug "#{name} died" + Process.debug "#{name} died" if (p = processes.delete(name)) p.dead! result[p] = status else - RemoteProcesses.warn "process server reported the exit "\ - "of '#{name}', but no process with "\ - "that name is registered" + Process.warn "process server reported the exit " \ + "of '#{name}', but no process with " \ + "that name is registered" end end @death_queue.clear @@ -298,22 +308,28 @@ def wait_termination(timeout = nil) # # The call does not block until the process has quit. You will have to # call #wait_termination to wait for the process end. - def stop(deployment_name, wait, cleanup: true, hard: false) + def stop(deployment_name, hard: false) socket.write(COMMAND_END) - Marshal.dump([deployment_name, cleanup, hard], socket) + Marshal.dump([deployment_name, hard], socket) raise Failed, "failed to quit #{deployment_name}" unless wait_for_ack - - join(deployment_name) if wait end - def kill_all(cleanup: false, hard: true) + def kill_all(hard: true) socket.write(COMMAND_KILL_ALL) - Marshal.dump([cleanup, hard], socket) + Marshal.dump([hard], socket) raise Failed, "failed kill_all" unless wait_for_ack Marshal.load(socket) end + def wait_running(*process_names) + socket.write(COMMAND_WAIT_RUNNING) + Marshal.dump(process_names, socket) + wait_for_answer do + return Marshal.load(socket) + end + end + def join(deployment_name) process = processes[deployment_name] return unless process @@ -328,6 +344,10 @@ def quit_server socket.write(COMMAND_QUIT) end + def disconnect + socket.close + end + def close socket.close end diff --git a/lib/syskit/roby_app/remote_processes/process.rb b/lib/syskit/process_managers/remote/process.rb similarity index 59% rename from lib/syskit/roby_app/remote_processes/process.rb rename to lib/syskit/process_managers/remote/process.rb index fbeb9b050..ed4ab6a6e 100644 --- a/lib/syskit/roby_app/remote_processes/process.rb +++ b/lib/syskit/process_managers/remote/process.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true -require "orocos/process" - module Syskit - module RobyApp - module RemoteProcesses - # Representation of a remote process started with ProcessClient#start - class Process < Orocos::ProcessBase + module ProcessManagers + module Remote + # Representation of a remote process started with {Manager} + class Process < ProcessBase + extend Logger::Hierarchy + extend Logger::Forward + # The ProcessClient instance that gives us access to the remote process # server attr_reader :process_client + # A string describing the host. It can be used to check if two processes # are running on the same host def host_id @@ -29,6 +31,8 @@ def initialize(name, deployment_model, process_client, pid) @process_client = process_client @pid = pid @alive = true + @ior_mappings = {} + super(name, deployment_model) end @@ -42,17 +46,11 @@ def dead! @alive = false end - # Returns the task context object for the process' task that has this - # name - def task(task_name) - process_client.name_service.get(task_name, process: self) - end - # Cleanly stop the process # # @see kill! - def kill(wait = true, cleanup: true, hard: false) - process_client.stop(name, wait, cleanup: cleanup, hard: hard) + def kill(hard: false) + process_client.stop(name, hard: hard) end # Wait for the @@ -60,7 +58,7 @@ def join process_client.join(name) end - # True if the process is running. This is an alias for running? + # True if the process is running. This is an alias for alive? def alive? @alive end @@ -70,20 +68,28 @@ def running? @alive end - # Resolve all tasks within the deployment - # - # A deployment is usually considered ready when all its tasks can be - # resolved successfully - def resolve_all_tasks(cache = {}) - Orocos::Process.resolve_all_tasks(self, cache) do |task_name| - task(task_name) + def resolve_all_tasks + model.task_activities.each_with_object({}) do |deployed_task, map| + mapped_name = mapped_name_of(deployed_task.name) + unless (ior = @ior_mappings[mapped_name]) + raise IORNotRegisteredError, + "no IOR is registered for #{task_name}" + end + + map[mapped_name] = + Orocos::TaskContext.new( + ior, name: mapped_name, + model: deployed_task.task_model, + process: self + ) end end - # Waits for the deployment to be ready. +timeout+ is the number of - # milliseconds we should wait. If it is nil, will wait indefinitely - def wait_running(timeout = nil) - Orocos::Process.wait_running(self, timeout) + # no-op, only here to please Orocos::TaskContext + def register_task(task); end + + def define_ior_mappings(mappings) + @ior_mappings = mappings end end end diff --git a/lib/syskit/roby_app/remote_processes/protocol.rb b/lib/syskit/process_managers/remote/protocol.rb similarity index 80% rename from lib/syskit/roby_app/remote_processes/protocol.rb rename to lib/syskit/process_managers/remote/protocol.rb index 49a0f52f2..12c6bf1d5 100644 --- a/lib/syskit/roby_app/remote_processes/protocol.rb +++ b/lib/syskit/process_managers/remote/protocol.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Syskit - module RobyApp - module RemoteProcesses + module ProcessManagers + module Remote DEFAULT_PORT = 20_202 COMMAND_GET_INFO = "I" @@ -12,7 +12,8 @@ module RemoteProcesses COMMAND_END = "E" COMMAND_QUIT = "Q" COMMAND_KILL_ALL = "K" - COMMAND_LOG_UPLOAD_FILE = "U" + COMMAND_WAIT_RUNNING = "W" + COMMAND_LOG_UPLOAD_FILE = "U" COMMAND_LOG_UPLOAD_STATE = "X" EVENT_DEAD_PROCESS = "D" diff --git a/lib/syskit/process_managers/remote/server.rb b/lib/syskit/process_managers/remote/server.rb new file mode 100644 index 000000000..a5ede01fd --- /dev/null +++ b/lib/syskit/process_managers/remote/server.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "socket" +require "fcntl" +require "net/ftp" +require "orocos" + +require "concurrent/atomic/atomic_reference" + +module Syskit + module ProcessManagers + module Remote + # Implementation of the syskit process server + module Server + extend Logger::Root(to_s, Logger::INFO) + end + end + end +end + +require "syskit/process_managers/remote/protocol" +require "syskit/process_managers/remote/server/ftp_upload" +require "syskit/process_managers/remote/server/log_upload_state" +require "syskit/process_managers/remote/server/process" +require "syskit/process_managers/remote/server/server" diff --git a/lib/syskit/process_managers/remote/server/ftp_upload.rb b/lib/syskit/process_managers/remote/server/ftp_upload.rb new file mode 100644 index 000000000..9f13b5c44 --- /dev/null +++ b/lib/syskit/process_managers/remote/server/ftp_upload.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + module Remote + module Server + # Encapsulation of the log file upload process + class FTPUpload + def initialize( # rubocop:disable Metrics/ParameterLists + host, port, certificate, user, password, file, + max_upload_rate: Float::INFINITY, + implicit_ftps: false + ) + + @host = host + @port = port + @certificate = certificate + @user = user + @password = password + @file = file + + @max_upload_rate = Float(max_upload_rate) + @implicit_ftps = implicit_ftps + end + + # Create a temporary file with the FTP server's public key, to pass + # to FTP.open + # + # @yieldparam [String] path the certificate path + def with_certificate + Tempfile.create do |cert_io| + cert_io.write @certificate + cert_io.flush + yield(cert_io.path) + end + end + + # Open the FTP connection + # + # @yieldparam [Net::FTP] + def open + with_certificate do |cert_path| + Net::FTP.open( + @host, + private_data_connection: false, port: @port, + implicit_ftps: @implicit_ftps, + ssl: { verify_mode: OpenSSL::SSL::VERIFY_PEER, + ca_file: cert_path } + ) do |ftp| + ftp.login(@user, @password) + yield(ftp) + end + end + end + + # Open the connection and transfer the file + # + # @return [LogUploadState::Result] + def open_and_transfer + open { |ftp| transfer(ftp) } + LogUploadState::Result.new(@file, true, nil) + rescue StandardError => e + LogUploadState::Result.new(@file, false, e.message) + end + + # Do transfer the file through the given connection + # + # @param [Net::FTP] ftp + def transfer(ftp) + last = Time.now + File.open(@file) do |file_io| + ftp.storbinary("STOR #{File.basename(@file)}", + file_io, Net::FTP::DEFAULT_BLOCKSIZE) do |buf| + now = Time.now + rate_limit(buf.size, now, last) + last = Time.now + end + end + end + + # @api private + # + # Sleep when needed to keep the expected transfer rate + def rate_limit(chunk_size, now, last) + duration = now - last + exp_duration = chunk_size / @max_upload_rate + # Do not wait, but do not try to "make up" for the bandwidth + # we did not use. The goal is to not affect the rest of the + # system + return if duration > exp_duration + + sleep(exp_duration - duration) + end + end + end + end + end +end diff --git a/lib/syskit/process_managers/remote/server/log_upload_state.rb b/lib/syskit/process_managers/remote/server/log_upload_state.rb new file mode 100644 index 000000000..5c5e7602a --- /dev/null +++ b/lib/syskit/process_managers/remote/server/log_upload_state.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + module Remote + module Server + # State of the asynchronous file transfers managed by {Server} + class LogUploadState + attr_reader :pending_count + + Result = Struct.new :file, :success, :message do + def success? + success + end + end + + def initialize(pending_count, results) + @pending_count = pending_count + @results = results + end + + def each_result(&block) + @results.each(&block) + end + end + end + end + end +end diff --git a/lib/syskit/process_managers/remote/server/process.rb b/lib/syskit/process_managers/remote/server/process.rb new file mode 100644 index 000000000..d8d4ab592 --- /dev/null +++ b/lib/syskit/process_managers/remote/server/process.rb @@ -0,0 +1,434 @@ +# frozen_string_literal: true + +module Syskit + module ProcessManagers + module Remote + module Server + # The representation of an Orocos process. It manages + # starting the process and cleaning up when the process + # dies. + class Process + extend Logger::Forward + extend Logger::Hierarchy + include Logger::Forward + include Logger::Hierarchy + + # The component process ID + # + # @return [Integer,nil] + attr_reader :name + + # The component process ID + # + # @return [Integer,nil] + attr_reader :pid + + # Creates a new Process instance which will be able to + # start and supervise the execution of the given Orocos + # component + # + # @param [String] name the process name + # @param [OroGen::Spec::Deployment] model the process deployment' + # + # @overload initialize(name, model_name = name) + # deprecated form + # @param [String] name the process name + # @param [String] model_name the name of the deployment model + # + def initialize(name, deployment_name, loader, working_directory) + binfile = + if loader.respond_to?(:find_deployment_binfile) + loader.find_deployment_binfile(deployment_name) + else + loader.available_deployments + .fetch(deployment_name).binfile + end + unless binfile + raise ArgumentError, + "cannot find deployment #{deployment_name}" + end + + @name = name + @name_mappings = {} + + @env = {} + @command = binfile + @arguments = [] + @working_directory = working_directory + + @redirect_output = "%m-%p.txt" + @redirect_orocos_logger_output = "orocos.%m-%p.txt" + + @execution_mode = {} + end + + def self.tracing_library_path + tracing_pkg = + Utilrb::PkgConfig.new("orocos-rtt-#{Orocos.orocos_target}") + + File.join( + tracing_pkg.libdir, + "liborocos-rtt-traces-#{Orocos.orocos_target}.so" + ) + end + + def enable_tracing + @env["LD_PRELOAD"] = self.class.tracing_library_path + end + + def add_name_mappings(mappings) + @arguments += mappings.map do |old, new| + "--rename=#{old}:#{new}" + end + end + + def setup_log_level(log_level) + unless VALID_LOG_LEVELS.include?(log_level) + raise ArgumentError, + "'#{log_level}' is not a valid log level. " \ + "Valid options are #{valid_levels}." + end + + @env["BASE_LOG_LEVEL"] = log_level.to_s.upcase + end + + def setup_corba(name_service_ip) + if name_service_ip + @env["ORBInitRef"] = + "NameService=corbaname::#{name_service_ip}" + else + @arguments << "--register-on-name-server" << "0" + end + end + + # Command line arguments have to be of type --